Treasury, buyback and burn
A fixed part of every fee goes to a contract with no owner. Anyone can make it spend a slice of what it holds on the project token and burn it. This page explains the rules it follows, and why they are what they are.
The flow
- A swap pays its fee. The policy picks the total fee. The hook takes 10% of it, as claims on the pool manager, and mints them to the Treasury in the same transaction; the rest is the liquidity providers’. The hook itself never holds anything.
- The Treasury accumulates claims in the pool’s two currencies. In the usual swap, where you say how much you pay, the share is taken from the token you receive, so the claims arrive in the currency being bought; over time both build up.
- Anyone calls
buybackAndBurn(currency). The Treasury spends a slice of its claims in that currency to buy the project token from a plain Uniswap v4 pool, inside a price limit, and burns what it receives.
Nothing calls it by itself, and nothing pays the caller. Anyone can make a call, and the repository has a keeper that does: see Trigger it yourself.
The rules
| Rule | Value | Why |
|---|---|---|
| Owner | None | No one can redirect, pause or withdraw. There is no admin function and no function that pays a person. |
| What it can spend on | The project token, through one pool per fee currency | Claims in a currency that has no buyback pool cannot be spent at all, so the hook only accepts pools whose two currencies the Treasury can spend. |
| The buyback pools | Plain Uniswap v4 pools of the project token against a fee currency, with a static fee (0.30% here) and the Treasury’s price oracle as their only hook | The oracle records each pool’s price and takes no part in a swap. The Treasury accepts only pools set up this way. |
| Per call | At most 5% of its claims in that currency | Keeps any single call small, and spreads buying over time. A slice that is too big for the pool is filled in part, and the rest stays as claims. |
| Frequency | One call per currency per hour | Limits how often anyone can trigger a trade around. A call that buys nothing fails and does not start the hour. |
| Price limit | Never more than 300 ticks (about 3%) above the pool’s average price of the last 30 minutes | The average comes from the oracle, which records the pool’s tick before the first swap of every second. It cannot be moved inside one transaction. |
| What happens to the token | Burned in the same transaction | The supply goes down; the call is logged on-chain. Claims in the project token itself are burned directly. |
| The share | 10% of every fee, fixed at deployment (the hook accepts at most 20%) | It is an immutable of the hook; it cannot be raised. |
What the price limit does, and what it does not
A call is a trade that anyone can try to trade around, so the limit is what protects it. It is measured against an average that cannot be moved inside one transaction, so a sandwich around a call costs the Treasury at most the band. In the tests, front-running one or two slices of the buyback pool made the Treasury pay 1% and 2% over the honest price, and front-running five or more made the call revert, because the pool stood above the limit. An earlier design measured its guard against the pool’s spot price in the same transaction: there, a sandwich cost the Treasury 2% to 17% of a call’s burn, and a position placed at the moved price took 63% of a slice. Those attacks, and a gift of claims that froze the buyback, no longer work in the tests.
What remains. Holding the price up for minutes moves the average by the share of the 30-minute window for which it is held (20 minutes moves it by two thirds), and that costs whoever does it against everyone who trades. A new buyback pool has no 30 minutes of price history yet, so calls fail with OracleNotReady until it has. If the pool is thin, a call buys what the pool offers inside the limit and the rest stays as claims; if the pool’s liquidity is withdrawn, a call buys nothing and fails with NothingBought. The tests are in contracts/test/MyelinTreasuryAttacks.t.sol, and the review is in docs/SELF_REVIEW.md.
The token
On Robinhood Chain the project token is Myelin (MYL), contract 0x6C71AB1c7CbbA4194Fe956AA9e1fe4895d09fF43. The Treasury buys it back with its share of every fee and burns it. Burning is not a promise about a price, and the backtest does not show that liquidity providers get the share back in a better fee: see the results. 10% is a value chosen, not a recommendation.
Trigger it yourself
The Treasury page shows what the Treasury holds and every call so far. To make a call yourself you need a little ETH for gas, because the transaction is yours; the tokens come from the Treasury, and the caller receives nothing.
cast call 0xEE9B3C0F79e9240D966ad211572cd194cdc36770 "claimsOf(address)(uint256)" 0x0000000000000000000000000000000000000000 --rpc-url https://rpc.mainnet.chain.robinhood.com
cast call 0xEE9B3C0F79e9240D966ad211572cd194cdc36770 "nextBuyback(address)(uint256,uint256)" 0x0000000000000000000000000000000000000000 --rpc-url https://rpc.mainnet.chain.robinhood.com
cast call 0xEE9B3C0F79e9240D966ad211572cd194cdc36770 "guard(address)(uint160,uint160,bool)" 0x0000000000000000000000000000000000000000 --rpc-url https://rpc.mainnet.chain.robinhood.com
cast send 0xEE9B3C0F79e9240D966ad211572cd194cdc36770 "buybackAndBurn(address)" 0x0000000000000000000000000000000000000000 --rpc-url https://rpc.mainnet.chain.robinhood.com --private-key $PRIVATE_KEY
To have it done for you, run the keeper from the app folder of the repository: app/scripts/keeper.mjs looks every five minutes and, for each currency, sends a call when one is due and would go through (it simulates first, so it never sends one that would revert). Give it a key of its own that holds only gas, as KEEPER_KEY. Anyone can run one.
Contract reference
| Function or event | What it does |
|---|---|
buybackAndBurn(address currency) | Spends a slice of the claims in `currency` on the project token and burns it. Reverts if called too soon (TooSoon), if there is nothing to spend (NothingToSpend), if the buyback pool has no price history yet (OracleNotReady), if the pool stands above the price limit (PriceTooHigh) or if nothing was bought (NothingBought). |
claimsOf(address currency) | The claims the Treasury holds in a currency. |
nextBuyback(address currency) | The most the next call would spend, and the time from which it may be made. |
twap(address currency) | The pool’s average tick over the last 30 minutes, and whether the oracle has enough history to give it. |
guard(address currency) | The price limit of the next call, the pool’s current price, and whether the pool is within the limit. |
totalBurned() | Total of the project token burned by this Treasury. |
event BuybackAndBurn(currency, amountIn, burned) | Emitted on every call: what was actually spent (less than the slice if the pool could not fill it inside the limit) and what was burned. |