Smart contracts
The Solidity project lives in contracts/ (Hardhat + TypeScript). Two contracts:
AXONEscrow.sol— milestone escrow.MockUSDC.sol— a testnet ERC‑20 standing in for USDC (mintable by the treasury).
AXONEscrow — lifecycle
stateDiagram-v2
[*] --> Draft: createAgreement
Draft --> Active: acceptAgreement (counterparty)
Draft --> Cancelled: cancelAgreement (initiator, unfunded)
Active --> Cancelled: cancelAgreement (initiator, unfunded)
Active --> Funded: fundMilestone (initiator)
Funded --> Funded: markMilestoneComplete / releaseMilestone
Funded --> Disputed: raiseDispute (either party)
Disputed --> Completed: acceptResolution
Funded --> Completed: all milestones released
Key functions
| Function | Caller | Effect |
|---|---|---|
createAgreement(counterparty, offChainId, titles[], amounts[]) |
initiator | Creates a draft agreement; returns its id. |
acceptAgreement(id) |
counterparty | Draft → Active (enables funding). |
fundMilestone(id, index) |
initiator | Pulls USDC into escrow; snapshots feeBpsAtFunding. |
markMilestoneComplete(id, index) |
counterparty | Starts the 5‑day permissionless‑release timeout. |
releaseMilestone(id, index) |
initiator any time, or anyone after 5 days | Pays the counterparty net (fee deducted at the snapshotted rate). |
raiseDispute(id) |
either | Freezes the agreement (no fund/mark/release while disputed). |
proposeResolution(id, recipientBps) |
either | Proposes a split of the funded pot (latest overwrites). |
acceptResolution(id) |
other party | Settles the pot per the split, fee on the snapshotted rate. |
cancelAgreement(id) |
initiator | Scraps an unfunded agreement (Draft/Active only). |
setFee(bps) / setFeeRecipient(addr) |
owner | Update the live fee for future fundings (zero‑addr guard on recipient). |
Events
All events carry offChainId indexed, so they're filterable on BaseScan and joinable to off‑chain rows:
AgreementCreated, AgreementAccepted, MilestoneFunded, MilestoneMarkedComplete, MilestoneReleased(…, viaTimeout, …), DisputeRaised, DisputeResolutionProposed, DisputeResolved, AgreementCancelled.
Why the fee snapshot matters
The owner can change the platform fee, but only for milestones funded after the change. Each milestone records the fee in force at fundMilestone time and uses that rate at release or dispute settlement — so funds already in escrow can never be charged a higher fee retroactively. This is a trust guarantee for counterparties.
Build, test, deploy
cd contracts
pnpm install
npx hardhat compile # build artifacts
npx hardhat test # run the contract test suite
# deploy (Base Sepolia) — uses a fresh key; supports FEE_RECIPIENT / OWNER_AFTER_DEPLOY
npx hardhat run scripts/deploy.ts --network baseSepolia
After deploy, set the new address in the backend env (ESCROW_CONTRACT_ADDRESS / the blockchain config) and rebuild the services that touch escrow (agreements, funding, settlements, reconciliation). The full procedure is in docs/CONTRACT_REDEPLOY_RUNBOOK.md.
Verification: see
docs/BASESCAN_VERIFY_AND_OFFCHAINID_EVENTS.mdfor verifying the contract on BaseScan and reading the indexedoffChainIdevents.