Settlement contract
The on-chain endpoint where aa-native payments land. Replay-protected, allowlist-gated, UUPS-upgradeable.
The SettlementContract is the on-chain piece of NERO 402. Every aa-native payment ends in a call to its settle(...) function. The contract is a single shared deployment used by every merchant; merchants are arguments to the call, not separate contracts.
Function signature
function settle(
address merchant,
address token,
uint256 amount,
bytes32 requestHash
) external payable nonReentrant whenNotPaused;The function:
- Reverts on zero values, unallowed tokens, and reused
requestHash. - Marks
isSettled[requestHash] = true(replay protection). - Calls
IERC20(token).safeTransferFrom(msg.sender, merchant, amount). - Post-checks that the merchant's balance increased by exactly
amount. - Emits
ReceiptLog(merchant, msg.sender, token, amount, requestHash).
msg.sender is the paying SCW. The SCW must have approved the settlement contract for at least amount of token before the call. The reference SDK encodes the approval and the settlement in one executeBatch([approve, settle]) UserOp, so they are atomic.
State
mapping(bytes32 requestHash => bool settled) public isSettled;
mapping(address token => bool allowed) public isTokenAllowed;
uint256[48] private __gap;isSettled is the authoritative replay registry — once a requestHash is true, the contract refuses to settle it again. isTokenAllowed is an owner-managed allowlist of the tokens the contract will accept transfers for. The __gap reserves storage for future upgrades.
Replay protection
The requestHash field is a keccak256 of (merchantAddress, chainId, httpMethod, canonicalEndpoint, timestampBucket, clientNonce). The merchant address is part of the hash, so a payload signed for merchant A produces a different hash from one a malicious party would need to settle against merchant B. Because isSettled is global, the same hash never settles twice regardless of which merchant the call names.
Upgrade pattern
The contract uses the OpenZeppelin v5 UUPS proxy pattern. An ERC1967Proxy is deployed in front of an upgradeable implementation. The implementation calls _disableInitializers() in its constructor so it can never be driven directly. _authorizeUpgrade is owner-only and rejects the zero address.
The upgrade authorization path adds non-trivial audit surface; the contract is currently pre-audit and will undergo external review before the v1.0 release.
Custom errors
error UnexpectedNativeValue(); // settle() called with msg.value != 0
error ZeroAddress(); // merchant or token is 0x0
error ZeroAmount(); // amount is 0
error ZeroRequestHash(); // requestHash is bytes32(0)
error TokenNotAllowed(address); // token is not on the allowlist
error AlreadySettled(bytes32); // requestHash has already been settledThese are the only revert paths. A reverted UserOp surfaces in the facilitator's /settle response as errorCode: "user_op_failed" with the surfaced reason.
Deployment addresses
-
Mainnet (
eip155:1689): proxy0x5eCfc64f2339992668f555918674B604F97B412D, implementation0xb7f16185619c476ce3fd3fd9e8b6186e340802f6. -
Testnet (
eip155:689): proxy0x925dbba44570683ac8da99be08bc5ece8cf5a8c6.
The proxy address is the only address agents and merchants need to know. Implementations may be upgraded behind it.
Verifying settlements externally
A merchant who needs strong assurance can independently verify the on-chain state of any settlement by reading the ReceiptLog event:
event ReceiptLog(
address indexed merchant,
address indexed payer,
address indexed token,
uint256 amount,
bytes32 requestHash
);The reference Playground's live ledger panel does exactly this — it polls getLogs on the settlement contract independently of the facilitator's reports.
The contract source is contracts/src/SettlementContract.sol. The complete ABI is on the Contract reference page.