Skip to main content
What you’ll be able to do
  • Test an escrow contract locally.
  • Deploy it to Metal Testnet.
  • Verify the deployed code, configuration.
Deploy a contract when an integration needs its own onchain rules. This guide builds an escrow for one payment. A buyer deposits tokens, then an approver can release them to a recipient before a deadline. From the deadline onward, the buyer can reclaim the deposit. The escrow calls an existing MIP20 token to move funds. To deploy a MIP20 token, use Create a MIP20 token.

Before you start

  • Install Foundry and confirm forge --version and cast --version work. The commands use Foundry 1.8.5.
  • Have Node.js 22 or later for the commands that read timestamps and deployment output.
  • Set METAL_RPC_URL to your testnet RPC endpoint.
  • Load the test signer’s key into PRIVATE_KEY through local secret configuration.
  • Set RECIPIENT_ADDRESS to a different test account that will receive the payment.
The commands use one disposable account as contract deployer, buyer, and approver. The contract accepts separate buyer and approver addresses when an integration needs distinct roles.

Contract roles and rules

Each deployment serves one payment. Associate its chain ID and contract address with a single payment ID in your application, enforced by a database uniqueness constraint. The buyer calls approve on the token contract to let the escrow spend the deposit amount, then calls deposit. The escrow collects the tokens with transferFrom. A reentrancy guard prevents the token from calling back into the escrow’s deposit, release, or refund functions during a transfer. Each transfer must succeed and increase the destination’s balance by the full amount. If a token call fails or quarantines the funds, the escrow call reverts all changes, including token balances, allowance, and any quarantine receipt. After a failed deposit, the escrow remains Empty. After a failed release or refund, it remains Funded, so the caller can resolve the failure and retry.

Create the contract project

Use a separate contract directory in the application, or create an empty one:
Configure the compiler in foundry.toml:
This configuration compiles for Metal’s Osaka EVM revision. Forge downloads the selected compiler on the first build. The contract declares its token interface directly, so no library dependencies are needed. Define PaymentEscrow in src/PaymentEscrow.sol. Its constructor fixes the token, participants, and deadline for that payment:

Test the rules locally

The tests in test/PaymentEscrow.t.sol use a token stub to check release, refund, and rollback locally. The Vm interface exposes Foundry’s controls for advancing time and checking that calls revert:
Compile the contract and run the tests:
After these tests pass, extend the suite to cover the remaining cases below before deploying: To test caller permissions, declare the external prank(address) function in Vm, then call vm.prank(otherAddress) before the escrow call. To test rollback, add a boolean failure flag and setter to TestToken. Make its transfer methods return false when the flag is set. Use vm.expectRevert(bytes("token transfer")) to check the rejection, then assert that state and balances are unchanged. The quarantine flag models a transfer that returns true while crediting another address. Extend the release test to cover deposit and refund, checking deposit credit and recipient credit respectively. For a failed deposit, also check that the allowance and stored amount remain unchanged. Run the completed suite with forge test -vv.

Deploy to Metal Testnet

This deployment uses *USDZ for both deposits and transaction fees. It submits standard Ethereum transactions, which use the default gas token and must set native value to zero on Metal. See Fees to select another gas token. Run these commands in the same shell, with the prerequisite variables set:
Check that the signing account’s balance in the gas token, *USDZ, covers deployment fees, and leave enough for deposits and later transactions. The balance is returned in base units. *USDZ uses 18 decimals, so one token equals 10^18 base units. The deadline is one hour from the system’s current time. Use synchronized system time and choose a deadline that leaves enough time to integrate the contract before deposits and releases expire.
The explicit gas limit is sized for this contract and compiler configuration. It avoids gas estimates that can fail when tooling checks the native balance. The signing account’s balance in the gas token, *USDZ, must cover the maximum gas cost.

Verify deployment and finality

Retrieve the deployment transaction’s receipt and the latest finalized block:
Require receipt status equal to 0x1 and its block number (decoded from hex) at or below finalized.number. Copy the receipt’s blockNumber into RECEIPT_BLOCK, then check its canonical block:
The returned block’s hash must equal the receipt’s blockHash. If the finalized head has not reached the receipt’s block, wait and repeat the checks. Settlement and Finality describes the field encodings and completion rules.
Confirm that the receipt’s contractAddress equals ESCROW_ADDRESS and that cast code returns more than 0x. The configuration values must match the constructor inputs, and the initial state must be 0 (Empty). Keep a deployment record with the chain ID, contract address, deployment transaction hash, deployer, constructor inputs, compiler settings, and source version. Include the ABI generated at out/PaymentEscrow.sol/PaymentEscrow.json so the application can call the deployed contract. Follow Integrate a contract with this deployment’s address and ABI to submit calls and record their verified results.

Diagnose failures

Deployment is complete when the local tests pass, the deployment is finalized, the contract address has code, and its initial configuration matches the constructor inputs.