Skip to content

Commit

Permalink
Merge pull request #129 from stakedotlink/native-link-withdrawals-res…
Browse files Browse the repository at this point in the history
…olutions

Native link withdrawals resolutions
  • Loading branch information
BkChoy authored Nov 28, 2024
2 parents b21dd85 + b079273 commit f4b615f
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 29 deletions.
14 changes: 14 additions & 0 deletions contracts/core/StakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ contract StakingPool is StakingRewardsPool {

error SenderNotAuthorized();
error InvalidDeposit();
error NothingStaked();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -345,6 +346,12 @@ contract StakingPool is StakingRewardsPool {
* @param _feeBasisPoints fee in basis points
**/
function addFee(address _receiver, uint256 _feeBasisPoints) external onlyOwner {
uint256[] memory strategyIdxs = new uint256[](strategies.length);
for (uint256 i = 0; i < strategyIdxs.length; ++i) {
strategyIdxs[i] = i;
}
_updateStrategyRewards(strategyIdxs, "");

fees.push(Fee(_receiver, _feeBasisPoints));
require(_totalFeesBasisPoints() <= 4000, "Total fees must be <= 40%");
}
Expand All @@ -362,6 +369,12 @@ contract StakingPool is StakingRewardsPool {
) external onlyOwner {
require(_index < fees.length, "Fee does not exist");

uint256[] memory strategyIdxs = new uint256[](strategies.length);
for (uint256 i = 0; i < strategyIdxs.length; ++i) {
strategyIdxs[i] = i;
}
_updateStrategyRewards(strategyIdxs, "");

if (_feeBasisPoints == 0) {
fees[_index] = fees[fees.length - 1];
fees.pop();
Expand Down Expand Up @@ -431,6 +444,7 @@ contract StakingPool is StakingRewardsPool {
* @param _amount amount to deposit
**/
function donateTokens(uint256 _amount) external {
if (totalStaked == 0) revert NothingStaked();
token.safeTransferFrom(msg.sender, address(this), _amount);
totalStaked += _amount;
emit DonateTokens(msg.sender, _amount);
Expand Down
2 changes: 2 additions & 0 deletions contracts/core/interfaces/IStakingRewardsPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ interface IStakingRewardsPool is IERC677 {
function totalShares() external view returns (uint256);

function totalSupply() external view returns (uint256);

function transferShares(address _recipient, uint256 _sharesAmount) external returns (bool);
}
2 changes: 2 additions & 0 deletions contracts/core/interfaces/IWithdrawalPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity 0.8.15;
interface IWithdrawalPool {
function getTotalQueuedWithdrawals() external view returns (uint256);

function minWithdrawalAmount() external view returns (uint256);

function deposit(uint256 _amount) external;

function queueWithdrawal(address _account, uint256 _amount) external;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ contract LSTRewardsSplitterController is Ownable {
uint256 principalDeposits = splitter.principalDeposits();
if (balance != 0) {
if (balance != principalDeposits) splitter.splitRewards();
splitter.withdraw(balance, _account);
splitter.withdraw(splitter.principalDeposits(), _account);
}

delete splitters[_account];
Expand Down
47 changes: 33 additions & 14 deletions contracts/core/priorityPool/PriorityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl
// address of oracle contract that handles LST distribution
address public distributionOracle;

// min amount of tokens that can be deposited into the staking pool in a single tx
// min amount of tokens that can be deposited into the staking pool strategies in a single tx
uint128 public queueDepositMin;
// max amount of tokens that can be deposited into the staking pool in a single tx
// max amount of tokens that can be deposited into the staking pool strategies in a single tx
uint128 public queueDepositMax;
// current status of the pool
PoolStatus public poolStatus;
Expand All @@ -52,9 +52,9 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl

// total number of tokens queued for deposit into the staking pool
uint256 public totalQueued;
// total number of tokens deposited into the staking pool since the last distribution
// total number of tokens deposited into the staking pool or swapped for LSTs since the last distribution
uint256 public depositsSinceLastUpdate;
// total number of shares received for tokens deposited into the staking pool since the last distribution
// total number of shares received for depositsSinceLastUpdate
uint256 private sharesSinceLastUpdate;

// list of all accounts that have ever queued tokens
Expand Down Expand Up @@ -102,6 +102,7 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl
error InvalidAmount();
error StatusAlreadySet();
error InsufficientLiquidity();
error WithdrawFailed();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand Down Expand Up @@ -238,8 +239,8 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl
if (msg.sender == address(token)) {
_deposit(_sender, _value, shouldQueue, data);
} else if (msg.sender == address(stakingPool)) {
uint256 amountQueued = _withdraw(_sender, _value, shouldQueue);
token.safeTransfer(_sender, _value - amountQueued);
uint256 amountWithdrawn = _withdraw(_sender, _value, shouldQueue, true);
token.safeTransfer(_sender, amountWithdrawn);
} else {
revert UnauthorizedToken();
}
Expand Down Expand Up @@ -314,7 +315,12 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl
address(this),
toWithdraw
);
toWithdraw = _withdraw(account, toWithdraw, _shouldQueueWithdrawal);
toWithdraw -= _withdraw(
account,
toWithdraw,
_shouldQueueWithdrawal,
toWithdraw == _amountToWithdraw
);
}

token.safeTransfer(account, _amountToWithdraw - toWithdraw);
Expand Down Expand Up @@ -440,8 +446,8 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl
}

/**
* @notice Returns the amount of new deposits into the staking pool since the last call to
* updateDistribution and the amount of shares received for those deposits
* @notice Returns the total number of tokens deposited into the staking pool or swapped for LSTs since
* the last call to updateDistribution and the amount of shares received for those tokens
* @return amount of deposits
* @return amount of shares
*/
Expand Down Expand Up @@ -654,16 +660,20 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl
* @param _account account to withdraw for
* @param _amount amount to withdraw
* @param _shouldQueueWithdrawal whether a withdrawal should be queued if the the full amount cannot be satisfied
* @return the amount of tokens that were queued for withdrawal
* @param _shouldRevertOnZero whether call should revert if nothing is withdrawn or queued for withdrawal
* @return the amount of tokens that were withdrawn
**/
function _withdraw(
address _account,
uint256 _amount,
bool _shouldQueueWithdrawal
bool _shouldQueueWithdrawal,
bool _shouldRevertOnZero
) internal returns (uint256) {
if (poolStatus == PoolStatus.CLOSED) revert WithdrawalsDisabled();

uint256 toWithdraw = _amount;
uint256 withdrawn;
uint256 queued;

if (totalQueued != 0) {
uint256 toWithdrawFromQueue = toWithdraw <= totalQueued ? toWithdraw : totalQueued;
Expand All @@ -672,15 +682,24 @@ contract PriorityPool is UUPSUpgradeable, OwnableUpgradeable, PausableUpgradeabl
depositsSinceLastUpdate += toWithdrawFromQueue;
sharesSinceLastUpdate += stakingPool.getSharesByStake(toWithdrawFromQueue);
toWithdraw -= toWithdrawFromQueue;
withdrawn = toWithdrawFromQueue;
}

if (toWithdraw != 0) {
if (!_shouldQueueWithdrawal) revert InsufficientLiquidity();
withdrawalPool.queueWithdrawal(_account, toWithdraw);

if (toWithdraw >= withdrawalPool.minWithdrawalAmount()) {
withdrawalPool.queueWithdrawal(_account, toWithdraw);
queued = toWithdraw;
} else {
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toWithdraw);
}
}

emit Withdraw(_account, _amount - toWithdraw);
return toWithdraw;
if (_shouldRevertOnZero && withdrawn + queued == 0) revert WithdrawFailed();
if (_amount != toWithdraw) emit Withdraw(_account, _amount - toWithdraw);

return withdrawn;
}

/**
Expand Down
37 changes: 35 additions & 2 deletions contracts/core/priorityPool/WithdrawalPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,39 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable {
emit Withdraw(owner, amountToWithdraw);
}

/**
* @notice Executes a group of fully finalized withdrawals
* @dev used by owner in the case that withdrawalBatchIdCutoff cannot be updated due to
* outstanding withdrawals
* @param _withdrawalIds list of withdrawal ids to execute
* @param _batchIds list of batch ids corresponding to withdrawal ids
*/
function forceWithdraw(
uint256[] calldata _withdrawalIds,
uint256[] calldata _batchIds
) external onlyOwner {
for (uint256 i = 0; i < _withdrawalIds.length; ++i) {
uint256 withdrawalId = _withdrawalIds[i];
Withdrawal memory withdrawal = queuedWithdrawals[_withdrawalIds[i]];
uint256 batchId = _batchIds[i];
WithdrawalBatch memory batch = withdrawalBatches[batchId];
address owner = withdrawalOwners[withdrawalId];

if (withdrawalId <= withdrawalBatches[batchId - 1].indexOfLastWithdrawal)
revert InvalidWithdrawalId();
if (withdrawalId > batch.indexOfLastWithdrawal) revert InvalidWithdrawalId();

uint256 amountToWithdraw = withdrawal.partiallyWithdrawableAmount +
(uint256(batch.stakePerShares) * uint256(withdrawal.sharesRemaining)) /
1e18;
delete queuedWithdrawals[withdrawalId];
delete withdrawalOwners[withdrawalId];

token.safeTransfer(owner, amountToWithdraw);
emit Withdraw(owner, amountToWithdraw);
}
}

/**
* @notice Queues a withdrawal of liquid staking tokens for an account
* @param _account address of account
Expand Down Expand Up @@ -386,11 +419,11 @@ contract WithdrawalPool is UUPSUpgradeable, OwnableUpgradeable {

// find the last batch where all withdrawals have no funds remaining
for (uint256 i = newWithdrawalBatchIdCutoff; i < numBatches; ++i) {
newWithdrawalBatchIdCutoff = i;

if (withdrawalBatches[i].indexOfLastWithdrawal >= newWithdrawalIdCutoff) {
break;
}

newWithdrawalBatchIdCutoff = i;
}

withdrawalIdCutoff = uint128(newWithdrawalIdCutoff);
Expand Down
9 changes: 7 additions & 2 deletions contracts/core/test/LSTMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ contract LSTMock is ERC677 {
return (super.balanceOf(_account) * mulitplierBasisPoints) / 10000;
}

function getSharesByStake(uint256 _amount) external view returns (uint256) {
function getSharesByStake(uint256 _amount) public view returns (uint256) {
return (_amount * 10000) / mulitplierBasisPoints;
}

function getStakeByShares(uint256 _sharesAmount) external view returns (uint256) {
function getStakeByShares(uint256 _sharesAmount) public view returns (uint256) {
return (_sharesAmount * mulitplierBasisPoints) / 10000;
}

function setMultiplierBasisPoints(uint256 _multiplierBasisPoints) external {
mulitplierBasisPoints = _multiplierBasisPoints;
}

function transferShares(address _recipient, uint256 _sharesAmount) external returns (bool) {
_transfer(msg.sender, _recipient, getStakeByShares(_sharesAmount));
return true;
}
}
2 changes: 1 addition & 1 deletion contracts/linkStaking/CommunityVCS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ contract CommunityVCS is VaultControllerStrategy {
uint128 _vaultDeploymentThreshold,
uint128 _vaultDeploymentAmount,
address _vaultDepositController
) public initializer {
) public reinitializer(2) {
if (address(token) == address(0)) {
__VaultControllerStrategy_init(
_token,
Expand Down
17 changes: 13 additions & 4 deletions contracts/linkStaking/FundFlowController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,12 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable {
totalDepositRoom[i] = depositRoom;

if (_vaultGroups[i] == curUnbondedVaultGroup) {
totalUnbonded = _getTotalUnbonded(vaults, numVaultGroups, _vaultGroups[i]);
totalUnbonded = _getTotalUnbonded(
vaults,
numVaultGroups,
_vaultGroups[i],
depositIndex
);
}
}

Expand Down Expand Up @@ -384,7 +389,8 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable {
uint256 nextGroupTotalUnbonded = _getTotalUnbonded(
vaults,
numVaultGroups,
_nextUnbondedVaultGroup
_nextUnbondedVaultGroup,
depositIndex
);

return (curGroupVaultsToUnbond, curGroupTotalDepositRoom, nextGroupTotalUnbonded);
Expand All @@ -396,6 +402,7 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable {
* @param _numVaultGroups total number of vault groups
* @param _vaultGroup index of vault group
* @param _vaultMaxDeposits max deposits per vault
* @param _depositIndex global deposit index
* @return total deposit room
* @return list of non-empty vaults
*/
Expand Down Expand Up @@ -434,16 +441,18 @@ contract FundFlowController is UUPSUpgradeable, OwnableUpgradeable {
* @param _vaults list of all vaults
* @param _numVaultGroups total number of vault groups
* @param _vaultGroup index of vault group
* @param _depositIndex global deposit index
* @return total unbonded
*/
function _getTotalUnbonded(
address[] memory _vaults,
uint256 _numVaultGroups,
uint256 _vaultGroup
uint256 _vaultGroup,
uint256 _depositIndex
) internal view returns (uint256) {
uint256 totalUnbonded;

for (uint256 i = _vaultGroup; i < _vaults.length; i += _numVaultGroups) {
for (uint256 i = _vaultGroup; i < _depositIndex; i += _numVaultGroups) {
if (!IVault(_vaults[i]).claimPeriodActive() || IVault(_vaults[i]).isRemoved()) continue;

totalUnbonded += IVault(_vaults[i]).getPrincipalDeposits();
Expand Down
4 changes: 3 additions & 1 deletion contracts/linkStaking/OperatorStakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,16 @@ contract OperatorStakingPool is Initializable, UUPSUpgradeable, OwnableUpgradeab

/**
* @notice Withdraws tokens
* @param _operator address of operator with withdraw for
* @param _operator address of operator to withdraw for
* @param _amount amount to withdraw
**/
function _withdraw(address _operator, uint256 _amount) private {
uint256 sharesAmount = lst.getSharesByStake(_amount);
shareBalances[_operator] -= sharesAmount;
totalShares -= sharesAmount;

lst.transferShares(_operator, sharesAmount);

emit Withdraw(_operator, _amount, sharesAmount);
}

Expand Down
15 changes: 14 additions & 1 deletion contracts/linkStaking/OperatorVCS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ contract OperatorVCS is VaultControllerStrategy {
uint256 _vaultMaxDeposits,
uint256 _operatorRewardPercentage,
address _vaultDepositController
) public reinitializer(3) {
) public reinitializer(4) {
if (address(token) == address(0)) {
__VaultControllerStrategy_init(
_token,
Expand Down Expand Up @@ -325,10 +325,23 @@ contract OperatorVCS is VaultControllerStrategy {
vaults[i] = vaults[i + 1];
}
vaults.pop();
delete vaultMapping[vault];

token.safeTransfer(address(stakingPool), token.balanceOf(address(this)));
}

/**
* @notice Manually unbonds a vault
* @dev a vault can only be manually unbonded if the operator has been removed from the
* Chainlink staking contract
* @param _index index of vault
*/
function unbondVault(uint256 _index) external onlyOwner {
IVault vault = vaults[_index];
if (!IVault(vault).isRemoved()) revert OperatorNotRemoved();
vaults[_index].unbond();
}

/**
* @notice Updates accounting for any number of vault groups
* @dev used to correct minor accounting errors that result from the removal or slashing
Expand Down
7 changes: 6 additions & 1 deletion contracts/linkStaking/base/VaultControllerStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ contract VaultDepositController is Strategy {
// if vault is empty and equal to withdrawal index, increment withdrawal index to the next vault in the group
if (deposits == 0 && vaultIndex == group.withdrawalIndex) {
group.withdrawalIndex += uint64(globalState.numVaultGroups);
if (group.withdrawalIndex > globalState.depositIndex) {
if (group.withdrawalIndex >= globalState.depositIndex) {
group.withdrawalIndex = uint64(groupIndex);
}
}
Expand Down Expand Up @@ -266,6 +266,11 @@ contract VaultDepositController is Strategy {
uint256 deposits = vault.getPrincipalDeposits();
uint256 canDeposit = _maxDeposits - deposits;

if (vault.isRemoved()) {
++i;
continue;
}

// cannot leave a vault with less than minimum deposits
if (deposits < _minDeposits && toDeposit < (_minDeposits - deposits)) {
break;
Expand Down
Loading

0 comments on commit f4b615f

Please sign in to comment.