diff --git a/README.md b/README.md index c661fc8..0eceb33 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Install Foundry (https://book.getfoundry.sh/getting-started/installation) and th forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit forge install OpenZeppelin/openzeppelin-contracts --no-commit ``` +The Zilliqa 2.0 deposit contract must be compiled for the tests in this repository to work. Specify the folder containing the `deposit.sol` file in `remappings.txt`: +``` +@zilliqa/zq2/=/home/user/zq2/zilliqa/src/contracts/ +``` ## Contract Deployment The delegation contract is used by delegators to stake and unstake ZIL with the respective validator. It acts as the validator node's control address and interacts with the `Deposit` system contract. @@ -82,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)" \ @@ -185,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 ``` @@ -197,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/claim.sh b/claim.sh index 4ac0616..fa586bf 100755 --- a/claim.sh +++ b/claim.sh @@ -24,7 +24,7 @@ if [[ "$variant" == "ILiquidDelegation" ]]; then echo taxedRewardsAfterClaiming = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') fi -staker_wei_after=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Claimed(address,uint256,bytes)" --rpc-url http://localhost:4201 | grep "data") if [[ "$tmp" != "" ]]; then @@ -48,9 +48,9 @@ if [[ "$variant" == "ILiquidDelegation" ]]; then echo taxedRewardsBeforeClaiming = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') fi -staker_wei_before=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -echo claimed amount - gas fee = $(bc -l <<< "scale=18; $staker_wei_after-$staker_wei_before") wei +echo claimed amount - gas fee = $(bc -l <<< "scale=18; $stakerWeiAfter-$stakerWeiBefore") wei if [[ "$tmp" != "" ]]; then echo event Claimed\($staker, $d1, $d2\) emitted; fi diff --git a/remappings.txt b/remappings.txt index 5e817ed..6b6aa79 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,3 @@ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ \ No newline at end of file +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@zilliqa/zq2/=/home/zoltan/Github/zq2/zilliqa/src/contracts/ \ No newline at end of file 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/unstake_Delegation.s.sol b/script/unstake_Delegation.s.sol index d763845..bba5c25 100644 --- a/script/unstake_Delegation.s.sol +++ b/script/unstake_Delegation.s.sol @@ -49,6 +49,7 @@ contract Unstake is Script { ); if (amount == 0) { + vm.prank(msg.sender); amount = INonLiquidDelegation(address(delegation)).getDelegatedStake(); } } else 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/Delegation.sol b/src/Delegation.sol index a78f34b..7cbb297 100644 --- a/src/Delegation.sol +++ b/src/Delegation.sol @@ -7,8 +7,11 @@ interface Delegation { event Staked(address indexed delegator, uint256 amount, bytes data); event Unstaked(address indexed delegator, uint256 amount, bytes data); event Claimed(address indexed delegator, uint256 amount, bytes data); + event CommissionPaid(address indexed owner, uint256 commission); function stake() external payable; function unstake(uint256) external; function claim() external; + function collectCommission() external; + function stakeRewards() external; } \ No newline at end of file diff --git a/src/Deposit.sol b/src/Deposit.sol deleted file mode 100644 index 6dd543b..0000000 --- a/src/Deposit.sol +++ /dev/null @@ -1,637 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.20; - -struct Withdrawal { - uint256 startedAt; - uint256 amount; -} - -// Implementation of a double-ended queue of `Withdrawal`s, backed by a circular buffer. -library Deque { - struct Withdrawals { - Withdrawal[] values; - // The physical index of the first element, if it exists. If `len == 0`, the value of `head` is unimportant. - uint256 head; - // The number of elements in the queue. - uint256 len; - } - - // Returns the physical index of an element, given its logical index. - function physicalIdx( - Withdrawals storage deque, - uint256 idx - ) internal view returns (uint256) { - uint256 physical = deque.head + idx; - // Wrap the physical index in case it is out-of-bounds of the buffer. - if (physical >= deque.values.length) { - return physical - deque.values.length; - } else { - return physical; - } - } - - function length(Withdrawals storage deque) internal view returns (uint256) { - return deque.len; - } - - // Get the element at the given logical index. Reverts if `idx >= queue.length()`. - function get( - Withdrawals storage deque, - uint256 idx - ) internal view returns (Withdrawal storage) { - if (idx >= deque.len) { - revert("element does not exist"); - } - - uint256 pIdx = physicalIdx(deque, idx); - return deque.values[pIdx]; - } - - // Push an empty element to the back of the queue. Returns a reference to the new element. - function pushBack( - Withdrawals storage deque - ) internal returns (Withdrawal storage) { - // Add more space in the buffer if it is full. - if (deque.len == deque.values.length) { - deque.values.push(); - } - - uint256 idx = physicalIdx(deque, deque.len); - deque.len += 1; - - return deque.values[idx]; - } - - // Pop an element from the front of the queue. Note that this returns a reference to the element in storage. This - // means that further mutations of the queue may invalidate the returned element. Do not use this return value - // after calling any other mutations on the queue. - function popFront( - Withdrawals storage deque - ) internal returns (Withdrawal storage) { - if (deque.len == 0) { - revert("queue is empty"); - } - - uint256 oldHead = deque.head; - deque.head = physicalIdx(deque, 1); - deque.len -= 1; - return deque.values[oldHead]; - } - - // Peeks the element at the back of the queue. Note that this returns a reference to the element in storage. This - // means that further mutations of the queue may invalidate the returned element. Do not use this return value - // after calling any other mutations on the queue. - function back( - Withdrawals storage deque - ) internal view returns (Withdrawal storage) { - if (deque.len == 0) { - revert("queue is empty"); - } - - return get(deque, deque.len - 1); - } - - // Peeks the element at the front of the queue. Note that this returns a reference to the element in storage. This - // means that further mutations of the queue may invalidate the returned element. Do not use this return value - // after calling any other mutations on the queue. - function front( - Withdrawals storage deque - ) internal view returns (Withdrawal storage) { - if (deque.len == 0) { - revert("queue is empty"); - } - - return get(deque, 0); - } -} - -using Deque for Deque.Withdrawals; - -struct CommitteeStakerEntry { - // The index of the value in the `stakers` array plus 1. - // Index 0 is used to mean a value is not present. - uint256 index; - // Invariant: `balance >= minimumStake` - uint256 balance; -} - -struct Committee { - // Invariant: Equal to the sum of `balances` in `stakers`. - uint256 totalStake; - bytes[] stakerKeys; - mapping(bytes => CommitteeStakerEntry) stakers; -} - -struct Staker { - // The address used for authenticating requests from this staker to the deposit contract. - // Invariant: `controlAddress != address(0)`. - address controlAddress; - // The address which rewards for this staker will be sent to. - address rewardAddress; - // libp2p peer ID, corresponding to the staker's `blsPubKey` - bytes peerId; - // Invariants: Items are always sorted by `startedAt`. No two items have the same value of `startedAt`. - Deque.Withdrawals withdrawals; -} - -// Parameters passed to the deposit contract constructor, for each staker who should be in the initial committee. -struct InitialStaker { - bytes blsPubKey; - bytes peerId; - address rewardAddress; - address controlAddress; - uint256 amount; -} - -contract Deposit { - // The committee in the current epoch and the 2 epochs following it. The value for the current epoch - // is stored at index (currentEpoch() % 3). - Committee[3] _committee; - - // All stakers. Keys into this map are stored by the `Committee`. - mapping(bytes => Staker) _stakersMap; - // Mapping from `controlAddress` to `blsPubKey` for each staker. - mapping(address => bytes) _stakerKeys; - - // The latest epoch for which the committee was calculated. It is implied that no changes have (yet) occurred in - // future epochs, either because those epochs haven't happened yet or because they have happened, but no deposits - // or withdrawals were made. - uint64 latestComputedEpoch; - - uint256 public immutable minimumStake; - uint256 public immutable maximumStakers; - - uint64 public immutable blocksPerEpoch; - - modifier onlyControlAddress(bytes calldata blsPubKey) { - require(blsPubKey.length == 48); - require( - _stakersMap[blsPubKey].controlAddress == msg.sender, - "sender is not the control address" - ); - _; - } - - constructor( - uint256 _minimumStake, - uint256 _maximumStakers, - uint64 _blocksPerEpoch, - InitialStaker[] memory initialStakers - ) { - minimumStake = _minimumStake; - maximumStakers = _maximumStakers; - blocksPerEpoch = _blocksPerEpoch; - latestComputedEpoch = currentEpoch(); - - for (uint i = 0; i < initialStakers.length; i++) { - InitialStaker memory initialStaker = initialStakers[i]; - bytes memory blsPubKey = initialStaker.blsPubKey; - bytes memory peerId = initialStaker.peerId; - address rewardAddress = initialStaker.rewardAddress; - address controlAddress = initialStaker.controlAddress; - uint256 amount = initialStaker.amount; - - require(blsPubKey.length == 48); - require(peerId.length == 38); - require( - controlAddress != address(0), - "control address cannot be zero" - ); - - Committee storage currentCommittee = committee(); - require( - currentCommittee.stakerKeys.length < maximumStakers, - "too many stakers" - ); - - Staker storage staker = _stakersMap[blsPubKey]; - // This must be a new staker, meaning the control address must be zero. - require( - staker.controlAddress == address(0), - "staker already exists" - ); - - if (amount < minimumStake) { - revert("stake is less than minimum stake"); - } - - _stakerKeys[controlAddress] = blsPubKey; - staker.peerId = peerId; - staker.rewardAddress = rewardAddress; - staker.controlAddress = controlAddress; - - currentCommittee.totalStake += amount; - currentCommittee.stakers[blsPubKey].balance = amount; - currentCommittee.stakers[blsPubKey].index = - currentCommittee.stakerKeys.length + - 1; - currentCommittee.stakerKeys.push(blsPubKey); - } - } - - function currentEpoch() public view returns (uint64) { - return uint64(block.number / blocksPerEpoch); - } - - function committee() private view returns (Committee storage) { - if (latestComputedEpoch <= currentEpoch()) { - // If the current epoch is after the latest computed epoch, it is implied that no changes have happened to - // the committee since the latest computed epoch. Therefore, it suffices to return the committee at that - // latest computed epoch. - return _committee[latestComputedEpoch % 3]; - } else { - // Otherwise, the committee has been changed. The caller who made the change will have pre-computed the - // result for us, so we can just return it. - return _committee[currentEpoch() % 3]; - } - } - - function leaderFromRandomness( - uint256 randomness - ) private view returns (bytes memory) { - Committee storage currentCommittee = committee(); - // Get a random number in the inclusive range of 0 to (totalStake - 1) - uint256 position = randomness % currentCommittee.totalStake; - uint256 cummulative_stake = 0; - - // TODO: Consider binary search for performance. Or consider an alias method for O(1) performance. - for (uint256 i = 0; i < currentCommittee.stakerKeys.length; i++) { - bytes memory stakerKey = currentCommittee.stakerKeys[i]; - uint256 stakedBalance = currentCommittee.stakers[stakerKey].balance; - - cummulative_stake += stakedBalance; - - if (position < cummulative_stake) { - return stakerKey; - } - } - - revert("Unable to select next leader"); - } - - function leaderAtView( - uint256 viewNumber - ) public view returns (bytes memory) { - uint256 randomness = uint256( - keccak256(bytes.concat(bytes32(viewNumber))) - ); - return leaderFromRandomness(randomness); - } - - function getStakers() public view returns (bytes[] memory) { - return committee().stakerKeys; - } - - function getTotalStake() public view returns (uint256) { - return committee().totalStake; - } - - function getStakersData() - public - view - returns ( - bytes[] memory stakerKeys, - uint256[] memory balances, - Staker[] memory stakers - ) - { - Committee storage currentCommittee = committee(); - stakerKeys = currentCommittee.stakerKeys; - balances = new uint256[](stakerKeys.length); - stakers = new Staker[](stakerKeys.length); - for (uint i = 0; i < stakerKeys.length; i++) { - bytes memory key = stakerKeys[i]; - balances[i] = currentCommittee.stakers[key].balance; - stakers[i] = _stakersMap[key]; - } - } - - function getStake(bytes calldata blsPubKey) public view returns (uint256) { - require(blsPubKey.length == 48); - - // We don't need to check if `blsPubKey` is in `stakerKeys` here. If the `blsPubKey` is not a staker, the - // balance will default to zero. - return committee().stakers[blsPubKey].balance; - } - - function getFutureStake( - bytes calldata blsPubKey - ) public view returns (uint256) { - require(blsPubKey.length == 48); - - // if `latestComputedEpoch > currentEpoch()` - // then `latestComputedEpoch` determines the future committee we need - // otherwise there are no committee changes after `currentEpoch()` - // i.e. `latestComputedEpoch` determines the most recent committee - Committee storage latestCommittee = _committee[latestComputedEpoch % 3]; - - // We don't need to check if `blsPubKey` is in `stakerKeys` here. If the `blsPubKey` is not a staker, the - // balance will default to zero. - return latestCommittee.stakers[blsPubKey].balance; - } - - function getRewardAddress( - bytes calldata blsPubKey - ) public view returns (address) { - require(blsPubKey.length == 48); - if (_stakersMap[blsPubKey].controlAddress == address(0)) { - revert("not staked"); - } - return _stakersMap[blsPubKey].rewardAddress; - } - - function getControlAddress( - bytes calldata blsPubKey - ) public view returns (address) { - require(blsPubKey.length == 48); - if (_stakersMap[blsPubKey].controlAddress == address(0)) { - revert("not staked"); - } - return _stakersMap[blsPubKey].controlAddress; - } - - function setRewardAddress( - bytes calldata blsPubKey, - address rewardAddress - ) public onlyControlAddress(blsPubKey) { - _stakersMap[blsPubKey].rewardAddress = rewardAddress; - } - - function setControlAddress( - bytes calldata blsPubKey, - address controlAddress - ) public onlyControlAddress(blsPubKey) { - _stakersMap[blsPubKey].controlAddress = controlAddress; - } - - function getPeerId( - bytes calldata blsPubKey - ) public view returns (bytes memory) { - require(blsPubKey.length == 48); - if (_stakersMap[blsPubKey].controlAddress == address(0)) { - revert("not staked"); - } - return _stakersMap[blsPubKey].peerId; - } - - function updateLatestComputedEpoch() internal { - // If the latest computed epoch is less than two epochs ahead of the current one, we must fill in the missing - // epochs. This just involves copying the committee from the previous epoch to the next one. It is assumed that - // the caller will then want to update the future epochs. - if (latestComputedEpoch < currentEpoch() + 2) { - Committee storage latestComputedCommittee = _committee[ - latestComputedEpoch % 3 - ]; - // Note the early exit condition if `latestComputedEpoch + 3` which ensures this loop will not run more - // than twice. This is acceptable because we only store 3 committees at a time, so once we have updated two - // of them to the latest computed committee, there is no more work to do. - for ( - uint64 i = latestComputedEpoch + 1; - i <= currentEpoch() + 2 && i < latestComputedEpoch + 3; - i++ - ) { - // The operation we want to do is: `_committee[i % 3] = latestComputedCommittee` but we need to do it - // explicitly because `stakers` is a mapping. - - // Delete old keys from `_committee[i % 3].stakers`. - for (uint j = 0; j < _committee[i % 3].stakerKeys.length; j++) { - delete _committee[i % 3].stakers[ - _committee[i % 3].stakerKeys[j] - ]; - } - - _committee[i % 3].totalStake = latestComputedCommittee - .totalStake; - _committee[i % 3].stakerKeys = latestComputedCommittee - .stakerKeys; - for ( - uint j = 0; - j < latestComputedCommittee.stakerKeys.length; - j++ - ) { - bytes storage stakerKey = latestComputedCommittee - .stakerKeys[j]; - _committee[i % 3].stakers[ - stakerKey - ] = latestComputedCommittee.stakers[stakerKey]; - } - } - - latestComputedEpoch = currentEpoch() + 2; - } - } - - // keep in-sync with zilliqa/src/precompiles.rs - function _popVerify( - bytes memory pubkey, - bytes memory signature - ) internal view returns (bool) { - bytes memory input = abi.encodeWithSelector( - hex"bfd24965", // bytes4(keccak256("popVerify(bytes,bytes)")) - signature, - pubkey - ); - //TODO: don't remove the next line - return true; - uint inputLength = input.length; - bytes memory output = new bytes(32); - bool success; - assembly { - success := staticcall( - gas(), - 0x5a494c80, // "ZIL\x80" - add(input, 0x20), - inputLength, - add(output, 0x20), - 32 - ) - } - require(success, "popVerify"); - bool result = abi.decode(output, (bool)); - return result; - } - - function deposit( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature, - address rewardAddress - ) public payable { - require(blsPubKey.length == 48); - require(peerId.length == 38); - require(signature.length == 96); - - // Verify signature as a proof-of-possession of the private key. - bool pop = _popVerify(blsPubKey, signature); - require(pop, "rogue key check"); - - Staker storage staker = _stakersMap[blsPubKey]; - - if (msg.value < minimumStake) { - revert("stake is less than minimum stake"); - } - - _stakerKeys[msg.sender] = blsPubKey; - staker.peerId = peerId; - staker.rewardAddress = rewardAddress; - staker.controlAddress = msg.sender; - - updateLatestComputedEpoch(); - - Committee storage futureCommittee = _committee[ - (currentEpoch() + 2) % 3 - ]; - - require( - futureCommittee.stakerKeys.length < maximumStakers, - "too many stakers" - ); - require( - futureCommittee.stakers[blsPubKey].index == 0, - "staker already exists" - ); - - futureCommittee.totalStake += msg.value; - futureCommittee.stakers[blsPubKey].balance = msg.value; - futureCommittee.stakers[blsPubKey].index = - futureCommittee.stakerKeys.length + - 1; - futureCommittee.stakerKeys.push(blsPubKey); - } - - function depositTopup() public payable { - bytes storage stakerKey = _stakerKeys[msg.sender]; - require(stakerKey.length != 0, "staker does not exist"); - - updateLatestComputedEpoch(); - - Committee storage futureCommittee = _committee[ - (currentEpoch() + 2) % 3 - ]; - require( - futureCommittee.stakers[stakerKey].index != 0, - "staker does not exist" - ); - futureCommittee.totalStake += msg.value; - futureCommittee.stakers[stakerKey].balance += msg.value; - } - - function unstake(uint256 amount) public { - bytes storage stakerKey = _stakerKeys[msg.sender]; - require(stakerKey.length != 0, "staker does not exist"); - Staker storage staker = _stakersMap[stakerKey]; - - updateLatestComputedEpoch(); - - Committee storage futureCommittee = _committee[ - (currentEpoch() + 2) % 3 - ]; - - require( - futureCommittee.stakers[stakerKey].index != 0, - "staker does not exist" - ); - //TODO: keep the next line commented out - //require(futureCommittee.stakerKeys.length > 1, "too few stakers"); - require( - futureCommittee.stakers[stakerKey].balance >= amount, - "amount is greater than staked balance" - ); - - if (futureCommittee.stakers[stakerKey].balance - amount == 0) { - // Remove the staker from the future committee, because their staked amount has gone to zero. - futureCommittee.totalStake -= amount; - - uint256 deleteIndex = futureCommittee.stakers[stakerKey].index - 1; - uint256 lastIndex = futureCommittee.stakerKeys.length - 1; - - if (deleteIndex != lastIndex) { - // Move the last staker in `stakerKeys` to the position of the staker we want to delete. - bytes storage lastStakerKey = futureCommittee.stakerKeys[ - lastIndex - ]; - futureCommittee.stakerKeys[deleteIndex] = lastStakerKey; - // We need to remember to update the moved staker's `index` too. - futureCommittee.stakers[lastStakerKey].index = futureCommittee - .stakers[stakerKey] - .index; - } - - // It is now safe to delete the final staker in the list. - futureCommittee.stakerKeys.pop(); - delete futureCommittee.stakers[stakerKey]; - - // Note that we leave the staker in `_stakersMap` forever. - } else { - require( - futureCommittee.stakers[stakerKey].balance - amount >= - minimumStake, - "unstaking this amount would take the validator below the minimum stake" - ); - - // Partial unstake. The staker stays in the committee, but with a reduced stake. - futureCommittee.totalStake -= amount; - futureCommittee.stakers[stakerKey].balance -= amount; - } - - // Enqueue the withdrawal for this staker. - Deque.Withdrawals storage withdrawals = staker.withdrawals; - Withdrawal storage currentWithdrawal; - // We know `withdrawals` is sorted by `startedAt`. We also know `block.timestamp` is monotonically - // non-decreasing. Therefore if there is an existing entry with a `startedAt = block.timestamp`, it must be - // at the end of the queue. - if ( - withdrawals.length() != 0 && - withdrawals.back().startedAt == block.timestamp - ) { - // They have already made a withdrawal at this time, so grab a reference to the existing one. - currentWithdrawal = withdrawals.back(); - } else { - // Add a new withdrawal to the end of the queue. - currentWithdrawal = withdrawals.pushBack(); - currentWithdrawal.startedAt = block.timestamp; - } - currentWithdrawal.amount += amount; - } - - function withdraw() public { - _withdraw(0); - } - - function withdraw(uint256 count) public { - _withdraw(count); - } - - function withdrawalPeriod() public view returns (uint256) { - // shorter unbonding period for testing deposit withdrawals - if (block.chainid == 33469) return 5 minutes; - return 2 weeks; - } - - function _withdraw(uint256 count) internal { - uint256 releasedAmount = 0; - - Staker storage staker = _stakersMap[_stakerKeys[msg.sender]]; - - Deque.Withdrawals storage withdrawals = staker.withdrawals; - count = (count == 0 || count > withdrawals.length()) - ? withdrawals.length() - : count; - - while (count > 0) { - Withdrawal storage withdrawal = withdrawals.front(); - if (withdrawal.startedAt + withdrawalPeriod() <= block.timestamp) { - releasedAmount += withdrawal.amount; - withdrawals.popFront(); - } else { - // Thanks to the invariant on `withdrawals`, we know the elements are ordered by `startedAt`, so we can - // break early when we encounter any withdrawal that isn't ready to be released yet. - break; - } - count -= 1; - } - - (bool sent, ) = msg.sender.call{value: releasedAmount}(""); - require(sent, "failed to send"); - } -} 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 cb89ab5..66ce5ef 100644 --- a/src/LiquidDelegationV2.sol +++ b/src/LiquidDelegationV2.sol @@ -37,8 +37,6 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { function reinitialize() reinitializer(version() + 1) public { } - event CommissionPaid(address indexed owner, uint256 rewardsBefore, uint256 commission); - // called when stake withdrawn from the deposit contract is claimed // but not called when rewards are assigned to the reward address receive() payable external { @@ -172,7 +170,7 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { value: commission }(""); require(success, "transfer of commission failed"); - emit CommissionPaid(owner(), rewards, commission); + emit CommissionPaid(owner(), commission); } function claim() public override whenNotPaused { @@ -184,20 +182,31 @@ 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, ""); } - //TODO: make it onlyOwnerOrContract and call it every time someone stakes, unstakes or claims? - function stakeRewards() public onlyOwner { - // before the balance changes deduct the commission from the yet untaxed rewards + function stakeRewards() public override onlyOwner { + LiquidDelegationStorage storage $ = _getLiquidDelegationStorage(); + // rewards must be taxed before deposited since + // they will not be taxed when they are unstaked taxRewards(); - if (address(this).balance > getTotalWithdrawals()) + // we must not deposit the funds we need to pay out the claims + if (address(this).balance > getTotalWithdrawals()) { + // TODO: moving funds between rewards and deposit should + // be okay but it's not, because the price calculation + // assumes the rewards are higher than the taxed rewards + // but after moving the rewards to the deposit they are not + // not only the rewards (balance) will be reduced + // by the deposit topup but also the taxed rewards + $.taxedRewards -= address(this).balance - getTotalWithdrawals(); _increaseDeposit(address(this).balance - getTotalWithdrawals()); + } + // TODO: replace address(this).balance everywhere with getRewards() } function collectCommission() public override onlyOwner { 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 3cd0e6c..be66e19 100644 --- a/src/NonLiquidDelegationV2.sol +++ b/src/NonLiquidDelegationV2.sol @@ -37,10 +37,13 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { // respective staker that can be fully/partially // transferred to the staker mapping(address => uint256) allWithdrawnRewards; - // the last staking index up to which the rewards + // the last staking nextStakingIndex up to which the rewards // of the respective staker have been calculated // and added to allWithdrawnRewards - mapping(address => uint64) lastWithdrawnRewardIndex; + mapping(address => uint64) lastWithdrawnStakingIndex; + // the amount that has already been withdrawn from the + // constantly growing rewards accrued since the last staking + mapping(address => uint256) withdrawnAfterLastStaking; // balance of the reward address minus the // rewards accrued since the last staking int256 totalRewards; @@ -91,13 +94,15 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { uint64[] memory stakingIndices, uint64 firstStakingIndex, uint256 allWithdrawnRewards, - uint64 lastWithdrawnRewardIndex + uint64 lastWithdrawnStakingIndex, + uint256 withdrawnAfterLastStaking ) { NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage(); stakingIndices = $.stakingIndices[_msgSender()]; firstStakingIndex = $.firstStakingIndex[_msgSender()]; allWithdrawnRewards = $.allWithdrawnRewards[_msgSender()]; - lastWithdrawnRewardIndex = $.lastWithdrawnRewardIndex[_msgSender()]; + lastWithdrawnStakingIndex = $.lastWithdrawnStakingIndex[_msgSender()]; + withdrawnAfterLastStaking = $.withdrawnAfterLastStaking[_msgSender()]; } function getDelegatedStake() public view returns(uint256 result) { @@ -108,7 +113,6 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { } event RewardPaid(address indexed owner, uint256 reward); - event CommissionPaid(address indexed owner, uint256 commission); // called by the node's account that deployed this contract and is its owner // to request the node's activation as a validator using the delegated stake @@ -153,10 +157,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, ""); } @@ -196,14 +200,16 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { function rewards(uint64 additionalSteps) public view returns(uint256) { NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage(); - (uint256 result, , ) = _rewards(additionalSteps); - return result - result * getCommissionNumerator() / DENOMINATOR + $.allWithdrawnRewards[_msgSender()]; + (uint256 resultInTotal, , , ) = _rewards(additionalSteps); + resultInTotal -= $.withdrawnAfterLastStaking[_msgSender()]; + return resultInTotal - resultInTotal * getCommissionNumerator() / DENOMINATOR + $.allWithdrawnRewards[_msgSender()]; } function rewards() public view returns(uint256) { NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage(); - (uint256 result, , ) = _rewards(); - return result - result * getCommissionNumerator() / DENOMINATOR + $.allWithdrawnRewards[_msgSender()]; + (uint256 resultInTotal, , , ) = _rewards(); + resultInTotal -= $.withdrawnAfterLastStaking[_msgSender()]; + return resultInTotal - resultInTotal * getCommissionNumerator() / DENOMINATOR + $.allWithdrawnRewards[_msgSender()]; } function taxRewards(uint256 untaxedRewards) internal returns (uint256) { @@ -211,12 +217,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; } @@ -233,77 +239,120 @@ 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 result, uint64 i, uint64 index) = additionalSteps == type(uint64).max ? + ( + uint256 resultInTotal, + uint256 resultAfterLastStaking, + uint64 posInStakingIndices, + uint64 nextStakingIndex + ) = additionalSteps == type(uint64).max ? _rewards() : _rewards(additionalSteps); // the caller has not delegated any stake - if (index == 0) - return 0; - uint256 taxedRewards = taxRewards(result); + if (nextStakingIndex == 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 + // last time (`withdrawnAfterLastStaking`) - this is essential because the reward + // amount since the last staking is growing all the time, but only the delta accrued + // since the last withdrawal shall be taken into account in the current withdrawal + ($.withdrawnAfterLastStaking[_msgSender()], resultInTotal) = (resultAfterLastStaking, resultInTotal - $.withdrawnAfterLastStaking[_msgSender()]); + uint256 taxedRewards = taxRewards(resultInTotal); $.allWithdrawnRewards[_msgSender()] += taxedRewards; - $.firstStakingIndex[_msgSender()] = i; - $.lastWithdrawnRewardIndex[_msgSender()] = index - 1; + $.firstStakingIndex[_msgSender()] = posInStakingIndices; + $.lastWithdrawnStakingIndex[_msgSender()] = nextStakingIndex - 1; if (amount == type(uint256).max) amount = $.allWithdrawnRewards[_msgSender()]; 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); - //TODO: shouldn't we return amount instead? - return taxedRewards; + return (amount, taxedRewards); } - function _rewards() internal view returns(uint256 result, uint64 i, uint64 index) { + function _rewards() internal view returns ( + uint256 resultInTotal, + uint256 resultAfterLastStaking, + uint64 posInStakingIndices, + uint64 nextStakingIndex + ) { return _rewards(type(uint64).max); } - function _rewards(uint64 additionalSteps) internal view returns(uint256 result, uint64 i, uint64 index) { + function _rewards(uint64 additionalSteps) internal view returns ( + uint256 resultInTotal, + uint256 resultAfterLastStaking, + uint64 posInStakingIndices, + uint64 nextStakingIndex + ) { NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage(); - uint64 firstIndex; - for (i = $.firstStakingIndex[_msgSender()]; i < $.stakingIndices[_msgSender()].length; i++) { - index = $.stakingIndices[_msgSender()][i]; - uint256 amount = $.stakings[index].amount; - if (index < $.lastWithdrawnRewardIndex[_msgSender()]) - index = $.lastWithdrawnRewardIndex[_msgSender()]; - uint256 total = $.stakings[index].total; - index++; - if (firstIndex == 0) - firstIndex = index; - while (i == $.stakingIndices[_msgSender()].length - 1 ? index < $.stakings.length : index <= $.stakingIndices[_msgSender()][i+1]) { + uint64 firstStakingIndex; + for ( + posInStakingIndices = $.firstStakingIndex[_msgSender()]; + posInStakingIndices < $.stakingIndices[_msgSender()].length; + posInStakingIndices++ + ) { + nextStakingIndex = $.stakingIndices[_msgSender()][posInStakingIndices]; + uint256 amount = $.stakings[nextStakingIndex].amount; + if (nextStakingIndex < $.lastWithdrawnStakingIndex[_msgSender()]) + nextStakingIndex = $.lastWithdrawnStakingIndex[_msgSender()]; + uint256 total = $.stakings[nextStakingIndex].total; + nextStakingIndex++; + if (firstStakingIndex == 0) + firstStakingIndex = nextStakingIndex; + while ( + posInStakingIndices == $.stakingIndices[_msgSender()].length - 1 ? + nextStakingIndex < $.stakings.length : + nextStakingIndex <= $.stakingIndices[_msgSender()][posInStakingIndices+1] + ) { if (total > 0) - result += $.stakings[index].rewards * amount / total; - total = $.stakings[index].total; - index++; - if (index - firstIndex > additionalSteps) - return (result, i, index); + resultInTotal += $.stakings[nextStakingIndex].rewards * amount / total; + total = $.stakings[nextStakingIndex].total; + nextStakingIndex++; + if (nextStakingIndex - firstStakingIndex > additionalSteps) + return (resultInTotal, resultAfterLastStaking, posInStakingIndices, nextStakingIndex); } // all rewards recorded in the stakings were taken into account - if (index == $.stakings.length) { - // ensure that the next time we call withdrawRewards() the last index + if (nextStakingIndex == $.stakings.length) { + // ensure that the next time we call withdrawRewards() the last nextStakingIndex // representing the rewards accrued since the last staking are not // included in the result any more - however, what if there have - // been no stakings i.e. the last index remains the same, but there + // been no stakings i.e. the last nextStakingIndex remains the same, but there // have been additional rewards - how can we determine the amount of // rewards added since we called withdrawRewards() last time? - // index++; + // nextStakingIndex++; // the last step is to add the rewards accrued since the last staking - if (total > 0) - result += (int256(getRewards()) - $.totalRewards).toUint256() * amount / total; + if (total > 0) { + resultAfterLastStaking = (int256(getRewards()) - $.totalRewards).toUint256() * amount / total; + resultInTotal += resultAfterLastStaking; + } } } - // ensure that the next time the function is called the initial value of i refers - // to the last amount and total among the stakingIndices of the staker that already + // ensure that the next time the function is called the initial value of posInStakingIndices + // refers to the last amount and total among the stakingIndices of the staker that already // existed during the current call of the function so that we can continue from there - if (i > 0) - i--; + if (posInStakingIndices > 0) + posInStakingIndices--; } function collectCommission() public override {} diff --git a/stake.sh b/stake.sh index 212e898..6dd17c2 100755 --- a/stake.sh +++ b/stake.sh @@ -24,7 +24,7 @@ if [[ "$variant" == "ILiquidDelegation" ]]; then echo taxedRewardsAfterStaking = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') fi -staker_wei_after=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Staked(address,uint256,bytes)" --rpc-url http://localhost:4201 | grep "data") if [[ "$tmp" != "" ]]; then @@ -62,8 +62,8 @@ if [[ "$variant" == "ILiquidDelegation" ]]; then echo staked ZIL shares: $(bc -l <<< "scale=18; $3/$price/10^18") LST fi -staker_wei_before=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -echo staked amount + gas fee = $(bc -l <<< "scale=18; $staker_wei_before-$staker_wei_after") wei +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +echo staked amount + gas fee = $(bc -l <<< "scale=18; $stakerWeiBefore-$stakerWeiAfter") wei if [[ "$tmp" != "" ]]; then echo event Staked\($staker, $d1, $d2\) emitted; fi diff --git a/stakeRewards.sh b/stakeRewards.sh new file mode 100755 index 0000000..a023f61 --- /dev/null +++ b/stakeRewards.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +if [ $# -lt 2 ]; then + echo "Provide the delegation contract address and the validator or stakker private key." + exit 1 +fi + +staker=$(cast wallet address $2) + +temp=$(forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" $1 | tail -n 1) +variant=$(sed -E 's/\s\s([a-zA-Z0-9]+)/\1/' <<< "$temp") +if [[ "$variant" == "$temp" ]]; then + echo Incompatible delegation contract at $1 + exit 1 +fi + +owner=$(cast call $1 "owner()(address)" --block latest --rpc-url http://localhost:4201) + +if [ "$variant" == "ILiquidDelegation" ] && [ "$staker" != "$owner" ]; then + echo Rewards must be staked by the validator and it is not $staker + exit 1 +fi + +forge script script/stakeRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" $1 --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) + +rewardsAfterStaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +taxedRewardsAfterStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +depositAfterStaking=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo rewardsAfterStaking = $rewardsAfterStaking +echo taxedRewardsAfterStaking = $taxedRewardsAfterStaking +echo depositAfterStaking = $depositAfterStaking + +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +ownerWeiAfter=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) + +tmp1=$(cast logs --from-block $block_num --to-block $block_num --address $1 "CommissionPaid(address,uint256)" --rpc-url http://localhost:4201 | grep "data") +if [[ "$tmp1" != "" ]]; then + tmp1=${tmp1#*: } + tmp1=$(cast abi-decode --input "x(uint256)" $tmp1 | sed 's/\[[^]]*\]//g') + tmp1=(${tmp1}) + d1=${tmp1[0]} + #d1=$(echo $tmp1 | sed -n -e 1p | sed 's/\[[^]]*\]//g') +fi + +echo $(date +"%T,%3N") $block_num + +block_num=$((block_num-1)) +block=$(echo $block_num | cast to-hex --base-in 10) + +stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +ownerWeiBefore=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) + +rewardsBeforeStaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +taxedRewardsBeforeStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +depositBeforeStaking=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo rewardsBeforeStaking = $rewardsBeforeStaking +echo taxedRewardsBeforeStaking = $taxedRewardsBeforeStaking +echo depositBeforeStaking = $depositBeforeStaking + +if [[ "$variant" == "ILiquidDelegation" ]]; then + echo validator commission - gas fee = $(bc -l <<< "scale=18; $ownerWeiAfter-$ownerWeiBefore") wei +else + echo staker gas fee = $(bc -l <<< "scale=18; $stakerWeiAfter-$stakerWeiBefore") wei + echo validator commission = $(bc -l <<< "scale=18; $ownerWeiAfter-$ownerWeiBefore") wei +fi +echo total reward reduction = $(bc -l <<< "scale=18; $rewardsBeforeStaking-$rewardsAfterStaking") wei + +if [[ "$tmp1" != "" ]]; then echo event CommissionPaid\($staker, $d1\) emitted; fi + +echo $(date +"%T,%3N") $block_num \ No newline at end of file diff --git a/test/LiquidDelegation.t.sol b/test/LiquidDelegation.t.sol index 147f8ce..8b05133 100644 --- a/test/LiquidDelegation.t.sol +++ b/test/LiquidDelegation.t.sol @@ -6,12 +6,18 @@ import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol"; import {NonRebasingLST} from "src/NonRebasingLST.sol"; import {WithdrawalQueue} from "src/BaseDelegation.sol"; import {Delegation} from "src/Delegation.sol"; -import {Deposit, InitialStaker} from "src/Deposit.sol"; +import {Deposit, InitialStaker} from "@zilliqa/zq2/deposit.sol"; import {Console} from "src/Console.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {Test, Vm} from "forge-std/Test.sol"; import "forge-std/console.sol"; +contract PopVerifyPrecompile { + function popVerify(bytes memory, bytes memory) public pure returns(bool) { + return true; + } +} + contract LiquidDelegationTest is Test { address payable proxy; LiquidDelegationV2 delegation; @@ -118,6 +124,10 @@ contract LiquidDelegationTest is Test { console.log("Deposit.maximumStakers() =", Deposit(delegation.DEPOSIT_CONTRACT()).maximumStakers()); console.log("Deposit.blocksPerEpoch() =", Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch()); //*/ + + vm.etch(address(0x5a494c80), address(new PopVerifyPrecompile()).code); + + vm.stopPrank(); } function run( diff --git a/test/NonLiquidDelegation.t.sol b/test/NonLiquidDelegation.t.sol index ee0a185..6a5282f 100644 --- a/test/NonLiquidDelegation.t.sol +++ b/test/NonLiquidDelegation.t.sol @@ -5,13 +5,19 @@ import {NonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {NonLiquidDelegationV2} from "src/NonLiquidDelegationV2.sol"; import {WithdrawalQueue} from "src/BaseDelegation.sol"; import {Delegation} from "src/Delegation.sol"; -import {Deposit, InitialStaker} from "src/Deposit.sol"; +import {Deposit, InitialStaker} from "@zilliqa/zq2/deposit.sol"; import {Console} from "src/Console.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {Test, Vm} from "forge-std/Test.sol"; import "forge-std/console.sol"; +contract PopVerifyPrecompile { + function popVerify(bytes memory, bytes memory) public pure returns(bool) { + return true; + } +} + contract NonLiquidDelegationTest is Test { address payable proxy; address owner; @@ -122,6 +128,9 @@ contract NonLiquidDelegationTest is Test { console.log("Deposit.maximumStakers() =", Deposit(delegation.DEPOSIT_CONTRACT()).maximumStakers()); console.log("Deposit.blocksPerEpoch() =", Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch()); //*/ + + vm.etch(address(0x5a494c80), address(new PopVerifyPrecompile()).code); + vm.stopPrank(); } @@ -217,9 +226,16 @@ contract NonLiquidDelegationTest is Test { s = string.concat(s, "0.0%\t\t"); console.log(s); } - (uint64[] memory stakingIndices, uint64 firstStakingIndex, uint256 allWithdrawnRewards, uint64 lastWithdrawnRewardIndex) = delegation.getStakingData(); - Console.log("stakingIndices: %s", stakingIndices); - console.log("firstStakingIndex: %s lastWithdrawnRewardIndex: %s allWithdrawnRewards: %s", firstStakingIndex, lastWithdrawnRewardIndex, allWithdrawnRewards); + ( + uint64[] memory stakingIndices, + uint64 firstStakingIndex, + uint256 allWithdrawnRewards, + uint64 lastWithdrawnRewardIndex, + uint256 withdrawnAfterLastStaking + ) = delegation.getStakingData(); + Console.log("stakingIndices = [ %s]", stakingIndices); + console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), uint(lastWithdrawnRewardIndex)); + console.log("allWithdrawnRewards = %s withdrawnAfterLastStaking = %s", allWithdrawnRewards, withdrawnAfterLastStaking); } //TODO: add assertions @@ -673,10 +689,12 @@ contract NonLiquidDelegationTest is Test { uint64[] memory stakingIndices, uint64 firstStakingIndex, uint256 allWithdrawnRewards, - uint64 lastWithdrawnRewardIndex + uint64 lastWithdrawnRewardIndex, + uint256 withdrawnAfterLastStaking ) = delegation.getStakingData(); Console.log("stakingIndices = [ %s]", stakingIndices); - console.log("firstStakingIndex = %s allWithdrawnRewards = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), allWithdrawnRewards, uint(lastWithdrawnRewardIndex)); + console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), uint(lastWithdrawnRewardIndex)); + console.log("allWithdrawnRewards = %s withdrawnAfterLastStaking = %s", allWithdrawnRewards, withdrawnAfterLastStaking); vm.recordLogs(); vm.expectEmit( @@ -696,10 +714,12 @@ contract NonLiquidDelegationTest is Test { stakingIndices, firstStakingIndex, allWithdrawnRewards, - lastWithdrawnRewardIndex + lastWithdrawnRewardIndex, + withdrawnAfterLastStaking ) = delegation.getStakingData(); Console.log("stakingIndices = [ %s]", stakingIndices); - console.log("firstStakingIndex = %s allWithdrawnRewards = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), allWithdrawnRewards, uint(lastWithdrawnRewardIndex)); + console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), uint(lastWithdrawnRewardIndex)); + console.log("allWithdrawnRewards = %s withdrawnAfterLastStaking = %s", allWithdrawnRewards, withdrawnAfterLastStaking); Console.log("contract balance: %s.%s%s", address(delegation).balance); Console.log("staker balance: %s.%s%s", staker[i-1].balance); diff --git a/rewards.sh b/withdrawRewards.sh similarity index 83% rename from rewards.sh rename to withdrawRewards.sh index 72f36b9..6d3bb5b 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) @@ -40,8 +40,8 @@ owner=$(cast call $1 "owner()(address)" --block $block_num --rpc-url http://loca rewardsAfterWithdrawal=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') echo rewardsAfterWithdrawal = $rewardsAfterWithdrawal -staker_wei_after=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -owner_wei_after=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +ownerWeiAfter=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) tmp1=$(cast logs --from-block $block_num --to-block $block_num --address $1 "RewardPaid(address,uint256)" --rpc-url http://localhost:4201 | grep "data") if [[ "$tmp1" != "" ]]; then @@ -76,16 +76,16 @@ stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url http://l commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') -staker_wei_before=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -owner_wei_before=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +ownerWeiBefore=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) x=$(cast call $1 "rewards()(uint256)" --from $staker --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') staker_rewards_before_withdrawal=$(cast to-unit $x ether) echo staker rewards before withdrawal: $staker_rewards_before_withdrawal ZIL echo staker rewards after withdrawal: $staker_rewards_after_withdrawal ZIL -echo withdrawn rewards - gas fee = $(bc -l <<< "scale=18; $staker_wei_after-$staker_wei_before") wei -echo validator commission = $(bc -l <<< "scale=18; $owner_wei_after-$owner_wei_before") wei +echo withdrawn rewards - gas fee = $(bc -l <<< "scale=18; $stakerWeiAfter-$stakerWeiBefore") wei +echo validator commission = $(bc -l <<< "scale=18; $ownerWeiAfter-$ownerWeiBefore") wei echo total reward reduction = $(bc -l <<< "scale=18; $rewardsBeforeWithdrawal-$rewardsAfterWithdrawal") wei if [[ "$tmp1" != "" ]]; then echo event RewardPaid\($staker, $d1\) emitted; fi