Skip to content

Commit

Permalink
Merge pull request #162 from yieldnest/feature/fix-rewards-withdrawals
Browse files Browse the repository at this point in the history
add reward handling param + use latest redemption rate only
  • Loading branch information
danoctavian authored Sep 2, 2024
2 parents e7f0480 + 1bf13c8 commit 7185573
Show file tree
Hide file tree
Showing 13 changed files with 542 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/StakingNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea
}

//--------------------------------------------------------------------------------------
//---------------------------------- EXPEDITED WITHDRAWAL ---------------------------
//---------------------------------- SURPLUS WITHDRAWAL ---------------------------
//--------------------------------------------------------------------------------------

/**
Expand Down
53 changes: 46 additions & 7 deletions src/StakingNodesManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface StakingNodesManagerEvents {
event RegisteredStakingNodeImplementationContract(address upgradeableBeaconAddress, address implementationContract);
event UpgradedStakingNodeImplementationContract(address implementationContract, uint256 nodesCount);
event NodeInitialized(address nodeAddress, uint64 initializedVersion);
event PrincipalWithdrawalProcessed(uint256 nodeId, uint256 amountToReinvest, uint256 amountToQueue);
event PrincipalWithdrawalProcessed(uint256 nodeId, uint256 amountToReinvest, uint256 amountToQueue, uint256 rewardsAmount);
event ETHReceived(address sender, uint256 amount);
}

Expand Down Expand Up @@ -55,6 +55,8 @@ contract StakingNodesManager is
error NoValidatorsProvided();
error ValidatorRegistrationPaused();
error InvalidRewardsType(RewardsType rewardsType);
error ValidatorUnused(bytes publicKey);
error ValidatorNotWithdrawn(bytes publicKey, IEigenPod.VALIDATOR_STATUS status);

//--------------------------------------------------------------------------------------
//---------------------------------- ROLES -------------------------------------------
Expand Down Expand Up @@ -507,8 +509,10 @@ contract StakingNodesManager is
if (address(nodes[nodeId]) != msg.sender) {
revert NotStakingNode(msg.sender, nodeId);
}
_processRewards(nodeId, rewardsType, msg.value);
}

uint256 rewards = msg.value;
function _processRewards(uint256 nodeId, RewardsType rewardsType, uint256 rewards) internal {
IRewardsReceiver receiver;

if (rewardsType == RewardsType.ConsensusLayer) {
Expand All @@ -524,7 +528,7 @@ contract StakingNodesManager is
revert TransferFailed();
}

emit WithdrawnETHRewardsProcessed(nodeId, rewardsType, msg.value);
emit WithdrawnETHRewardsProcessed(nodeId, rewardsType, rewards);
}

/**
Expand All @@ -548,15 +552,32 @@ contract StakingNodesManager is
uint256 amountToReinvest = action.amountToReinvest;
uint256 amountToQueue = action.amountToQueue;

// Calculate the total amount to be processed by summing reinvestment and queuing amounts
uint256 totalAmount = amountToReinvest + amountToQueue;
// The rewardsAmount is trusted off-chain input provided in the WithdrawalAction struct.
// It represents the portion of the withdrawn amount that is considered as rewards.
// This value is determined off-chain by analyzing the difference between
// the initial stake and the total withdrawn amount.
//
// This design trade-off is a result of how Eigenlayer M3 pepe no long providees
// clear separation between principal and rewards amount and they both exit through the
// Queued Withdrawals mechanism.
//
// SECURITY NOTE:
// The accuracy and integrity of this value relies on the off-chain process
// that calculates it. There's an implicit trust that the WITHDRAWAL_MANAGER_ROLE
// will provide correct and verified data and that principal is not counted as Rewards
// and applied a fee.
uint256 rewardsAmount = action.rewardsAmount;

// Calculate the total amount to be processed by summing reinvestment, rewards and queuing amounts
uint256 totalAmount = amountToReinvest + amountToQueue + rewardsAmount;

// Retrieve the staking node object using the nodeId
IStakingNode node = nodes[nodeId];

// Deallocate the specified total amount of ETH from the staking node
node.deallocateStakedETH(totalAmount);


// If there is an amount specified to reinvest, process it through ynETH
if (amountToReinvest > 0) {
ynETH.processWithdrawnETH{value: amountToReinvest}();
Expand All @@ -569,11 +590,29 @@ contract StakingNodesManager is
revert TransferFailed();
}
}

// If there is an amount of rewards specified, handle that
if (rewardsAmount > 0) {

// IMPORTANT: Impact on totalAssets()
// After charging the rewards fee, the totalAssets() of the system may decrease.
// Steps:
// 1. The full rewardsAmount is removed from the staking node's balance (which is part of totalAssets).
// 2. Only the remainingRewards (after fees) are reinvested back to the system.
// 3. The fees are sent to a separate fee receiver and are no longer part of the system's totalAssets.

(bool sent, ) = address(rewardsDistributor.consensusLayerReceiver()).call{value: rewardsAmount}("");
if (!sent) {
revert TransferFailed();
}
// process rewards immediately to avoid large totalAssets() fluctuations
rewardsDistributor.processRewards();
}

// Emit an event to log the processed principal withdrawal details
emit PrincipalWithdrawalProcessed(nodeId, amountToReinvest, amountToQueue);
emit PrincipalWithdrawalProcessed(nodeId, amountToReinvest, amountToQueue, rewardsAmount);
}


//--------------------------------------------------------------------------------------
//---------------------------------- VIEWS -------------------------------------------
//--------------------------------------------------------------------------------------
Expand Down
36 changes: 28 additions & 8 deletions src/WithdrawalQueueManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,26 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr
//---------------------------------- WITHDRAWAL REQUESTS -----------------------------
//--------------------------------------------------------------------------------------


/**
* @notice Requests a withdrawal of a specified amount of redeemable assets without additional data.
* @dev This is a convenience function that calls the main requestWithdrawal function with empty data.
* @param amount The amount of redeemable assets to withdraw.
* @return tokenId The token ID associated with the withdrawal request.
*/
function requestWithdrawal(uint256 amount) external returns (uint256 tokenId) {
return requestWithdrawal(amount, "");
}

/**
* @notice Requests a withdrawal of a specified amount of redeemable assets.
* @dev Transfers the specified amount of redeemable assets from the sender to this contract, creates a withdrawal request,
* and mints a token representing this request. Emits a WithdrawalRequested event upon success.
* @param amount The amount of redeemable assets to withdraw.
* @param data Extra data payload associated with the request
* @return tokenId The token ID associated with the withdrawal request.
*/
function requestWithdrawal(uint256 amount) external nonReentrant returns (uint256 tokenId) {
function requestWithdrawal(uint256 amount, bytes memory data) public nonReentrant returns (uint256 tokenId) {
if (amount == 0) {
revert AmountMustBeGreaterThanZero();
}
Expand All @@ -174,7 +186,8 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr
feeAtRequestTime: withdrawalFee,
redemptionRateAtRequestTime: currentRate,
creationTimestamp: block.timestamp,
processed: false
processed: false,
data: data
});

pendingRequestedRedemptionAmount += calculateRedemptionAmount(amount, currentRate);
Expand Down Expand Up @@ -214,7 +227,14 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr
}

withdrawalRequests[tokenId].processed = true;
uint256 unitOfAccountAmount = calculateRedemptionAmount(request.amount, request.redemptionRateAtRequestTime);

// Redemption rate at claim time is the minimum between
// the redemption rate at request time and the current redemption Rate
uint256 currentRate = redemptionAssetsVault.redemptionRate();
uint256 redemptionRate = request.redemptionRateAtRequestTime < currentRate ? request.redemptionRateAtRequestTime : currentRate;

uint256 unitOfAccountAmount = calculateRedemptionAmount(request.amount, redemptionRate);

pendingRequestedRedemptionAmount -= unitOfAccountAmount;

_burn(tokenId);
Expand All @@ -228,10 +248,10 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr
}

// Transfer net amount (unitOfAccountAmount - feeAmount) to the receiver
redemptionAssetsVault.transferRedemptionAssets(receiver, unitOfAccountAmount - feeAmount);
redemptionAssetsVault.transferRedemptionAssets(receiver, unitOfAccountAmount - feeAmount, request.data);

if (feeAmount > 0) {
redemptionAssetsVault.transferRedemptionAssets(feeReceiver, feeAmount);
redemptionAssetsVault.transferRedemptionAssets(feeReceiver, feeAmount, request.data);
}

emit WithdrawalClaimed(tokenId, msg.sender, receiver, request);
Expand Down Expand Up @@ -286,14 +306,14 @@ contract WithdrawalQueueManager is IWithdrawalQueueManager, ERC721EnumerableUpgr
/**
* @notice Calculates the redemption amount based on the provided amount and the redemption rate at the time of request.
* @param amount The amount of the redeemable asset.
* @param redemptionRateAtRequestTime The redemption rate at the time the request was made, expressed in the same unit of decimals as the redeemable asset.
* @param redemptionRate The redemption rate expressed in the same unit of decimals as the redeemable asset.
* @return The calculated redemption amount, adjusted for the decimal places of the redeemable asset.
*/
function calculateRedemptionAmount(
uint256 amount,
uint256 redemptionRateAtRequestTime
uint256 redemptionRate
) public view returns (uint256) {
return amount * redemptionRateAtRequestTime / (10 ** redeemableAsset.decimals());
return amount * redemptionRate / (10 ** redeemableAsset.decimals());
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/IRedemptionAssetsVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ interface IRedemptionAssetsVault {
/// @dev This is only for INTERNAL USE
/// @param to The address to which the assets will be transferred.
/// @param amount The amount in unit of account
function transferRedemptionAssets(address to, uint256 amount) external;
/// @param data Extra data payload for redemption request
function transferRedemptionAssets(address to, uint256 amount, bytes calldata data) external;

/// @notice Withdraws redemption assets from the queue's balance
/// @param amount The amount in unit of account
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IStakingNodesManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface IStakingNodesManager {
uint256 nodeId;
uint256 amountToReinvest;
uint256 amountToQueue;
uint256 rewardsAmount;
}

function eigenPodManager() external view returns (IEigenPodManager);
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/IWithdrawalQueueManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ interface IWithdrawalQueueManager {
uint256 redemptionRateAtRequestTime;
uint256 creationTimestamp;
bool processed;
bytes data;
}

function requestWithdrawal(uint256 amount) external returns (uint256);
function requestWithdrawal(uint256 amount, bytes calldata data) external returns (uint256);
function claimWithdrawal(uint256 tokenId, address receiver) external;
function finalizeRequestsUpToIndex(uint256 _lastFinalizedIndex) external;
}
2 changes: 1 addition & 1 deletion src/ynETHRedemptionAssetsVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ contract ynETHRedemptionAssetsVault is IRedemptionAssetsVault, Initializable, Ac
* @param amount The amount of assets to transfer.
* @dev Requires the caller to be the redeemer and the contract to not be paused.
*/
function transferRedemptionAssets(address to, uint256 amount) public onlyRedeemer whenNotPaused nonReentrant {
function transferRedemptionAssets(address to, uint256 amount, bytes calldata /* data */) public onlyRedeemer whenNotPaused nonReentrant {
uint256 balance = availableRedemptionAssets();
if (balance < amount) {
revert InsufficientAssetBalance(ETH_ASSET, amount, balance);
Expand Down
69 changes: 69 additions & 0 deletions test/integration/M3/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ import {RewardsDistributor} from "../../../src/RewardsDistributor.sol";
import {StakingNode} from "../../../src/StakingNode.sol";
import {WithdrawalQueueManager} from "../../../src/WithdrawalQueueManager.sol";
import {ynETHRedemptionAssetsVault} from "../../../src/ynETHRedemptionAssetsVault.sol";
import {IStakingNode} from "../../../src/interfaces/IStakingNodesManager.sol";


import "forge-std/console.sol";
import "forge-std/Test.sol";

contract Base is Test, Utils {

bytes public constant ZERO_SIGNATURE = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
bytes constant ZERO_PUBLIC_KEY = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";

// Utils
ContractAddresses public contractAddresses;
ContractAddresses.ChainAddresses public chainAddresses;
Expand Down Expand Up @@ -214,4 +219,68 @@ contract Base is Test, Utils {
vm.stopPrank();
}
}

function createValidators(uint256[] memory nodeIds, uint256 count) public returns (uint40[] memory) {
uint40[] memory validatorIndices = new uint40[](count * nodeIds.length);
uint256 index = 0;

for (uint256 j = 0; j < nodeIds.length; j++) {
bytes memory withdrawalCredentials = stakingNodesManager.getWithdrawalCredentials(nodeIds[j]);

for (uint256 i = 0; i < count; i++) {
validatorIndices[index] = beaconChain.newValidator{value: 32 ether}(withdrawalCredentials);
index++;
}
}
return validatorIndices;
}

function registerValidators(uint256[] memory validatorNodeIds) public {
IStakingNodesManager.ValidatorData[] memory validatorData = new IStakingNodesManager.ValidatorData[](validatorNodeIds.length);

for (uint256 i = 0; i < validatorNodeIds.length; i++) {
bytes memory publicKey = abi.encodePacked(uint256(i));
publicKey = bytes.concat(publicKey, new bytes(ZERO_PUBLIC_KEY.length - publicKey.length));
validatorData[i] = IStakingNodesManager.ValidatorData({
publicKey: publicKey,
signature: ZERO_SIGNATURE,
nodeId: validatorNodeIds[i],
depositDataRoot: bytes32(0)
});
}

for (uint256 i = 0; i < validatorData.length; i++) {
uint256 amount = 32 ether;
bytes memory withdrawalCredentials = stakingNodesManager.getWithdrawalCredentials(validatorData[i].nodeId);
bytes32 depositDataRoot = stakingNodesManager.generateDepositRoot(validatorData[i].publicKey, validatorData[i].signature, withdrawalCredentials, amount);
validatorData[i].depositDataRoot = depositDataRoot;
}

vm.prank(actors.ops.VALIDATOR_MANAGER);
stakingNodesManager.registerValidators(validatorData);
}

function runSystemStateInvariants(
uint256 previousTotalAssets,
uint256 previousTotalSupply,
uint256[] memory previousStakingNodeBalances
) public {
assertEq(yneth.totalAssets(), previousTotalAssets, "Total assets integrity check failed");
assertEq(yneth.totalSupply(), previousTotalSupply, "Share mint integrity check failed");
for (uint i = 0; i < previousStakingNodeBalances.length; i++) {
IStakingNode stakingNodeInstance = stakingNodesManager.nodes(i);
uint256 currentStakingNodeBalance = stakingNodeInstance.getETHBalance();
assertEq(currentStakingNodeBalance, previousStakingNodeBalances[i], "Staking node balance integrity check failed for node ID: ");
}
}

function getAllStakingNodeBalances() public view returns (uint256[] memory) {
uint256[] memory balances = new uint256[](stakingNodesManager.nodesLength());
for (uint256 i = 0; i < stakingNodesManager.nodesLength(); i++) {
IStakingNode stakingNode = stakingNodesManager.nodes(i);
balances[i] = stakingNode.getETHBalance();
}
return balances;
}

}
4 changes: 2 additions & 2 deletions test/integration/M3/Withdrawals.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ contract M3WithdrawalsTest is Base {
uint256 public nodeId;

uint256 public constant AMOUNT = 32 ether;
bytes public constant ZERO_SIGNATURE = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";

//
// setup
Expand Down Expand Up @@ -230,7 +229,8 @@ contract M3WithdrawalsTest is Base {
_actions[0] = IStakingNodesManager.WithdrawalAction({
nodeId: nodeId,
amountToReinvest: AMOUNT / 2, // 16 ETH
amountToQueue: AMOUNT / 2 // 16 ETH
amountToQueue: AMOUNT / 2, // 16 ETH
rewardsAmount: 0
});
vm.prank(actors.ops.WITHDRAWAL_MANAGER);
stakingNodesManager.processPrincipalWithdrawals({
Expand Down
Loading

0 comments on commit 7185573

Please sign in to comment.