Contract
The Sorter and the envelopes on Robinhood Chain: storage, functions, events, invariants, caps and gas.
Two contracts and one interface live in contracts/src: Sorter.sol, Envelope.sol and IMarket.sol. The Pons adapter implements IMarket against the live Pons V2 ABI and is deployed alongside the Sorter at launch. Solidity 0.8.26, no inherited frameworks, so the bytecode stays small enough for Robinhood Chain deployments.
Deployment status: Launch soon. The Sorter address appears here and on the Ledger once it is live and verified.
Sorter storage
| Field | Type | Meaning |
|---|---|---|
| owner | address | the treasury; sets parameters, pauses, withdraws vault ETH |
| courier | address | the only caller of deliver |
| treasury | address | receives the fee on every envelope |
| token | address | $WHOZ; zero until launch, then set once and locked with lockToken() |
| market | IMarket | the Pons adapter |
| feeBps | uint16 | fee on each envelope's ETH, at most 200 (2 %); ships at 50 |
| maxPerOrder | uint256 | cap on one deliver; 0.5 ETH at deploy |
| dailyCap | uint256 | cap on deliver per UTC day; 5 ETH at deploy |
| paused | bool | stops deliver and sweep, never withdraw |
| totalOrders, totalEthIn, totalFees, totalTokensOut | uint256 | running totals, shown on the Ledger |
Functions
envelopeOf(address beneficiary) → address: the CREATE2 addresskeccak256(0xff, sorter, bytes32(uint160(beneficiary)), keccak256(type(Envelope).creationCode)). Pure derivation, valid before deployment.sweep(address beneficiary, uint256 minTokensOut) → tokensOut: permissionless. Requires not paused, token set, ETH in the envelope. Deploys the envelope if needed, drains it, takes the fee, buys, transfers. EmitsSwept.withdraw(address beneficiary): requiresmsg.sender == beneficiary. Moves the envelope's whole balance to the beneficiary. Works while paused and before launch. EmitsWithdrawn.deliver(bytes32 orderId, address beneficiary, uint256 amountWei, uint256 minTokensOut): courier only, for the instant path from the vault. Each order id once, capped per order and per UTC day. EmitsDelivered.- Owner:
setToken,lockToken,setCourier,setTreasury,setFee(≤200),setCaps,setMarket,pause,withdrawVault,rescueToken.
The buy
fee = amountWei * feeBps / 10_000 → treasury
tokensOut = market.buy{value: amountWei - fee}(token, minTokensOut)
token.transfer(beneficiary, balance delta of the Sorter)The Sorter forwards its own balance delta, not the adapter's return value, so a taxed token can never strand tokens in the Sorter.
Invariants
- ETH in an envelope goes to two places only: the Sorter during a sweep, or the beneficiary during a withdraw.
- The Sorter holds no user tokens between transactions.
delivernever exceedsdailyCapin a UTC day, whatever the courier key does.- No function pauses
withdraw, andsetMarketdoes not touch it. - The ABI rejects any fee above 2 %.
Events
Swept(address indexed envelope, address indexed beneficiary, uint256 ethIn, uint256 fee, uint256 tokensOut)
Withdrawn(address indexed envelope, address indexed beneficiary, uint256 amount)
Delivered(bytes32 indexed orderId, address indexed beneficiary, uint256 ethIn, uint256 fee, uint256 tokensOut)
Funded(address indexed from, uint256 amount)
TokenSet(address) CourierSet(address) FeeSet(uint16) CapsSet(uint256, uint256) Paused(bool)Envelope
About twenty lines. The constructor records the Sorter. receive() accepts ETH from anyone. drainTo(address) moves the balance and only the Sorter can call it. No owner, no upgrade path, no other entry point.
Deploying
forge script script/Deploy.s.sol:Deploy --rpc-url robinhood --private-key $PK \
--broadcast --slow --legacy --gas-estimate-multiplier 150
forge verify-contract <SORTER> src/Sorter.sol:Sorter --chain-id 4663 \
--verifier blockscout --verifier-url https://robinhoodchain.blockscout.com/apiRobinhood Chain needs --slow --legacy and the gas multiplier, or the CREATE runs out of gas.