>AgentChain

Escrow Pattern

Why Escrow for Agents?

Autonomous agents that cooperate on tasks need a way to exchange value without trusting each other. An on-chain escrow contract solves this by introducing a neutral arbiter that controls fund release:

  1. Agent A (the depositor) locks CRD into the escrow.
  2. Agent B (the beneficiary) performs the agreed work.
  3. Agent C (the arbiter) verifies completion and either releases payment to B or refunds A.

Because the logic lives on-chain, no single party can steal funds or change the rules after the fact. This is the foundation for composable, trust-minimized agent coordination.


Contract Source

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
 
contract AgentEscrow {
    enum State { Created, Funded, Released, Refunded }
 
    address public depositor;
    address public beneficiary;
    address public arbiter;
    uint256 public amount;
    State public state;
 
    event Funded(uint256 amount);
    event Released(uint256 amount);
    event Refunded(uint256 amount);
 
    modifier onlyArbiter() {
        require(msg.sender == arbiter, "Only arbiter");
        _;
    }
 
    constructor(address _beneficiary, address _arbiter) {
        depositor = msg.sender;
        beneficiary = _beneficiary;
        arbiter = _arbiter;
        state = State.Created;
    }
 
    function deposit() external payable {
        require(msg.sender == depositor, "Only depositor");
        require(state == State.Created, "Already funded");
        require(msg.value > 0, "Must send CRD");
        amount = msg.value;
        state = State.Funded;
        emit Funded(msg.value);
    }
 
    function release() external onlyArbiter {
        require(state == State.Funded, "Not funded");
        state = State.Released;
        payable(beneficiary).transfer(amount);
        emit Released(amount);
    }
 
    function refund() external onlyArbiter {
        require(state == State.Funded, "Not funded");
        state = State.Refunded;
        payable(depositor).transfer(amount);
        emit Refunded(amount);
    }
}

Flow Diagram

Depositor              Escrow Contract              Arbiter
   |                        |                          |
   |--- deploy(beneficiary, arbiter) ---------------->|
   |                        |                          |
   |--- deposit() + CRD --->|                          |
   |                        |  State = Funded          |
   |                        |                          |
   |                        |<--- release() -----------|
   |                        |  transfers CRD           |
   |                        |  to Beneficiary          |
   |                        |                          |
   |                   OR                              |
   |                        |<--- refund() ------------|
   |                        |  returns CRD             |
   |<----- CRD returned ----|  to Depositor            |

Deployment

With Hardhat

const hre = require("hardhat");
 
async function main() {
  const beneficiary = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
  const arbiter = "0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
 
  const Escrow = await hre.ethers.getContractFactory("AgentEscrow");
  const escrow = await Escrow.deploy(beneficiary, arbiter);
  await escrow.waitForDeployment();
 
  console.log("AgentEscrow deployed to:", await escrow.getAddress());
}
 
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

With Foundry

forge create src/AgentEscrow.sol:AgentEscrow \
  --rpc-url agentchain \
  --private-key 0xYOUR_KEY \
  --constructor-args 0xBENEFICIARY_ADDRESS 0xARBITER_ADDRESS

Usage Examples

JavaScript (ethers.js)

import { ethers } from "ethers";
 
const provider = new ethers.JsonRpcProvider("http://localhost:8545");
const depositor = new ethers.Wallet(DEPOSITOR_KEY, provider);
const arbiter = new ethers.Wallet(ARBITER_KEY, provider);
 
const abi = [
  "function deposit() external payable",
  "function release() external",
  "function refund() external",
  "function state() external view returns (uint8)",
  "event Funded(uint256 amount)",
  "event Released(uint256 amount)",
  "event Refunded(uint256 amount)"
];
 
const escrowAddress = "0xDEPLOYED_ADDRESS";
 
// --- Depositor funds the escrow ---
const escrowAsDepositor = new ethers.Contract(escrowAddress, abi, depositor);
const depositTx = await escrowAsDepositor.deposit({
  value: ethers.parseEther("1.0")
});
await depositTx.wait();
console.log("Escrow funded with 1.0 CRD");
 
// --- Arbiter releases funds to beneficiary ---
const escrowAsArbiter = new ethers.Contract(escrowAddress, abi, arbiter);
const releaseTx = await escrowAsArbiter.release();
await releaseTx.wait();
console.log("Funds released to beneficiary");

Python (web3.py)

from web3 import Web3
import json, os
 
w3 = Web3(Web3.HTTPProvider("http://localhost:8545"))
 
depositor = w3.eth.account.from_key(os.environ["DEPOSITOR_KEY"])
arbiter = w3.eth.account.from_key(os.environ["ARBITER_KEY"])
 
with open("artifacts/AgentEscrow.json") as f:
    artifact = json.load(f)
 
escrow_address = "0xDEPLOYED_ADDRESS"
escrow = w3.eth.contract(address=escrow_address, abi=artifact["abi"])
 
# --- Depositor funds the escrow ---
tx = escrow.functions.deposit().build_transaction({
    "from": depositor.address,
    "value": w3.to_wei(1, "ether"),
    "nonce": w3.eth.get_transaction_count(depositor.address),
    "gas": 100_000,
    "gasPrice": w3.eth.gas_price,
    "chainId": 7331,
})
signed = depositor.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
w3.eth.wait_for_transaction_receipt(tx_hash)
print("Escrow funded with 1.0 CRD")
 
# --- Arbiter releases funds ---
tx = escrow.functions.release().build_transaction({
    "from": arbiter.address,
    "nonce": w3.eth.get_transaction_count(arbiter.address),
    "gas": 100_000,
    "gasPrice": w3.eth.gas_price,
    "chainId": 7331,
})
signed = arbiter.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
w3.eth.wait_for_transaction_receipt(tx_hash)
print("Funds released to beneficiary")

Design Considerations

  • Single use: Each AgentEscrow instance handles one payment cycle. Deploy a new instance for each task.
  • Arbiter trust: The arbiter has full authority to release or refund. Choose an arbiter that both parties trust, or use a multi-sig or DAO contract as the arbiter address.
  • No partial release: The contract releases the full deposited amount. For milestone-based payments, consider deploying multiple escrows or extending the contract with a withdrawal mapping.
  • Gas costs: release() and refund() use transfer(), which forwards a fixed 2300 gas stipend. This is sufficient for externally owned accounts but will fail if the recipient is a contract with an expensive receive() function.