The Vault
The Vault is the contract that collects every royalty, holds the ETH, and lets you claim your share. There is no other place for fees to go. There is no admin function to drain it.
What flows in
| Source | Amount | Goes to |
|---|---|---|
| Secondary sale royalty | 10% of sale price | Vault → split 90/10 |
| Mint proceeds | 0.0015 ETH × quantity | Vault → 100% deployer |
The mint money is on a separate rail — it never touches the holder pool. Only secondary trades feed the holder yield.
The split
function _intake(uint256 amount) internal {
if (amount == 0) return;
uint256 deployerCut = (amount * DEPLOYER_BPS) / 10_000; // 10%
uint256 holdersCut = amount - deployerCut; // 90%
deployerBalance += deployerCut;
if (totalShares > 0) {
accETHPerShare += (holdersCut * ACC_PRECISION) / totalShares;
} else {
deployerBalance += holdersCut; // fallback only if no shares exist
}
emit RoyaltyReceived(msg.sender, amount, holdersCut, deployerCut);
}For every 1 ETH of royalty:
- 0.9 ETH → distributed to all holders, weighted by share
- 0.1 ETH → deployer wallet
The accumulator
The Vault uses a battle-tested pattern called a MasterChef accumulator. It runs in O(1) — you can claim in a single, cheap transaction no matter how many holders exist.
Every time fees arrive, a global counter accETHPerShare ticks up. Every turtle tracks how far it has claimed via rewardDebt. The pending amount is just the difference:
pending = (share × accETHPerShare) / 1e18 − rewardDebtThis is the same math that secures billions on Sushi, Pancake, and dozens of other DeFi protocols. It is correct, audited many times over, and impossible to drain repeatedly — once you claim, your rewardDebt updates and your pending falls back to zero until new royalties arrive.
Claiming
You can claim any time. You can claim any subset of your turtles. The Vault verifies you own them at claim time:
function claim(uint256[] calldata tokenIds) external nonReentrant {
uint256 accNow = accETHPerShare;
for (uint256 i; i < tokenIds.length; ) {
uint256 id = tokenIds[i];
if (glyphs.ownerOf(id) != msg.sender) revert NotOwnerOfToken();
uint32 share = glyphs.glyphState(id).shareScaled;
uint256 owed = (uint256(share) * accNow) / ACC_PRECISION - rewardDebt[id];
if (owed > 0) {
unclaimed[msg.sender] += owed;
rewardDebt[id] = (uint256(share) * accNow) / ACC_PRECISION;
}
unchecked { ++i; }
}
uint256 amount = unclaimed[msg.sender];
if (amount == 0) revert NothingToClaim();
unclaimed[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed();
}After claiming:
- Your
rewardDebtis updated for each claimed turtle. - A second
claimcall immediately after reverts withNothingToClaim— there is nothing left to take until new fees arrive.
1-year unclaimed recovery
If a wallet goes inactive for 365 days (no claim, no transfer, no forge), anyone can call recoverUnclaimed(user) to sweep their settled, un-withdrawn balance (the unclaimed[user] slot) back into the pool. The recovered ETH is then redistributed via the same 90/10 split.
This is abandonment protection, not a fund-grab. Active holders are never affected. The 365-day clock resets every time the wallet interacts with the Vault.
No admin can drain the Vault
Read the entire contract: there is no function that lets the deployer drain the holder pool. The deployer can only withdraw their own 10% cut:
function withdrawDeployer() external nonReentrant {
uint256 amount = deployerBalance;
if (amount == 0) revert NothingToClaim();
deployerBalance = 0;
(bool ok, ) = deployerWallet.call{value: amount}("");
if (!ok) revert TransferFailed();
}deployerWallet is immutable — set at deploy, cannot be changed. deployerBalance is incremented only by the 10% intake. The holder side of the pool sits in accETHPerShare and is only reachable via claim — which checks ownership.