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

feat: claim with nft #25

Merged
merged 3 commits into from
Oct 27, 2023
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
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;
madlabman marked this conversation as resolved.
Show resolved Hide resolved
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 <info@lido.fi>
// 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
madlabman marked this conversation as resolved.
Show resolved Hide resolved
);
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 <info@lido.fi>
// 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 <info@lido.fi>
// 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;
}
}
}