RFC-010: Nexus Escrow Smart Contract Specification
| Metadata | Value |
|---|---|
| Title | Nexus Escrow Smart Contract Specification |
| Version | 3.0.0 |
| Status | Standards Track (Draft) |
| Authors | Cipher & Nexus Architect Team |
| Created | 2026-02-24 |
| Updated | 2026-04-01 |
| Dependencies | RFC-005v4 (Payment Core), RFC-009 (Webhook Standard), RFC-001 (DID) |
| Target Chain | PlatON Devnet (chain_id: 20250407) |
| Payment Tokens | ERC-3009 compatible tokens (XSGD, USDC) |
| Contract Standards | ERC-20, EIP-3009, EIP-712, OpenZeppelin v5, UUPS Upgradeable |
| Gas Model | Relayer-sponsored (zero Gas for users) |
1. Abstract
This RFC defines the complete specification for Nexus Escrow smart contract v4.3.0. The contract acts as a payment escrow, implementing a gasless escrow transaction flow: "User signs -> Relayer sponsors deposit -> Merchant fulfills -> Funds released."
v3.0.0 key changes (relative to RFC v2.0.0):
- UUPS Proxy: Adopts an upgradeable proxy pattern, enabling on-chain contract logic upgrades
- Gasless Batch Deposit:
batchDepositGasless()-- Relayer submits batch deposits on behalf of users, zero Gas for users - Group Signature (Anti-MITM): EIP-712 signature verification ensuring batch transaction parameter integrity
- Cancel Mechanism:
cancelByOperator()-- Operator/merchant instant refund without waiting for timeout - Auto-Refund Unresolved Dispute:
refundUnresolvedDispute()-- Automatic refund after arbitration timeout - Multi-Token:
setToken()-- Supports switching between ERC-3009 compatible tokens - Fee Snapshot: Snapshots the fee rate at deposit time, preventing subsequent fee changes from affecting existing escrows
- MAX_BATCH_SIZE: Maximum 20 entries per batch, preventing Gas griefing
2. Motivation
2.1 Escrow vs Direct Transfer
Direct Transfer (original model) Escrow Contract (this RFC)
--------------------- ----------------------
User --transfer--> Merchant User --sign--> Relayer --deposit--> Contract --release--> Merchant
|
timeout? --refund--> User
dispute? --arbitrate--> ruling
cancel? --cancelByOperator--> instant refund2.2 Multi-Dimensional Comparison
| Dimension | Direct Transfer | Escrow Contract | Assessment |
|---|---|---|---|
| Security | Irreversible, no recourse | Timeout refund + arbitration + instant cancel | Escrow wins |
| On-chain Data | Transfer(from, to, value) -- 3 fields | Custom events with paymentId, orderRef, merchantDid | Escrow wins |
| Gas Cost | ~65K (user pays) | ~220K (Relayer pays, zero Gas for user) | Escrow wins (UX) |
| Dispute Handling | Entirely off-chain | On-chain arbitration + auto-refund | Escrow wins |
| Cross-chain Compatibility | No calldata | Contract address + calldata for bridge integration | Escrow wins |
3. Contract Architecture
3.1 UUPS Proxy Pattern
┌─────────────────┐ ┌──────────────────────────────┐
│ ERC1967 Proxy │ ──► │ NexusEscrow Impl v4.3.0 │
│ (stable address)│ │ (upgradeable logic) │
│ 0xeB33a9C2... │ │ 0xF6ED311f... │
└─────────────────┘ └──────────────────────────────┘- The proxy address is permanent; all interactions go through the proxy
- Upgrades are performed via
_authorizeUpgrade()(owner only) - Storage layout resides in the proxy; logic resides in the implementation
3.2 Contract Inheritance
contract NexusEscrow is
Initializable,
UUPSUpgradeable,
OwnableUpgradeable,
ReentrancyGuardUpgradeable4. State Machine
4.1 Status Enum
enum EscrowStatus {
NONE, // 0: Does not exist
DEPOSITED, // 1: Funds locked, awaiting merchant fulfillment
RELEASED, // 2: Merchant has withdrawn (fee deducted)
REFUNDED, // 3: Refunded (timeout / cancel / arbitration timeout)
DISPUTED, // 4: Under dispute, awaiting arbitration
RESOLVED_TO_MERCHANT, // 5: Arbitration ruled in favor of merchant
RESOLVED_TO_PAYER, // 6: Arbitration ruled in favor of payer
RESOLVED_SPLIT // 7: Arbitration split proportionally (new in v4.0.0)
}4.2 State Transition Diagram
┌──────────────────┐
│ DEPOSITED │ <-- deposit / batchDepositGasless
└────────┬─────────┘
│
┌──────────────┼──────────────┬───────────────┐
│ │ │ │
┌────────▼───────┐ ┌──▼──────────┐ ┌▼──────────┐ ┌▼───────────────┐
│ RELEASED │ │ REFUNDED │ │ REFUNDED │ │ DISPUTED │
│ (release) │ │ (timeout │ │(cancelBy │ │ (under │
│ │ │ refund) │ │ Operator) │ │ dispute) │
└────────────────┘ └─────────────┘ └────────────┘ └───────┬────────┘
│
┌──────────────┼──────────────────┐
│ │ │
┌──────▼──────┐ ┌────▼────────┐ ┌──────▼─────────┐
│ RESOLVED_ │ │ RESOLVED_ │ │ RESOLVED_ │
│ TO_MERCHANT │ │ SPLIT │ │ TO_PAYER │
│ (>=50% to │ │ (proportion-│ │ (<50% to │
│ merchant) │ │ al split) │ │ merchant) │
└─────────────┘ └─────────────┘ └────────────────┘
▲
│
refundUnresolvedDispute
(auto-refund on
arbitration timeout)4.3 Transition Rules
| Current Status | Target Status | Trigger Condition | Caller |
|---|---|---|---|
| (none) | DEPOSITED | deposit / batchDepositGasless | Relayer / User |
| DEPOSITED | RELEASED | release() | Core operator / merchant |
| DEPOSITED | REFUNDED | refund() (after timeout) | Anyone (public) |
| DEPOSITED | REFUNDED | cancelByOperator() (instant) | Core operator / merchant |
| DEPOSITED | DISPUTED | dispute() (within dispute window) | Payer |
| DISPUTED | RESOLVED_TO_MERCHANT | resolve(bps >= 5000) | Arbiter |
| DISPUTED | RESOLVED_TO_PAYER | resolve(bps < 5000) | Arbiter |
| DISPUTED | RESOLVED_SPLIT | resolve(0 < bps < 10000) | Arbiter |
| DISPUTED | RESOLVED_TO_PAYER | refundUnresolvedDispute() (arbitration timeout) | Anyone |
Terminal states: RELEASED, REFUNDED, RESOLVED_TO_MERCHANT, RESOLVED_TO_PAYER, RESOLVED_SPLIT
5. Data Structures
5.1 Escrow Record
struct Escrow {
address payer; // Payer address
address merchant; // Merchant recipient address
uint256 amount; // Token amount
bytes32 orderRef; // Merchant order reference hash
bytes32 merchantDid; // Merchant DID hash
bytes32 contextHash; // Order context hash
uint256 releaseDeadline; // Fulfillment deadline (ms, PlatON)
uint256 disputeDeadline; // Dispute window deadline (ms)
EscrowStatus status; // Current status
uint16 feeBps; // Fee rate snapshot at deposit time
}5.2 Batch Entry
struct BatchEntry {
bytes32 paymentId;
address merchant;
uint256 amount;
bytes32 orderRef;
bytes32 merchantDid;
bytes32 contextHash;
}5.3 Constants
uint16 constant MAX_FEE_BPS = 500; // 5% hard cap
uint256 constant MAX_BATCH_SIZE = 20; // Prevents Gas griefing6. Core Functions
6.1 Deposit Functions
depositWithAuthorization -- Single EIP-3009 Deposit
function depositWithAuthorization(
bytes32 _paymentId,
address _from,
address _merchant,
uint256 _amount,
bytes32 _orderRef,
bytes32 _merchantDid,
bytes32 _contextHash,
uint256 _validAfter,
uint256 _validBefore,
bytes32 _nonce,
uint8 _v, bytes32 _r, bytes32 _s
) external nonReentrantCalled by the Relayer, using the user's EIP-3009 signature to transfer tokens from the user's wallet into the contract.
batchDepositGasless -- Relayer-Sponsored Batch Deposit (v4.3.0 primary entry point)
function batchDepositGasless(
BatchEntry[] calldata entries,
uint256 totalAmount,
bytes32 groupId,
uint8 groupV, bytes32 groupR, bytes32 groupS,
address payer,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v, bytes32 r, bytes32 s
) external nonReentrantThis is the primary entry point for v4.3.0. Flow:
- Verify
entries.length <= MAX_BATCH_SIZE - Verify Group Signature (EIP-712): recover signer from
groupV/R/S, verify it is a coreOperator - Call
token.transferWithAuthorization(payer, address(this), totalAmount, ...)-- transfer the total amount in a single call - Create an independent Escrow record for each entry (with feeBps snapshot)
- Emit
Depositedevent for each entry +BatchDepositedevent
batchDepositWithGroupApproval -- User-Submitted Batch Deposit
function batchDepositWithGroupApproval(
BatchEntry[] calldata entries,
uint256 totalAmount,
bytes32 groupId,
uint8 groupV, bytes32 groupR, bytes32 groupS,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v, bytes32 r, bytes32 s
) external nonReentrantRetained for backward compatibility. Difference from batchDepositGasless: payer = msg.sender (user submits directly, pays their own Gas).
6.2 Release Functions
release -- Merchant/Operator Releases Funds
function release(bytes32 _paymentId) external nonReentrant- Caller: merchant or coreOperator
- Precondition: status == DEPOSITED
- Logic: deducts feeBps (snapshot value) as protocol fee, transfers remainder to merchant
- Emit:
PaymentReleased(paymentId, merchant, merchantAmount, fee)
6.3 Refund Functions
refund -- Timeout Refund
function refund(bytes32 _paymentId) external nonReentrant- Caller: anyone (public)
- Precondition: status == DEPOSITED && block.timestamp > releaseDeadline
- Emit:
PaymentRefunded(paymentId, payer, amount)
cancelByOperator -- Instant Cancel Refund (v4.1.0)
function cancelByOperator(bytes32 _paymentId) external nonReentrant- Caller: coreOperator or merchant
- Precondition: status == DEPOSITED (no timeout wait required)
- Emit:
PaymentRefunded(paymentId, payer, amount)
Difference: refund() requires waiting for releaseDeadline to expire; cancelByOperator() refunds immediately.
refundUnresolvedDispute -- Arbitration Timeout Refund (new in v4.0.0)
function refundUnresolvedDispute(bytes32 _paymentId) external nonReentrant- Caller: anyone (public)
- Precondition: status == DISPUTED && block.timestamp > disputeDeadline + arbitrationTimeout
- Logic: full refund to payer
- Emit:
DisputeAutoResolved(paymentId, payer, amount)
6.4 Dispute Functions
dispute -- Initiate Dispute
function dispute(bytes32 _paymentId, string calldata _reason) external nonReentrant- Caller: payer
- Precondition: status == DEPOSITED && block.timestamp <= disputeDeadline
- Emit:
PaymentDisputed(paymentId, payer, reason)
resolve -- Arbitration Ruling
function resolve(bytes32 _paymentId, uint16 _merchantBps) external nonReentrant- Caller: arbiter
- Precondition: status == DISPUTED
- Parameter:
_merchantBps(0-10000), the proportion awarded to the merchant- 10000 = all to merchant -> RESOLVED_TO_MERCHANT
- 0 = all to payer -> RESOLVED_TO_PAYER
- Other = proportional split -> RESOLVED_SPLIT (if 0 < bps < 10000)
- Emit:
DisputeResolved(paymentId, arbiter, toMerchant, merchantAmount, payerAmount)
6.5 Query Functions
function getEscrow(bytes32 _paymentId) external view returns (Escrow memory)
function isRefundable(bytes32 _paymentId) external view returns (bool)
function isDisputable(bytes32 _paymentId) external view returns (bool)
function isGroupIdUsed(bytes32 _groupId) external view returns (bool)
function computeDomainSeparator() external view returns (bytes32)6.6 Admin Functions
| Function | Purpose | Caller |
|---|---|---|
setArbiter(address, bool) | Add/remove arbiter | owner |
setCoreOperator(address, bool) | Add/remove operator address | owner |
setDefaultReleaseTimeout(uint256) | Set default fulfillment timeout | owner |
setDefaultDisputeWindow(uint256) | Set default dispute window | owner |
setArbitrationTimeout(uint256) | Set arbitration timeout | owner |
setProtocolFeeBps(uint16) | Set protocol fee rate (<=500) | owner |
setProtocolFeeRecipient(address) | Set protocol fee recipient address | owner |
setRequireGroupSig(bool) | Enforce Group Sig function usage | owner |
setToken(address) | Switch ERC-3009 token | owner |
7. Group Signature (Anti-MITM)
7.1 EIP-712 Domain
bytes32 constant NEXUS_GROUP_APPROVAL_TYPEHASH = keccak256(
"NexusGroupApproval(bytes32 groupId,bytes32 entriesHash,uint256 totalAmount)"
);
// Domain: Nexus, version 1, chainId, verifyingContract = escrow proxy7.2 Signing Flow
- Core builds the BatchDepositInstruction and computes entriesHash:
entriesHash = keccak256( abi.encode(entry0.paymentId, entry0.merchant, entry0.amount, ...) || abi.encode(entry1.paymentId, entry1.merchant, entry1.amount, ...) || ... ) - Core Operator signs
NexusGroupApproval(groupId, entriesHash, totalAmount) - The signature is appended to the BatchDepositInstruction and returned to the frontend
- On-chain
batchDepositGasless()verifies:ecrecover(digest, v, r, s) == coreOperator
7.3 Protection Goals
- Prevent man-in-the-middle tampering with batch entries (amounts, merchant addresses)
- Prevent replay attacks (usedGroupIds mapping)
- Ensure totalAmount == sum(entries.amount)
8. EIP-3009 Integration
8.1 Interface
interface IERC3009 is IERC20 {
function transferWithAuthorization(
address from, address to, uint256 value,
uint256 validAfter, uint256 validBefore, bytes32 nonce,
uint8 v, bytes32 r, bytes32 s
) external;
function authorizationState(address authorizer, bytes32 nonce) external view returns (bool);
}8.2 Signature Parameters
EIP-712 TypedData signed by the user:
{
domain: {
name: "XSGD", // Token contract EIP-712 name
version: "2", // Token contract EIP-712 version
chainId: 20250407,
verifyingContract: tokenAddress
},
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" }
]
},
message: {
from: payerAddress,
to: escrowProxyAddress,
value: totalAmountUint256,
validAfter: "0",
validBefore: Date.now() + 86400000, // +24h (milliseconds, PlatON)
nonce: randomBytes32
}
}PlatON Devnet Note:
block.timestampreturns milliseconds.validBeforemust be set in milliseconds.
9. Event Definitions
// Deposit events
event Deposited(bytes32 indexed paymentId, address indexed payer, address indexed merchant,
uint256 amount, bytes32 orderRef, bytes32 merchantDid, bytes32 contextHash,
uint256 releaseDeadline, uint256 disputeDeadline);
event BatchDeposited(bytes32 indexed groupId, address indexed payer,
uint256 totalAmount, uint256 count);
// Release events
event Released(bytes32 indexed paymentId, address indexed merchant,
uint256 merchantAmount, uint256 protocolFee);
// Refund events
event Refunded(bytes32 indexed paymentId, address indexed payer, uint256 amount);
// Dispute events
event Disputed(bytes32 indexed paymentId, address indexed payer, string reason);
event Resolved(bytes32 indexed paymentId, address indexed arbiter,
uint16 merchantBps, uint256 merchantAmount, uint256 payerAmount);
event DisputeAutoResolved(bytes32 indexed paymentId, address indexed payer, uint256 amount);
// Group Sig events
event GroupSigVerified(bytes32 indexed groupId, address indexed signer);
// Admin events
event ArbiterUpdated(address indexed arbiter, bool active);
event CoreOperatorUpdated(address indexed operator, bool active);
event ReleaseTimeoutUpdated(uint256 newTimeout);
event DisputeWindowUpdated(uint256 newWindow);
event ArbitrationTimeoutUpdated(uint256 newTimeout);
event ProtocolFeeUpdated(uint16 newBps);
event FeeRecipientUpdated(address indexed newRecipient);
event RequireGroupSigUpdated(bool required);
event TokenUpdated(address indexed newToken);10. Deployment Information
10.1 Current Deployment (PlatON Devnet, chain_id: 20250407)
| Contract | Address | Type |
|---|---|---|
| NexusEscrow (Proxy) | 0xeB33a9C2b4c7D3F44Fd5514F90C355AF6bb79236 | UUPS Proxy |
| NexusEscrow (Impl v4.3.0) | 0xF6ED311f8ea594572872E78DB945277ab37ECE37 | Implementation |
| XSGD Token | 0x0Fd437613dE3d14F4dDaB8331DC0f2C0C543BdD0 | ERC-3009 |
| USDC Token (legacy) | 0xFF8dEe9983768D0399673014cf77826896F97e4d | ERC-3009 |
| Relayer / Core Operator | 0xf7EA5d3f0Bf8185c4f3C2F405D9a71009CF4D920 | EOA |
10.2 Current Configuration
| Parameter | Value | Description |
|---|---|---|
| defaultReleaseTimeout | 86400000 (24h, ms) | Merchant fulfillment timeout |
| defaultDisputeWindow | 259200000 (72h, ms) | User dispute window |
| arbitrationTimeout | 604800000 (7d, ms) | Arbiter ruling timeout |
| protocolFeeBps | 30 (0.3%) | Protocol fee |
| requireGroupSig | false | Group Sig enforcement toggle |
| Active token | XSGD | Currently active token |
10.3 Version History
| Version | Impl Address | Date | Key Changes |
|---|---|---|---|
| v2.0.0 | (non-upgradeable, deprecated) | 2026-02-24 | Initial escrow (single deposit) |
| v4.0.0 | 0x2EF4dB5E0021d074286c36821Cc897d2605e542E | 2026-02-26 | UUPS, batchDepositWithGroupApproval, RESOLVED_SPLIT, refundUnresolvedDispute, MAX_BATCH_SIZE, feeBps snapshot |
| v4.1.0 | 0x7aC2e9C7D655B352f142b679B22f4B86b23A36eC | 2026-03-12 | cancelByOperator (instant refund) |
| v4.3.0 | 0xF6ED311f8ea594572872E78DB945277ab37ECE37 | 2026-04-01 | batchDepositGasless (Relayer-sponsored), setToken (multi-token) |
11. Gas Cost Estimates
| Operation | Gas Consumption | User Pays | Relayer Pays |
|---|---|---|---|
| EIP-3009 signing | 0 | 0 | 0 |
| batchDepositGasless (1 entry) | ~180,000 | 0 | 180,000 |
| batchDepositGasless (5 entries) | ~450,000 | 0 | 450,000 |
| release | ~80,000 | 0 | 80,000 |
| refund | ~75,000 | 0 | 75,000 |
| cancelByOperator | ~75,000 | 0 | 75,000 |
| dispute | ~60,000 | 60,000 | 0 |
| resolve | ~90,000 | 0 | 90,000 |
Under the escrow model, users only need Gas when actively initiating a dispute. All other operations are covered by the Relayer.
12. Security Design
12.1 Access Control
| Function | Permission |
|---|---|
| deposit / batchDepositGasless | Anyone (typically Relayer) |
| release | coreOperator or merchant |
| refund | Anyone (after timeout) |
| cancelByOperator | coreOperator or merchant |
| dispute | payer |
| resolve | arbiter |
| refundUnresolvedDispute | Anyone (after arbitration timeout) |
| All set* admin functions | owner |
| _authorizeUpgrade | owner |
12.2 Security Mechanisms
- ReentrancyGuard: All state-changing functions are protected with
nonReentrant - Input Validation: Zero address, zero amount, and self-payment checks
- Fee Cap:
MAX_FEE_BPS = 500(5% hard cap) - Batch Size Limit:
MAX_BATCH_SIZE = 20(prevents Gas griefing) - Fee Snapshot: feeBps is recorded at deposit time; subsequent fee changes do not affect existing escrows
- Group ID Replay Protection:
usedGroupIdsmapping prevents replay attacks - Group Sig Verification: On-chain verification of Core Operator signature
12.3 PlatON Devnet Special Notes
block.timestampreturns milliseconds (non-standard EVM behavior)- All timeout parameters (releaseTimeout, disputeWindow, arbitrationTimeout) are configured in milliseconds
- EIP-3009
validBeforemust also use millisecond timestamps
13. Upgrade Path
| Current (v4.3.0) | Future |
|---|---|
| Single chain (PlatON Devnet) | Multi-chain deployment (Base, Ethereum) |
| Single active token | Multi-token concurrent (per-payment token selection) |
| Operator arbitration | DAO governance arbitration |
| Fixed fee rate | Dynamic fee rates (by volume, merchant tier) |
| UUPS owner upgrade | Timelock + multisig upgrade governance |
14. Copyright
Copyright (c) 2026 Nexus Protocol. All Rights Reserved.