How it works
2. How it works
Life of a reward, from trade to claim.
Short version:
Every swap on the pool pays a fee in ETH to a hook. The hook forwards it to the token contract, which periodically swaps accumulated ETH into
$ASTEROIDand hands it to a tracker. The tracker updates one number, from which every holder's share is derived. Holders callclaim()when they want their$ASTEROID.
The three contracts
PAM is three contracts, deployed once, unchangeable.
PAMHook: Uniswap V4 hook. The pool manager calls it before and after every swap. Captures the fee.
PAM: the ERC-20 token. Holds supply, implements standard ERC-20, and receives ETH from the hook. When accumulated ETH crosses a threshold, runs the ETH → $ASTEROID sub-swap.
DividendTracker: accounting. Tracks every holder's share using the MagnifiedDividends pattern. One cumulative variable, constant-time regardless of holder count.
No treasury. No admin proxy. No upgrade mechanism. Three contracts plus the V4 pool are the whole system.
The currencies
| Asset | What it is | Where it lives |
|---|---|---|
$PAM | Our ERC-20 token | Held by users and by the V4 pool |
ETH | Native Ethereum | Paid as the fee, accumulated briefly, sold for $ASTEROID |
$ASTEROID | Reward token (0xf280…4126) | Bought on Uniswap V2, held in the tracker, claimed by holders |
Users see two: they pay/receive ETH when trading and they hold $PAM. $ASTEROID is claimable on demand, not auto-pushed.
Step 1: a swap happens
Someone buys $PAM with 1 ETH. The pool has our hook attached.
On submission, the V4 pool manager calls beforeSwap and afterSwap. Based on direction and exact-in/out, exactly one charges the fee.
Buy exact-input (typical: "1 ETH for as much $PAM as possible"):
beforeSwapruns.- Sees
zeroForOne=true(ETH →$PAM),amountSpecified<0(exact-input). - Computes 4% of 1 ETH = 0.04 ETH.
- Tells the pool manager to take 0.04 ETH from the swap.
- AMM now processes 0.96 ETH.
- Pool manager transfers 0.04 ETH to the hook.
- Hook calls
PAM.addFees{value: 0.04 ETH}().
Symmetric for sells (afterSwap on the ETH output side) and for exact-output. Every combination captures the fee as ETH.
From the user's wallet, they paid 1 ETH and got ~0.96 ETH worth of $PAM, priced at whatever the AMM gave for 0.96 ETH.
Step 2: ETH accumulates, then converts
The PAM contract accumulates ETH. It doesn't swap each fee individually: wastes gas and exposes tiny swaps to sandwiches. Waits until accumulated ETH crosses 0.01 ETH, then runs _swapAndDistribute:
- Caps at 0.1 ETH per trigger (
MAX_SWAP_PER_DISTRIBUTION). Larger balances process in chunks. 0.1 ETH is small enough that sandwich frontrunning isn't worth the gas. - Queries the V2 router for expected output and applies 5% slippage tolerance.
- Executes ETH →
$ASTEROIDviaETH/WETH/ASTEROID. - Measures received
$ASTEROIDvia before/after balance delta (robust against reward tokens with transfer fees, though$ASTEROIDhas none). - Transfers to the tracker, calls
distributeDividends(amount).
If any step fails (V2 outage, unusual state), the function catches, leaves ETH in the contract, emits SwapFailed. Next swap retries. A V2 issue never prevents $PAM trades.
Step 3: the tracker does the math
Naive "distribute proportionally" loops over all holders, O(n). At 2,000+ holders, gas-prohibitive.
Roger Wu's MagnifiedDividends pattern replaces the loop with a single running accumulator, O(1):
- One number,
accPerShare: cumulative dividends per share, high precision. - On distribute:
accPerShare += amount / totalTrackedSupply. One add. - On query:
userBalance * accPerShare - (corrections). One multiply.
Corrections close the math as balances change. Invariant: every user's computed owed amount equals what they would have earned if tracked individually in real-time. Fuzz-tested 5,000 random runs, invariant-tested 8,192 stateful operations. Holds.
Step 4: claim when you want
Accumulated $ASTEROID sits in the tracker until you claim. The frontend reads withdrawableDividendOf(yourAddress) (view, zero gas) and polls.
When ready, call claim(). The tracker reads your owed amount, marks it withdrawn (no double-claim), transfers $ASTEROID, emits DividendClaimed. Gas: ~80k, ~$0.19 at 1 gwei.
Why claim is manual
Automatic push (2021 pattern) made transfers expensive and broke DEX aggregators that don't expect ERC-20 transfers to have side effects. Manual claim keeps transfers cheap and standards-compliant. You pay ~80k gas once, when you want the $ASTEROID.
Tradeoff: forgotten rewards sit there forever. Anyone can call claimFor(otherAddress) paying their own gas.
Why V2 for the sub-swap
$ASTEROID has its deepest liquidity on Uniswap V2 and V3. Routing through an established pool gives price stability. Building our own pool would split liquidity.
Contract uses the V2 router (0x7a25…488D), ETH → WETH → $ASTEROID. Router address is hardcoded.
Edge cases
No holders yet. Tracker starts with zero tracked supply. _swapAndDistribute early-returns if trackedSupply == 0, so ETH accumulates safely until the airdrop populates holders.
V2 returns zero $ASTEROID. Before/after balance check catches this and exits cleanly, leaving ETH for the next attempt.
$PAM sent to the token contract by mistake. Tokens stuck. Contract is excluded from dividend tracking, so stuck tokens don't skew distributions. No rescue function: the same guarantee that prevents admin extraction also prevents recovery.
Gas costs
| Action | Typical gas | USD at 1 gwei |
|---|---|---|
| Normal swap (no fee trigger) | ~250k | ~$0.60 |
| Swap crossing the 0.01 ETH threshold | ~400k | ~$0.95 |
| Claim rewards | ~80k | ~$0.19 |
Transfer $PAM wallet-to-wallet | ~80k | ~$0.19 |
The threshold-crossing case is the unlucky user: every N swaps, one pays ~$0.35 more because it triggers the conversion. Spreads naturally.


