# 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$

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.

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;
}
```