Petaldocs

Keeper and relay

What claimAndBridge and flush do, why anyone can call them, why Across, and what the fee cap and hard ceiling bound.

The keeper is a convenience, not a trust point. It holds a key funded with a little ETH on both chains and nothing else; the key has no on-chain role. Everything it calls is a public function on an immutable contract that anyone else can call with the same effect.

The two calls

claimAndBridge(uint256 outputAmount) on a charity address, Robinhood Chain

  1. Reads the charity address's balance in Pons Fee Escrow and calls claim().
  2. Unwraps any WETH the charity address holds (an unfilled Across deposit refunds in WETH) and adds any ETH it already holds.
  3. Requires the total to be at least the minimum bridge amount, 0.01 ETH, immutable. Otherwise reverts BelowMinimum and nothing moves.
  4. Requires total - outputAmount to be at most the registry's live fee cap. The caller supplies outputAmount from a live Across quote; the contract only bounds the implied fee.
  5. Calls Across depositV3 with itself as depositor, the receiver for its config ID on Base as recipient, WETH on both sides as input and output token, inputAmount = total, msg.value = total, quoteTimestamp = block.timestamp, a 2-hour fill deadline, and an empty message. Emits ClaimedAndBridged(configId, inputAmount, outputAmount, receiver).

flush() on a receiver, Base

  1. Unwraps any WETH the receiver holds. Across pays contract recipients in WETH, never native ETH.
  2. Requires a non-zero balance.
  3. Calls donate.gg donateNative(configId, 0, receiver, "pons-charity-relay/robinhood") with the whole balance. Emits Flushed(configId, amount).

Neither function takes an address. claimAndBridge can only deposit toward the receiver computed for its own config ID; flush can only donate under its own config ID. A caller chooses when, never where.

Who calls them

Anyone. The keeper does it on a timer so nobody has to. If two keepers run, or a tick repeats, nothing doubles: every action re-reads chain state and simulates before sending, and a second claimAndBridge with nothing to claim reverts BelowMinimum.

To make the calls yourself: on Robinhood Chain, request an Across quote for native ETH from chain 4663 to chain 8453 for the charity address's claimable total, and call claimAndBridge(outputAmount) with the quoted output; the call reverts if the implied fee exceeds the live fee cap. On Base, call flush() on the receiver once the fill has landed. If the receiver has not been deployed yet, anyone can call deploy(configId) on the Base registry first; the address does not change.

The keeper's tick

The keeper runs on a 300-second tick. Each tick it reads the minimum bridge amount, the live fee cap, and every charity address the Robinhood registry has deployed. For each charity address it sums the Pons Fee Escrow balance and any held ETH; if the total reaches the minimum it requests an Across quote, skips the call if the implied fee is over the live fee cap or Across declines the route, re-reads the balance, and otherwise simulates and sends claimAndBridge. On Base it deploys any missing receiver and calls flush() on every receiver holding a balance worth the gas. Anything skipped is retried on the next tick.

Why Across

donate.gg's EVM contract lives on Base and Ethereum, not Robinhood Chain, so ETH has to cross. Robinhood Chain's native bridge to L1 takes 7 days and then needs another hop to Base. Across serves native ETH from Robinhood Chain (4663) to Base (8453) with about one second fill time and a fee far below the live fee cap; the example settlement paid 0.0000034 ETH on 0.011 ETH. Across is the only bridge route:

  • claimAndBridge calls depositV3 only. There is no fallback through the native bridge. If Across stops serving the route, ETH waits in Pons Fee Escrow and on the charity address. It is not lost, it does not move.
  • An unfilled deposit refunds to the charity address on Robinhood Chain, in WETH, and is included in the next bridge.
  • There is no Across message auto-flush. flush is a separate call. Adding auto-flush later would not change any address.
  • A single deposit is bounded by Across's per-route maximum. Very large accumulated balances may need to wait for liquidity.

Fee cap and hard ceiling

Across fees change, so a fixed fee bound would either overpay or stall. The registry holds a live fee cap, 0.001 ETH at launch, which the Safe can move. It can never exceed the hard ceiling of 0.002 ETH, which is immutable. A charity address reads the live cap at call time and rejects any outputAmount that implies a larger fee. If Across fees sit above the cap, the keeper skips the call and nothing moves until the Safe raises the cap or fees fall.

WETH unwrap

Across pays contract recipients in WETH and refunds unfilled deposits in WETH. Both the receiver and the charity address unwrap any WETH they hold before moving money, so a fill and a refund are handled the same way as native ETH. The relay's fork tests drive a real fill through the Base SpokePool's fillRelay to cover the unwrap.