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.

Version
1.0
Updated
2026-09-21
Source
THD contract sources · THD — Full Plan v1.43

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:

The pool's two fee legs
SwapFee taken inDestination
Buy THD with ETHETHthdPoolTreasury — fixed. Never through ProtocolFeeSplitter, never metered.
Sell THD for ETHTHDThe vote decides. Burned, or forwarded to ThdLpRewards.
The two outcomes
thdBurnWhat happens to the THD leg
true — burnThdFeeRouter calls burn(), re-reads totalSupply() and reverts unless the supply fell by exactly the amount. ThdBurned carries both supply figures.
false — recycleTransferred 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#

Every timing constant, and the literal it carries
ConstantValueSeconds
VOTE_DURATION1 days86,400
SNAPSHOT_LEAD7 days604,800
RAMP_WINDOW30 days2,592,000
RAMP_SAMPLES4—
THRESHOLD_BPS5100 — 51%—
  1. 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.
  2. 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.
  3. The owner publishes the LP Merkle root during those 7 days — setLpRoot is 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.
  4. 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.
  5. 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;
What the ramp means in practice
Held forSamples that see the balanceWeight
30 days or more4 of 4100%
8 days2 of 450%
1 day1 of 425%

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.

ExcludedHow
The unlock contract, every distributor, every vesting and claim contract, the quest vault, the spoils vaultThey never delegate, and THD’s auto-delegation deliberately skips addresses with code and the unlock contract by name. getPastVotes reads zero.
Platform treasuriesChecked live on every ballot against TreasuryRegistry and refused by name, with a dedicated error.
The protocol’s own locked liquidity positionExcluded 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; }
}
What each situation produces
SituationResult
A side reaches 51% of votes castThat 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 tieNothing changes.
Nobody votesNothing changes.
Nobody opened the vote at allNothing 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.

The router's shape
PropertyValue
Mutable statethdBurn, and nothing else.
Receipt behaviourIt 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 effectImmediately, 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.

ThdLpRewards claim rules, since 29-09-2026
RuleValueRefusal
THD claim gatePassed once: the minimum earned or spent on the platform, and X connectedClaimGateNotPassed(account, claimGate)
Opens24 hours after the THD/ETH pool is seeded (TGE)ClaimsNotOpen(account, seededAt, opensAt)
RoundsDaily cumulative roots, each stating the lifetime total it promisesPromiseExceeded(account, amount, totalClaimed, totalPromised)
VestNone—
DeadlineNone — 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.