Rewards
Bands
Rosters, ranks and coups on chain with every computation off it, a band supply gated on lifetime revenue, and what a band is paid.
Status#
The band contest is funded by the Spoils user pool, through SpoilsSplitter — it takes a share alongside the battlepass and seasons. See Spoils.
What a band is#
A band is a numbered on-chain roster: who is in it, what rank they hold, and who backed which coup. Three facts and nothing else. A band has no name on chain — the display name is metadata, and a band is a number here. Ids start at 1.
The rule that decides what is in the contract is stated in its own source: an assertion goes on chain, a computation does not. Volume, the four band statuses, the succession walk and the monthly winner are all computed off chain and are deliberately absent from storage.
| Writer | Can |
|---|---|
| The account itself | createBand, join, leave, optIn, acceptLeadership (when proposed) |
| The band’s leader | setRole, kick, proposeLeader, cancelLeadershipProposal, dissolveBand (alone in the band) — for their own band only |
| The resolver | resolveCoup, resolveHostileWin and concludeLostRace, and nothing else. It cannot open a coup, cannot opt anybody in, cannot touch a band with no open coup, cannot appoint a leader out of nowhere and cannot move a single wei. |
Gas may be sponsored: BandRelay.executeSigned accepts an EIP-712 signature under the “The Hood Bands” domain (verifying contract: the relay, which BandRegistry deploys), with no relayer allowlist — an opt-in is ActionKind.OptIn, with the coup in bandId and the side in value. Opening a coup is the one action with no sponsored form — a sponsor able to pay would decide who may open one.
Founding, joining and every hostile coup action carry a ForceAttestor attestation for the caller and the band, issued only when the caller holds a force — for a band action, its leader’s force. An unaligned account cannot found; nobody joins or raids a band of another force.
How many bands exist#
How many bands may exist at all follows an owner-set rule over the platform’s cumulative revenue, derived live and never stored:
// the cap createBand enforces; the rule is BandRegistry.bandCapRule(), set with setBandCapRule
cap = baseBands
+ max(0, lifetimeFeesWei - thresholdWei) / feesPerBandWei; // integer division| Lifetime protocol fees | Bands |
|---|---|
| 0 ETH | 25 |
| 1 ETH | 26 |
| 25.5 ETH | 50 |
| 100 ETH | 125 |
| Value | Opening | Bounds |
|---|---|---|
baseBands | 25 | 0 to 1,000 |
thresholdWei | 0 ETH | 0 to 100,000 ETH |
feesPerBandWei | 1 ETH | 1 wei to 1,000 ETH |
The owner may change any of the three with setBandCapRule; the call, and a fee splitter repoint, are refused when they would leave the cap below the bands that already exist (BandCapBelowExistingBands). The bands page reads the live rule.
Ranks, tithes and caps#
| Rank | Slots | Tithe each |
|---|---|---|
| Leader | 1, by construction | 6% |
| Vice-Leader | 1 — MAX_VICE_LEADERS | 4% |
| Director | 3 — MAX_DIRECTORS | 2% |
| Executive | 4 — MAX_EXECUTIVES | 1% |
| Member | The rest, to MAX_MEMBERS = 50 including the leader | None |
The slot caps are what make a per-officer rate safe: nine fixed seats mean the tithe total cannot grow with the band. The tithes themselves are applied off chain, in the root — this contract holds the ranks, not the money. Each tithe is prorated by the UTC days the rank was held that month: a rank change counts from the UTC day of its block, and the rank held at a day’s close is that day’s rank.
| Constant | Value | What it delays |
|---|---|---|
KICK_GRACE | 24 hours | A leader may not remove a member until this has elapsed since they joined. |
REJOIN_COOLDOWN | 24 hours | After voluntarily leaving, before joining or founding any band. A kicked member has no cooldown — the asymmetry is stored rather than branched. It applies to founding as well as joining, because otherwise leave-then-found is the free route around it. |
OPT_IN_WINDOW | 24 hours | How long a coup accepts opt-ins. |
SCORING_WINDOW | 7 days | How long volume counts after opt-in closes. A coup is therefore resolvable 8 days after it opens. |
COUP_COOLDOWN | 30 days | A rolling window from the previous coup’s resolution, before the same band may be couped again. |
A leader cannot leave while they lead. Leadership moves in two steps: the leader proposes a member (proposeLeader) and the seat moves only when that member accepts (acceptLeadership); the old leader is then an ordinary member and may leave. A leader who is the band’s only member may instead dissolve it (dissolveBand): the band ends, its number is never reused, its slot under the cap is freed, and the leader takes the usual 24-hour rejoin cooldown. Whatever the band vault still holds is paid to that leader (BandVault.claimDissolved): ETH and other currencies at once, THD and Hood launch tokens over 90 days. Joining, kicking, accepting leadership and dissolving are all refused while a coup is open on the band or a hostile race runs on it, and a leaver who opened the band’s open coup voids it: nobody is removed and the fee is not refunded.
Coups#
The coup fee is coupFeeWeiPerMember × memberCount, and msg.value must equal it exactly — overpayment is refused, because ETH in this contract has no way back out. The per-member figure is settable storage rather than a constant, because it is ETH-denominated and drifts; the design figure is 0.0005 ETH per member, which is 0.025 ETH against a full 50-member band.
The whole fee is forwarded to ProtocolFeeSplitter in the same transaction, using the measured msg.value and never address(this).balance — forwarding a balance would let anyone donate ETH and buy THD emission one-for-one, which is the hole closed on 23-08-2026.
Internal and hostile
| Internal | Hostile | |
|---|---|---|
| Who takes part | Members of the band | Addresses in no band, outside the 24-hour rejoin cooldown |
| At once on one band | One — may open over a running hostile race, and suspends it | Any number — a race |
| Carries when | Backers’ volume > leader’s + opposition’s | First coup whose counted volume reaches hostileVolumeBarWei() (10 ETH on production), with a full counted party of HOSTILE_MIN_TAKERS() = MAX_HOSTILE_TAKERS() = 10 |
| Registrations per coup | The roster | Up to MAX_HOSTILE_REGISTRATIONS() (20): the party of 10 and 10 in reserve. An excluded registration’s slot passes to the next in line. One live coup per account per race |
| On success | The initiator leads | The initiator leads, the party joins. Nobody is removed: the deposed leader and every officer become Members |
| On failure | Every backer is removed, as a kick — no rejoin cooldown | Nobody is removed; the fee is kept |
Hostile coups — the three rulings of 27-09-2026
| Rule | What happens | Enforced by |
|---|---|---|
| Force | Checked when you view the coup. Another force — or none — sees it but cannot register or open one. Switching after registering does not cancel it; the ordinary wrong-force rules then apply in the band. | The page, and the resolver: a wrong-force registration counts toward neither takers nor volume and is not seated |
| Rejoin cooldown | Never dropped. Inside it you cannot open a hostile coup, register for one, join or found a band. | BandRegistry — RejoinCooldownActive from membershipOf(account).rejoinAllowedAt |
| Race | The first coup to reach the bar, measured by block and log index, takes the band. Every rival of that round concludes as lost. Fees are not refunded. | resolveHostileWin with the crossing point, published in HostileRaceWon; concludeLostRace for the rivals |
Party, suspension and force — the rulings of 27-09-2026
| Rule | What happens | Enforced by |
|---|---|---|
| Party of ten | A win seats exactly 10. Nobody is removed: the deposed leader and every officer become Members. The band may reach 60 (50 + 10); joins are refused until it is back under 50. | BandRegistry — HOSTILE_MIN_TAKERS = MAX_HOSTILE_TAKERS = 10; resolveHostileWin takes the officer list and refuses NotAnOfficer / OfficersNotAllDemoted |
| Internal coup suspends the race | An internal coup may open over a running hostile race and holds it. Failed or voided: the race resumes with each coup’s remaining time. Carried: the race ends and every rival concludes as lost. | BandRegistry — HostileRaceSuspended; deadlines = stored + pausedTotal − pauseBase |
| No force, no band | An account on no force cannot found a band. A band whose leader holds no force accepts no hostile registration. If the leader’s force changes, the band follows and members on the other force get the 7-day wrong-force window. | The app — force is not readable on chain — and the resolver, which refuses every registration against a band with no force |
An ERC-20 trade counts at the posted price in force at its own block; with no price at that block it is excluded and the page shows how many trades were not counted — never face value.
What a band is paid#
BandRewardsDistributor performs no comparison, holds no volume, knows nothing about forces and cannot name a band. It holds the money and pays what a proof proves. The formula it implements off chain:
period = a UTC calendar month; the first starts with Spoils (seededAt + 24h) — pre-TGE volume never counts
volume = curve buys and sells + ETH stakes on settled markets, at event-time value, platform addresses excluded
banded = the leg's account was in a band at that leg, and the leg's force was the band leader's force then
contestants = every force whose bands generated banded volume that period (> 0)
winner = the contestant with the most band volume over the period
payout = the pot × the WEAKEST CONTESTANT's share of the period's banded volume
fewer than two contestants, or a tie at the top = nothing paid, the whole pot rolls
loser = nothing
tithes = Leader 6% · Vice 4% · 3 Directors 2% · 4 Executives 1% (20%, volume-gated)
each prorated: rate × UTC days held in that rank ÷ days in the month; the rest of the slice goes to the members
the rest = split by member volume across the winning force's banded members
leaves = one per member per month, keccak256(abi.encode(uint32 monthIndex, account, that month's THD)) — never a leader's to pass on“The reward is the strength of the opposition”: winning against nobody pays nothing, because the pot released is sized by the weakest contestant. A force with no banded volume in the period is not a contestant, so a dormant force cannot hold the pot at zero for everyone else.
| Property | Value |
|---|---|
| Root shape | One root per calendar month, published by the band rewards keeper (or the owner) once the month has ended: publishMonth(monthIndex, root, allocated). Nothing is claimable for VETO_WINDOW (24 hours), in which the owner or guardian Safe may veto. Leaf is keccak256(abi.encode(monthIndex, account, amount)), where the amount is the account’s figure for that month. monthIndex = year × 12 + (month − 1). |
| Vest | Linear over vestDuration() — 90 days — from the month’s first publication. A re-publication keeps that moment. |
| Claim window | claimWindow() — 360 days — from the month’s first publication. After it, the month’s allocated, unclaimed THD is burned by burnExpired(monthIndex), which anyone may call. |
| Claim gate | Passed once — the minimum earned or spent on the platform, and X connected — or a claim reverts ClaimGateNotPassed(account, claimGate). |
| Refusals on publication | MonthNotEnded, MonthClaimWindowClosed, AllocationBelowClaimed and AllocationExceedsFunding — a month may promise no more than the unreserved balance plus its own outstanding. |
| Claiming | Permissionless, and always pays account rather than the caller: claim(monthIndex, account, amount, proof) pays what has vested minus claimedOf[monthIndex][account]. |
| The remainder | Between 50% and 95% of the pot every month, by construction — and the unallocated part is never burned. It is next month’s pot. Only allocated-and-unclaimed THD burns at a deadline. |
reserved() = Σ over published months (allocated − claimed)
reserved() <= thd.balanceOf(this)
unallocatedBalance() = balance − reserved()The quarterly pot paid against the season leaderboard is separate: Seasons.