NERONERO 402
Core concepts

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:

  1. Reverts on zero values, unallowed tokens, and reused requestHash.
  2. Marks isSettled[requestHash] = true (replay protection).
  3. Calls IERC20(token).safeTransferFrom(msg.sender, merchant, amount).
  4. Post-checks that the merchant's balance increased by exactly amount.
  5. 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 settled

These 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): proxy 0x5eCfc64f2339992668f555918674B604F97B412D, implementation 0xb7f16185619c476ce3fd3fd9e8b6186e340802f6.

  • Testnet (eip155:689): proxy 0x925dbba44570683ac8da99be08bc5ece8cf5a8c6.

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.

On this page