-
Notifications
You must be signed in to change notification settings - Fork 19
FDT Exit Defense Mechanisms
The Maple protocol must have defensive functionality to manage how LPs and Stakers exit Pools and StakeLockers. This is for two reasons:
- Profit is earned in large discreet chunks (interest payments) rather than a steady stream of income, so short-term exploits of the interest distribution mechanism must be avoided.
- Defaults will incur losses for LP and Stakers, so premature withdrawals to avoid losses must be avoided.
To combat this, the Maple protocol uses two mechanisms:
- Lockup Period - A strict condition where all of a user's deposited tokens are locked for a certain amount of time.
- Cooldown - A condition where the user must "intend" to exit the contract and wait a specified amount of time before exiting, and must exit within a certain window. This is similar to AAVE's cooldown implementation.
It should be noted that the Pool and StakeLocker use these concepts in the same way, but they are managed in different contexts with different variables so they will each have dedicated sections on this page.
The Pool Lockup Period is a simple lockup period condition that specifies that if a user hasn't waited a specified period of time after deposit, they cannot withdraw. An example of this is a user who deposits on day 0. That user will not be able to withdraw any funds until day 90 if the lockup period for that Pool is 90 days.
In order to properly account for the scenario of a given LP depositing multiple times in a given Pool over time, an effective deposit date must be used. This timestamp value is a weighted representation of the effective single deposit date of the user based on their deposit amounts. This value is determined using the following equation:
coefficient = depositAmount / (currentDeposit + depositAmount)
depositDate = existingDepositDate + (block.timestamp - existingDepositDate) * coefficient
As a user deposits a larger amount compared to their existing balance, the effective deposit date asymptotically approaches the current timestamp. The figure below shows a scenario where a user has an existing deposit of 100 USDC deposited on Day 1. The line represents their effective deposit dates for different deposit amounts on Day 10. It can be seen that as the amounts get larger, the effective deposit date approaches Day 10.
In order to prevent sudden withdrawals after the lockupPeriod
has passed, a cooldown mechanism is used. This is dictated by the following globals variables:
-
lpCooldownPeriod
: Period (in secs) after which LPs are allowed to withdraw their funds from the Pool contract. -
lpWithdrawWindow
: Window of time (in secs) afterlpCooldownPeriod
that a user has to withdraw before their intent to withdraw is invalidated.
When a user deposits new funds into the Pool, their withdrawCooldown[user]
is set to zero. If they want to withdraw their liquidityAsset
from the Pool, they must call intendToWithdraw
. This will set their withdrawCooldown[user]
to block.timestamp
. The withdraw condition is shown below:
function isWithdrawAllowed(uint256 withdrawCooldown, IGlobals globals) public view returns (bool) {
return block.timestamp - (withdrawCooldown + globals.lpCooldownPeriod()) <= globals.lpWithdrawWindow();
}
This handles four scenarios:
-
Pre-Cooldown:
withdrawCooldown
is not set, soblock.timestamp - globals.lpCooldownPeriod() > globals.lpWithdrawWindow()
(returns false) -
During Cooldown:
withdrawCooldown + globals.lpCooldownPeriod() > block.timestamp
, so there is an overflow, making LHS > RHS (returns false) -
Withdraw Window:
block.timestamp - (withdrawCooldown + globals.lpCooldownPeriod()) <= globals.lpWithdrawWindow()
this is true, (returns true) -
After Withdraw Window:
block.timestamp - (withdrawCooldown + globals.lpCooldownPeriod())
is now greater thanglobals.lpWithdrawWindow()
(returns false)
To allow/encourage future composability with PoolFDT tokens, it was decided that PoolFDT transfers should be allowed without the sender having to intendToWithdraw
. However, the receiver of the funds must not be in an intending to withdraw state, this prevents the sender from being able to exit the Pool prematurely. This means they can either have an unset withdrawCooldown
or are past their withdraw window. The diagram below provides a visual representation of the cooldown mechanism.
The StakerLocker Lockup Period is a simple lockup period condition that specifies that if a user hasn't waited a specified period of time after staking, they cannot unstake. An example of this is a user who stakes on day 0. That user will not be able to unstake any BPTs until day 90 if the lockup period for that StakeLocker is 90 days.
In order to properly account for the scenario of a given Staker staking multiple times in a given StakeLocker over time, an effective stake date must be used. This timestamp value is a weighted representation of the effective single stake date of the user based on their stake amounts. This value is determined using the following equation:
coefficient = stakeAmount/(currentStake + stakeAmount)
stakeDate = existingStakeDate + (block.timestamp - existingStakeDate) * coefficient
As a user stakes a larger amount compared to their existing balance, the effective stake date asymptotically approaches the current timestamp. The figure below shows a scenario where a user has an existing stake of 100 BPTs deposited on Day 1. The line represents their effective deposit dates for different deposit amounts on Day 10. It can be seen that as the amounts get larger, the effective stake date approaches Day 10.
In order to prevent sudden withdrawals after the lockupPeriod
has passed, a cooldown mechanism is used. This is dictated by the following globals variables:
-
stakerCooldownPeriod
: Period (in secs) after which stakers are allowed to unstake their BPTs from the StakeLocker contract. -
stakerUnstakeWindow
: Window of time (in secs) afterstakerCooldownPeriod
that a user has to unstake before their intent to unstake is invalidated.
When a user stakes new BPTs into the StakeLocker, their unstakeCooldown[user]
is set to zero. If they want to unstake their BPTs from the StakeLocker, they must call intendToUnstake
. This will set their unstakeCooldown[user]
to block.timestamp
. The unstake condition is shown below:
function isUnstakeAllowed(address from) public view returns (bool) {
IGlobals globals = _globals();
return block.timestamp - (unstakeCooldown[from] + globals.stakerCooldownPeriod()) <= globals.stakerUnstakeWindow();
}
This handles four scenarios:
-
Pre-Cooldown:
unstakeCooldown
is not set, soblock.timestamp - globals.stakerCooldownPeriod() > globals.stakerUnstakeWindow()
(returns false) -
During Cooldown:
unstakeCooldown + globals.stakerCooldownPeriod() > block.timestamp
, so there is an overflow, making LHS > RHS (returns false) -
Unstake Window:
block.timestamp - (unstakeCooldown + globals.stakerCooldownPeriod()) <= globals.stakerUnstakeWindow()
this is true, (returns true) -
After Unstake Window:
block.timestamp - (unstakeCooldown + globals.stakerCooldownPeriod())
is now greater thanglobals.stakerUnstakeWindow()
(returns false)
To allow/encourage future composability with StakeLockerFDT tokens, it was decided that StakeLockerFDT transfers should be allowed without the sender having to intendToUnstake
. However, the receiver of the funds must not be in an intending to unstake state, this prevents the sender from being able to exit the StakeLocker prematurely. This means they can either have an unset unstakeCooldown
or are past their unstake window. The diagram below provides a visual representation of the cooldown mechanism.