# Token Geysers for Liquidity Provider Subsidies Token Geysers are a mechanism to distibute tokens in regular intervals. This document explains how the Token Geyser can be emploed to disburse LP subisdies. Distribution tokens - which are tokens allocated towards being distibuted as LP rewards - are added to a Locked Pool in the contract and become unlocked over time according to a once-configurable unlock schedule. Once unlocked, they are available to be claimed by users. The Token Geyser system has 2 pools of distribution tokens. These are Rowan tokens to be distributed as rewards.: - Locked distribution tokens $\mathbf S_L$ - Unlocked distribution tokens $\mathbf S_U$ ![](https://i.imgur.com/czZuKH9.jpg) The subsidies in the Token Geyser system aim to encourage LPs to perform longer-term liquidity adds in the Liquidity Provider Subsystem as opposed to shorter, more frequent intervals where liquidity is added. This improves predictability and indirectly improves system stability. ![](https://i.imgur.com/frqTejt.png) When a user Adds Liquidity in the Liquidity Provider Subsystem, they obtain LP tokens in exchange. The user can deposit these LP tokens ($\mathbf T$) into the Staked Pool in order to obtain ownership or stake over the Rowan tokens ($\mathbf S$) in the Unlocked Pool. The action of depositing to the Staked Pool is given by the `_stakeFor` function. While the user's LP tokens sit in the Staked Pool, the user accumulates Token Geyser time-bonus rewards. This reward is computed by the `computeNewReward` function. When LP tokens are locked up in the Staked Pool, users cannot use them to Remove Liquidity from the Liquidity Provider Subsystem. These rewards are materialized only when the user makes a Claim action. Upon a Claim action, the rewards accumulated up until that time are disbursed to the user. These rewards are in Rowan ($\mathbf S$). At any point, the user may unstake any amount of their LP tokens from the Staked Pool so that they may subsequently use these LP tokens to Remove Liquidity from the Liquidity Provider Subsystem. > **NOTE** > The user's stake is a function of the amount of LP tokens they deposited in the Staked Pool and the duration of their deposit. > The user's share of the Unlocked Pool is equal to their deposit duration divided by the global deposit duration. > ### Lock Tokens Distribution tokens can be locked in the Locked Pool $\mathbf S_L$ by the contract owner. The locked tokens then immediately start getting unlocked according to the unlock schedule, specifically in intervals specified by the `durationSec` - this can be specified to be a day, week, or any other duration depending on how frequently the system designer wants to unlock tokens. ``` Solidity lockTokens(amount, durationSec): lockedTokens = totalLocked() # Total amount of tokens in the Locked Pool if lockedTokens > 0: mintedLockedShares = totalLockedShares*amount/lockedTokens else: mintedLockedShares = amount * initialSharesPerToken totalLockedShares = totalLockedShares + mintedLockedShares ``` ### Unlock Schedule The Unlock Schedule determines when the tokens from the Locked Pool $\mathbf S_L$ get unlocked and sent to the Unlocked pool $\mathbf S_U$. ``` Solidity UnlockSchedule initialLockedShares = mintedLockedShares lastUnlockTimestampSec = now # current system time endAtSec = now + durationSec durationSec = durationSec ``` ### Unlock Tokens Moves distribution tokens from the Locked Pool $\mathbf S_L$ to the Unlocked Pool $\mathbf S_U$, according to the previously defined unlock schedules. ``` Solidity unlockTokens(): unlockedTokens = 0 # initialization lockedTokens = totalLocked() # Total amount of tokens in Locked Pool # If there is no ownership of shares of the Unlocked Pool through user staking, then all tokens from the Locked Pool get unlocked. if totalLockedShares == 0: unlockedTokens = lockedTokens # General case: Unlock tokens from Locked Pool according to Unlock Schedule and send to Unlocked Pool else: unlockedShares = 0 # initialization for (s = 0; s < unlockSchedules.length; s++): unlockedShares = unlockedShares + unlockScheduleShares(s)); unlockedTokens = unlockedShares * lockedTokens / totalLockedShares totalLockedShares = totalLockedShares - unlockedShares ``` ### Unlock Schedule Shares Computes the number of unlockable shares according to the unlock schedule. This depends on the time since last unlock. ``` Solidity unlockScheduleShares(s) schedule = unlockSchedules[s] if(schedule.unlockedShares >= schedule.initialLockedShares) { return 0 } sharesToUnlock = 0 # initialization # Special case to handle any leftover dust from integer division if (now >= schedule.endAtSec): sharesToUnlock = (schedule.initialLockedShares - schedule.unlockedShares); schedule.lastUnlockTimestampSec = schedule.endAtSec; } # General case else: sharesToUnlock = now - schedule.lastUnlockTimestampSec * schedule.initialLockedShares / schedule.durationSec; schedule.lastUnlockTimestampSec = now; } # Update total unlockedShares to keep track of state schedule.unlockedShares = schedule.unlockedShares + sharesToUnlock return sharesToUnlock; ``` ### Compute New Rewards Rewards are an additional time-bonus to a distribution amount. This is necessary to encourage long-term deposits instead of frequent recurring stakes/unstakes in the Staked Pool $\mathbf T_S$. #### Parameters | Name | Description | Symbol | |----|----|----| currentRewardTokens| The current number of distribution tokens already alotted for this unclaim op.| $\mathbf T_{CR}$ | stakingShareSeconds| The stakingShare-seconds that are being burned for new distribution tokens| $s$ | stakeTimeSec | Length of time for which the tokens were claimed. Needed to calculate the time-bonus | $t$ | |subsidy | Updated amount of distribution tokens to award, with any bonus included on the newly added tokens | $d_\mathbf L$ | ``` Solidity computeNewReward(currentRewardTokens, stakingShareSeconds, stakeTimeSec): newRewardTokens = totalUnlocked() * stakingShareSeconds / _totalStakingShareSeconds # If the user deposits for longer than the bonus period, they receive an additional reward if (stakeTimeSec >= bonusPeriodSec): return currentRewardTokens + newRewardTokens } # BONUS_DECIMALS is a parameter chosen and set by the designer when configuring the timed bonus system oneHundredPct = 10**BONUS_DECIMALS; # The bonus linearly scales with the duration of user's stake bonusedReward = startBonus + (oneHundredPct - startBonus * stakeTimeSec / bonusPeriodSec) * newRewardTokens / oneHundredPct return currentRewardTokens + bonusedReward ``` ### Stake Allows a user to deposit an `amount` of tokens to the Staked Pool in exchange for ownership or stake in the UnlockedPool. ``` Solidity stakeFor(address of staker, address of beneficiary, amount to stake) # TODO: handle all conditions on require params if totalStakingShares > 0: mintedStakingShares = totalStakingShares * amount / totalStaked() else: mintedStakingShares = amount * _initialSharesPerToken require(mintedStakingShares > 0, 'TokenGeyser: Stake amount is too small') updateAccounting(); // 1. User Accounting totals = _userTotals[beneficiary] totals.stakingShares = totals.stakingShares + mintedStakingShares totals.lastAccountingTimestampSec = now newStake = Stake(mintedStakingShares, now) _userStakes[beneficiary].push(newStake); // 2. Global Accounting totalStakingShares = totalStakingShares + mintedStakingShares // Already set in updateAccounting() // _lastAccountingTimestampSec = now; // interactions require(_stakingPool.token().transferFrom(staker, address(_stakingPool), amount), 'TokenGeyser: transfer into staking pool failed'); emit Staked(beneficiary, amount, totalStakedFor(beneficiary), ""); } ``` ### Unstake Allows a user to withdraw an `amount` of tokens from what they had previously deposited in the Staked Pool by letting go of ownership or stake in the UnlockedPool. By unstaking, the user gets their alotted amount of distribution tokens which is their reward. ``` Solidity unstake(amount): updateAccounting() # TODO: handle all conditions on require params // 1. User Accounting totals = _userTotals[msg.sender] accountStakes = _userStakes[msg.sender] // Redeem from most recent stake and go backwards in time. stakingShareSecondsToBurn = 0 sharesLeftToBurn = stakingSharesToBurn rewardAmount = 0 # initialize while (sharesLeftToBurn > 0) { # retrieve the previous stake lastStake = accountStakes[accountStakes.length - 1]; # compute how much time has passed after previous stake stakeTimeSec = now - lastStake.timestampSec newStakingShareSecondsToBurn = 0 #initialize if (lastStake.stakingShares <= sharesLeftToBurn): // fully redeem a past stake # Burn amount is computed based on the lesser of the two - stakingShares of past stake or sharesLeftToBurn newStakingShareSecondsToBurn = lastStake.stakingShares * stakeTimeSec rewardAmount = computeNewReward(rewardAmount, newStakingShareSecondsToBurn, stakeTimeSec) stakingShareSecondsToBurn = stakingShareSecondsToBurn + newStakingShareSecondsToBurn # subtract the full past stake amount from number of shares left to burn as it is a full redemption sharesLeftToBurn = sharesLeftToBurn - lastStake.stakingShares accountStakes.length-- } else { // partially redeem a past stake # Burn amount is computed based on the lesser of the two - stakingShares of past stake or sharesLeftToBurn newStakingShareSecondsToBurn = sharesLeftToBurn * stakeTimeSec rewardAmount = computeNewReward(rewardAmount, newStakingShareSecondsToBurn, stakeTimeSec) stakingShareSecondsToBurn = stakingShareSecondsToBurn + newStakingShareSecondsToBurn # subtract the redemeed amount i.e. sharesLeftToBurn from the total requested amount lastStake.stakingShares = lastStake.stakingShares - sharesLeftToBurn # set sharesLeftToBurn to lowest admissible amount, 0, as all sharesLeftToBurn would be used up in a partial redemption case sharesLeftToBurn = 0 } } # Keep track of state totals.stakingShareSeconds = totals.stakingShareSeconds - stakingShareSecondsToBurn totals.stakingShares = totals.stakingShares - stakingSharesToBurn // Already set in updateAccounting // totals.lastAccountingTimestampSec = now; // 2. Global Accounting # Substract duration and shares proportionally to what is unstaked _totalStakingShareSeconds = _totalStakingShareSeconds - stakingShareSecondsToBurn totalStakingShares = totalStakingShares - stakingSharesToBurn // Already set in updateAccounting // _lastAccountingTimestampSec = now; // interactions require(_stakingPool.transfer(msg.sender, amount), 'TokenGeyser: transfer out of staking pool failed'); require(_unlockedPool.transfer(msg.sender, rewardAmount), 'TokenGeyser: transfer out of unlocked pool failed'); emit Unstaked(msg.sender, amount, totalStakedFor(msg.sender), ""); emit TokensClaimed(msg.sender, rewardAmount); require(totalStakingShares == 0 || totalStaked() > 0, "TokenGeyser: Error unstaking. Staking shares exist, but no staking tokens do"); return rewardAmount; } ```