Smart Contracts

TransviaEscrow — the settlement brain on Arc Testnet. Deposits, atomic settlement, EIP-712 claims, refunds.

Transvia settles on one contract: TransviaEscrow — open source, MIT, dependency-free Solidity (^0.8.24, no OpenZeppelin). It locks USDC deposits per transfer, settles the FX + payout + fee legs atomically, and lets recipients authorize payouts with EIP-712 signatures.

Network facts

FactValue
NetworkArc Testnet (Circle's EVM L1)
Chain ID5042002
Gas tokenUSDC
USDC (ERC-20, 6 dp)0x3600000000000000000000000000000000000000
Explorertestnet.arcscan.app
Gas floor20 Gwei maxFeePerGas (Arc mempool minimum)

The deployed escrow contract address is recorded in the repository (contracts/README.md) together with the full deployment record — that file is the authoritative source and addresses are never hardcoded in docs.

The contract surface

FunctionCallerWhat it does
deposit(intentId, amountUsdc, recipient, timeoutAt, rate)senderPulls USDC and locks it against the intent
settle(intentId, oracleRate)agent onlyFX check → net payout → fee split, one atomic tx
claim(intentId, signature, deadline)anyone (recipient-signed)EIP-712 close-out; relays allowed
refund(intentId)sender or agentFull refund once the timeout has passed
agentFallback(intentId, reason)agent onlyCompliance/timeout refund, even pre-timeout
pause() / unpause()ownerCircuit breaker over all money movement

One transfer, as a UML sequence

   Sender         Transvia API       TransviaEscrow (Arc)     Settle engine     Recipient
      │               │                     │                    │                │
      │ POST /api/send (quoteId, recipient) │                    │                │
      │──────────────▶│                     │                    │                │
      │               │deposit(intentId, amountUsdc,             │                │
      │               │recipient, timeoutAt, rate)               │                │
      │               │────────────────────▶│                    │                │
      │               │     Deposited       │                    │                │
      │               │◀────────────────────│                    │                │
      │               │                     │  settle(intentId,  │                │
      │               │                     │  oracleRate)       │                │
      │               │◀────────────────────│────────────────────│                │
      │               │   note: one atomic tx — FX leg (±2% band)                │
      │               │   payout leg (net USDC) · fees 35/5/10 bps               │
      │               │◀────────────────────│───────────────────────────────────▶│
      │               │  FxLegSettled · PayoutReleased · FeeSplit                │
      │               │                     │  claim(intentId, EIP-712 sig)      │
      │               │                     │◀───────────────────────────────────│
      │               │     Claimed         │  (anyone may relay —               │
      │               │◀────────────────────│   the signature authorizes)        │

Read it left to right: the deposit is a user action, settle() is the automation engine acting as the contract's only agent, and the final claim() belongs to the recipient — gasless, relayer-agnostic, authorized by an EIP-712 signature over the Claim(intentId, recipient, deadline) struct, not by whoever broadcasts it.

Settlement, honestly

The destination-currency conversion (USDC → EUR, MXN, …) is computed at quote time offchain and locked to the transfer. What the escrow settles onchain is net USDC: the recipient is paid from escrow, and the settlement transaction emits the conversion data — FxLegSettled carries the rate and destination amount for the offchain FX layer. The onchain truth is simple and verifiable: who was paid, how much net USDC, and at what rate.

The three settlement legs

settle() executes in one transaction and reverts as a whole if any leg fails:

  1. FX leg — the oracle rate must be within ±2% of the rate locked at deposit, else the whole settlement reverts (RateOutOfBounds).
  2. Payout leg — net USDC (gross minus fees) is transferred to the recipient, fixed at deposit.
  3. Fee split — 0.35% FX/liquidity, 0.10% protocol, and 0.05% automation go to three immutable treasuries in the same tx. Sub-unit dust stays with the recipient.

Events are the indexing surface

Index TransviaEscrow's own events — not raw USDC Transfer logs. On Arc, native-USDC transfers surface through the system emitter rather than the token contract:

Deposited · FxLegSettled · PayoutReleased · FeeSplit · Claimed · Refunded · AgentFallback

EIP-712 claims

The recipient signs a Claim(string intentId, address recipient, uint256 deadline) authorization over the contract's domain (TransviaEscrow, version 1). Anyone may relay the call — authorization comes from the signature, not the caller — so the recipient pays no gas. If the automation agent never settled, claim pays out the full locked amount with zero fees: the agentless fast path. No service provided, no fee taken.

Testing and deployment

Thirteen Foundry tests cover deposit locking, the Arc zero-address guard, the 3-leg settle, the ±2% rate band, real EIP-712 claim signatures, refund gating, and the pause breaker:

cd contracts && forge build && forge test -vvv

Deploys run against Arc Testnet; if a transaction is rejected as underpriced, append --with-gas-price 20gwei (the 20 Gwei floor). The complete record — contract address, deploy transaction, deployer — lives in contracts/README.md. See Architecture for how the app talks to it.