A blockchain supply-chain app records each step of an item's journey (created, paid, shipped, received) in a smart contract that every party can read and nobody can quietly rewrite. This tutorial builds a small version on Ethereum with Solidity and Foundry: a contract that holds the buyer's payment in escrow until delivery is confirmed, tests for it, a local deployment, and the commands to interact with it.
Install Foundry, write the contract in Solidity 0.8, and test it with forge test. Run a local chain with anvil, deploy with forge create, and call the contract with cast. Move to a public testnet such as Sepolia before any real money is involved, and never deploy unaudited code to mainnet. Truffle and Ganache, used by older tutorials, have been discontinued.
This contract is a teaching example. It has not been audited. Smart contracts that hold money are attacked constantly, and a bug cannot be patched once deployed. Get a professional audit before real funds go near a contract.
1. What the blockchain does, and what it doesn't
The chain gives every participant the same tamper-evident record of who did what and when, and the contract enforces the rules: only the seller can mark an item shipped, only the buyer can confirm receipt, and the payment is released only after that.
It cannot prove that the physical goods match the record. If someone scans the wrong crate, the chain faithfully records the wrong fact. Real systems pair the contract with trusted inputs (signed scans, IoT devices or audited partners). Also remember that everything on a public chain is public and permanent: never write personal data such as names, phone numbers or addresses on-chain.
2. The toolchain in 2026
| Older tutorials used | Use today | Why |
|---|---|---|
| Truffle | Foundry (or Hardhat) | Consensys discontinued Truffle and Ganache in 2023 |
| Ganache | Anvil (Foundry's local node) | Starts instantly with funded test accounts |
| Solidity 0.5 / 0.6 | Solidity 0.8.x | Built-in overflow checks and custom errors |
| web3.js | viem or ethers v6 | Actively maintained JavaScript libraries |
address.transfer() | call{value: …}("") with checks | transfer forwards a fixed gas amount that can break payments |
Install Foundry on Linux or macOS (on Windows, use WSL):
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init supply-chain
cd supply-chainforge init creates src/, test/ and script/ folders with a sample Counter contract; delete the sample files.
3. The contract
Create src/SupplyChain.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract SupplyChain {
enum Status { Created, Paid, Shipped, Received }
struct Item {
string name;
uint256 price;
address payable seller;
address buyer;
Status status;
}
uint256 public itemCount;
mapping(uint256 => Item) public items;
event ItemCreated(uint256 indexed id, address indexed seller, uint256 price);
event ItemPaid(uint256 indexed id, address indexed buyer);
event ItemShipped(uint256 indexed id);
event ItemReceived(uint256 indexed id);
error UnknownItem();
error WrongStatus(Status expected, Status actual);
error WrongAmount(uint256 expected, uint256 sent);
error NotAuthorised();
error PaymentFailed();
modifier inStatus(uint256 id, Status expected) {
if (id >= itemCount) revert UnknownItem();
Status actual = items[id].status;
if (actual != expected) revert WrongStatus(expected, actual);
_;
}
function createItem(string calldata name, uint256 price) external returns (uint256 id) {
id = itemCount++;
items[id] = Item(name, price, payable(msg.sender), address(0), Status.Created);
emit ItemCreated(id, msg.sender, price);
}
function pay(uint256 id) external payable inStatus(id, Status.Created) {
Item storage item = items[id];
if (msg.value != item.price) revert WrongAmount(item.price, msg.value);
item.buyer = msg.sender;
item.status = Status.Paid;
emit ItemPaid(id, msg.sender);
}
function ship(uint256 id) external inStatus(id, Status.Paid) {
if (msg.sender != items[id].seller) revert NotAuthorised();
items[id].status = Status.Shipped;
emit ItemShipped(id);
}
function confirmReceived(uint256 id) external inStatus(id, Status.Shipped) {
Item storage item = items[id];
if (msg.sender != item.buyer) revert NotAuthorised();
item.status = Status.Received; // update state first...
emit ItemReceived(id);
(bool ok, ) = item.seller.call{value: item.price}(""); // ...then pay
if (!ok) revert PaymentFailed();
}
}Three design points matter:
- State lives in
storage. Changing amemorycopy of a struct does nothing to the stored item, a bug that appears in many older examples. - Escrow. The buyer's payment stays in the contract until the buyer confirms receipt, so neither side has to trust the other with the money.
- Checks, effects, interactions. The status is updated before the payment is sent, which blocks re-entrancy attacks. Production contracts often go further and let sellers withdraw their balance themselves (a "pull payment").
4. Test it
Create test/SupplyChain.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {SupplyChain} from "../src/SupplyChain.sol";
contract SupplyChainTest is Test {
SupplyChain chain;
address seller = makeAddr("seller");
address buyer = makeAddr("buyer");
function setUp() public {
chain = new SupplyChain();
vm.deal(buyer, 1 ether);
}
function _paidItem() internal returns (uint256 id) {
vm.prank(seller);
id = chain.createItem("Rice, 25 kg", 0.01 ether);
vm.prank(buyer);
chain.pay{value: 0.01 ether}(id);
}
function test_FullLifecycle() public {
uint256 id = _paidItem();
vm.prank(seller);
chain.ship(id);
vm.prank(buyer);
chain.confirmReceived(id);
assertEq(seller.balance, 0.01 ether);
}
function test_RevertWhen_StrangerShips() public {
uint256 id = _paidItem();
vm.expectRevert(SupplyChain.NotAuthorised.selector);
chain.ship(id);
}
}Run forge build and then forge test -vv. vm.prank makes the next call come from a chosen address, and vm.deal gives an address test ether. Add a test for every rule the contract enforces, including the wrong payment amount and actions taken in the wrong order.
5. Deploy locally and interact
Start a local chain in one terminal:
anvilAnvil prints ten funded test accounts with their private keys. These keys are public; use them only on your local chain. In a second terminal, deploy with the first key:
forge create src/SupplyChain.sol:SupplyChain \
--rpc-url http://127.0.0.1:8545 \
--private-key <anvil key 0> --broadcastNote the "Deployed to" address, then walk an item through its life with cast:
C=<deployed address>; RPC=http://127.0.0.1:8545
cast send $C "createItem(string,uint256)" "Rice, 25 kg" 10000000000000000 --rpc-url $RPC --private-key <anvil key 0>
cast send $C "pay(uint256)" 0 --value 0.01ether --rpc-url $RPC --private-key <anvil key 1>
cast send $C "ship(uint256)" 0 --rpc-url $RPC --private-key <anvil key 0>
cast send $C "confirmReceived(uint256)" 0 --rpc-url $RPC --private-key <anvil key 1>
cast call $C "itemCount()(uint256)" --rpc-url $RPC6. Moving to a testnet
- Get an RPC endpointfor a public testnet such as Sepolia from a node provider, and some free test ether from a faucet.
- Store your key safely.Run
cast wallet import deployer --interactiveto keep it in an encrypted keystore, then use--account deployerinstead of--private-key. Never commit keys to Git. - Deploy and verifythe contract source on a block explorer so partners can read exactly what they are trusting.
- Build the front endwith viem or ethers v6: read items with
items(id), listen for the events, and let users sign transactions from their own wallet.
7. Running this on Domain India
- Front end. A built, static DApp front end (HTML, JavaScript and CSS) can be uploaded to any Domain India shared hosting plan like any other website. The blockchain work happens in your users' wallets and your RPC provider, not on our server.
- Back end or indexer in Node.js. The App Platform detects Node.js apps automatically; anything else needs a Dockerfile.
- Your own Ethereum node. Nodes are long-running processes that need far more storage than a website, so they can't run on shared hosting. On a self-managed VPS you have root access; check the plan's disk against your Ethereum client's current storage requirements first.
Our Acceptable Use Policy prohibits cryptocurrency mining without explicit permission and fake exchanges or token scams on every service.
Can I still use Truffle and Ganache?
Consensys discontinued both in 2023, so they no longer get updates. Use Foundry, with Anvil as the local chain, or Hardhat.
Which Solidity version should I use?
A current 0.8.x release. The 0.8 series checks for arithmetic overflow automatically and supports custom errors, which older tutorials written for 0.5 or 0.6 lack.
Why not use transfer() to pay the seller?
transfer() forwards a small fixed amount of gas, so payments to smart-contract wallets can fail. Use call with a value, check the result, and update state before sending.
Can a blockchain prove my goods were really delivered?
No. It proves what was recorded, by whom and when. The record is only as accurate as the scans, devices or people that feed it.
Should I store customer details on the blockchain?
No. Data on a public blockchain is visible to everyone and cannot be deleted. Store personal data off-chain and record only references or hashes.
Can I run an Ethereum node on Domain India shared hosting?
No. A node is a long-running process with large storage needs, and shared hosting stops long-running processes. Use a self-managed VPS, or connect to an RPC provider.
Ready to launch your DApp? Host the front end on cPanel hosting, run a Node.js back end on the App Platform, or choose a VPS for full control. Questions? Open a support ticket.
Node.js apps are detected automatically; anything else deploys from your own Dockerfile.
See App Platform plans