From a94e825b67c899ad2955e426fb041c6ef94720e3 Mon Sep 17 00:00:00 2001 From: DrZoltanFazekas Date: Tue, 26 Nov 2024 15:04:35 +0100 Subject: [PATCH] Add support for staking rewards --- README.md | 23 +++++++++---- script/stakeRewards_Delegation.s.sol | 34 +++++++++++++++++++ ...s.sol => withdrawRewards_Delegation.s.sol} | 2 +- src/BaseDelegation.sol | 2 ++ src/LiquidDelegation.sol | 4 +++ src/LiquidDelegationV2.sol | 4 +-- src/NonLiquidDelegation.sol | 4 +++ src/NonLiquidDelegationV2.sol | 28 ++++++++++----- rewards.sh => withdrawRewards.sh | 2 +- 9 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 script/stakeRewards_Delegation.s.sol rename script/{rewards_Delegation.s.sol => withdrawRewards_Delegation.s.sol} (97%) rename rewards.sh => withdrawRewards.sh (95%) diff --git a/README.md b/README.md index 8176bc2..0eceb33 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 using `same` for the second argument to leave the commission percentage unchanged and `true` for the third argument. Replacing the second argument with `same` and the third argument with `false` only displays the current commission rate. ## Validator Activation -If you node's account has enough ZIL for the minimum stake required, you can activate your node as a validator with a deposit of e.g. 10 million ZIL. Run +If your node's account has enough ZIL for the minimum stake required, you can activate your node as a validator with a deposit of e.g. 10 million ZIL. Run ```bash cast send --legacy --value 10000000ether --rpc-url http://localhost:4201 --private-key $PRIVATE_KEY \ 0x7a0b7e6d24ede78260c9ddbd98e828b0e11a8ea2 "deposit(bytes,bytes,bytes)" \ @@ -189,8 +189,13 @@ To query how much ZIL you can already claim, run cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "getClaimable()(uint256)" --from 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --block latest --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') ether ``` -## Withdrawing Rewards -In the non-liquid variant of staking delegators can withdraw their share of the rewards earned by the validator. To query the amount of rewards available, run +## Staking and Withdrawing Rewards +In the liquid staking variant, only you as the node operator can stake the rewards accrued by the node. To do so, run +```bash +forge script script/stakeRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... +``` + +In the non-liquid variant of staking, delegators can stake or withdraw their share of the rewards earned by the validator. To query the amount of rewards available, run ```bash cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "rewards()(uint256)" --from 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --block latest --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') ether ``` @@ -201,8 +206,14 @@ cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "rewards(uin ``` Note that `n` actually denotes the number of additional (un)stakings so that at least one is always reflected in the result, even if you specified `n = 0`. -Last but not least, to withdraw 1 ZIL of rewards using `n = 100`, run +To withdraw 1 ZIL of rewards using `n = 100`, run +```bash +forge script script/withdrawRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, string)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000000000000000000 100 --private-key 0x... +``` +with the private key of a staked account. To withdrawn as much as possible with the given value of `n` set the amount to `all`. To withdraw the chosen amount without setting `n` replace `n` with `all`. To withdraw all rewards replace both the amount and `n` with `all`. + +Last but not least, in order to stake rewards instead of withdrawing them, run ```bash -forge script script/rewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, string)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000000000000000000 100 --private-key 0x... +forge script script/stakeRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... ``` -with the private key of an staked account. To withdrawn as much as possible with the given value of `n` set the amount to `all`. To withdraw the chosen amount without setting `n` replace `n` with `all`. To withdraw all rewards replace both the amount and `n` with `all`. +with the private key of a staked account. diff --git a/script/stakeRewards_Delegation.s.sol b/script/stakeRewards_Delegation.s.sol new file mode 100644 index 0000000..097a12f --- /dev/null +++ b/script/stakeRewards_Delegation.s.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {BaseDelegation} from "src/BaseDelegation.sol"; +import "forge-std/console.sol"; + +contract StakeRewards is Script { + + function run(address payable proxy) external { + + BaseDelegation delegation = BaseDelegation( + proxy + ); + + console.log("Running version: %s", + delegation.version() + ); + + console.log("Current stake: %s wei \r\n Current rewards: %s wei", + delegation.getStake(), + delegation.getRewards() + ); + + vm.broadcast(); + + delegation.stakeRewards(); + + console.log("New stake: %s wei \r\n New rewards: %s wei", + delegation.getStake(), + delegation.getRewards() + ); + } +} \ No newline at end of file diff --git a/script/rewards_Delegation.s.sol b/script/withdrawRewards_Delegation.s.sol similarity index 97% rename from script/rewards_Delegation.s.sol rename to script/withdrawRewards_Delegation.s.sol index 46173d1..77ec220 100644 --- a/script/rewards_Delegation.s.sol +++ b/script/withdrawRewards_Delegation.s.sol @@ -6,7 +6,7 @@ import {NonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import "forge-std/console.sol"; -contract Rewards is Script { +contract WithdrawRewards is Script { using Strings for string; function run(address payable proxy, string calldata amount, string calldata additionalSteps) external { diff --git a/src/BaseDelegation.sol b/src/BaseDelegation.sol index c15afef..83ab407 100644 --- a/src/BaseDelegation.sol +++ b/src/BaseDelegation.sol @@ -177,6 +177,8 @@ abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2Ste function collectCommission() public virtual; + function stakeRewards() public virtual; + function getClaimable() public virtual view returns(uint256 total) { BaseDelegationStorage storage $ = _getBaseDelegationStorage(); WithdrawalQueue.Fifo storage fifo = $.withdrawals[_msgSender()]; diff --git a/src/LiquidDelegation.sol b/src/LiquidDelegation.sol index d0e4471..b2ba7ce 100644 --- a/src/LiquidDelegation.sol +++ b/src/LiquidDelegation.sol @@ -60,6 +60,10 @@ contract LiquidDelegation is BaseDelegation, ILiquidDelegation { revert("not implemented"); } + function stakeRewards() public override { + revert("not implemented"); + } + function getPrice() public view returns(uint256) { revert("not implemented"); } diff --git a/src/LiquidDelegationV2.sol b/src/LiquidDelegationV2.sol index 910d0ae..78c354a 100644 --- a/src/LiquidDelegationV2.sol +++ b/src/LiquidDelegationV2.sol @@ -184,15 +184,15 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { taxRewards(); // withdraw the unstaked deposit once the unbonding period is over _withdrawDeposit(); + $.taxedRewards -= total; (bool success, ) = _msgSender().call{ value: total }(""); require(success, "transfer of funds failed"); - $.taxedRewards -= total; emit Claimed(_msgSender(), total, ""); } - function stakeRewards() public onlyOwner { + function stakeRewards() public override onlyOwner { // before the balance changes deduct the commission from the yet untaxed rewards taxRewards(); if (address(this).balance > getTotalWithdrawals()) diff --git a/src/NonLiquidDelegation.sol b/src/NonLiquidDelegation.sol index 98c041f..9723257 100644 --- a/src/NonLiquidDelegation.sol +++ b/src/NonLiquidDelegation.sol @@ -55,6 +55,10 @@ contract NonLiquidDelegation is BaseDelegation, INonLiquidDelegation { revert("not implemented"); } + function stakeRewards() public override { + revert("not implemented"); + } + function rewards() public view returns(uint256) { revert("not implemented"); } diff --git a/src/NonLiquidDelegationV2.sol b/src/NonLiquidDelegationV2.sol index 6d3a3da..1f6fd13 100644 --- a/src/NonLiquidDelegationV2.sol +++ b/src/NonLiquidDelegationV2.sol @@ -158,10 +158,10 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { return;*/ // withdraw the unstaked deposit once the unbonding period is over _withdrawDeposit(); + $.totalRewards -= int256(total); (bool success, ) = _msgSender().call{ value: total }(""); - $.totalRewards -= int256(total); require(success, "transfer of funds failed"); emit Claimed(_msgSender(), total, ""); } @@ -218,12 +218,12 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { uint256 commission = untaxedRewards * getCommissionNumerator() / DENOMINATOR; if (commission == 0) return untaxedRewards; + $.totalRewards -= int256(commission); // commissions are not subject to the unbonding period (bool success, ) = owner().call{ value: commission }(""); require(success, "transfer of commission failed"); - $.totalRewards -= int256(commission); emit CommissionPaid(owner(), commission); return untaxedRewards - commission; } @@ -240,11 +240,26 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { return withdrawRewards(amount, type(uint64).max); } + function withdrawRewards(uint256 amount, uint64 additionalSteps) public whenNotPaused returns(uint256 taxedRewards) { + (amount, taxedRewards) = _useRewards(amount, additionalSteps); + (bool success, ) = _msgSender().call{value: amount}(""); + require(success, "transfer of rewards failed"); + emit RewardPaid(_msgSender(), amount); + } + + function stakeRewards() public override { + (uint256 amount, ) = _useRewards(type(uint256).max, type(uint64).max); + if (_isActivated()) + _increaseDeposit(amount); + _append(int256(amount)); + emit Staked(_msgSender(), amount, ""); + } + // if there have been more than 11,000 stakings or unstakings since the delegator's last reward // withdrawal, calling withdrawAllRewards() would exceed the block gas limit additionalSteps is // the number of additional stakings from which the rewards are withdrawn if zero, the rewards // are only withdrawn from the first staking from which they have not been withdrawn yet - function withdrawRewards(uint256 amount, uint64 additionalSteps) public whenNotPaused returns(uint256) { + function _useRewards(uint256 amount, uint64 additionalSteps) internal whenNotPaused returns(uint256, uint256) { NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage(); ( uint256 resultInTotal, @@ -256,7 +271,7 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { _rewards(additionalSteps); // the caller has not delegated any stake if (nextStakingIndex == 0) - return 0; + return (0, 0); // store the rewards accrued since the last staking (`resultAfterLastStaking`) // in order to know next time how much the caller has already withdrawn, and // reduce the current withdrawal (`resultInTotal`) by the amount that was stored @@ -273,10 +288,7 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { require(amount <= $.allWithdrawnRewards[_msgSender()], "can not withdraw more than accrued"); $.allWithdrawnRewards[_msgSender()] -= amount; $.totalRewards -= int256(amount); - (bool success, ) = _msgSender().call{value: amount}(""); - require(success, "transfer of rewards failed"); - emit RewardPaid(_msgSender(), amount); - return taxedRewards; + return (amount, taxedRewards); } function _rewards() internal view returns ( diff --git a/rewards.sh b/withdrawRewards.sh similarity index 95% rename from rewards.sh rename to withdrawRewards.sh index 72f36b9..fa92646 100755 --- a/rewards.sh +++ b/withdrawRewards.sh @@ -30,7 +30,7 @@ if [[ "$variant" != "INonLiquidDelegation" ]]; then exit 1 fi -forge script script/rewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, string)" $1 $amount $steps --private-key $2 +forge script script/withdrawRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, string)" $1 $amount $steps --private-key $2 block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) block_num=$(echo $block | tr -d '"' | cast to-dec --base-in 16)