Docs·Rewards and mechanics
Rewards and mechanics
The burn / recycle vote
One boolean, decided monthly by holders with no quorum and no keeper: whether the THD/ETH pool's THD fee leg is destroyed or paid to liquidity providers.
Status#
This is the platform’s first governance surface, and it is deliberately the narrowest one that can exist: a single boolean, decided monthly by THD holders, with no keeper, no override and no key that can set it. The platform is never a destination under either outcome.
The question on the ballot#
The THD/ETH pool takes a fee on both sides of a swap, and the two legs are in different currencies. Only one of them is in question:
| Swap | Fee taken in | Destination |
|---|---|---|
| Buy THD with ETH | ETH | thdPoolTreasury — fixed. Never through ProtocolFeeSplitter, never metered. |
| Sell THD for ETH | THD | The vote decides. Burned, or forwarded to ThdLpRewards. |
thdBurn | What happens to the THD leg |
|---|---|
true — burn | ThdFeeRouter calls burn(), re-reads totalSupply() and reverts unless the supply fell by exactly the amount. ThdBurned carries both supply figures. |
false — recycle | Transferred to ThdLpRewards, the immutable LP distributor, and paid out against a cumulative Merkle root. |
There is no third branch, and the platform is not one of the two.
The calendar#
| Constant | Value | Seconds |
|---|---|---|
VOTE_DURATION | 1 days | 86,400 |
SNAPSHOT_LEAD | 7 days | 604,800 |
RAMP_WINDOW | 30 days | 2,592,000 |
RAMP_SAMPLES | 4 | — |
THRESHOLD_BPS | 5100 — 51% | — |
- A vote’s id is the month index,
year × 12 + (month − 1). A vote opens at 00:00:00 UTC on the 1st and closes 24 hours later. - The snapshot is 7 days before it opens, so it falls on the 25th or the 22nd of the previous month. Nobody chooses it; it is derived.
- The owner publishes the LP Merkle root during those 7 days —
setLpRootis refused before the snapshot and frozen absolutely from the instant the vote opens. The 7 days exist only so the root can be built and checked; the wallet half needs no root at all. openVote()is permissionless and takes no arguments. It does not start a window — the calendar does — it materialises the record for a month whose window has already begun, and it refuses once the window has closed.finalise(voteId)is permissionless and runs after the close.
The clock is a timestamp, not a block number, because THD overrides clock() and CLOCK_MODE(). A block-number clock would shift every offset at once, in the same direction, whenever block time moved — and every off-chain reader would compute the wrong snapshot and never find out, because the call still returns a number.
Votes finalise in order: a vote id at or below the last finalised one is refused, so a month nobody opened cannot later drag the flag backwards.
Weight, and the four-sample ramp#
Wallet weight is the arithmetic mean of THD.getPastVotes at four instants — the snapshot, and 7, 14 and 30 days before it:
samples = [ snapshot, snapshot - 7 days, snapshot - 14 days, snapshot - 30 days ];
weight = ( Σ thd.getPastVotes(account, samples[i]) ) / 4;| Held for | Samples that see the balance | Weight |
|---|---|---|
| 30 days or more | 4 of 4 | 100% |
| 8 days | 2 of 4 | 50% |
| 1 day | 1 of 4 | 25% |
A hard 30-day cutoff was considered and rejected: a cliff would stop the people the LP programme is spending 10% of supply to attract from voting on whether it continues.
No delegation transaction is needed. THD auto-self-delegates on an address’s first receipt, so an ordinary holder has voting power without ever having thought about it. A Safe or any other address with code does not auto-delegate and must delegate manually.
LPs read zero from getPastVotes, because their THD sits inside a Uniswap V4 position. They prove weight instead against the published root, with a leaf of keccak256(abi.encode(voteId, account, lpWeight)), and the proven weight is added to their wallet weight and cast as one ballot on one side. If no root is published for a month, LPs lose their vote that month and the wallet half is unaffected.
One ballot per address per vote, and it cannot be changed. A zero-weight ballot is refused rather than silently counted as an abstention.
Who does not vote#
The principle: THD votes when it belongs to somebody who could sell it.
| Excluded | How |
|---|---|
| The unlock contract, every distributor, every vesting and claim contract, the quest vault, the spoils vault | They never delegate, and THD’s auto-delegation deliberately skips addresses with code and the unlock contract by name. getPastVotes reads zero. |
| Platform treasuries | Checked live on every ballot against TreasuryRegistry and refused by name, with a dedicated error. |
| The protocol’s own locked liquidity position | Excluded from the LP root off chain. |
The threshold, and the sticky default#
// ThdVoting.finalise
outcome = router.thdBurn(); // the sticky default: whatever is in force
total = burnVotes + recycleVotes;
if (total != 0) {
if (burnVotes * 10_000 >= THRESHOLD_BPS * total) { outcome = true; decided = true; }
else if (recycleVotes * 10_000 >= THRESHOLD_BPS * total) { outcome = false; decided = true; }
}| Situation | Result |
|---|---|
| A side reaches 51% of votes cast | That side wins and the flag is set to it. |
| A side has a majority below 51% | Nothing changes. 50.5% is a majority and is not the threshold. |
| A tie | Nothing changes. |
| Nobody votes | Nothing changes. |
| Nobody opened the vote at all | Nothing changes. |
There is no quorum, and the threshold is expressed in basis points so the comparison is exact integer arithmetic rather than a percentage that has been rounded.
What applies the result#
finalise writes the flag by calling ThdFeeRouter.setTHDBurn(bool), and only when the vote decided something that differs from the flag already in force. Access control on that setter is one line: the caller must be the immutable voting address. There is no owner, no guardian and no override on the router at all.
| Property | Value |
|---|---|
| Mutable state | thdBurn, and nothing else. |
| Receipt behaviour | It holds, never forwards on receipt. Forwarding would let a downstream revert wedge collectFees() on an ownerless contract. |
flush() | Permissionless. The ETH and THD legs flush independently, so a treasury that stops accepting ETH cannot stop the supply falling. |
| When the flag takes effect | Immediately, for the next flush — applied to whatever is held at that moment, not to fees accrued under the old flag. |
thdDestination() | The zero address while burning, because a burn has no destination. That zero is an answer, not a missing pointer. |
ThdLpRewards pays liquidity providers against a cumulative Merkle root the owner publishes. A claim pays cumulative − claimedOf[account] and always pays the address the root names, never the caller — so a claim can be relayed, batched or sponsored without the relayer redirecting a wei. A cumulative root that went backwards is refused in the claimant’s own transaction, which is what makes a replaceable root safe: a restatement can never become a clawback. There is no deadline, no expiry and no sweep, and no owner path moves THD out.
| Rule | Value | Refusal |
|---|---|---|
| THD claim gate | Passed once: the minimum earned or spent on the platform, and X connected | ClaimGateNotPassed(account, claimGate) |
| Opens | 24 hours after the THD/ETH pool is seeded (TGE) | ClaimsNotOpen(account, seededAt, opensAt) |
| Rounds | Daily cumulative roots, each stating the lifetime total it promises | PromiseExceeded(account, amount, totalClaimed, totalPromised) |
| Vest | None | — |
| Deadline | None — nothing expires or burns | — |
The LP allocation is 1,000 bps of supply — 100,000,000 THD, paid to ThdLpRewards as its bucket beneficiary — and recycled fee THD arrives in the same balance with no accounting that distinguishes them. See the eleven allocations.