ZecpassGet a pass

Contract

Two contracts and one adapter, all in contracts/src, Solidity 0.8.26, no OpenZeppelin (Robinhood Chain deployments have to stay small). Every function is listed here with what it requires and what it emits.

Zecpass (the hub)

Storage

Field Type Meaning
owner address The treasury. Can change every parameter, withdraw vault ETH, pause.
relayer address The only caller of deliver.
treasury address Receives the fee on every pass.
token address $ZECPASS. Zero until the launch, then set by the owner or the relayer and locked with lockToken().
market IMarket The PonsMarket adapter.
feeBps uint16 Fee on the ETH of every pass, at most 200 (2 percent). Default 50.
maxPerOrder uint256 Cap on one deliver, in wei. Default 0.5 ETH.
dailyCap uint256 Cap on the sum of deliver per UTC day. Default 5 ETH.
paused bool Stops deliver and sweep. Never stops withdraw.
delivered[orderId] bool Each order id delivers once.
spentOnDay[day] uint256 Wei delivered on block.timestamp / 1 days.
totalOrders, totalEthIn, totalFees, totalTokensOut uint256 Running totals, shown on the stats page.

Functions

receive() — accepts ETH into the vault from anyone, emits Funded(from, amount). The market adapter also refunds through it during a curve buy; those refunds stay in the vault.

passOf(address beneficiary) → address — the CREATE2 address of the beneficiary's pass: keccak256(0xff, hub, bytes32(uint160(beneficiary)), keccak256(type(Pass).creationCode)). Pure derivation, no state, valid before deployment.

deliver(bytes32 orderId, address beneficiary, uint256 amountWei, uint256 minTokensOut) → tokensOut — rail one. Requires: caller is relayer; not paused; delivered[orderId] false; amountWei <= maxPerOrder; spentOnDay[today] + amountWei <= dailyCap; vault balance at least amountWei; token set. Effects: marks the order delivered, adds to the day's spend, runs the buy, emits Delivered(orderId, beneficiary, 1, ethIn, fee, tokensOut).

sweep(address beneficiary, uint256 minTokensOut) → tokensOut — rail two, permissionless. Requires: not paused; token set; the pass holds ETH. Effects: deploys the Pass at passOf(beneficiary) if it has no code yet, calls drain() on it to pull the ETH into the hub, runs the buy with exactly that amount, emits Swept(pass, beneficiary, ethIn, fee, tokensOut).

withdraw(address beneficiary) — the escape hatch. Requires: msg.sender == beneficiary. Works while paused and while the token is unset. Effects: deploys the pass if needed, moves its whole ETH balance to the beneficiary, emits Withdrawn(pass, beneficiary, amount).

Owner: setToken(address) (owner or relayer, until lockToken(); the relayer is allowed so that launch day needs no treasury signature), lockToken() (owner only, needs a token set), setOwner, setRelayer, setTreasury, setFee(uint16 ≤ 200), setCaps(maxPerOrder, dailyCap), setMarket, pause(bool), withdrawVault(amount, to), rescueToken(erc20, to).

The buy, internally

fee       = amountWei * feeBps / 10_000        → treasury, by call, revert if it fails
tokensOut = market.buy{value: amountWei - fee}(token, minTokensOut)   → tokens arrive at the hub
token.safeTransfer(beneficiary, tokensOut)
totals += …

The tokens forwarded are the hub's balance delta, not the market's return value, so a token with a transfer tax could never strand anything in the hub. receive() uses a transient flag so that ETH pulled from a pass, or refunded by the curve mid-buy, is not announced as a Funded deposit.

One Pons detail the relayer respects: buys in the same block as the launch are taxed 99 percent by the curve's anti-snipe rule, and the rate falls to the normal level a few seconds later. A sweep is never that fast in practice, but a manual sweep in the launch block would be.

minTokensOut is a slippage floor. The relayer passes 0: on a bonding curve the price at the moment of the sweep is the price, and a failed sweep would leave ETH sitting in a pass for nothing. A user calling sweep themselves can set it.

Invariants

  • ETH in a pass can only go two places: the hub during a sweep (and then to the treasury and the market in the same transaction), or the beneficiary during withdraw. The Pass contract has no other function.
  • The hub never holds user tokens across transactions: _buy transfers everything the market returned.
  • deliver can never spend more than dailyCap per UTC day, whatever the relayer key does.
  • withdraw cannot be blocked by the owner. There is no function that pauses it, and setMarket does not affect it.
  • Fee is bounded by 2 percent at the ABI level.

Events

Funded(address indexed from, uint256 amount)
Delivered(bytes32 indexed orderId, address indexed beneficiary, uint8 rail, uint256 ethIn, uint256 fee, uint256 tokensOut)
Swept(address indexed pass, address indexed beneficiary, uint256 ethIn, uint256 fee, uint256 tokensOut)
Withdrawn(address indexed pass, address indexed beneficiary, uint256 amount)
TokenSet(address) RelayerSet(address) FeeSet(uint16) CapsSet(uint256, uint256) Paused(bool)

Pass

Twenty lines. hub is set to the deployer in the constructor, receive() accepts ETH, drain() and drainTo(address) move the balance and can only be called by the hub. It has no owner, no upgrade path and no other entry point. Because the address is derived from the beneficiary and the hub only, a pass can be paid before it is deployed and by anyone: Relay, a NEAR Intents solver, or a friend.

PonsMarket

Copied from an earlier Robinhood Chain project where it was tested against the live Pons V2 on a fork. It knows the phases of a Pons token:

Phase Where the buy goes
0, curve curve.buy{value}(quoteIn, minTokensOut, recipient); the curve refunds any unfillable remainder, the adapter flushes it back to the caller
1, swept the curve is full but the pool does not exist yet: the adapter calls factory.createGraduatedPool(token) and then trades on the pool
2, pool a Uniswap V4 swap through PoolManager.unlock, ETH for token, with the Pons hook taking its fee after the swap
3, rescued reverts NoMarket: Pons cancelled the launch, nothing to buy

It also exposes priceOf, progressOf and liquidityOf, which the site uses for the stats page.

Gas

Measured by the test suite on a fork of Robinhood Chain against the live Pons V2 (forge test -vv, 16 tests):

Operation Gas
Deploy Zecpass 1,699,504 (plus 1,637,366 for PonsMarket if deployed fresh)
deliver, on the curve 407,866
deliver, on the Uniswap V4 pool 137,821
sweep, first time (deploys the pass) 284,332
sweep, again, on the curve 88,662
sweep, again, on the pool 122,231
withdraw 43,826 to 230,621
the one buy that creates the V4 pool at graduation about 1,050,000

At Robinhood Chain's base fee (about 0.09 gwei) a sweep costs well under a cent. The relayer pays it.

Deployment

Deployed on 2026-09-21 at 0x2d2F1e1c7B90272728a627605dD07FC46eF79232, owner and treasury 0xe568…E269, relayer 0x5C9A…951F, market 0xd039…5bA0, fee 50 bps, caps 0.5 ETH per order and 5 ETH per day.

forge script script/Deploy.s.sol:Deploy --rpc-url robinhood --private-key $PK \
  --broadcast --slow --gas-estimate-multiplier 150 --legacy --with-gas-price 90000000
forge verify-contract <HUB> src/Zecpass.sol:Zecpass --chain-id 4663 \
  --verifier blockscout --verifier-url https://robinhoodchain.blockscout.com/api

The flags are not optional on this chain: without --slow --legacy and the multiplier, the CREATE runs out of gas. After the token launches: cast send <HUB> "setToken(address)" <TOKEN> from the owner, then lockToken() once the address is confirmed on Pons.