Skip to content

72-hour linear vesting

Every TIDE you claim arrives on a slider, not in a lump. Instead of crediting your full reward instantly, the hook deposits it into a linear vesting tranche that unlocks smoothly over vestingDuration72 hours in production. This applies to all claimed gains, including activated lottery prizes.

The point is to make claim-and-dump unprofitable: a holder who claims and immediately sells can only take what has actually vested so far, and selling an NFT before its tranche finishes forfeits the unvested remainder to the lottery.

Claimed rewards are stored per user in the Vest struct on the hook:

struct Vest {
uint128 claimable; // fully-unlocked TIDE, withdrawable now
uint128 lockedTotal; // size of the active linear tranche
uint128 lockedWithdrawn; // how much of this tranche has already been pulled
uint64 start; // when the active tranche began vesting
}
mapping(address => Vest) public vests;

A tranche begins at start and unlocks linearly until start + vestingDuration. The amount of the current tranche that has vested so far is _grossVested:

function _grossVested(Vest memory v) private view returns (uint256) {
if (v.lockedTotal == 0) return 0;
uint256 elapsed = block.timestamp - v.start;
if (elapsed >= vestingDuration) return v.lockedTotal;
return uint256(v.lockedTotal) * elapsed / vestingDuration;
}

So the unlocked fraction of the active tranche is:

unlockedFraction = min(1, (now - start) / vestingDuration)
grossVested = lockedTotal * unlockedFraction

Before start-relative time reaches vestingDuration, vesting is strictly proportional to elapsed time. After it, the tranche is fully vested.

Three view functions report your live position:

FunctionReturns
claimableNow(address user)TIDE you can withdraw this instant: claimable + (grossVested - lockedWithdrawn)
lockedOf(address user)TIDE still locked: lockedTotal - grossVested
vestEndsAt(address user)start + vestingDuration for the active tranche (or 0 if nothing is locked)

claimableNow is the sum of two buckets: TIDE already realized into claimable from past tranches, plus the freshly-vested-but-not-yet-pulled slice of the current tranche. It grows continuously toward vestEndsAt, which is exactly what the vesting bar on the claim dashboard animates each second.

withdrawVested — pull the unlocked part, clock untouched

Section titled “withdrawVested — pull the unlocked part, clock untouched”

To move vested TIDE into your wallet you call withdrawVested():

function withdrawVested() external nonReentrant {
Vest storage v = vests[msg.sender];
uint256 grossVested = _grossVested(v);
uint256 payout = uint256(v.claimable) + (grossVested - v.lockedWithdrawn);
// ...
v.claimable = 0;
v.lockedWithdrawn = uint128(grossVested);
// transfer `payout` TIDE to the user, emit VestWithdrawn(user, payout)
}

Two properties matter here:

  • It does not restart the clock. withdrawVested only zeroes claimable and advances lockedWithdrawn to the amount vested so far. It never touches start, so the remaining locked balance keeps vesting on its original schedule — withdrawing does not penalize you.
  • It pays out the already-vested portion only. The still-locked remainder (lockedTotal - grossVested) stays in the tranche and continues to unlock.

A new claim re-locks the remainder (anti-dump)

Section titled “A new claim re-locks the remainder (anti-dump)”

The asymmetric, deliberately holder-favoring part: claiming again re-locks whatever is still locked. When new rewards are deposited via _depositVesting, the function first realizes the already-vested portion into claimable, then starts a fresh tranche sized as remainingLocked + amt with start = block.timestamp and lockedWithdrawn = 0:

function _depositVesting(address user, uint256 amt) private {
Vest storage v = vests[user];
uint256 grossVested = _grossVested(v);
uint256 newClaimable = uint256(v.claimable) + (grossVested - v.lockedWithdrawn);
uint256 remainingLocked = uint256(v.lockedTotal) - grossVested;
uint256 newLocked = remainingLocked + amt;
// v = { claimable: newClaimable, lockedTotal: newLocked, lockedWithdrawn: 0, start: now }
emit Vested(user, amt, newLocked, block.timestamp + vestingDuration);
}

The effect:

  • What you have already vested is safe — it is moved into claimable and is unaffected by the new tranche.
  • What was still locked is rolled into the new tranche and starts vesting over a full fresh vestingDuration from now.

This means stacking claims pushes your locked TIDE’s finish line forward. It is an intentional anti-dump bias — the protocol rewards holders who let a tranche mature over those who repeatedly claim to chase the unlock. withdrawVested (above) is the escape hatch that does not re-lock, so you can always pull the vested slice without resetting anything.

EventEmitted when
Vested(user, amountAdded, lockedTotal, vestEnd)A vesting tranche is (re)started — on every claim that deposits rewards, and on activated lottery prizes
VestWithdrawn(user, amount)Vested TIDE is withdrawn to the user’s wallet

See Events & custom errors for the full surface, and Claim = in-pool buyback for how ETH fees become the TIDE that lands in a tranche in the first place.