What you’ll be able to do
- Test an escrow contract locally.
- Deploy it to Metal Testnet.
- Verify the deployed code, configuration.
Before you start
- Install Foundry and confirm
forge --versionandcast --versionwork. 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_URLto your testnet RPC endpoint. - Load the test signer’s key into
PRIVATE_KEYthrough local secret configuration. - Set
RECIPIENT_ADDRESSto a different test account that will receive the payment.
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:foundry.toml:
PaymentEscrow in src/PaymentEscrow.sol. Its constructor fixes the
token, participants, and deadline for that payment:
Test the rules locally
The tests intest/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:
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:
*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.
*USDZ, must cover the maximum
gas cost.
Verify deployment and finality
Retrieve the deployment transaction’s receipt and the latest finalized block: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:
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.
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.