Skip to content

Commit

Permalink
add withdraw native ETH functions in NDC
Browse files Browse the repository at this point in the history
  • Loading branch information
gus-stdr committed Feb 13, 2024
1 parent 5acc61e commit 026d228
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 45 deletions.
64 changes: 35 additions & 29 deletions contracts/LRTDepositPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -250,33 +250,7 @@ contract LRTDepositPool is ILRTDepositPool, LRTConfigRoleChecker, PausableUpgrad
/// @dev only callable by LRT admin
/// @param nodeDelegatorAddress NodeDelegator contract address
function removeNodeDelegatorContractFromQueue(address nodeDelegatorAddress) public onlyLRTAdmin {
// 1. revert if node delegator contract has asset in eigenlayer asset strategies

// 1.1 check if NDC has native ETH balance in eigen layer
if (INodeDelegator(nodeDelegatorAddress).getETHEigenPodBalance() > 0) {
revert NodeDelegatorHasETHInEigenlayer();
}

// 1.2 check if NDC has LST balance in eigen layer
(address[] memory assets, uint256[] memory assetBalances) =
INodeDelegator(nodeDelegatorAddress).getAssetBalances();

uint256 assetsLength = assets.length;
for (uint256 i; i < assetsLength;) {
if (assetBalances[i] > 0) {
revert NodeDelegatorHasAssetBalance(assets[i], assetBalances[i]);
}

if (IERC20(assets[i]).balanceOf(nodeDelegatorAddress) > 0) {
revert NodeDelegatorHasAssetBalance(assets[i], IERC20(assets[i]).balanceOf(nodeDelegatorAddress));
}

unchecked {
++i;
}
}

// 2. remove node delegator contract from queue
// 1. check if node delegator contract is in queue
uint256 length = nodeDelegatorQueue.length;
uint256 ndcIndex;

Expand All @@ -286,6 +260,7 @@ contract LRTDepositPool is ILRTDepositPool, LRTConfigRoleChecker, PausableUpgrad
break;
}

// 1.1 If node delegator contract is not found in queue, revert
if (i == length - 1) {
revert NodeDelegatorNotFound();
}
Expand All @@ -295,9 +270,40 @@ contract LRTDepositPool is ILRTDepositPool, LRTConfigRoleChecker, PausableUpgrad
}
}

// 2.1 remove from isNodeDelegator mapping
// 2. revert if node delegator contract has any asset balances.

// 2.1 check if NDC has native ETH balance in eigen layer and in itself.
if (
INodeDelegator(nodeDelegatorAddress).getETHEigenPodBalance() > 0
|| address(nodeDelegatorAddress).balance > 0
) {
revert NodeDelegatorHasETH();
}

// 2.2 check if NDC has LST balance
address[] memory supportedAssets = lrtConfig.getSupportedAssetList();
uint256 supportedAssetsLength = supportedAssets.length;

uint256 assetBalance;
for (uint256 i; i < supportedAssetsLength; i++) {
if (supportedAssets[i] == LRTConstants.ETH_TOKEN) {
// ETH already checked above.
continue;
}

assetBalance = IERC20(supportedAssets[i]).balanceOf(nodeDelegatorAddress)
+ INodeDelegator(nodeDelegatorAddress).getAssetBalance(supportedAssets[i]);

if (assetBalance > 0) {
revert NodeDelegatorHasAssetBalance(supportedAssets[i], assetBalance);
}
}

// 3. remove node delegator contract from queue

// 3.1 remove from isNodeDelegator mapping
isNodeDelegator[nodeDelegatorAddress] = 0;
// 2.2 remove from nodeDelegatorQueue
// 3.2 remove from nodeDelegatorQueue
nodeDelegatorQueue[ndcIndex] = nodeDelegatorQueue[length - 1];
nodeDelegatorQueue.pop();

Expand Down
37 changes: 30 additions & 7 deletions contracts/NodeDelegator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LRTConfigRoleChecker, ILRTConfig } from "./utils/LRTConfigRoleChecker.s
import { INodeDelegator } from "./interfaces/INodeDelegator.sol";
import { IStrategy } from "./interfaces/IStrategy.sol";
import { IEigenStrategyManager } from "./interfaces/IEigenStrategyManager.sol";
import { IEigenDelayedWithdrawalRouter } from "./interfaces/IEigenDelayedWithdrawalRouter.sol";

import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
Expand Down Expand Up @@ -161,9 +162,6 @@ contract NodeDelegator is INodeDelegator, LRTConfigRoleChecker, PausableUpgradea
// feature is activated. Additionally, ensure verification of both staked but unverified and staked and verified
// ETH native supply NDCs as provided to Eigenlayer.
ethStaked = stakedButUnverifiedNativeETH;
if (address(eigenPod) != address(0)) {
ethStaked += address(eigenPod).balance;
}
}

/// @notice Stake ETH from NDC into EigenLayer. it calls the stake function in the EigenPodManager
Expand All @@ -173,12 +171,14 @@ contract NodeDelegator is INodeDelegator, LRTConfigRoleChecker, PausableUpgradea
/// @param depositDataRoot The deposit data root of the validator
/// @dev Only LRT Operator should call this function
/// @dev Exactly 32 ether is allowed, hence it is hardcoded
/// @dev offchain checks withdraw credentials authenticity
function stake32Eth(
bytes calldata pubkey,
bytes calldata signature,
bytes32 depositDataRoot
)
external
whenNotPaused
onlyLRTOperator
{
// Call the stake function in the EigenPodManager
Expand All @@ -191,6 +191,31 @@ contract NodeDelegator is INodeDelegator, LRTConfigRoleChecker, PausableUpgradea
emit ETHStaked(pubkey, 32 ether);
}

/// @dev initiate a delayed withdraw of the ETH before the eigenpod is verified
/// which will be available to claim after withdrawalDelay blocks
function initiateWithdrawRewards() external onlyLRTOperator {
uint256 eigenPodBalance = address(eigenPod).balance;
uint256 ethValidatorMinBalanceThreshold = 16 ether;
if (eigenPodBalance > ethValidatorMinBalanceThreshold) {
revert InvalidRewardAmount();
}

eigenPod.withdrawBeforeRestaking();
emit ETHRewardsWithdrawInitiated(eigenPodBalance);
}

/// @dev claims back the withdrawal amount initiated to this nodeDelegator contract
/// once withdrawal amount is claimable
function claimRewards(uint256 maxNumberOfDelayedWithdrawalsToClaim) external onlyLRTOperator {
uint256 balanceBefore = address(this).balance;
address delayedRouterAddr = eigenPod.delayedWithdrawalRouter();
IEigenDelayedWithdrawalRouter elDelayedRouter = IEigenDelayedWithdrawalRouter(delayedRouterAddr);
elDelayedRouter.claimDelayedWithdrawals(address(this), maxNumberOfDelayedWithdrawalsToClaim);
uint256 balanceAfter = address(this).balance;

emit ETHRewardsClaimed(balanceAfter - balanceBefore);
}

/// @dev Triggers stopped state. Contract must not be paused.
function pause() external onlyLRTManager {
_pause();
Expand All @@ -212,8 +237,6 @@ contract NodeDelegator is INodeDelegator, LRTConfigRoleChecker, PausableUpgradea
emit ETHDepositFromDepositPool(msg.value);
}

/// @dev allow NodeDelegator to receive ETH rewards
receive() external payable {
emit ETHRewardsReceived(msg.value);
}
/// @dev allow NodeDelegator to receive ETH
receive() external payable { }
}
6 changes: 6 additions & 0 deletions contracts/interfaces/IEigenDelayedWithdrawalRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.21;

interface IEigenDelayedWithdrawalRouter {
function claimDelayedWithdrawals(address recipient, uint256 maxNumberOfDelayedWithdrawalsToClaim) external;
}
7 changes: 7 additions & 0 deletions contracts/interfaces/IEigenPod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ library BeaconChainProofs {
}

interface IEigenPod {
/// @return delayedWithdrawalRouter address of eigenlayer delayedWithdrawalRouter,
/// which does book keeping of delayed withdrawls
function delayedWithdrawalRouter() external returns (address);

/// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false
function withdrawBeforeRestaking() external;

/**
* @notice This function verifies that the withdrawal credentials of the podOwner are pointed to
* this contract. It also verifies the current (not effective) balance of the validator. It verifies the provided
Expand Down
5 changes: 3 additions & 2 deletions contracts/interfaces/ILRTConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface ILRTConfig {
error CallerNotLRTConfigManager();
error CallerNotLRTConfigOperator();
error CallerNotLRTConfigAllowedRole(string role);
error CannotUpdateStrategyAsItHasFundsNDCFunds(address ndc, uint256 amount);
error InvalidMaxRewardAmount();

// Events
event SetToken(bytes32 key, address indexed tokenAddr);
Expand All @@ -19,8 +21,7 @@ interface ILRTConfig {
event AssetDepositLimitUpdate(address indexed asset, uint256 depositLimit);
event AssetStrategyUpdate(address indexed asset, address indexed strategy);
event SetRSETH(address indexed rsETH);

error CannotUpdateStrategyAsItHasFundsNDCFunds(address ndc, uint256 amount);
event UpdateMaxRewardAmount(uint256 maxRewardAmount);

// methods

Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/ILRTDepositPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface ILRTDepositPool {
error MinimumAmountToReceiveNotMet();
error NodeDelegatorNotFound();
error NodeDelegatorHasAssetBalance(address assetAddress, uint256 assetBalance);
error NodeDelegatorHasETHInEigenlayer();
error NodeDelegatorHasETH();

//events
event MaxNodeDelegatorLimitUpdated(uint256 maxNodeDelegatorLimit);
Expand Down
4 changes: 3 additions & 1 deletion contracts/interfaces/INodeDelegator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ interface INodeDelegator {
event ETHDepositFromDepositPool(uint256 depositAmount);
event EigenPodCreated(address indexed eigenPod, address indexed podOwner);
event ETHStaked(bytes valPubKey, uint256 amount);
event ETHRewardsReceived(uint256 amount);
event ETHRewardsClaimed(uint256 amount);
event ETHRewardsWithdrawInitiated(uint256 amount);

// errors
error TokenTransferFailed();
error StrategyIsNotSetForAsset();
error InvalidETHSender();
error InvalidRewardAmount();

// getter
function stakedButUnverifiedNativeETH() external view returns (uint256);
Expand Down
2 changes: 1 addition & 1 deletion contracts/utils/LRTConstants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ library LRTConstants {
bytes32 public constant EIGEN_POD_MANAGER = keccak256("EIGEN_POD_MANAGER");

// native ETH as ERC20 for ease of implementation
address public constant ETH_TOKEN = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
address public constant ETH_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

// Operator Role
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
Expand Down
9 changes: 6 additions & 3 deletions test/LRTDepositPoolTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ contract LRTOracleMock {
contract MockNodeDelegator {
address[] public assets;
uint256[] public assetBalances;
uint256 public mockAssetBalance;

uint256 private _stakedButUnverifiedNativeETH;
uint256 private _eigenPodBalance;

constructor(address[] memory _assets, uint256[] memory _assetBalances) {
assets = _assets;
assetBalances = _assetBalances;
mockAssetBalance = 1e18;
}

function getAssetBalance(address) external pure returns (uint256) {
return 1e18;
function getAssetBalance(address) external view returns (uint256) {
return mockAssetBalance;
}

function getAssetBalances() external view returns (address[] memory, uint256[] memory) {
Expand All @@ -47,6 +49,7 @@ contract MockNodeDelegator {
function removeAssetBalance() external {
assetBalances[0] = 0;
assetBalances[1] = 0;
mockAssetBalance = 0;
}

function transferBackToLRTDepositPool(address asset, uint256 amount) external {
Expand Down Expand Up @@ -637,7 +640,7 @@ contract LRTDepositPoolAddNodeDelegatorContractToQueue is LRTDepositPoolTest {
}
}

contract LTRRemoveNodeDelegatorFromQueue is LRTDepositPoolTest {
contract LRTDepositPoolRemoveNodeDelegatorFromQueue is LRTDepositPoolTest {
address public nodeDelegatorContractOne;
address public nodeDelegatorContractTwo;
address public nodeDelegatorContractThree;
Expand Down
50 changes: 49 additions & 1 deletion test/integration/LRTNativeEthStakingIntegrationTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ contract LRTNativeEthStakingIntegrationTest is Test {

// add oracle for ETH
address oneETHOracle = address(new OneETHPriceOracle());
vm.prank(manager);
vm.startPrank(manager);
lrtOracle.updatePriceOracleFor(LRTConstants.ETH_TOKEN, oneETHOracle);
vm.stopPrank();
}

function setUp() public {
Expand Down Expand Up @@ -197,6 +198,53 @@ contract LRTNativeEthStakingIntegrationTest is Test {
);
}

function test_withdrawRewards() external {
// create eigen pod
vm.prank(manager);
nodeDelegator1.createEigenPod();

address eigenPod = address(nodeDelegator1.eigenPod());
// same eigenPod address should be created
assertEq(eigenPod, 0xf7483e448c1B94Ea557A53d99ebe7b4feE0c91df, "Wrong eigenPod address");

// stake 32 eth for validator1
bytes memory pubkey =
hex"8ff0088bf2bc73a41c74d1b1c6c997e4963ceffde55a09fef27596016d919b74b45372e8aa69fda5aac38a0c1a38dfd5";
bytes memory signature = hex"95e07ee28de0316ecdf9b528c222d81242898ee0095e284582bb453d331b7760"
hex"6d8dca23ab8980459ea8a9b9710e2f740fceb1a1c221a7fd75eb3ef4a6b68809"
hex"f3e76387f01f5d31718e6306375b20b29cb08d1374c7fb125d50c1b2f5a5cc0b";

bytes32 depositDataRoot = hex"6f30f44f0d8dada6ba5d8fd617c727020c01c697587d1a04ff6661be656198bc";

vm.deal(address(nodeDelegator1), 32 ether);
vm.startPrank(operator);
nodeDelegator1.stake32Eth(pubkey, signature, depositDataRoot);

// eigenPod receives some rewards
uint256 rewardsAmount = 2.5 ether;
vm.deal(address(eigenPod), rewardsAmount);

assertEq(address(eigenPod).balance, rewardsAmount);

nodeDelegator1.initiateWithdrawRewards();

// rewards moves to delayedWithdrawalRouter
assertEq(address(eigenPod).balance, 0);

console.log("block number before vm.roll: ", block.number);
// set block to 7 days after so that rewards can be claimed
vm.roll(block.number + 50_400);
console.log("block number after vm.roll: ", block.number);

uint256 ndcBalanceBefore = address(nodeDelegator1).balance;

nodeDelegator1.claimRewards(1);

assertEq(address(nodeDelegator1).balance, ndcBalanceBefore + rewardsAmount);

vm.stopPrank();
}

function test_removeNDCs() external {
// ------ STEP1: add NDCs ---------

Expand Down

0 comments on commit 026d228

Please sign in to comment.