Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native link withdrawals resolutions #129

Merged
merged 16 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading