Skip to content

Treasury & buyback-burn

The Treasury is the protocol’s supply sink. It receives forfeited TIDE whose lottery prize was never claimed, and it can do exactly one thing with that TIDE: burn it. The dev — in the role of guardian — controls when the burn happens, but there is no function anywhere in the contract that pays funds out to anyone. This is the structural core of TIDE’s no-rug guarantee: the dev holds the timing button, never the cash.

The Treasury is the terminal endpoint of the forfeited-vesting lottery. There are two paths by which TIDE lands there:

  1. Expired prizes (the common path). When you sell or transfer a Tide-LP NFT before your rewards finish vesting, the unvested remainder is forfeited and raffled to another holder. The winner has prizeActivationWindow to claim it. If they don’t, anyone can permissionlessly call expirePrize(winner), which sweeps the un-activated prize to the Treasury:

    _transfer(address(this), address(treasury), amount);
    emit PrizeExpired(winner, amount);
  2. No-eligible-winner fallback (rare). When a forfeit occurs but the lottery draw finds no eligible winner, the forfeit is normally redistributed pro-rata to all live shares. Only in the edge case where totalShares == 0 does it route to the Treasury instead:

    if (totalShares > 0) {
    accFeesPerShareTIDE += forfeited * ACC_SCALE / totalShares;
    emit PrizeRedistributed(forfeited);
    } else {
    _transfer(address(this), address(treasury), forfeited);
    }

Both paths deposit TIDE that was already forfeited — never anyone’s held balance, vested rewards, or the liquidity position. The Treasury can only accumulate value that holders chose to abandon by exiting early.

The amount sitting in the Treasury, waiting to be burned, is just its TIDE balance:

function pendingBurn() external view returns (uint256) {
return tide.balanceOf(address(this));
}

The dapp surfaces this as the Treasury’s “queued to burn” figure (see ../dapp/overview.md).

The guardian can call executeBuyback() at any time. It burns the entire Treasury-held balance and nothing else:

function executeBuyback() external onlyGuardian {
uint256 bal = tide.balanceOf(address(this));
if (bal > 0) tide.burn(bal);
emit BuybackBurned(bal);
}

The burn is routed through the hook’s burn(amount), which is itself gated so that only the Treasury can call it:

function burn(uint256 amount) external {
if (msg.sender != address(treasury)) revert TreasuryOnly();
_burn(address(treasury), amount);
}

Burning TIDE permanently reduces total supply. Because holding is the position, shrinking supply mechanically raises every remaining holder’s pro-rata share of the one pool. The guardian gains nothing from triggering it — there is no fee, no skim, no payout to the caller. The only effect is supply down, everyone else’s slice up. This is the on-chain expression of the project’s flywheel slogan, “exits enrich those who stay.”

The guardian is set at construction. In the live deployment the constructor wires the Treasury with the hook’s owner as guardian:

treasury = new Treasury(address(this), _owner);

The role is transferable, so it can later be handed to a multisig or timelock, but the handoff grants no new powers — the recipient still can only burn:

function transferGuardian(address to) external onlyGuardian {
if (to == address(0)) revert ZeroGuardian();
emit GuardianTransferred(guardian, to);
guardian = to;
}

The guardian’s complete authority over the Treasury is therefore: trigger a burn, and hand the burn button to someone else. That is the entire privileged surface.

The Treasury has a bare receive() so it won’t reject ETH that might be routed to it:

receive() external payable {}

In the standard flow nothing sends ETH here — forfeits arrive as TIDE. The contract is honest about this: as its own NatSpec notes, “there is no path to send it back out to anyone, so it can only sit.” Any ETH that ever lands there is stuck, not extractable by the guardian. The current executeBuyback() burns the TIDE balance only; an ETH→TIDE buyback-then-burn is a possible future extension, not a path that pays the guardian.

The Treasury is the place a malicious dev would normally drain. Here, structurally, they can’t:

PropertyTreasury guarantee
Custody of user fundsNone — the Treasury only ever holds already-forfeited TIDE, never held balances or the LP position.
Withdrawal path to the devDoes not exist. No function transfers value out except executeBuyback, which burns it.
What the guardian controlsOnly the timing of the burn, plus handing the role to another address.
What the guardian gains from a burnNothing. Supply shrinks; every other holder’s share grows.
Upgradeability / proxy / pauseNone.
Detector cleanlinessNo skim/withdraw/owner-drain pattern, so honeypot/rug scanners read it as clean.

This is the literal meaning of “the dev holds the button, not the cash”: the dev controls the timing of supply reduction, never the custody of funds.

ItemSignatureMeaning
EventBuybackBurned(uint256 tideBurned)All Treasury-held TIDE was burned.
EventGuardianTransferred(address indexed from, address indexed to)Guardian role handed off.
EventPrizeExpired(address indexed winner, uint256 amount)An un-activated prize was swept to the Treasury (emitted by the hook).
ErrorGuardianOnly()Caller is not the guardian.
ErrorZeroGuardian()transferGuardian target is the zero address.

For the full contract reference, addresses, and the rest of the system’s events and errors, see ../contracts/treasury.md and ../contracts/events-and-errors.md.