Skip to content

Commit

Permalink
feat: claim with nft (#25)
Browse files Browse the repository at this point in the history
* feat: claim with nft

* fix: after merge
  • Loading branch information
vgorkavenko authored Oct 27, 2023
1 parent 8049c0d commit 959a4cb
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 5 deletions.
47 changes: 47 additions & 0 deletions src/CSAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ICSModule } from "./interfaces/ICSModule.sol";
import { ILido } from "./interfaces/ILido.sol";
import { IWstETH } from "./interfaces/IWstETH.sol";
import { ICSFeeDistributor } from "./interfaces/ICSFeeDistributor.sol";
import { IWithdrawalQueue } from "./interfaces/IWithdrawalQueue.sol";

contract CSAccountingBase {
event ETHBondDeposited(
Expand Down Expand Up @@ -42,6 +43,11 @@ contract CSAccountingBase {
address to,
uint256 amount
);
event ETHRewardsRequested(
uint256 indexed nodeOperatorId,
address to,
uint256 amount
);
}

contract CSAccounting is CSAccountingBase, AccessControlEnumerable {
Expand Down Expand Up @@ -548,6 +554,43 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable {
emit WstETHRewardsClaimed(nodeOperatorId, rewardAddress, wstETHAmount);
}

/// @notice Request full reward (fee + bond) in Withdrawal NFT (unstETH) for the given node operator available for this moment.
/// @dev reverts if amount isn't between MIN_STETH_WITHDRAWAL_AMOUNT and MAX_STETH_WITHDRAWAL_AMOUNT
/// @param rewardsProof merkle proof of the rewards.
/// @param nodeOperatorId id of the node operator to request rewards for.
/// @param cumulativeFeeShares cummulative fee shares for the node operator.
/// @return requestIds an array of the created withdrawal request ids
function requestRewardsETH(
bytes32[] memory rewardsProof,
uint256 nodeOperatorId,
uint256 cumulativeFeeShares,
uint256 ETHAmount
) external returns (uint256[] memory requestIds) {
(
address managerAddress,
address rewardAddress
) = _getNodeOperatorAddresses(nodeOperatorId);
_isSenderEligibleToClaim(managerAddress);
uint256 claimableShares = _pullFeeRewards(
rewardsProof,
nodeOperatorId,
cumulativeFeeShares
);
uint256 toClaim = ETHAmount < _ethByShares(claimableShares)
? _sharesByEth(ETHAmount)
: claimableShares;
uint256[] memory amounts = new uint256[](1);
amounts[0] = _lido().getPooledEthByShares(toClaim);
requestIds = _withdrawalQueue().requestWithdrawals(
amounts,
rewardAddress
);
bondShares[nodeOperatorId] -= toClaim;
totalBondShares -= toClaim;
emit ETHRewardsRequested(nodeOperatorId, rewardAddress, amounts[0]);
return requestIds;
}

/// @notice Penalize bond by burning shares
/// @param nodeOperatorId id of the node operator to penalize bond for.
/// @param shares amount shares to burn.
Expand Down Expand Up @@ -575,6 +618,10 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable {
return ICSFeeDistributor(FEE_DISTRIBUTOR);
}

function _withdrawalQueue() internal view returns (IWithdrawalQueue) {
return IWithdrawalQueue(LIDO_LOCATOR.withdrawalQueue());
}

function _getNodeOperatorActiveKeys(
uint256 nodeOperatorId
) internal view returns (uint256) {
Expand Down
11 changes: 11 additions & 0 deletions src/interfaces/IWithdrawalQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

interface IWithdrawalQueue {
function requestWithdrawals(
uint256[] calldata _amounts,
address _owner
) external returns (uint256[] memory requestIds);
}
131 changes: 130 additions & 1 deletion test/CSAccounting.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ import { WstETHMock } from "./helpers/mocks/WstETHMock.sol";
import { LidoLocatorMock } from "./helpers/mocks/LidoLocatorMock.sol";
import { CommunityStakingModuleMock } from "./helpers/mocks/CommunityStakingModuleMock.sol";
import { CommunityStakingFeeDistributorMock } from "./helpers/mocks/CommunityStakingFeeDistributorMock.sol";
import { WithdrawalQueueMockBase, WithdrawalQueueMock } from "./helpers/mocks/WithdrawalQueueMock.sol";

import { Fixtures } from "./helpers/Fixtures.sol";

contract CSAccountingTest is Test, Fixtures, PermitTokenBase, CSAccountingBase {
contract CSAccountingTest is
Test,
Fixtures,
PermitTokenBase,
CSAccountingBase,
WithdrawalQueueMockBase
{
LidoLocatorMock internal locator;
WstETHMock internal wstETH;
LidoMock internal stETH;
WithdrawalQueueMock internal wq;

Stub internal burner;

CSAccounting public accounting;
Expand Down Expand Up @@ -911,6 +921,125 @@ contract CSAccountingTest is Test, Fixtures, PermitTokenBase, CSAccountingBase {
accounting.claimRewardsWstETH(new bytes32[](1), 0, 1, 1 ether);
}

function test_requestRewardsETH() public {
_createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 });
vm.deal(address(feeDistributor), 0.1 ether);
vm.prank(address(feeDistributor));
uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0));

vm.deal(user, 32 ether);
vm.startPrank(user);
stETH.submit{ value: 32 ether }({ _referal: address(0) });
accounting.depositStETH(user, 0, 32 ether);

uint256 requestedAsUnstETH = stETH.getPooledEthByShares(sharesAsFee);
uint256 requestedAsUnstETHAsShares = stETH.getSharesByPooledEth(
requestedAsUnstETH
);

vm.expectEmit(
true,
true,
true,
true,
address(locator.withdrawalQueue())
);
emit WithdrawalRequested(
1,
address(accounting),
user,
requestedAsUnstETH,
requestedAsUnstETHAsShares
);
vm.expectEmit(true, true, true, true, address(accounting));
emit ETHRewardsRequested(
0,
user,
stETH.getPooledEthByShares(sharesAsFee)
);

uint256 bondSharesBefore = accounting.getBondShares(0);
uint256[] memory requestIds = accounting.requestRewardsETH(
new bytes32[](1),
0,
sharesAsFee,
UINT256_MAX
);
uint256 bondSharesAfter = accounting.getBondShares(0);

assertEq(requestIds.length, 1, "request ids length should be 1");
assertEq(
bondSharesAfter,
bondSharesBefore,
"bond shares should not change after request"
);
assertEq(
stETH.sharesOf(address(locator.withdrawalQueue())),
requestedAsUnstETHAsShares,
"shares of withdrawal queue should be equal to requested shares"
);
assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0");
}

function test_requestRewardsETH_WithDesirableValue() public {
_createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 });
vm.deal(address(feeDistributor), 0.1 ether);
vm.prank(address(feeDistributor));
uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0));

vm.deal(user, 32 ether);
vm.startPrank(user);
stETH.submit{ value: 32 ether }({ _referal: address(0) });
accounting.depositStETH(user, 0, 32 ether);

uint256 requestedAsShares = stETH.getSharesByPooledEth(0.05 ether);
uint256 requestedAsUnstETH = stETH.getPooledEthByShares(
requestedAsShares
);
uint256 requestedAsUnstETHAsShares = stETH.getSharesByPooledEth(
requestedAsUnstETH
);

vm.expectEmit(
true,
true,
true,
true,
address(locator.withdrawalQueue())
);
emit WithdrawalRequested(
1,
address(accounting),
user,
requestedAsUnstETH,
requestedAsUnstETHAsShares
);
vm.expectEmit(true, true, true, true, address(accounting));
emit ETHRewardsRequested(0, user, requestedAsUnstETH);

uint256 bondSharesBefore = accounting.getBondShares(0);
uint256[] memory requestIds = accounting.requestRewardsETH(
new bytes32[](1),
0,
sharesAsFee,
0.05 ether
);
uint256 bondSharesAfter = accounting.getBondShares(0);

assertEq(requestIds.length, 1, "request ids length should be 1");
assertEq(
bondSharesAfter,
(bondSharesBefore + sharesAsFee) - requestedAsShares,
"bond shares after should be equal to before and fee minus requested shares"
);
assertEq(
stETH.sharesOf(address(locator.withdrawalQueue())),
requestedAsUnstETHAsShares,
"shares of withdrawal queue should be equal to requested shares"
);
assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0");
}

function test_penalize_LessThanDeposit() public {
_createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 });
vm.deal(user, 32 ether);
Expand Down
8 changes: 7 additions & 1 deletion test/helpers/Fixtures.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StdCheats } from "forge-std/StdCheats.sol";
import { LidoMock } from "./mocks/LidoMock.sol";
import { WstETHMock } from "./mocks/WstETHMock.sol";
import { LidoLocatorMock } from "./mocks/LidoLocatorMock.sol";
import { WithdrawalQueueMock } from "./mocks/WithdrawalQueueMock.sol";
import { Stub } from "./mocks/Stub.sol";

contract Fixtures is StdCheats {
Expand All @@ -24,7 +25,12 @@ contract Fixtures is StdCheats {
_sharesAmount: 7059313073779349112833523
});
burner = new Stub();
locator = new LidoLocatorMock(address(stETH), address(burner));
WithdrawalQueueMock wq = new WithdrawalQueueMock(address(stETH));
locator = new LidoLocatorMock(
address(stETH),
address(burner),
address(wq)
);
wstETH = new WstETHMock(address(stETH));
}
}
6 changes: 4 additions & 2 deletions test/helpers/Permit.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
Expand Down
8 changes: 7 additions & 1 deletion test/helpers/mocks/LidoLocatorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ pragma solidity 0.8.21;
contract LidoLocatorMock {
address public l;
address public b;
address public wq;

constructor(address _lido, address _burner) {
constructor(address _lido, address _burner, address _wq) {
l = _lido;
b = _burner;
wq = _wq;
}

function lido() external view returns (address) {
Expand All @@ -19,4 +21,8 @@ contract LidoLocatorMock {
function burner() external view returns (address) {
return b;
}

function withdrawalQueue() external view returns (address) {
return wq;
}
}
55 changes: 55 additions & 0 deletions test/helpers/mocks/WithdrawalQueueMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.21;

import { IStETH } from "../../../src/interfaces/IStETH.sol";

contract WithdrawalQueueMockBase {
/// @dev Contains both stETH token amount and its corresponding shares amount
event WithdrawalRequested(
uint256 indexed requestId,
address indexed requestor,
address indexed owner,
uint256 amountOfStETH,
uint256 amountOfShares
);
}

contract WithdrawalQueueMock is WithdrawalQueueMockBase {
IStETH public stETH;

uint256 public constant MIN_STETH_WITHDRAWAL_AMOUNT = 100;

uint256 public constant MAX_STETH_WITHDRAWAL_AMOUNT = 1000 ether;

constructor(address _stETH) {
stETH = IStETH(_stETH);
}

function requestWithdrawals(
uint256[] calldata _amounts,
address _owner
) external returns (uint256[] memory requestIds) {
requestIds = new uint256[](_amounts.length);
for (uint256 i = 0; i < _amounts.length; ++i) {
require(
_amounts[i] <= MAX_STETH_WITHDRAWAL_AMOUNT,
"amount is greater than MAX_STETH_WITHDRAWAL_AMOUNT"
);
require(
_amounts[i] >= MIN_STETH_WITHDRAWAL_AMOUNT,
"amount is less than MIN_STETH_WITHDRAWAL_AMOUNT"
);
stETH.transferFrom(msg.sender, address(this), _amounts[i]);
emit WithdrawalRequested(
i + 1,
msg.sender,
_owner,
_amounts[i],
stETH.getSharesByPooledEth(_amounts[i])
);
requestIds[i] = i + 1;
}
}
}

0 comments on commit 959a4cb

Please sign in to comment.