Skip to content

Commit

Permalink
staking proxy contract
Browse files Browse the repository at this point in the history
  • Loading branch information
BkChoy committed Dec 5, 2024
1 parent 956d6ce commit e0fff67
Show file tree
Hide file tree
Showing 6 changed files with 800 additions and 0 deletions.
293 changes: 293 additions & 0 deletions contracts/core/StakingProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

import "./interfaces/IStakingPool.sol";
import "./interfaces/IPriorityPool.sol";
import "./interfaces/IWithdrawalPool.sol";
import "./interfaces/IERC677.sol";
import "./interfaces/ISDLPool.sol";

/**
* @title Staking Proxy
* @notice Enables a staker to deposit tokens and earn rewards without ever directly interacting with the LST contracts
* @dev When tokens are queued for deposit, the corresponding liquid staking tokens will be distributed using a merkle
* tree. The tree is updated once a certain threshold of LSTs is reached at which point LSTs can be claimed. In order
* for this contract to claim its LSTs (and execute some other funcion calls), merkle data must be passed as an argument.
* This data is stored on IPFS at the hash which can be queried from this contract. For some functions, this data can be
* used as is but for ones that require a merkle proof, a merkle tree must be generated using the IPFS data, then a merkle
* proof generated using the tree.
* @dev Data may or may not need to be passed when depositing and/or withdrawing depending on the underlying LST implementation.
* If data does need to be passed, it will need to be fetched from an external source specific to that LST.
*/
contract StakingProxy is UUPSUpgradeable, OwnableUpgradeable {
using SafeERC20Upgradeable for IERC20Upgradeable;

// address of asset token
IERC20Upgradeable public token;
// address of liquid staking token
IStakingPool public lst;
// address of priority pool
IPriorityPool public priorityPool;
// address of withdrawal pool
IWithdrawalPool public withdrawalPool;
// address of SDL pool
ISDLPool public sdlPool;

// address authorized to deposit/withdraw asset tokens
address public staker;

error SenderNotAuthorized();
error InvalidToken();
error InvalidValue();
error InvalidTokenId();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/**
* @notice Initializes the contract
* @param _token address of asset token
* @param _lst address of liquid staking token
* @param _priorityPool address of priority pool
* @param _withdrawalPool address of withdrawal pool
* @param _sdlPool address of SDL pool
* @param _staker address authorized to deposit/withdraw asset tokens
*/
function initialize(
address _token,
address _lst,
address _priorityPool,
address _withdrawalPool,
address _sdlPool,
address _staker
) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();

token = IERC20Upgradeable(_token);
lst = IStakingPool(_lst);
priorityPool = IPriorityPool(_priorityPool);
token.safeApprove(_priorityPool, type(uint256).max);
IERC20Upgradeable(_lst).safeApprove(_priorityPool, type(uint256).max);
withdrawalPool = IWithdrawalPool(_withdrawalPool);
sdlPool = ISDLPool(_sdlPool);
staker = _staker;
}

/**
* @notice Reverts if sender is not staker
*/
modifier onlyStaker() {
if (msg.sender != staker) revert SenderNotAuthorized();
_;
}

/**
* @notice Returns the IPFS hash of the current merkle distribution tree
* @dev returns CIDv0 with no prefix - prefix must be added and hash must be properly encoded offchain
* @return IPFS hash
*/
function getMerkleIPFSHash() external view returns (bytes32) {
return priorityPool.ipfsHash();
}

/**
* @notice Returns the total amount of liquid staking tokens held by this contract
* @dev excludes unclaims LSTs sitting in the priority pool
* @return total LSTs held by contract
*/
function getTotalDeposits() external view returns (uint256) {
return lst.balanceOf(address(this));
}

/**
* @notice Returns the total amount of tokens queued for deposit into the staking pool by this contract
* @param _distributionAmount amount as recorded in this contract's merkle tree entry (stored on IPFS)
* @return total tokens queued for deposit
*/
function getTotalQueuedForDeposit(uint256 _distributionAmount) external view returns (uint256) {
return priorityPool.getQueuedTokens(address(this), _distributionAmount);
}

/**
* @notice Returns the total amount of tokens queued for withdrawal from the staking pool by this contract
* @return total tokens queued for withdrawal
*/
function getTotalQueuedForWithdrawal() external view returns (uint256) {
return withdrawalPool.getAccountTotalQueuedWithdrawals(address(this));
}

/**
* @notice Returns the total amount of withdrawable tokens for this contract
* @param _distributionAmount amount as recorded in this contract's merkle tree entry (stored on IPFS)
* @return total amount withdrawable from the priority pool
* @return total amount withdrawable from the withdrawal pool
* @return withdrawal ids for withdrawal from withdrawal pool
* @return batch ids for withdrawal from withdrawal pool
*/
function getTotalWithdrawable(
uint256 _distributionAmount
) external view returns (uint256, uint256, uint256[] memory, uint256[] memory) {
(uint256[] memory withdrawalIds, uint256 withdrawable) = withdrawalPool
.getFinalizedWithdrawalIdsByOwner(address(this));
uint256[] memory batchIds = withdrawalPool.getBatchIds(withdrawalIds);

uint256 priorityPoolCanWithdraw = priorityPool.canWithdraw(
address(this),
_distributionAmount
);

return (priorityPoolCanWithdraw, withdrawable, withdrawalIds, batchIds);
}

/**
* @notice Returns the total amount of claimable liquid staking tokens for this contract
* @param _distributionSharesAmount shares amount as recorded in this contract's merkle tree entry (stored on IPFS)
* @return total claimable LSTs
*/
function getTotalClaimableLSTs(
uint256 _distributionSharesAmount
) external view returns (uint256) {
return priorityPool.getLSDTokens(address(this), _distributionSharesAmount);
}

/**
* @notice ERC677 implementation to receive deposits
* @param _sender address of sender
* @param _value value of transfer
* @param _calldata encoded deposit data
*/
function onTokenTransfer(address _sender, uint256 _value, bytes calldata _calldata) external {
if (msg.sender != address(token)) revert InvalidToken();
if (_sender != staker) revert SenderNotAuthorized();
if (_value == 0) revert InvalidValue();

bytes[] memory data = abi.decode(_calldata, (bytes[]));
IERC677(address(token)).transferAndCall(
address(priorityPool),
_value,
abi.encode(true, data)
);
}

/**
* @notice Deposits tokens and/or queues tokens for deposit into the staking pool
* @param _amount amount of tokens to deposit
* @param _data encoded deposit data
*/
function deposit(uint256 _amount, bytes[] calldata _data) external onlyStaker {
token.safeTransferFrom(msg.sender, address(this), _amount);
priorityPool.deposit(_amount, true, _data);
}

/**
* @notice Withdraws tokens and/or queues tokens for withdrawal from the staking pool
* @dev if there is any amount withdrawable from the withdrawal pool, the entire amount will be withdrawn
* even if it exceeds _amountToWithdraw
* @param _amountToWithdraw amount of tokens to withdraw
* @param _distributionAmount amount as recorded in this contract's merkle tree entry (stored on IPFS)
* @param _distributionSharesAmount shares amount as recorded in this contract's merkle tree entry (stored on IPFS)
* @param _merkleProof merkle proof for this contract's merkle tree entry (generated using IPFS data)
* @param _withdrawalIds list of withdrawal ids required if finalizing queued withdrawals
* @param _batchIds list of batch ids required if finalizing queued withdrawals
* @param _data encoded withdrawal data
*/
function withdraw(
uint256 _amountToWithdraw,
uint256 _distributionAmount,
uint256 _distributionSharesAmount,
bytes32[] calldata _merkleProof,
uint256[] calldata _withdrawalIds,
uint256[] calldata _batchIds,
bytes[] calldata _data
) external onlyStaker {
uint256 availableTokens;

if (_withdrawalIds.length != 0) {
withdrawalPool.withdraw(_withdrawalIds, _batchIds);
availableTokens = token.balanceOf(address(this));
}

if (availableTokens < _amountToWithdraw) {
priorityPool.withdraw(
_amountToWithdraw - availableTokens,
_distributionAmount,
_distributionSharesAmount,
_merkleProof,
true,
true,
_data
);
availableTokens = token.balanceOf(address(this));
}

token.safeTransfer(msg.sender, availableTokens);
}

/**
* @notice Claims liquid staking tokens from the priority pool
* @param _amount amount as recorded in this contract's merkle tree entry (stored on IPFS)
* @param _sharesAmount shares amount as recorded in this contract's merkle tree entry (stored on IPFS)
* @param _merkleProof merkle proof for this contract's merkle tree entry (generated from IPFS data)
*/
function claimLSTs(
uint256 _amount,
uint256 _sharesAmount,
bytes32[] calldata _merkleProof
) external {
priorityPool.claimLSDTokens(_amount, _sharesAmount, _merkleProof);
}

/**
* @notice Returns a list of reSDL token ids held by this contract
* @return list of token ids
*/
function getRESDLTokenIds() external view returns (uint256[] memory) {
return sdlPool.getLockIdsByOwner(address(this));
}

/**
* @notice Called when an reSDL token is transferred to this contract using safeTransfer
*/
function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) {
return this.onERC721Received.selector;
}

/**
* @notice Withdraws an reSDL token
* @param _tokenId id of token
* @param _receiver address to receive token
*/
function withdrawRESDLToken(uint256 _tokenId, address _receiver) external onlyOwner {
if (sdlPool.ownerOf(_tokenId) != address(this)) revert InvalidTokenId();
IERC721(address(sdlPool)).safeTransferFrom(address(this), _receiver, _tokenId);
}

/**
* @notice Claims rewards from the SDL Pool
* @dev rewards will be redistributed to the SDL Pool
* @param _tokens list of tokens to claim rewards for
*/
function claimRESDLRewards(address[] calldata _tokens) external {
sdlPool.withdrawRewards(_tokens);
for (uint256 i = 0; i < _tokens.length; ++i) {
uint256 balance = IERC20Upgradeable(_tokens[i]).balanceOf(address(this));
if (balance != 0) {
IERC20Upgradeable(_tokens[i]).safeTransfer(address(sdlPool), balance);
}
}
sdlPool.distributeTokens(_tokens);
}

/**
* @dev Checks authorization for contract upgrades
*/
function _authorizeUpgrade(address) internal override onlyOwner {}
}
30 changes: 30 additions & 0 deletions contracts/core/interfaces/IPriorityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,41 @@ interface IPriorityPool {

function poolStatus() external view returns (PoolStatus);

function ipfsHash() external view returns (bytes32);

function canWithdraw(
address _account,
uint256 _distributionAmount
) external view returns (uint256);

function getQueuedTokens(
address _account,
uint256 _distributionAmount
) external view returns (uint256);

function getLSDTokens(
address _account,
uint256 _distributionShareAmount
) external view returns (uint256);

function deposit(uint256 _amount, bool _shouldQueue, bytes[] calldata _data) external;

function withdraw(
uint256 _amountToWithdraw,
uint256 _amount,
uint256 _sharesAmount,
bytes32[] calldata _merkleProof,
bool _shouldUnqueue,
bool _shouldQueueWithdrawal,
bytes[] calldata _data
) external;

function claimLSDTokens(
uint256 _amount,
uint256 _sharesAmount,
bytes32[] calldata _merkleProof
) external;

function pauseForUpdate() external;

function setPoolStatus(PoolStatus _status) external;
Expand Down
2 changes: 2 additions & 0 deletions contracts/core/interfaces/ISDLPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface ISDLPool is IRewardsPoolController {

function ownerOf(uint256 _lockId) external view returns (address);

function getLockIdsByOwner(address _owner) external view returns (uint256[] memory);

function supportedTokens() external view returns (address[] memory);

function handleOutgoingRESDL(
Expand Down
10 changes: 10 additions & 0 deletions contracts/core/interfaces/IWithdrawalPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@ interface IWithdrawalPool {

function minWithdrawalAmount() external view returns (uint256);

function getAccountTotalQueuedWithdrawals(address _account) external view returns (uint256);

function getFinalizedWithdrawalIdsByOwner(
address _account
) external view returns (uint256[] memory, uint256);

function getBatchIds(uint256[] memory _withdrawalIds) external view returns (uint256[] memory);

function deposit(uint256 _amount) external;

function withdraw(uint256[] calldata _withdrawalIds, uint256[] calldata _batchIds) external;

function queueWithdrawal(address _account, uint256 _amount) external;

function performUpkeep(bytes calldata _performData) external;
Expand Down
11 changes: 11 additions & 0 deletions test/core/priorityPool/withdrawal-pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ describe('WithdrawalPool', () => {

assert.equal(fromEther(await stakingPool.balanceOf(withdrawalPool.target)), 1750)
assert.equal(fromEther(await withdrawalPool.getTotalQueuedWithdrawals()), 1750)
assert.equal(
fromEther(await withdrawalPool.getAccountTotalQueuedWithdrawals(accounts[0])),
1500
)
assert.equal(fromEther(await withdrawalPool.getAccountTotalQueuedWithdrawals(accounts[1])), 250)

assert.deepEqual(
(await withdrawalPool.getWithdrawalIdsByOwner(accounts[0])).map((id) => Number(id)),
[1, 3]
Expand Down Expand Up @@ -104,6 +110,11 @@ describe('WithdrawalPool', () => {
assert.equal(fromEther(await token.balanceOf(withdrawalPool.target)), 400)
assert.equal(fromEther(await stakingPool.balanceOf(withdrawalPool.target)), 1350)
assert.equal(fromEther(await withdrawalPool.getTotalQueuedWithdrawals()), 1350)
assert.equal(
fromEther(await withdrawalPool.getAccountTotalQueuedWithdrawals(accounts[0])),
1100
)
assert.equal(fromEther(await withdrawalPool.getAccountTotalQueuedWithdrawals(accounts[1])), 250)
assert.equal(Number(await withdrawalPool.indexOfNextWithdrawal()), 1)
assert.deepEqual(
(await withdrawalPool.getWithdrawals([1, 2, 3])).map((d: any) => [
Expand Down
Loading

0 comments on commit e0fff67

Please sign in to comment.