Skip to content

Commit

Permalink
Merge pull request #81 from stakedotlink/insurance-pool
Browse files Browse the repository at this point in the history
Insurance pool
  • Loading branch information
BkChoy authored Jun 1, 2024
2 parents 045c042 + 4c07e69 commit 61d63ee
Show file tree
Hide file tree
Showing 26 changed files with 1,178 additions and 191 deletions.
222 changes: 222 additions & 0 deletions contracts/core/InsurancePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;

import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";

import "./base/StakingRewardsPool.sol";
import "./interfaces/IRewardsPool.sol";

/**
* @title Insurance Pool
* @notice Allows users to stake LP tokens to earn rewards while insuring the staking pool from significant slashing events
*/
contract InsurancePool is StakingRewardsPool {
using SafeERC20Upgradeable for IERC20Upgradeable;

uint256 public totalDeposits;
IRewardsPool public rewardsPool;

address public rebaseController;
uint256 public maxClaimAmountBP;
bool public claimInProgress;

uint64 public withdrawalDelayDuration;
uint64 public withdrawalWindowDuration;
mapping(address => uint64) private withdrawalRequests;

event InitiateClaim();
event ExecuteClaim(uint256 amount);
event ResolveClaim();
event RequestWithdrawal(address indexed account, uint64 withdrawalStartTime);
event SetWithdrawalParams(uint64 withdrawalDelayDuration, uint64 withdrawalWindowDuration);

error SenderNotAuthorized();
error ClaimInProgress();
error ExceedsMaxClaimAmount();
error InvalidClaimAmount();
error NoClaimInProgress();
error WithdrawalWindowInactive();

function initialize(
address _lpToken,
string memory _liquidTokenName,
string memory _liquidTokenSymbol,
address _rebaseController,
uint256 _maxClaimAmountBP,
uint64 _withdrawalDelayDuration,
uint64 _withdrawalWindowDuration
) public initializer {
__StakingRewardsPool_init(_lpToken, _liquidTokenName, _liquidTokenSymbol);
rebaseController = _rebaseController;
if (_maxClaimAmountBP > 9000) revert InvalidClaimAmount();
maxClaimAmountBP = _maxClaimAmountBP;
withdrawalDelayDuration = _withdrawalDelayDuration;
withdrawalWindowDuration = _withdrawalWindowDuration;
}

modifier onlyRebaseController() {
if (msg.sender != rebaseController) revert SenderNotAuthorized();
_;
}

modifier whileNoClaimInProgress() {
if (claimInProgress) revert ClaimInProgress();
_;
}

/**
* @notice deposits tokens into the pool
* @dev will delete any active or upcoming withdrawal window
* @param _amount amount of tokens to deposit
*/
function deposit(uint256 _amount) external whileNoClaimInProgress {
if (withdrawalRequests[msg.sender] != 0) delete withdrawalRequests[msg.sender];

rewardsPool.updateReward(msg.sender);
token.safeTransferFrom(msg.sender, address(this), _amount);
_mint(msg.sender, _amount);
totalDeposits += _amount;
}

/**
* @notice withdraws tokens from the pool
* @param _amount amount of tokens to withdraw
*/
function withdraw(uint256 _amount) external whileNoClaimInProgress {
if (!canWithdraw(msg.sender)) revert WithdrawalWindowInactive();

rewardsPool.updateReward(msg.sender);
_burn(msg.sender, _amount);
totalDeposits -= _amount;
token.safeTransfer(msg.sender, _amount);
}

/**
* @notice requests a withdrawal and initiates the withdrawal delay period
*/
function requestWithdrawal() external {
uint64 withdrawalStartTime = uint64(block.timestamp) + withdrawalDelayDuration;
withdrawalRequests[msg.sender] = withdrawalStartTime;
emit RequestWithdrawal(msg.sender, withdrawalStartTime);
}

/**
* @notice returns whether an account's withdrawal is active
* @param _account address of account
* @return canWithdraw whether withdrawal window is active
*/
function canWithdraw(address _account) public view returns (bool) {
if (withdrawalDelayDuration == 0) return true;
(uint64 start, uint64 end) = getWithdrawalWindow(_account);
return block.timestamp >= start && block.timestamp < end;
}

/**
* @notice returns an account's current active or upcoming withdrawal window
* @param _account address of account
* @return start time and end time of withdrawal window
*/
function getWithdrawalWindow(address _account) public view returns (uint64, uint64) {
uint64 withdrawalStartTime = withdrawalRequests[_account];
if (withdrawalDelayDuration == 0 || block.timestamp >= withdrawalStartTime + withdrawalWindowDuration) return (0, 0);
return (withdrawalStartTime, withdrawalStartTime + withdrawalWindowDuration);
}

/**
* @notice initiates the claim process
*/
function initiateClaim() external onlyRebaseController whileNoClaimInProgress {
claimInProgress = true;
emit InitiateClaim();
}

/**
* @notice executes a claim by withdrawing tokens from the pool
* @dev will cause all stakers' balances to decrease by the percentage that is withdrawn
* @param _amount amount of tokens to withdraw
*/
function executeClaim(uint256 _amount) external onlyOwner {
if (!claimInProgress) revert NoClaimInProgress();
if (_amount > (totalDeposits * maxClaimAmountBP) / 10000) revert ExceedsMaxClaimAmount();

totalDeposits -= _amount;
token.safeTransfer(msg.sender, _amount);

emit ExecuteClaim(_amount);
}

/**
* @notice resolves the claim process
*/
function resolveClaim() external onlyRebaseController {
if (!claimInProgress) revert NoClaimInProgress();

claimInProgress = false;
emit ResolveClaim();
}

/**
* @notice returns an account's staked amount for use by the rewards pool
* controlled by this contract
* @dev shares are used so this contract can rebase without affecting rewards
* @param _account account address
* @return account's staked amount
*/
function staked(address _account) external view returns (uint256) {
return sharesOf(_account);
}

/**
* @notice returns the total staked amount for use by the rewards pool
* controlled by this contract
* @dev shares are used so this contract can rebase without affecting rewards
* @return total staked amount
*/
function totalStaked() external view returns (uint256) {
return totalShares;
}

/**
* @notice sets the address of the rewards pool
* @param _rewardsPool address of rewards pool
*/
function setRewardsPool(address _rewardsPool) external onlyOwner {
rewardsPool = IRewardsPool(_rewardsPool);
}

/**
* @notice sets the address of the rebase controller
* @param _rebaseController address of rebase controller
*/
function setRebaseController(address _rebaseController) external onlyOwner {
rebaseController = _rebaseController;
}

/**
* @notice sets the maximum size of a single claim in basis points with respect to the size of the pool
* @param _maxClaimAmountBP max claim amount in basis points
*/
function setMaxClaimAmountBP(uint256 _maxClaimAmountBP) external onlyOwner {
if (_maxClaimAmountBP > 9000) revert InvalidClaimAmount();
maxClaimAmountBP = _maxClaimAmountBP;
}

/**
* @notice sets the withdrawal parameters
* @param _withdrawalDelayDuration amount of time required to wait before withdrawaing
* @param _withdrawalWindowDuration amount of time a withdrawal can be executed for after the delay has elapsed
*/
function setWithdrawalParams(uint64 _withdrawalDelayDuration, uint64 _withdrawalWindowDuration) external onlyOwner {
withdrawalDelayDuration = _withdrawalDelayDuration;
withdrawalWindowDuration = _withdrawalWindowDuration;
emit SetWithdrawalParams(_withdrawalDelayDuration, _withdrawalWindowDuration);
}

/**
* @notice returns the total amount of assets staked in the pool
* @return total staked amount
*/
function _totalStaked() internal view override returns (uint256) {
return totalDeposits;
}
}
164 changes: 164 additions & 0 deletions contracts/core/RebaseController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;

import "@openzeppelin/contracts/access/Ownable.sol";

import "./interfaces/IStakingPool.sol";
import "./interfaces/IPriorityPool.sol";
import "./interfaces/IStrategy.sol";
import "./interfaces/ISDLPoolCCIPControllerPrimary.sol";
import "./interfaces/IInsurancePool.sol";

/**
* @title Rebase Controller
* @notice Updates and distributes rewards across the staking pool and cross-chain SDL Pools
* @dev Chainlink automation should call updateRewards periodically under normal circumstances and call performUpkeep
* in the case of a negative rebase in the staking pool
*/
contract RebaseController is Ownable {
IStakingPool public stakingPool;
IPriorityPool public priorityPool;
ISDLPoolCCIPControllerPrimary public sdlPoolCCIPController;
IInsurancePool public insurancePool;

address public rebaseBot;
uint256 public maxRebaseLossBP;

error NoStrategiesToUpdate();
error PositiveDepositChange();
error InvalidMaxRebaseLoss();
error PoolClosed();
error SenderNotAuthorized();

constructor(
address _stakingPool,
address _priorityPool,
address _sdlPoolCCIPController,
address _insurancePool,
address _rebaseBot,
uint256 _maxRebaseLossBP
) {
stakingPool = IStakingPool(_stakingPool);
priorityPool = IPriorityPool(_priorityPool);
sdlPoolCCIPController = ISDLPoolCCIPControllerPrimary(_sdlPoolCCIPController);
insurancePool = IInsurancePool(_insurancePool);
rebaseBot = _rebaseBot;
if (_maxRebaseLossBP > 9000) revert InvalidMaxRebaseLoss();
maxRebaseLossBP = _maxRebaseLossBP;
}

modifier onlyRebaseBot() {
if (msg.sender != rebaseBot) revert SenderNotAuthorized();
_;
}

/**
* @notice updates strategy rewards in the staking pool and distributes rewards to cross-chain SDL pools
* @param _strategyIdxs indexes of strategies to update rewards for
* @param _data encoded data to be passed to each strategy
* @param _gasLimits list of gas limits to use for CCIP messages on secondary chains
**/
function updateRewards(
uint256[] calldata _strategyIdxs,
bytes calldata _data,
uint256[] calldata _gasLimits
) external onlyRebaseBot {
if (priorityPool.poolStatus() == IPriorityPool.PoolStatus.CLOSED) revert PoolClosed();

stakingPool.updateStrategyRewards(_strategyIdxs, _data);
sdlPoolCCIPController.distributeRewards(_gasLimits);
}

/**
* @notice returns whether or not rewards should be updated due to a neagtive rebase,
* the strategies to update, and their total deposit change
* @dev should be called by a custom bot (not CL automation)
* @return upkeepNeeded whether or not rewards should be updated
* @return performData abi encoded list of strategy indexes to update and their total deposit change
**/
function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) {
if (priorityPool.poolStatus() == IPriorityPool.PoolStatus.CLOSED) return (false, "0x");

address[] memory strategies = stakingPool.getStrategies();
bool[] memory strategiesToUpdate = new bool[](strategies.length);
uint256 totalStrategiesToUpdate;
int256 totalDepositChange;

for (uint256 i = 0; i < strategies.length; ++i) {
int256 depositChange = IStrategy(strategies[i]).getDepositChange();
if (depositChange < 0) {
strategiesToUpdate[i] = true;
totalStrategiesToUpdate++;
totalDepositChange += depositChange;
}
}

if (totalStrategiesToUpdate != 0) {
uint256[] memory strategyIdxs = new uint256[](totalStrategiesToUpdate);
uint256 strategiesAdded;

for (uint256 i = 0; i < strategiesToUpdate.length; ++i) {
if (strategiesToUpdate[i]) {
strategyIdxs[strategiesAdded] = i;
strategiesAdded++;
}
}

return (true, abi.encode(strategyIdxs, uint256(-1 * totalDepositChange)));
}

return (false, "0x");
}

/**
* @notice Updates rewards in the case of a negative rebase or pauses the priority
* pool if losses exceed the maximum
* @dev should be called by a custom bot (not CL automation)
* @param _performData abi encoded list of strategy indexes to update and their total deposit change
*/
function performUpkeep(bytes calldata _performData) external onlyRebaseBot {
if (priorityPool.poolStatus() == IPriorityPool.PoolStatus.CLOSED) revert PoolClosed();

(uint256[] memory strategiesToUpdate, uint256 totalDepositChange) = abi.decode(_performData, (uint256[], uint256));

if (strategiesToUpdate.length == 0 || totalDepositChange == 0) revert NoStrategiesToUpdate();

if ((10000 * totalDepositChange) / stakingPool.totalSupply() > maxRebaseLossBP) {
priorityPool.setPoolStatus(IPriorityPool.PoolStatus.CLOSED);
insurancePool.initiateClaim();
} else {
stakingPool.updateStrategyRewards(strategiesToUpdate, "");
}
}

/**
* @notice Reopens the priority pool and insurance pool after they were paused as a result
* of a significant slashing event and rebases the staking pool
* @dev sender should ensure all strategies with losses are included in the index list and
* all strategies with gains are excluded
* @param _strategyIdxs list of strategy indexes to update
*/
function reopenPool(uint256[] calldata _strategyIdxs) external onlyOwner {
priorityPool.setPoolStatus(IPriorityPool.PoolStatus.OPEN);
insurancePool.resolveClaim();
stakingPool.updateStrategyRewards(_strategyIdxs, "");
}

/**
* @notice sets the rebase bot
* @param _rebaseBot address of rebase bot
*/
function setRebaseLossBot(address _rebaseBot) external onlyOwner {
rebaseBot = _rebaseBot;
}

/**
* @notice sets the maximum basis point amount of the total amount staked in the staking pool that can be
* lost in a single rebase without pausing the pool
* @param _maxRebaseLossBP max basis point loss
*/
function setMaxRebaseLossBP(uint256 _maxRebaseLossBP) external onlyOwner {
if (_maxRebaseLossBP > 9000) revert InvalidMaxRebaseLoss();
maxRebaseLossBP = _maxRebaseLossBP;
}
}
Loading

0 comments on commit 61d63ee

Please sign in to comment.