HOOZ

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

FieldTypeMeaning
owneraddressthe treasury; sets parameters, pauses, withdraws vault ETH
courieraddressthe only caller of deliver
treasuryaddressreceives the fee on every envelope
tokenaddress$WHOZ; zero until launch, then set once and locked with lockToken()
marketIMarketthe Pons adapter
feeBpsuint16fee on each envelope's ETH, at most 200 (2 %); ships at 50
maxPerOrderuint256cap on one deliver; 0.5 ETH at deploy
dailyCapuint256cap on deliver per UTC day; 5 ETH at deploy
pausedboolstops deliver and sweep, never withdraw
totalOrders, totalEthIn, totalFees, totalTokensOutuint256running totals, shown on the Ledger

Functions

  • envelopeOf(address beneficiary) → address: the CREATE2 address keccak256(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. Emits Swept.
  • withdraw(address beneficiary): requires msg.sender == beneficiary. Moves the envelope's whole balance to the beneficiary. Works while paused and before launch. Emits Withdrawn.
  • 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. Emits Delivered.
  • 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.
  • deliver never exceeds dailyCap in a UTC day, whatever the courier key does.
  • No function pauses withdraw, and setMarket does 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/api

Robinhood Chain needs --slow --legacy and the gas multiplier, or the CREATE runs out of gas.