Published before launch. They do not change afterwards. Once the winners list is committed to the escrow there is no owner, no withdraw and no fix — so every rule is stated here in full, and nothing is decided later.
number we choose, not an assumption.
The escrow sets `cutoff` in start(), so the 30-day clock begins at graduation rather
than at deploy. It reads ZERO until then. THREE states are therefore distinguished,
and only the last proceeds:
cutoff() == 0 NOT STARTED -> refuse
cutoff() > now RUNNING -> refuse
cutoff() <= now ENDED -> proceed
Zero is in the past, so a two-state "has the cutoff passed?" test PASSES on a game
that has not begun — it would snapshot an unstarted or mid-flight game and commit a
root over it. `started()` is read as well and cross-checked against `cutoff() != 0`;
if the two disagree the script refuses rather than choosing one.
Once ENDED, the cutoff timestamp is resolved to the last block at or before it and
the script REFUSES if --cutoff-block disagrees. The block is still passed explicitly
so a run is reproducible from its arguments alone, but it is checked against the
contract rather than trusted.
The run also refuses unless the cutoff is at least 15 blocks deep
(engine.CONFIRMATIONS = 15, about 7 seconds on BSC) — a reorgable cutoff would make
the payout itself reorgable.
its first inbound transfer to the cutoff block's timestamp. This is the SAME code
path as the live engine — `engine.apply()` is imported, not reimplemented, so the
scoreboard people watched for 30 days and the payout cannot disagree.
the net balance change, never per Transfer log. Post-graduation the token is
fee-on-transfer, so one buy emits several Transfers (pool -> buyer for the tokens,
buyer -> vault for the tax); judged per log, every buyer appears as `from` on its
own purchase and dies instantly.
For a wallet w in transaction T:
counted_out(w,T) = sum of outbound legs, EXCLUDING legs to a mechanical fee
sink (the tax vault or the token contract). A tax leg is
involuntary and is not a sale.
offsettable_in(w,T) = sum of inbound legs whose sender is a POOL, the bonding
curve, or a mint (0x0). NOTHING ELSE offsets.
dead(w,T) <=> counted_out > offsettable_in
Death is permanent: accrual is zeroed and never resumes, and later buys do not
resurrect. Legs are grouped by (block, transactionIndex) and processed in
(block, transactionIndex, logIndex) order, so the result is deterministic.
Inbound from another WALLET does not offset an outbound. That is the residual case:
A sells 1M to the pool and receives 1M from B in the same transaction. Its balance
nets to zero, but it sold, and offsettable_in is 0 because B is not a pool - so A is
dead. Netting on the raw balance delta would let any seller be rescued by an inbound
leg they arranged themselves, which is trivially farmable.
Worked cases:
BUY in 1M from pool, out 30k tax -> out 0, offset 1M ALIVE
SELL out 1M to pool (+ tax) -> out 1M, offset 0 DEAD
RECEIVE + tax in 1M from EOA, out 30k tax -> out 0 ALIVE
SEND to friend out 1M to another wallet -> out 1M, offset 0 DEAD
sell + rebuy out 1M pool, in 1M pool -> 1M vs 1M, not > ALIVE
Implemented once in `engine.apply` and shared with the live scoreboard (R2). Every
case above is asserted in test_apply.py.
how tokens were acquired does not change that they were held. Self-airdropping to
farm extra wallets is not an exploit: the sending wallet's outbound kills it, so the
tokens simply move, they do not multiply.
(a) STRUCTURAL - addresses that are not holders by construction:
the token contract - the tax vault (dividendContract()) - the tax processor
- the flap bonding curve / Portal - the escrow - 0x0...0 - 0x...dEaD
- PancakeSwap feeTo (a permanent sink that would otherwise read as the
ultimate diamond hand)
- every liquidity pool, DETECTED AS: PancakeSwap V2 getPair(token, quote), V3
getPool(token, quote, fee) over fee tiers 100/500/2500/10000, for each quote
in {WBNB, USDT, USDC, BUSD}; plus any counterparty in the token's own
Transfer graph for which token.pools(addr) returns true. The method is
stated because "every detected pool" is not reproducible without it.
(b) TEAM - an EXPLICIT PUBLISHED LIST, fixed before launch, passed as --team-list and
hashed into this output as team_list_hash. The file is part of the frozen rules.
An empty list must be passed explicitly; there is no default.
WHY THE TEAM IS NOT DEDUCED FROM THE CHAIN. A previous version excluded "every
address the deployer sent BNB to before the cutoff". That is unimplementable as
written. Etherscan caps every query at 10,000 rows, so a deployer with 48,018
transactions before the cutoff yields a RECENCY WINDOW of roughly 11-24 days rather
than a complete set - a bound the rule never stated and no auditor could reproduce.
Two honest implementations of the same sentence found 8 addresses and 39.
On the token used to validate this, both sets produced an identical root, because
every deployer-funded wallet was already dead or had zero accrual. That is luck, not
correctness: in a 30-day game a deployer-funded wallet that buys and HOLDS stays
alive with accrual > 0 - precisely the case the clause exists to catch - and 8 vs 39
is real money.
The clause was never an anti-attack measure; it is a credibility measure. So it is
now a commitment we NAME and publish, not a query nobody can reproduce.
gross_i = floor(POT_WEI * accrual_i / total_accrual)
Integer arithmetic throughout - Python ints are arbitrary precision, so there is no
float anywhere in the payout path. ROUNDING IS DOWN, ALWAYS, PER RECIPIENT.
POT_WEI IS THE ESCROW'S ON-CHAIN BALANCE AT THE CUTOFF BLOCK, read from chain via
--escrow. It is not the vault's inflow, not an estimate, and not a number we choose.
claim() pays with Address.sendValue, so if the committed tree's total exceeded the
escrow's real balance the LAST claimants would revert - holding a valid proof
against an empty contract, with no owner and no fix. The script REFUSES TO RUN
without --escrow for exactly that reason.
THERE IS EXACTLY ONE THRESHOLD, AND IT IS THIS ONE. The earlier PAYOUT_FLOOR_WEI of
1e15 was REMOVED, not kept alongside. It does not appear anywhere in this file or in
the code, and no wallet is tested against it. If both were live a wallet would need
gross > 0.0015 BNB to be paid, and the exclusion rate would rise further than any
figure we publish. A grep for PAYOUT_FLOOR is the check.
GAS_PER_CLAIM_WEI a PUBLISHED CONSTANT in this file, not a live estimate — a
live one would make the leaves non-deterministic.
gross_i = floor(POT_WEI * accrual_i / total_accrual) (R6)
net_i = gross_i - GAS_PER_CLAIM_WEI
net_i <= 0 -> NO LEAF. This IS the floor, and it is mechanical: a wallet
is excluded exactly when its share does not cover the cost of
delivering it, never because we picked a threshold.
leaf amount = net_i
Recipients bear their own gas through this deduction. "No fee" means no protocol
fee; everyone in crypto pays gas and nobody calls that a fee.
THE OPS LEAF. One additional leaf:
address = OPS_WALLET, which PUSHES every claim
amount = GAS_PER_CLAIM_WEI * (number of winner leaves)
It is the delivery budget, and it is in the published list like any other leaf so
anyone can check it is exactly the sum of the deductions and not one wei more.
Note the identity: sum(net_i) + ops = sum(gross_i) over winners. The ops leaf
reclaims the deductions exactly; it cannot over-collect without the arithmetic
visibly failing.
WITHOUT THE OPS LEAF the deducted gas would be stranded: the escrow has no withdraw
function, so it would sit there and burn at sweep while ops still paid for every
claim out of pocket. The deduction would fund nothing.
THE GAS PRICE CAP IS PART OF THE RULE. GAS_PER_CLAIM_WEI = 0.0005 BNB was set
against a MEASURED 75,063 gas per claim - executed against the deployed escrow's own
verified bytecode on a real EVM, not modelled - at a ceiling of 5 gwei, which is what
BSC quoted when the constant was fixed. OPS COMMITS TO PUSHING CLAIMS ONLY AT OR
BELOW 5 GWEI. Without that cap the constant is arbitrary; with it, it is checkable:
75,063 x 5 gwei = 0.000375 BNB, and 0.0005 covers it with 33% headroom.
UNSPENT GAS BUDGET IS BURNED. If delivery costs less than the budget, the remainder
is burned and the burn transaction hash is published. There is no version of this
where we profit from the estimate being generous. Note the asymmetry, which runs
against instinct: overshooting costs RECIPIENTS (the surplus burns), undershooting
costs OPS. The constant is therefore the smallest value that covers the measured
cost at the capped price, not a comfortable round number.
gross shares of wallets excluded by R7 — is never assigned to anyone. It stays in
the escrow and BURNS. No one, including us, can retrieve it. The exact residue is
reported in the output so it can be checked against the escrow after the last claim.
Excluded shares are NOT redistributed. Redistributing would change every other
payout, which changes who clears the deduction, which changes the set again — it
does not converge cleanly and it is far harder to audit.
unique, so no tie-break is ever needed and none is defined. `index` is the position
in that sorted list, from 0. This ordering is a pure function of the eligible set.
leaf = keccak256(keccak256(abi.encode(uint256 index, address account,
uint256 amount)))
DOUBLE-hashed, and abi.encode — every field padded to 32 bytes, 96 bytes in total —
NOT abi.encodePacked. The double hash is what makes a leaf undistinguishable from an
internal node: with single-hashed packed leaves a 64-byte internal node can be
replayed as a leaf, which is the standard second-preimage attack on Merkle trees.
Internal nodes are keccak256 of the two children CONCATENATED IN ASCENDING BYTE
ORDER, so proofs carry no position bits. Leaves are SORTED BY HASH ASCENDING and
placed in a complete binary tree of 2n-1 nodes, filled from the end in reverse
(OpenZeppelin `makeMerkleTree`). Tree shape is part of the spec: the same leaves in
a pair-in-order tree with odd-node promotion give a DIFFERENT ROOT.
This is verified against the real @openzeppelin/merkle-tree library in
test_merkle_oz.py — root, every leaf hash, and every proof. It is deliberately NOT
verified only against our own hasher: an earlier version of this file was
self-consistent, agreed with itself perfectly, and would have made every single
claim revert with InvalidProof because it matched nothing the escrow computes.
state is read: not the engine's state file, not its rank ledger, nothing but chain
data. `--verify` re-runs and diffs against a previous output, which is the check to
run before committing the root.
═══════════════════════════════════════════════════════════════════════════════