diff --git a/README.md b/README.md index 05d8e55..ebe334c 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ forge install OpenZeppelin/openzeppelin-contracts --no-commit ``` ## 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. `DelegationV1` is the initial implementation of the delegation contract is upgradeable: `DelegationV2` deploys a `NonRebasingLST` contract when it is initialized and `DelegationV3` adds the newest features. +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. `Delegation` is the initial implementation of the delegation contract that creates a `NonRebasingLST` contract when it is initialized. `DelegationV2` implements staking, unstaking and other features. The delegation contract shall be deployed and upgraded by the account with the private key that was used to run the validator node and was used to generate its BLS keypair and peer id. Make sure the `PRIVATE_KEY` environment variable is set accordingly. -To deploy `DelegationV1` run -``` -forge script script/deploy_Delegation.s.sol --rpc-url https://api.zq2-devnet.zilliqa.com --broadcast --legacy +To deploy `Delegation` run +```bash +forge script script/deploy_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy ``` You will see an output like this: ``` @@ -29,8 +29,8 @@ You will see an output like this: You will need the proxy address from the above output in all commands below. To upgrade the contract to `DelegationV2`, run -``` -forge script script/upgrade_Delegation.s.sol --rpc-url https://api.zq2-devnet.zilliqa.com --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 +```bash +forge script script/upgrade_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 ``` The output will look like this: @@ -42,79 +42,111 @@ The output will look like this: Upgraded to version: 2 ``` -To upgrade the contract to `DelegationV3`, replace line 33 in `upgrade_Delegation.s.sol` with -```solidity -new DelegationV3() +## Contract Configuration + +Now or at a later time you can set the commission on the rewards the validator earns to e.g. 10% as follows: +```bash +forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint16)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000 ``` -and run + +The output will contain the following information: ``` -forge script script/upgrade_Delegation.s.sol --rpc-url https://api.zq2-devnet.zilliqa.com --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 + Running version: 2 + LST address: 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 + Old commission rate: 0.0% + New commission rate: 10.0% ``` -again. -The output will look like this: -``` - Signer is 0x15fc323DFE5D5DCfbeEdc25CEcbf57f676634d77 - Upgrading from version: 2 - Owner is 0x15fc323DFE5D5DCfbeEdc25CEcbf57f676634d77 - New implementation deployed: 0x90A65311b6C7246FFD1F212C123cfE351a6d65A9 - Upgraded to version: 3 +Note that the commission rate is specified as an integer to be devided by the `DENOMINATOR` which can be retrieved from the delegation contract: +```bash +cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "DENOMINATOR()(uint256)" --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g' ``` ## Validator Activation -Now you are ready to use the contract to activate your node as a validator with a deposit of e.g. 10 million ZIL. Run -``` -cast send --legacy --value 10000000ether --rpc-url https://api.zq2-devnet.zilliqa.com --private-key $PRIVATE_KEY \ +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 +```bash +cast send --legacy --value 10000000ether --rpc-url http://localhost:4201 --private-key $PRIVATE_KEY \ 0x7a0b7e6d24ede78260c9ddbd98e828b0e11a8ea2 "deposit(bytes,bytes,bytes)" \ 0x92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c \ 0x002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f \ 0xb14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a ``` -with the BLS public key, the peer id and the BLS signature of your node. Note that the peer id must be converted from base58 to hex. - -Make sure your node's account has the 10 million ZIL and your node is fully synced before you run the above command. +with the BLS public key, the peer id and the BLS signature of your node. Note that the peer id must be converted from base58 to hex. Make sure your node is fully synced before you run the above command. Note that the reward address registered for your validator node will be the address of the delegation contract (the proxy contract to be more precise). +Alternatively, you can proceed to the next section and delegate stake until the contract's balance reaches the 10 million ZIL minimum stake required for the activation, and then run +```bash +cast send --legacy --rpc-url http://localhost:4201 --private-key $PRIVATE_KEY \ +0x7a0b7e6d24ede78260c9ddbd98e828b0e11a8ea2 "deposit2(bytes,bytes,bytes)" \ +0x92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c \ +0x002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f \ +0xb14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a +``` +to deposit all of it. + ## Staking and Unstaking If the above transaction was successful and the node became a validator, it can accept delegations. In order to stake e.g. 200 ZIL, run -``` -forge script script/stake_Delegation.s.sol --rpc-url https://api.zq2-devnet.zilliqa.com --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... +```bash +forge script script/stake_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 200000000000000000000 --private-key 0x... ``` with the private key of the delegator account. Make sure the account's balance can cover the transaction fees plus the 200 ZIL to be delegated. The output will look like this: ``` - Running version: 3 - Current stake: 10000000000000000000000000 - Current rewards: 110314207650273223687 + Running version: 2 + Current stake: 10000000000000000000000000 wei + Current rewards: 110314207650273223687 wei LST address: 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 - Owner balance: 10000000000000000000000000 - Staker balance: 0 - Staker balance: 199993793908430833324 + Owner balance: 10000000000000000000000000 LST + Staker balance before: 99899145245801454561224 wei 0 LST + Staker balance after: 99699145245801454561224 wei 199993793908430833324 LST ``` Note that the staker LST balance in the output will be different from the actual LST balance which you can query by running -``` -cast call 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 "balanceOf(address)(uint256)" 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --rpc-url https://api.zq2-devnet.zilliqa.com +```bash +cast call 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 "balanceOf(address)(uint256)" 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g' ``` This is due to the fact that the above output was generated based on the local script execution before the transaction got submitted to the network. You can copy the LST address from the above output and add it to your wallet to transfer your liquid staking tokens to another account if you want to. -Last but not least, to unstake, run +To query the current price of an LST, run +```bash +cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "getPrice()(uint256)" --block latest --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') ether ``` -forge script script/unstake_Delegation.s.sol --rpc-url https://api.zq2-devnet.zilliqa.com --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... + +To unstake e.g. 100 LST, run +```bash +forge script script/unstake_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 100000000000000000000 --private-key 0x... ``` with the private key of an account that holds some LST. The output will look like this: ``` - Running version: 3 - Current stake: 10000000000000000000000000 - Current rewards: 331912568306010928520 + Running version: 2 + Current stake: 10000000000000000000000000 wei + Current rewards: 331912568306010928520 wei LST address: 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 - Owner balance: 10000000000000000000000000 - Staker balance: 199993784619390291653 - Staker balance: 0 + Owner balance: 10000000000000000000000000 LST + Staker balance before: 99698814298179759361224 wei 199993784619390291653 LST + Staker balance after: 99698814298179759361224 wei 99993784619390291653 LST ``` + +Last but not least, to claim the amount that is available after the unbonding period, run +```bash +forge script script/claim_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... +``` +with the private key of an account that unstaked some LST. + +The output will look like this: +``` + Running version: 2 + Staker balance before: 99698086421983460161224 wei + Staker balance after: 99798095485861371162343 wei +``` + +To query how much ZIL you can already claim, run +```bash +cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "getClaimable()(uint256)" --from 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --block latest --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') ether +``` \ No newline at end of file diff --git a/claim.sh b/claim.sh new file mode 100755 index 0000000..9795232 --- /dev/null +++ b/claim.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +if [ $# -ne 2 ]; then + echo "Provide the delegation contract address and a staker private key as arguments." + exit 1 +fi + +staker=$(cast wallet address $2) + +forge script script/claim_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" $1 --private-key $2 -vvvv + +block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) +block_num=$(echo $block | tr -d '"' | cast to-dec --base-in 16) + +echo rewardsAfterClaiming = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo taxedRewardsAfterClaiming = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + +staker_wei_after=$(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)" --rpc-url http://localhost:4201 | grep "data") +if [[ "$tmp" != "" ]]; then + tmp=${tmp#*: } + tmp=$(cast abi-decode --input "x(uint256)" $tmp | sed 's/\[[^]]*\]//g') + tmp=(${tmp}) + d1=${tmp[0]} + #d1=$(echo $tmp | 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) + +echo rewardsBeforeClaiming = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo taxedRewardsBeforeClaiming = $(cast call $1 "getTaxedRewards()(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) + +echo claimed amount - gas fee = $(bc -l <<< "scale=18; $staker_wei_after-$staker_wei_before") wei +if [[ "$tmp" != "" ]]; then echo event Claimed\($staker, $d1\) emitted; fi +echo $(date +"%T,%3N") $block_num diff --git a/script/claim_Delegation.s.sol b/script/claim_Delegation.s.sol new file mode 100644 index 0000000..eabb46c --- /dev/null +++ b/script/claim_Delegation.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {DelegationV2} from "src/DelegationV2.sol"; +import "forge-std/console.sol"; + +contract Claim is Script { + function run(address payable proxy) external { + + address staker = msg.sender; + + DelegationV2 delegation = DelegationV2( + proxy + ); + + console.log("Running version: %s", + delegation.version() + ); + + console.log("Staker balance before: %s wei", + staker.balance + ); + + vm.broadcast(); + + delegation.claim(); + + console.log("Staker balance after: %s wei", + staker.balance + ); + } +} \ No newline at end of file diff --git a/script/commission_Delegation.s.sol b/script/commission_Delegation.s.sol new file mode 100644 index 0000000..0d14727 --- /dev/null +++ b/script/commission_Delegation.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {NonRebasingLST} from "src/NonRebasingLST.sol"; +import {DelegationV2} from "src/DelegationV2.sol"; +import {Console} from "src/Console.sol"; +import "forge-std/console.sol"; + +contract Stake is Script { + function run(address payable proxy, uint16 commissionNumerator) external { + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + DelegationV2 delegation = DelegationV2( + proxy + ); + + console.log("Running version: %s", + delegation.version() + ); + + NonRebasingLST lst = NonRebasingLST(delegation.getLST()); + console.log("LST address: %s", + address(lst) + ); + + Console.log("Old commission rate: %s.%s%s%%", + delegation.getCommissionNumerator(), + 2 + ); + + vm.broadcast(deployerPrivateKey); + + delegation.setCommissionNumerator(commissionNumerator); + + Console.log("New commission rate: %s.%s%s%%", + delegation.getCommissionNumerator(), + 2 + ); + } +} \ No newline at end of file diff --git a/script/deploy_Delegation.s.sol b/script/deploy_Delegation.s.sol index 833f0fe..fde9d04 100644 --- a/script/deploy_Delegation.s.sol +++ b/script/deploy_Delegation.s.sol @@ -15,7 +15,6 @@ contract Deploy is Script { vm.startBroadcast(deployerPrivateKey); address implementation = address( - //new Delegation{salt: "zilliqa"}() new Delegation() ); @@ -25,7 +24,6 @@ contract Deploy is Script { ); address payable proxy = payable( - //new ERC1967Proxy{salt: "zilliqa"}(implementation, initializerCall) new ERC1967Proxy(implementation, initializerCall) ); @@ -39,10 +37,6 @@ contract Deploy is Script { proxy ); - delegation.stake(); - delegation.unstake(); - delegation.claim(); - console.log("Deployed version: %s", delegation.version() ); diff --git a/script/deposit_Delegation.s.sol b/script/deposit_Delegation.s.sol deleted file mode 100644 index 9bfa664..0000000 --- a/script/deposit_Delegation.s.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.26; - -import {Script} from "forge-std/Script.sol"; -import {NonRebasingLST} from "src/NonRebasingLST.sol"; -import {DelegationV3} from "src/DelegationV3.sol"; -import "forge-std/console.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; - -contract Deposit is Script { - function run(address payable proxy) external { - - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - address owner = vm.addr(deployerPrivateKey); - - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); - - DelegationV3 delegation = DelegationV3( - proxy - ); -/* - console.log("Running version: %s", - delegation.version() - ); -*/ - //TODO: output the arguments to use with cast send since forge script will fail when it tries to execute the script locally and can't call the BLS signature verification precompile - /*vm.broadcast(deployerPrivateKey); - - delegation.deposit{ - value: 10_000_000 ether - }( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), - bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), //"12D3KooWQDT1rcThrxoSmnCt9n35jrhy5wo4BHsM5JuVz8LstQpN" - bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") - ); - - console.log("Current stake: %s \r\n Current rewards: %s", - delegation.getStake(), - delegation.getRewards() - ); - */ - bytes memory input = abi.encodeWithSignature( - "deposit(bytes,bytes,bytes)", - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), - bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), //"12D3KooWQDT1rcThrxoSmnCt9n35jrhy5wo4BHsM5JuVz8LstQpN" - bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") - ); - string memory output = 'cast send'; - output = string.concat(output, ' --legacy --value 10000000ether --rpc-url https://api.zq2-devnet.zilliqa.com --private-key '); - output = string.concat(output, Strings.toHexString(deployerPrivateKey)); - output = string.concat(output, ' '); - output = string.concat(output, Strings.toHexString(address(delegation))); - /*console.log("%s \\", output); - console.logBytes(input);*/ - output = string.concat(output, ' "deposit(bytes,bytes,bytes)"'); - output = string.concat(output, ' 0x92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c'); - output = string.concat(output, ' 0x002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f'); - output = string.concat(output, ' 0xb14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a'); - console.log(output); - - // use this only for testing if deposit transaction not possible (e.g. no fully synced node available) - /*delegation.setup( - bytes(hex"b0447d886f8499bc0fd4aa21da63d71a0175ddd005d217a00c5304e1272e4a79a7df0ecb878a343582c9f2ca78c8c17f"), - bytes(hex"0024080112203f260505ee97570cbc034831097eddf177c4a49151dffb129abdc209329cc7e0") - ); - */ -/* - NonRebasingLST lst = NonRebasingLST(delegation.getLST()); - console.log("LST address: %s", - address(lst) - ); - - console.log("Owner LST balance: %s", - lst.balanceOf(owner) - ); -*/ - } -} \ No newline at end of file diff --git a/script/stake_Delegation.s.sol b/script/stake_Delegation.s.sol index 8910a33..82077d8 100644 --- a/script/stake_Delegation.s.sol +++ b/script/stake_Delegation.s.sol @@ -3,21 +3,17 @@ pragma solidity ^0.8.26; import {Script} from "forge-std/Script.sol"; import {NonRebasingLST} from "src/NonRebasingLST.sol"; -import {DelegationV3} from "src/DelegationV3.sol"; +import {DelegationV2} from "src/DelegationV2.sol"; import "forge-std/console.sol"; contract Stake is Script { - function run(address payable proxy) external { + function run(address payable proxy, uint256 amount) external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address owner = vm.addr(deployerPrivateKey); - //console.log("Owner is %s", owner); - - //address staker = 0xd819fFcE7A58b1E835c25617Db7b46a00888B013; address staker = msg.sender; - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); - DelegationV3 delegation = DelegationV3( + DelegationV2 delegation = DelegationV2( proxy ); @@ -25,7 +21,7 @@ contract Stake is Script { delegation.version() ); - console.log("Current stake: %s \r\n Current rewards: %s", + console.log("Current stake: %s wei \r\n Current rewards: %s wei", delegation.getStake(), delegation.getRewards() ); @@ -35,22 +31,23 @@ contract Stake is Script { address(lst) ); - console.log("Owner balance: %s", + console.log("Owner balance: %s LST", lst.balanceOf(owner) ); - console.log("Staker balance: %s", + console.log("Staker balance before: %s wei %s LST", + staker.balance, lst.balanceOf(staker) ); - //vm.broadcast(staker); vm.broadcast(); delegation.stake{ - value: 200 ether + value: amount }(); - console.log("Staker balance: %s", + console.log("Staker balance after: %s wei %s LST", + staker.balance, lst.balanceOf(staker) ); } diff --git a/script/unstake_Delegation.s.sol b/script/unstake_Delegation.s.sol index 7fbc5aa..06e29a8 100644 --- a/script/unstake_Delegation.s.sol +++ b/script/unstake_Delegation.s.sol @@ -3,21 +3,17 @@ pragma solidity ^0.8.26; import {Script} from "forge-std/Script.sol"; import {NonRebasingLST} from "src/NonRebasingLST.sol"; -import {DelegationV3} from "src/DelegationV3.sol"; +import {DelegationV2} from "src/DelegationV2.sol"; import "forge-std/console.sol"; contract Unstake is Script { - function run(address payable proxy) external { + function run(address payable proxy, uint256 amount) external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address owner = vm.addr(deployerPrivateKey); - //console.log("Owner is %s", owner); - - //address staker = 0xd819fFcE7A58b1E835c25617Db7b46a00888B013; address staker = msg.sender; - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); - DelegationV3 delegation = DelegationV3( + DelegationV2 delegation = DelegationV2( proxy ); @@ -25,7 +21,7 @@ contract Unstake is Script { delegation.version() ); - console.log("Current stake: %s \r\n Current rewards: %s", + console.log("Current stake: %s wei \r\n Current rewards: %s wei", delegation.getStake(), delegation.getRewards() ); @@ -35,23 +31,27 @@ contract Unstake is Script { address(lst) ); - console.log("Owner balance: %s", + console.log("Owner balance: %s LST", lst.balanceOf(owner) ); - uint256 stakerBalance = lst.balanceOf(staker); - console.log("Staker balance: %s", - stakerBalance + console.log("Staker balance before: %s wei %s LST", + staker.balance, + lst.balanceOf(staker) ); - //vm.broadcast(staker); + if (amount == 0) { + amount = lst.balanceOf(staker); + } + vm.broadcast(); delegation.unstake( - stakerBalance + amount ); - console.log("Staker balance: %s", + console.log("Staker balance after: %s wei %s LST", + staker.balance, lst.balanceOf(staker) ); } diff --git a/script/upgrade_Delegation.s.sol b/script/upgrade_Delegation.s.sol index ac9027e..0ad8281 100644 --- a/script/upgrade_Delegation.s.sol +++ b/script/upgrade_Delegation.s.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.26; import {Script} from "forge-std/Script.sol"; import {Delegation} from "src/Delegation.sol"; import {DelegationV2} from "src/DelegationV2.sol"; -import {DelegationV3} from "src/DelegationV3.sol"; import "forge-std/console.sol"; contract Upgrade is Script { @@ -13,8 +12,6 @@ contract Upgrade is Script { address owner = vm.addr(deployerPrivateKey); console.log("Signer is %s", owner); - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); - Delegation oldDelegation = Delegation( proxy ); diff --git a/src/Console.sol b/src/Console.sol new file mode 100644 index 0000000..ab394a0 --- /dev/null +++ b/src/Console.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +import "forge-std/console.sol"; + +library Console { + function log(string memory format, uint256 amount, uint8 precision) pure internal { + string memory zeros = ""; + uint256 decimals = amount % 10**precision; + while (decimals > 0 && decimals < 10**(precision - 1)) { + //console.log("%s %s", zeros, decimals); + zeros = string.concat(zeros, "0"); + decimals *= 10; + } + console.log( + format, + amount / 10**precision, + zeros, + amount % 10**precision + ); + } + + function log(string memory format, uint256 amount) pure internal { + return log(format, amount, 18); + } +} \ No newline at end of file diff --git a/src/Delegation.sol b/src/Delegation.sol index ce55d4c..9d59a9f 100644 --- a/src/Delegation.sol +++ b/src/Delegation.sol @@ -1,18 +1,17 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "src/NonRebasingLST.sol"; -// the contract is supposed to be deployed with the node's signer account contract Delegation is Initializable, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { /// @custom:storage-location erc7201:zilliqa.storage.Delegation struct Storage { - bytes blsPubKey; - bytes peerId; + address lst; } // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.Delegation")) - 1)) & ~bytes32(uint256(0xff)) @@ -40,29 +39,18 @@ contract Delegation is Initializable, PausableUpgradeable, Ownable2StepUpgradeab __Ownable_init(initialOwner); __Ownable2Step_init(); __UUPSUpgradeable_init(); + Storage storage $ = _getStorage(); + $.lst = address(new NonRebasingLST(address(this))); } function _authorizeUpgrade(address newImplementation) internal onlyOwner override {} - // this is to receive rewards receive() payable external { } - // called by the node's account that deployed this contract and is its owner - function deposit( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature - ) public payable onlyOwner { + function getLST() public view returns(address) { Storage storage $ = _getStorage(); - $.blsPubKey = blsPubKey; - $.peerId = peerId; - } - - function stake() public payable {} - - function unstake() public {} - - function claim() public{} + return $.lst; + } } diff --git a/src/DelegationV2.sol b/src/DelegationV2.sol index 59fd839..f58e1fe 100644 --- a/src/DelegationV2.sol +++ b/src/DelegationV2.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; @@ -7,19 +7,63 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "src/NonRebasingLST.sol"; +library WithdrawalQueue { + + //TODO: add it to the variables and implement a getter and an onlyOwner setter + // since a governance vote can change the unbonding period anytime or fetch + // it from the deposit contract + uint256 public constant UNBONDING_PERIOD = 30; //approx. 30s, used only for testing + + struct Item { + uint256 blockNumber; + uint256 amount; + } + + struct Fifo { + uint256 first; + uint256 last; + mapping(uint256 => Item) items; + } + + function queue(Fifo storage fifo, uint256 amount) internal { + fifo.items[fifo.last] = Item(block.number + UNBONDING_PERIOD, amount); + fifo.last++; + } + + function dequeue(Fifo storage fifo) internal returns(Item memory result) { + require(fifo.first < fifo.last, "queue empty"); + result = fifo.items[fifo.first]; + delete fifo.items[fifo.first]; + fifo.first++; + } + + function ready(Fifo storage fifo, uint256 index) internal view returns(bool) { + return index < fifo.last && fifo.items[index].blockNumber <= block.number; + } + + function ready(Fifo storage fifo) internal view returns(bool) { + return ready(fifo, fifo.first); + } +} + // the contract is supposed to be deployed with the node's signer account -// TODO: add events contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { + using WithdrawalQueue for WithdrawalQueue.Fifo; + /// @custom:storage-location erc7201:zilliqa.storage.Delegation struct Storage { + address lst; bytes blsPubKey; bytes peerId; - address lst; + uint256 commissionNumerator; + uint256 taxedRewards; + mapping(address => WithdrawalQueue.Fifo) withdrawals; + uint256 totalWithdrawals; } // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.Delegation")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant STORAGE_POSITION = 0x4432bdf0e567007e5ad3c8ad839a7f885ef69723eaa659dd9f06e98a97274300; + bytes32 private constant STORAGE_POSITION = 0x669e9cfa685336547bc6d91346afdd259f6cd8c0cb6d0b16603b5fa60cb48800; function _getStorage() private pure returns (Storage storage $) { assembly { @@ -29,6 +73,7 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade uint256 public constant MIN_DELEGATION = 100 ether; address public constant DEPOSIT_CONTRACT = 0x000000000000000000005a494C4445504F534954; + uint256 public constant DENOMINATOR = 10_000; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -40,30 +85,35 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade } function reinitialize() reinitializer(version() + 1) public { - Storage storage $ = _getStorage(); - $.lst = address(new NonRebasingLST(address(this))); } function _authorizeUpgrade(address newImplementation) internal onlyOwner override {} event Staked(address indexed delegator, uint256 amount, uint256 shares); - event UnStaked(address indexed delegator, uint256 amount, uint256 shares); + event Unstaked(address indexed delegator, uint256 amount, uint256 shares); + event Claimed(address indexed delegator, uint256 amount); + event CommissionPaid(address indexed owner, uint256 rewardsBefore, uint256 committion); - // only for test purposes - receive() payable external {} + // called when stake withdrawn from the deposit contract is claimed + // but not called when rewards are assigned to the reward address + receive() payable external { + Storage storage $ = _getStorage(); + // do not deduct commission from the withdrawn stake + $.taxedRewards += msg.value; + } - // called by the node's account that deployed this contract and is its owner - // with at least the minimum stake to request activation as a validator - function deposit( + function _deposit( bytes calldata blsPubKey, bytes calldata peerId, - bytes calldata signature - ) public payable onlyOwner { + bytes calldata signature, + uint256 depositAmount + ) internal { Storage storage $ = _getStorage(); + require($.blsPubKey.length == 0, "deposit already performed"); $.blsPubKey = blsPubKey; $.peerId = peerId; (bool success, bytes memory data) = DEPOSIT_CONTRACT.call{ - value: msg.value + value: depositAmount }( //abi.encodeWithSignature("deposit(bytes,bytes,bytes,address,address)", //TODO: replace next line with the previous one once the signer address is implemented @@ -76,38 +126,204 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade //owner() ) ); - NonRebasingLST($.lst).mint(owner(), msg.value); require(success, "deposit failed"); + } + + // 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 + function deposit2( + bytes calldata blsPubKey, + bytes calldata peerId, + bytes calldata signature + ) public onlyOwner { + _deposit( + blsPubKey, + peerId, + signature, + address(this).balance + ); + } + + // called by the node's account that deployed this contract and is its owner + // with at least the minimum stake to request the node's activation as a validator + // before any stake is delegated to it + function deposit( + bytes calldata blsPubKey, + bytes calldata peerId, + bytes calldata signature + ) public payable onlyOwner { + _deposit( + blsPubKey, + peerId, + signature, + msg.value + ); + Storage storage $ = _getStorage(); + require(NonRebasingLST($.lst).totalSupply() == 0, "stake already delegated"); + NonRebasingLST($.lst).mint(owner(), msg.value); } function stake() public payable whenNotPaused { require(msg.value >= MIN_DELEGATION, "delegated amount too low"); - //TODO: topup deposit by msg.value so that msg.value becomes part of getStake() + uint256 shares; Storage storage $ = _getStorage(); - uint256 shares = NonRebasingLST($.lst).totalSupply() * msg.value / (getStake() + getRewards()); + // deduct commission from the rewards only if already activated as a validator + // otherwise getRewards() returns 0 but taxedRewards would be greater than 0 + if ($.blsPubKey.length > 0) { + // the delegated amount is temporarily part of the rewards as it's in the balance + // add to the taxed rewards to avoid commission and remove it again after taxing + $.taxedRewards += msg.value; + // before calculating the shares deduct the commission from the yet untaxed rewards + taxRewards(); + $.taxedRewards -= msg.value; + } + if (NonRebasingLST($.lst).totalSupply() == 0) + shares = msg.value; + else + shares = NonRebasingLST($.lst).totalSupply() * msg.value / (getStake() + $.taxedRewards); NonRebasingLST($.lst).mint(msg.sender, shares); + // increase the deposit only if already activated as a validator + if ($.blsPubKey.length > 0) { + (bool success, bytes memory data) = DEPOSIT_CONTRACT.call{ + value: msg.value + }( + abi.encodeWithSignature("tempIncreaseDeposit(bytes)", + $.blsPubKey + ) + ); + require(success, "deposit increase failed"); + } emit Staked(msg.sender, msg.value, shares); } function unstake(uint256 shares) public whenNotPaused { + uint256 amount; Storage storage $ = _getStorage(); + // before calculating the amount deduct the commission from the yet untaxed rewards + taxRewards(); + if (NonRebasingLST($.lst).totalSupply() == 0) + amount = shares; + else + amount = (getStake() + $.taxedRewards) * shares / NonRebasingLST($.lst).totalSupply(); + $.withdrawals[msg.sender].queue(amount); + $.totalWithdrawals += amount; + if ($.blsPubKey.length > 0) { + // maintain a balance that is always sufficient to cover the claims + if (address(this).balance < $.totalWithdrawals) { + (bool success, bytes memory data) = DEPOSIT_CONTRACT.call( + abi.encodeWithSignature("tempDecreaseDeposit(bytes,uint256)", + $.blsPubKey, + $.totalWithdrawals - address(this).balance + ) + ); + require(success, "deposit decrease failed"); + } + } NonRebasingLST($.lst).burn(msg.sender, shares); - uint256 amount = (getStake() + getRewards()) * shares / NonRebasingLST($.lst).totalSupply(); - //TODO: don't the transfer the amount, msg.sender can claim it after the unbonding period - msg.sender.call{ - value: amount + emit Unstaked(msg.sender, amount, shares); + } + + function getCommissionNumerator() public view returns(uint256) { + Storage storage $ = _getStorage(); + return $.commissionNumerator; + } + + function setCommissionNumerator(uint256 _commissionNumerator) public onlyOwner { + require(_commissionNumerator < DENOMINATOR, "invalid commission"); + Storage storage $ = _getStorage(); + $.commissionNumerator = _commissionNumerator; + } + + // return the amount of ZIL equivalent to 1 LST (share) + function getPrice() public view returns(uint256 amount) { + Storage storage $ = _getStorage(); + uint256 rewards = getRewards(); + uint256 commission = (rewards - $.taxedRewards) * $.commissionNumerator / DENOMINATOR; + if (NonRebasingLST($.lst).totalSupply() == 0) + amount = 1 ether; + else + amount = (getStake() + rewards - commission) * 1 ether / NonRebasingLST($.lst).totalSupply(); + } + + function taxRewards() internal { + Storage storage $ = _getStorage(); + uint256 rewards = getRewards(); + uint256 commission = (rewards - $.taxedRewards) * $.commissionNumerator / DENOMINATOR; + $.taxedRewards = rewards - commission; + if (commission == 0) + return; + // commissions are not subject to the unbonding period + (bool success, bytes memory data) = owner().call{ + value: commission }(""); - emit UnStaked(msg.sender, amount, shares); + require(success, "transfer of commission failed"); + emit CommissionPaid(owner(), rewards, commission); + } + + function getClaimable() public view returns(uint256 total) { + Storage storage $ = _getStorage(); + WithdrawalQueue.Fifo storage fifo = $.withdrawals[msg.sender]; + uint256 index = fifo.first; + while (fifo.ready(index)) { + total += fifo.items[index].amount; + index++; + } } function claim() public whenNotPaused { - } + Storage storage $ = _getStorage(); + uint256 total; + while ($.withdrawals[msg.sender].ready()) + total += $.withdrawals[msg.sender].dequeue().amount; + /*if (total == 0) + return;*/ + // before the balance changes deduct the commission from the yet untaxed rewards + taxRewards(); + //TODO: claim all deposit withdrawals requested whose unbonding period is over + (bool success, bytes memory data) = msg.sender.call{ + value: total + }(""); + require(success, "transfer of funds failed"); + $.totalWithdrawals -= total; + $.taxedRewards -= total; + emit Claimed(msg.sender, total); + } - function restake() public onlyOwner{ + //TODO: make it onlyOwnerOrContract and call it every time someone stakes, unstakes or claims? + function restakeRewards() public onlyOwner { + Storage storage $ = _getStorage(); + // before the balance changes deduct the commission from the yet untaxed rewards + taxRewards(); + if ($.blsPubKey.length > 0) { + (bool success, bytes memory data) = DEPOSIT_CONTRACT.call{ + value: address(this).balance - $.totalWithdrawals + }( + abi.encodeWithSignature("tempIncreaseDeposit(bytes)", + $.blsPubKey + ) + ); + require(success, "deposit increase failed"); + } + } + + function collectCommission() public onlyOwner { + taxRewards(); + } + + function getTaxedRewards() public view returns(uint256) { + Storage storage $ = _getStorage(); + return $.taxedRewards; } + function getTotalWithdrawals() public view returns(uint256) { + Storage storage $ = _getStorage(); + return $.totalWithdrawals; + } + function getRewards() public view returns(uint256) { Storage storage $ = _getStorage(); + if ($.blsPubKey.length == 0) + return 0; (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( abi.encodeWithSignature("getRewardAddress(bytes)", $.blsPubKey) ); @@ -118,6 +334,8 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade function getStake() public view returns(uint256) { Storage storage $ = _getStorage(); + if ($.blsPubKey.length == 0) + return address(this).balance; (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( abi.encodeWithSignature("getStake(bytes)", $.blsPubKey) ); diff --git a/src/DelegationV3.sol b/src/DelegationV3.sol deleted file mode 100644 index df4a635..0000000 --- a/src/DelegationV3.sol +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import "src/NonRebasingLST.sol"; - -// the contract is supposed to be deployed with the node's signer account -// TODO: add events -contract DelegationV3 is Initializable, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { - - /// @custom:storage-location erc7201:zilliqa.storage.Delegation - struct Storage { - bytes blsPubKey; - bytes peerId; - address lst; - } - - // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.Delegation")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant STORAGE_POSITION = 0x4432bdf0e567007e5ad3c8ad839a7f885ef69723eaa659dd9f06e98a97274300; - - function _getStorage() private pure returns (Storage storage $) { - assembly { - $.slot := STORAGE_POSITION - } - } - - uint256 public constant MIN_DELEGATION = 100 ether; - address public constant DEPOSIT_CONTRACT = 0x000000000000000000005a494C4445504F534954; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - function version() public view returns(uint64) { - return _getInitializedVersion(); - } - - function reinitialize() reinitializer(version() + 1) public { - } - - function _authorizeUpgrade(address newImplementation) internal onlyOwner override {} - - event Staked(address indexed delegator, uint256 amount, uint256 shares); - event UnStaked(address indexed delegator, uint256 amount, uint256 shares); - - // currently not called as there is no transaction for issuing rewards - receive() payable external { - require (msg.sender == 0x0000000000000000000000000000000000000000, "rewards must be issues by zero address"); - // topup deposit by msg.value to restake the rewards - // or use them for instant stake withdrawals - } - - // called by the node's account that deployed this contract and is its owner - // with at least the minimum stake to request activation as a validator - function deposit( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature - ) public payable onlyOwner { - Storage storage $ = _getStorage(); - $.blsPubKey = blsPubKey; - $.peerId = peerId; - (bool success, bytes memory data) = DEPOSIT_CONTRACT.call{ - value: msg.value - }( - //abi.encodeWithSignature("deposit(bytes,bytes,bytes,address,address)", - //TODO: replace next line with the previous one once the signer address is implemented - abi.encodeWithSignature("deposit(bytes,bytes,bytes,address)", - blsPubKey, - peerId, - signature, - address(this) - //TODO: enable next line once the signer address is implemented - //owner() - ) - ); - NonRebasingLST($.lst).mint(owner(), msg.value); - require(success, "deposit failed"); - } - - event Log(uint256 _totalSupply, uint256 _msgValue, uint256 _getStake, uint256 _getReward, uint256 shares); - function stake() public payable whenNotPaused { - require(msg.value >= MIN_DELEGATION, "delegated amount too low"); - //TODO: topup deposit by msg.value so that msg.value becomes part of getStake() - Storage storage $ = _getStorage(); - uint256 _totalSupply = NonRebasingLST($.lst).totalSupply(); - uint256 _msgValue = msg.value; - uint256 _getRewards = getRewards(); - uint256 _getStake = getStake(); - //uint256 shares = NonRebasingLST($.lst).totalSupply() * msg.value / (getStake() + getRewards()); - uint256 shares = _totalSupply * _msgValue / (_getStake + _getRewards); - emit Log(_totalSupply, _msgValue, _getStake, _getRewards, shares); - NonRebasingLST($.lst).mint(msg.sender, shares); - emit Staked(msg.sender, msg.value, shares); - } - - function unstake(uint256 shares) public whenNotPaused { - Storage storage $ = _getStorage(); - NonRebasingLST($.lst).burn(msg.sender, shares); - uint256 amount = (getStake() + getRewards()) * shares / NonRebasingLST($.lst).totalSupply(); - //TODO: deduct the commission - //TODO: don't transfer the amount, msg.sender can claim it after the unbonding period - (bool success, bytes memory data) = msg.sender.call{ - value: amount - }(""); - require(success, "transfer of funds failed"); - emit UnStaked(msg.sender, amount, shares); - } - - function claim() public whenNotPaused { - // - } - - function restake() public onlyOwner{ - // - } - -/* function getRewards() public view returns(uint256){ - return 24391829365079365070369; - } -*/ - function getRewards() public view returns(uint256) { - Storage storage $ = _getStorage(); - (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( - abi.encodeWithSignature("getRewardAddress(bytes)", $.blsPubKey) - ); - require(success, "could not retrieve reward address"); - address rewardAddress = abi.decode(data, (address)); - return rewardAddress.balance; - } - -/* //TODO: replace with the below getStake2() function once stake() tops up the deposit - function getStake() public view returns(uint256) { - return getStake2() + address(this).balance; - } -*/ - function getStake() public view returns(uint256) { - Storage storage $ = _getStorage(); - (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( - abi.encodeWithSignature("getStake(bytes)", $.blsPubKey) - ); - require(success, "could not retrieve staked amount"); - return abi.decode(data, (uint256)); - } - - function getLST() public view returns(address) { - Storage storage $ = _getStorage(); - return $.lst; - } - - // only for testing purposes, will be removed later - function setup(bytes calldata blsPubKey, bytes calldata peerId) public onlyOwner { - Storage storage $ = _getStorage(); - $.blsPubKey = blsPubKey; - $.peerId = peerId; - owner().call{ - value: address(this).balance - }(""); - $.lst = address(new NonRebasingLST(address(this))); - NonRebasingLST($.lst).mint(owner(), getStake()); - } - -} \ No newline at end of file diff --git a/src/Deposit.sol b/src/Deposit.sol new file mode 100644 index 0000000..53eb810 --- /dev/null +++ b/src/Deposit.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.20; + +struct Staker { + // The index of this staker's `blsPubKey` in the `_stakerKeys` array, plus 1. 0 is used for non-existing entries. + uint256 keyIndex; + // Invariant: `balance >= minimumStake` + uint256 balance; + address rewardAddress; + bytes peerId; +} + +contract Deposit { + bytes[] _stakerKeys; + mapping(bytes => Staker) _stakersMap; + uint256 public totalStake; + + uint256 public _minimumStake; + uint256 public _maximumStakers; + + constructor(uint256 minimumStake, uint256 maximumStakers) { + _minimumStake = minimumStake; + _maximumStakers = maximumStakers; + } + + function leaderFromRandomness( + uint256 randomness + ) private view returns (bytes memory) { + // Get a random number in the inclusive range of 0 to (totalStake - 1) + uint256 position = randomness % totalStake; + uint256 cummulative_stake = 0; + + for (uint256 i = 0; i < _stakerKeys.length; i++) { + bytes storage stakerKey = _stakerKeys[i]; + Staker storage staker = _stakersMap[stakerKey]; + + cummulative_stake += staker.balance; + + if (position < cummulative_stake) { + return stakerKey; + } + } + + revert("Unable to select next leader"); + } + + function leader() public view returns (bytes memory) { + return leaderFromRandomness(uint256(block.prevrandao)); + } + + function leaderAtView( + uint256 viewNumber + ) public view returns (bytes memory) { + uint256 randomness = uint256( + keccak256(bytes.concat(bytes32(viewNumber))) + ); + return leaderFromRandomness(randomness); + } + + // Temporary function to manually remove a staker. Can be called by the reward address of any staker with more than + // 10% stake. Will be removed later in development. + function tempRemoveStaker(bytes calldata blsPubKey) public { + require(blsPubKey.length == 48); + + // Inefficient, but its fine because this is temporary. + for (uint256 i = 0; i < _stakerKeys.length; i++) { + bytes storage stakerKey = _stakerKeys[i]; + Staker storage staker = _stakersMap[stakerKey]; + + // Check if the call is authorised. + if ( + msg.sender == staker.rewardAddress && + staker.balance > (totalStake / 10) + ) { + // The call is authorised, so we can delete the specified staker. + Staker storage stakerToDelete = _stakersMap[blsPubKey]; + + // Delete this staker's key from `_stakerKeys`. Swap the last element in the array into the deleted position. + bytes storage swappedStakerKey = _stakerKeys[ + _stakerKeys.length - 1 + ]; + Staker storage swappedStaker = _stakersMap[swappedStakerKey]; + _stakerKeys[stakerToDelete.keyIndex - 1] = swappedStakerKey; + swappedStaker.keyIndex = stakerToDelete.keyIndex; + + // The last element is now the element we want to delete. + _stakerKeys.pop(); + + // Reduce the total stake, but don't refund to the removed staker + totalStake -= stakerToDelete.balance; + + // Delete the staker from `_stakersMap` too. + delete _stakersMap[blsPubKey]; + + return; + } + } + revert( + "call must come from a reward address corresponding to a staker with more than 10% stake" + ); + } + + // keep in-sync with zilliqa/src/precompiles.rs + function _popVerify( + bytes memory pubkey, + bytes memory signature + ) private view returns (bool) { + bytes memory input = abi.encodeWithSelector( + hex"bfd24965", // bytes4(keccak256("popVerify(bytes,bytes)")) + signature, + pubkey + ); + // mocked to make tests work + 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); + + require(_stakerKeys.length < _maximumStakers, "too many stakers"); + + // Verify signature as a proof-of-possession of the private key. + bool pop = _popVerify(blsPubKey, signature); + require(pop, "rogue key check"); + + uint256 keyIndex = _stakersMap[blsPubKey].keyIndex; + if (keyIndex == 0) { + // The staker will be at index `_stakerKeys.length`. We also need to add 1 to avoid the 0 sentinel value. + _stakersMap[blsPubKey].keyIndex = _stakerKeys.length + 1; + _stakerKeys.push(blsPubKey); + } + + _stakersMap[blsPubKey].balance += msg.value; + totalStake += msg.value; + + if (_stakersMap[blsPubKey].balance < _minimumStake) { + revert("stake less than minimum stake"); + } + + _stakersMap[blsPubKey].rewardAddress = rewardAddress; + _stakersMap[blsPubKey].peerId = peerId; + } + + // temporary function to test liquid staking + function tempIncreaseDeposit(bytes calldata blsPubKey) public payable { + Staker storage staker = _stakersMap[blsPubKey]; + require(staker.keyIndex != 0, "unknown staker"); + require(staker.rewardAddress == msg.sender, "invalid sender"); + staker.balance += msg.value; + totalStake += msg.value; + } + + // temporary function to test liquid staking + function tempDecreaseDeposit( + bytes calldata blsPubKey, + uint256 amount + ) public { + Staker storage staker = _stakersMap[blsPubKey]; + require(staker.keyIndex != 0, "unknown staker"); + require(staker.rewardAddress == msg.sender, "invalid sender"); + staker.balance -= amount; + require( + staker.balance == 0 || staker.balance >= _minimumStake, + "stake too low" + ); + totalStake -= amount; + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "withdrawal failed"); + } + + function setStake( + bytes calldata blsPubKey, + bytes calldata peerId, + address rewardAddress, + uint256 amount + ) public { + require(msg.sender == address(0)); + require(blsPubKey.length == 48); + require(peerId.length == 38); + + if (amount < _minimumStake) { + revert("stake less than minimum stake"); + } + + totalStake -= _stakersMap[blsPubKey].balance; + _stakersMap[blsPubKey].balance = amount; + totalStake += amount; + _stakersMap[blsPubKey].rewardAddress = rewardAddress; + _stakersMap[blsPubKey].peerId = peerId; + uint256 keyIndex = _stakersMap[blsPubKey].keyIndex; + if (keyIndex == 0) { + // The staker will be at index `_stakerKeys.length`. We also need to add 1 to avoid the 0 sentinel value. + _stakersMap[blsPubKey].keyIndex = _stakerKeys.length + 1; + _stakerKeys.push(blsPubKey); + } + } + + function getStake(bytes calldata blsPubKey) public view returns (uint256) { + require(blsPubKey.length == 48); + + return _stakersMap[blsPubKey].balance; + } + + function getRewardAddress( + bytes calldata blsPubKey + ) public view returns (address) { + require(blsPubKey.length == 48); + if (_stakersMap[blsPubKey].rewardAddress == address(0)) { + revert("not staked"); + } + return _stakersMap[blsPubKey].rewardAddress; + } + + function getStakers() public view returns (bytes[] memory) { + return _stakerKeys; + } + + function getStakersData() + public + view + returns (bytes[] memory stakerKeys, Staker[] memory stakers) + { + stakerKeys = _stakerKeys; + stakers = new Staker[](stakerKeys.length); + for (uint256 i = 0; i < stakerKeys.length; i++) { + stakers[i] = _stakersMap[stakerKeys[i]]; + } + } + + function getPeerId( + bytes calldata blsPubKey + ) public view returns (bytes memory) { + require(blsPubKey.length == 48); + if (_stakersMap[blsPubKey].rewardAddress == address(0)) { + revert("not staked"); + } + return _stakersMap[blsPubKey].peerId; + } +} \ No newline at end of file diff --git a/src/NonRebasingLST.sol b/src/NonRebasingLST.sol index c9bd82a..55e9496 100644 --- a/src/NonRebasingLST.sol +++ b/src/NonRebasingLST.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/stake.sh b/stake.sh new file mode 100755 index 0000000..24193c6 --- /dev/null +++ b/stake.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +if [ $# -ne 3 ]; then + echo "Provide the delegation contract address, a staker private key and an amount in wei as arguments." + exit 1 +fi + +staker=$(cast wallet address $2) + +forge script script/stake_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" $1 $3 --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) + +echo rewardsAfterStaking = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo taxedRewardsAfterStaking = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + +staker_wei_after=$(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,uint256)" --rpc-url http://localhost:4201 | grep "data") +if [[ "$tmp" != "" ]]; then + tmp=${tmp#*: } + tmp=$(cast abi-decode --input "x(uint256,uint256)" $tmp | sed 's/\[[^]]*\]//g') + tmp=(${tmp}) + d1=${tmp[0]} + d2=${tmp[1]} + #d1=$(echo $tmp | sed -n -e 1p | sed 's/\[[^]]*\]//g') + #d2=$(echo $tmp | sed -n -e 2p | 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) + +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') +echo rewardsBeforeStaking = $rewardsBeforeStaking +echo taxedRewardsBeforeStaking = $taxedRewardsBeforeStaking + +lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url http://localhost:4201) + +totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +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') +price=$(bc -l <<< "scale=36; ($stake+$rewardsBeforeStaking-($rewardsBeforeStaking-$taxedRewardsBeforeStaking)*$commissionNumerator/$denominator)/$totalSupply") +price0=$(cast call $1 "getPrice()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + +echo LST price: $price \~ $(cast to-unit $price0 ether) +echo staked ZIL shares: $(bc -l <<< "scale=18; $3/$price/10^18") LST + +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 + +if [[ "$tmp" != "" ]]; then echo event Staked\($staker, $d1, $d2\) emitted; fi + +echo $(date +"%T,%3N") $block_num \ No newline at end of file diff --git a/state.sh b/state.sh new file mode 100755 index 0000000..7553506 --- /dev/null +++ b/state.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +if [ $# -ne 2 ]; then + echo "Provide the delegation contract address and a staker address as arguments." + exit 1 +fi + +block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) +block_num=$(echo $block | tr -d '"' | cast to-dec --base-in 16) +echo $(date +"%T,%3N") $block_num + +owner=$(cast call $1 "owner()(address)" --block $block_num --rpc-url http://localhost:4201) +lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url http://localhost:4201) + +rewardsBeforeUnstaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +#rewardsBeforeUnstaking=$(cast rpc eth_getBalance $1 $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +taxedRewardsBeforeUnstaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo rewardsBeforeUnstaking = $rewardsBeforeUnstaking +echo taxedRewardsBeforeUnstaking = $taxedRewardsBeforeUnstaking + +x=$(cast call $lst "balanceOf(address)(uint256)" $owner --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +owner_lst=$(cast to-unit $x ether) +x=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +owner_zil=$(cast to-unit $x ether) +echo owner: $owner_lst LST && echo owner: $owner_zil ZIL + +x=$(cast call $lst "balanceOf(address)(uint256)" $2 --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +staker_lst=$(cast to-unit $x ether) +x=$(cast rpc eth_getBalance $2 $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +staker_zil=$(cast to-unit $x ether) +echo staker: $staker_lst LST && echo staker: $staker_zil ZIL + +totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +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') +price=$(bc -l <<< "scale=36; ($stake+$rewardsBeforeUnstaking-($rewardsBeforeUnstaking-$taxedRewardsBeforeUnstaking)*$commissionNumerator/$denominator)/$totalSupply") +price0=$(cast call $1 "getPrice()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +claimable=$(cast call $1 "getClaimable()(uint256)" --from $2 --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + +echo LST supply: $(cast to-unit $totalSupply ether) ZIL +echo LST price: $price \~ $(cast to-unit $price0 ether) +echo staker LST value: $(bc -l <<< "scale=18; $staker_lst*$price") ZIL +echo staker claimable: $(cast to-unit $claimable ether) ZIL + +echo validator stake: $(cast to-unit $stake ether) ZIL +echo pending withdrawals: $(cast call $1 "getTotalWithdrawals()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') wei \ No newline at end of file diff --git a/test/Delegation.t.sol b/test/Delegation.t.sol new file mode 100644 index 0000000..9fd2b3e --- /dev/null +++ b/test/Delegation.t.sol @@ -0,0 +1,870 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +import {Delegation} from "src/Delegation.sol"; +import {DelegationV2} from "src/DelegationV2.sol"; +import {NonRebasingLST} from "src/NonRebasingLST.sol"; +import {Deposit} from "src/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 DelegationTest is Test { + address payable proxy; + address owner; + address staker = 0xd819fFcE7A58b1E835c25617Db7b46a00888B013; + + function setUp() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + owner = vm.addr(deployerPrivateKey); + //console.log("Signer is %s", owner); + vm.deal(owner, 100_000 ether); + vm.startPrank(owner); + + address oldImplementation = address( + new Delegation() + ); + + bytes memory initializerCall = abi.encodeWithSelector( + Delegation.initialize.selector, + owner + ); + + proxy = payable( + new ERC1967Proxy(oldImplementation, initializerCall) + ); + /* + console.log( + "Proxy deployed: %s \r\n Implementation deployed: %s", + proxy, + oldImplementation + ); + //*/ + Delegation oldDelegation = Delegation( + proxy + ); + /* + console.log("Deployed version: %s", + oldDelegation.version() + ); + + console.log("Owner is %s", + oldDelegation.owner() + ); + //*/ + address payable newImplementation = payable( + new DelegationV2() + ); + /* + console.log("New implementation deployed: %s", + newImplementation + ); + //*/ + bytes memory reinitializerCall = abi.encodeWithSelector( + DelegationV2.reinitialize.selector + ); + + oldDelegation.upgradeToAndCall( + newImplementation, + reinitializerCall + ); + + DelegationV2 delegation = DelegationV2( + proxy + ); + /* + console.log("Upgraded to version: %s", + delegation.version() + ); + //*/ + NonRebasingLST lst = NonRebasingLST(delegation.getLST()); + /* + console.log("LST address: %s", + address(lst) + ); + + Console.log("Old commission rate: %s.%s%s%%", + delegation.getCommissionNumerator(), + 2 + ); + //*/ + uint256 commissionNumerator = 1_000; + delegation.setCommissionNumerator(commissionNumerator); + /* + Console.log("New commission rate: %s.%s%s%%", + delegation.getCommissionNumerator(), + 2 + ); + //*/ + + //vm.deployCodeTo("Deposit.sol", delegation.DEPOSIT_CONTRACT()); + vm.etch( + delegation.DEPOSIT_CONTRACT(), //0x000000000000000000005a494C4445504F534954, + address(new Deposit(10_000_000 ether, 256)).code + ); + vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(3)), bytes32(uint256(10_000_000 ether))); + vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(4)), bytes32(uint256(256))); + /* + console.log("Deposit._minimimStake() =", Deposit(delegation.DEPOSIT_CONTRACT())._minimumStake()); + console.log("Deposit._maximumStakers() =", Deposit(delegation.DEPOSIT_CONTRACT())._maximumStakers()); + //*/ + } + + function run( + uint256 depositAmount, + uint256 rewardsBeforeStaking, + uint256 taxedRewardsBeforeStaking, + uint256 delegatedAmount, + uint8 numberOfDelegations, + uint256 rewardsAccruedAfterEach, + uint256 rewardsBeforeUnstaking, + uint256 blocksUntil, + bool initialDeposit + ) public { + DelegationV2 delegation = DelegationV2(proxy); + NonRebasingLST lst = NonRebasingLST(delegation.getLST()); + + if (initialDeposit) { + vm.deal(owner, owner.balance + depositAmount); + vm.startPrank(owner); + + delegation.deposit{ + value: depositAmount + }( + bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), + bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), + bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") + ); + } else { + vm.deal(staker, staker.balance + depositAmount); + vm.startPrank(staker); + + vm.expectEmit( + true, + false, + false, + true, + address(delegation) + ); + emit DelegationV2.Staked( + staker, + depositAmount, + depositAmount + ); + + delegation.stake{ + value: depositAmount + }(); + + vm.startPrank(owner); + + delegation.deposit2( + bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), + bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), + bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") + ); + } + + vm.store(address(delegation), 0x669e9cfa685336547bc6d91346afdd259f6cd8c0cb6d0b16603b5fa60cb48804, bytes32(taxedRewardsBeforeStaking)); + vm.deal(address(delegation), rewardsBeforeStaking); + vm.deal(staker, 100_000 ether); + vm.startPrank(staker); + + Console.log("Deposit before staking: %s.%s%s ZIL", + delegation.getStake() + ); + + Console.log("Rewards before staking: %s.%s%s ZIL", + delegation.getRewards() + ); + + Console.log("Taxed rewards before staking: %s.%s%s ZIL", + delegation.getTaxedRewards() + ); + + Console.log("Staker balance before staking: %s.%s%s ZIL", + staker.balance + ); + + Console.log("Staker balance before staking: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply before staking: %s.%s%s LST", + lst.totalSupply() + ); + + uint256 ownerZILBefore; + uint256 ownerZILAfter; + uint256 loggedAmount; + uint256 loggedShares; + uint256 totalShares; + uint256 rewardsAfterStaking; + uint256 taxedRewardsAfterStaking; + uint256 rewardsDelta = rewardsBeforeStaking - taxedRewardsBeforeStaking; + Vm.Log[] memory entries; + + for (uint8 j = 0; j < numberOfDelegations; j++) { + console.log("staking %s --------------------------------", j + 1); + + vm.recordLogs(); + + vm.expectEmit( + true, + false, + false, + false, + address(delegation) + ); + emit DelegationV2.Staked( + staker, + delegatedAmount, + lst.totalSupply() * delegatedAmount / (delegation.getStake() + delegation.getRewards()) + ); + + ownerZILBefore = delegation.owner().balance; + + delegation.stake{ + value: delegatedAmount + }(); + + ownerZILAfter = delegation.owner().balance; + + entries = vm.getRecordedLogs(); + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == keccak256("Staked(address,uint256,uint256)")) { + (loggedAmount, loggedShares) = abi.decode(entries[i].data, (uint256, uint256)); + assertEq(loggedAmount, delegatedAmount, "staked amount mismatch"); + } + } + totalShares += loggedShares; + + Console.log("Owner commission after staking: %s.%s%s ZIL", + ownerZILAfter - ownerZILBefore + ); + assertEq(rewardsDelta * delegation.getCommissionNumerator() / delegation.DENOMINATOR(), ownerZILAfter - ownerZILBefore, "commission mismatch after staking"); + + Console.log("Deposit after staking: %s.%s%s ZIL", + delegation.getStake() + ); + + rewardsAfterStaking = delegation.getRewards(); + Console.log("Rewards after staking: %s.%s%s ZIL", + rewardsAfterStaking + ); + + taxedRewardsAfterStaking = delegation.getTaxedRewards(); + Console.log("Taxed rewards after staking: %s.%s%s ZIL", + taxedRewardsAfterStaking + ); + + Console.log("Staker balance after staking: %s.%s%s ZIL", + staker.balance + ); + + Console.log("Staker balance after staking: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply after staking: %s.%s%s LST", + lst.totalSupply() + ); + + vm.deal(address(delegation), address(delegation).balance + rewardsAccruedAfterEach); + rewardsDelta = delegation.getRewards() - taxedRewardsAfterStaking; + } + + vm.deal(address(delegation), rewardsBeforeUnstaking); + + uint256 lstPrice = 10**18 * 1 ether * ((delegation.getStake() + delegation.getRewards() - (delegation.getRewards() - delegation.getTaxedRewards()) * delegation.getCommissionNumerator() / delegation.DENOMINATOR())) / lst.totalSupply(); + Console.log("LST price: %s.%s%s", + lstPrice + ); + assertEq(lstPrice / 10**18, delegation.getPrice(), "price mismatch"); + + Console.log("LST value: %s.%s%s", + totalShares * lstPrice / 10**18 / 1 ether + ); + + vm.recordLogs(); + + vm.expectEmit( + true, + false, + false, + false, + address(delegation) + ); + emit DelegationV2.Unstaked( + staker, + (delegation.getStake() + delegation.getRewards()) * lst.balanceOf(staker) / lst.totalSupply(), + lst.balanceOf(staker) + ); + + uint256 stakerLSTBefore = lst.balanceOf(staker); + ownerZILBefore = delegation.owner().balance; + + uint256 shares = initialDeposit ? lst.balanceOf(staker) : lst.balanceOf(staker) - depositAmount; + assertEq(totalShares, shares, "staked shares balance mismatch"); + + delegation.unstake( + shares + ); + + uint256 stakerLSTAfter = lst.balanceOf(staker); + ownerZILAfter = delegation.owner().balance; + + entries = vm.getRecordedLogs(); + + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == keccak256("Unstaked(address,uint256,uint256)")) { + (loggedAmount, loggedShares) = abi.decode(entries[i].data, (uint256, uint256)); + } + } + assertEq(totalShares * lstPrice / 10**18 / 1 ether, loggedAmount, "unstaked amount mismatch"); + assertEq(shares, loggedShares, "unstaked shares mismatch"); + assertEq(shares, stakerLSTBefore - stakerLSTAfter, "shares balance mismatch"); + + Console.log("Owner commission after unstaking: %s.%s%s ZIL", + ownerZILAfter - ownerZILBefore + ); + + assertEq((rewardsBeforeUnstaking - taxedRewardsAfterStaking) * delegation.getCommissionNumerator() / delegation.DENOMINATOR(), ownerZILAfter - ownerZILBefore, "commission mismatch after unstaking"); + + Console.log("Deposit after unstaking: %s.%s%s ZIL", + delegation.getStake() + ); + + uint256 rewardsAfterUnstaking = delegation.getRewards(); + Console.log("Rewards after unstaking: %s.%s%s ZIL", + rewardsAfterUnstaking + ); + + uint256 taxedRewardsAfterUnstaking = delegation.getTaxedRewards(); + Console.log("Taxed rewards after unstaking: %s.%s%s ZIL", + taxedRewardsAfterUnstaking + ); + + uint256 stakerBalanceAfterUnstaking = staker.balance; + Console.log("Staker balance after unstaking: %s.%s%s ZIL", + stakerBalanceAfterUnstaking + ); + + Console.log("Staker balance after unstaking: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply after unstaking: %s.%s%s LST", + lst.totalSupply() + ); + + vm.roll(block.number + blocksUntil); + + vm.recordLogs(); + + uint256 unstakedAmount = loggedAmount; // the amount we logged on unstaking + Console.log("Unstaked amount: %s.%s%s ZIL", unstakedAmount); + + vm.expectEmit( + true, + false, + false, + false, + address(delegation) + ); + emit DelegationV2.Claimed( + staker, + unstakedAmount + ); + + uint256 stakerZILBefore = staker.balance; + ownerZILBefore = delegation.owner().balance; + + delegation.claim(); + + uint256 stakerZILAfter = staker.balance; + ownerZILAfter = delegation.owner().balance; + + entries = vm.getRecordedLogs(); + + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == keccak256("Claimed(address,uint256)")) { + loggedAmount = abi.decode(entries[i].data, (uint256)); + } + } + assertEq(loggedAmount, unstakedAmount, "unstaked vs claimed amount mismatch"); + assertEq(loggedAmount, stakerZILAfter - stakerZILBefore, "claimed amount vs staker balance mismatch"); + + Console.log("Owner commission after claiming: %s.%s%s ZIL", + ownerZILAfter - ownerZILBefore + ); + assertEq((rewardsAfterUnstaking - taxedRewardsAfterUnstaking) * delegation.getCommissionNumerator() / delegation.DENOMINATOR(), ownerZILAfter - ownerZILBefore, "commission mismatch after claiming"); + + Console.log("Deposit after claiming: %s.%s%s ZIL", + delegation.getStake() + ); + + Console.log("Rewards after claiming: %s.%s%s ZIL", + delegation.getRewards() + ); + + Console.log("Taxed rewards after claiming: %s.%s%s ZIL", + delegation.getTaxedRewards() + ); + + Console.log("Staker balance after claiming: %s.%s%s ZIL", + staker.balance + ); + assertEq(staker.balance, stakerBalanceAfterUnstaking + unstakedAmount, "final staker balance mismatch"); + + Console.log("Staker balance after claiming: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply after claiming: %s.%s%s LST", + lst.totalSupply() + ); + + } + + function test_1a_LargeStake_Late_NoRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_1b_LargeStake_Early_NoRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 1 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_2a_LargeStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_3a_SmallStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 100 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_4a_LargeStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 100_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_5a_SmallStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 100_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 100 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_2b_LargeStake_Late_SmallValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_3b_SmallStake_Late_SmallValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 100 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + false // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_4b_LargeStake_Late_LargeValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 100_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + false // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_5b_SmallStake_Late_LargeValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 100_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 100 ether; + uint256 rewardsBeforeStaking = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + false // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_2c_LargeStake_Early_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 1 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_3c_SmallStake_Early_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 100 ether; + uint256 rewardsBeforeStaking = 1 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + false // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_4c_LargeStake_Early_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 100_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 1 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + false // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_5c_SmallStake_Early_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 100_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 delegatedAmount = 100 ether; + uint256 rewardsBeforeStaking = 1 * 51_000 ether * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + false // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_6a_ManyVsOneStake_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 110_000_000 ether; + uint256 delegatedAmount = 10_000 ether; + uint256 rewardsBeforeStaking = 51_000 ether / uint256(60) * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 9, // numberOfDelegations + // 5s of rewards between the delegations; always check if + // (numberOfDelegations - 1) * rewardsAccruedAfterEach <= rewardsBeforeUnstaking + 5 * 51_000 ether / uint256(3600) * depositAmount / totalDeposit, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 51_000 ether / uint256(60) * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + function test_6b_OneVsManyStakes_UnstakeAll() public { + staker = 0x092E5E57955437876dA9Df998C96e2BE19341670; + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 110_000_000 ether; + uint256 delegatedAmount = 90_000 ether; + uint256 rewardsBeforeStaking = 51_000 ether / uint256(60) * depositAmount / totalDeposit; + uint256 taxedRewardsBeforeStaking = 0; + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("taxedRewardsAfterStaking = %s.%s%s", taxedRewardsAfterStaking); + run( + depositAmount, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + taxedRewardsAfterStaking + 51_000 ether / uint256(60) * depositAmount / totalDeposit, // rewardsBeforeUnstaking + 30, // after unstaking wait blocksUntil claiming + true // initialDeposit using funds held by the node, otherwise delegated by a staker + ); + } + + /* + To compare the results of Foundry tests and a real network, use the bash scripts below + to stake, unstake and claim on the network your local node is connected to. + + Before and after running the STAKING, UNSTAKING and CLAIMING scripts presented below, + always execute the STATE script to capture the values needed in the Foundry test below. + + STATE: + chmod +x state.sh && ./state.sh + + STAKING: + chmod +x stake.sh && ./stake.sh 10000000000000000000000 + + UNSTAKING: + chmod +x unstake.sh && ./unstake.sh + + CLAIMING: + chmod +x claim.sh && ./claim.sh + + Before running the test, replace the address on the first line with + */ + function test_0_ReproduceRealNetwork() public { + staker = 0xd819fFcE7A58b1E835c25617Db7b46a00888B013; + uint256 delegatedAmount = 10_000 ether; + // Insert the following values output by the STATE script below + uint256 rewardsBeforeStaking = 197818620596390326580; + uint256 taxedRewardsBeforeStaking = 166909461128204338052; + // Compare the taxedRewardsAfterStaking output by the STATE script + // with the value logged by the test below + uint256 taxedRewardsAfterStaking = + rewardsBeforeStaking - (rewardsBeforeStaking - taxedRewardsBeforeStaking) / uint256(10); + Console.log("Expected taxed rewards after staking: %s.%s%s ZIL", taxedRewardsAfterStaking); + // Insert the following value output by the UNSTAKING script + uint256 rewardsBeforeUnstaking = 233367080700403454378; + run( + 10_000_000 ether, + rewardsBeforeStaking, + taxedRewardsBeforeStaking, + delegatedAmount, + 1, // numberOfDelegations + 0, // rewardsAccruedAfterEach + rewardsBeforeUnstaking, + 30, // blocksUntil claiming + true // initialDeposit + ); + // Replace the values below in the same order with the values output by the STATE script + // run after the CLAIMING script or logged by the CLAIMING script itself + // the staker's ZIL balance in wei according to the STATE script after claiming + // the staker's ZIL balance in wei according to the STATE script before claiming + // the claiming transaction fee in wei output by the CLAIMING script + Console.log("Expected staker balance after claiming: %s.%s%s ZIL", + 100_000 ether - delegatedAmount + + 100013.464887553198739807 ether - 90013.819919979031083499 ether + 0.3897714316896 ether + ); + // Replace the values below in the same order with values output by the STATE script + // run before the STAKING and after the UNSTAKE scripts or logged by those script themselves + // the owner's ZIL balance in wei according to the STATE script after unstaking + // the owner's ZIL balance in wei according to the STATE script before staking + // the transaction fees in wei output by the STAKING and UNSTAKING scripts + Console.log("Actual owner commission: %s.%s%s ZIL", + 100032.696802178975738911 ether - 100025.741948627073967394 ether + + 0.6143714334864 ether + 0.8724381022176 ether + ); + // Compare the value logged above with the sum of the following values + // you will see after running the test: + // Owner commission after staking + // Owner commission after unstaking + } + +} \ No newline at end of file diff --git a/unstake.sh b/unstake.sh new file mode 100755 index 0000000..4b555fb --- /dev/null +++ b/unstake.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +if [ $# -lt 2 ]; then + echo "Provide the delegation contract address, a staker private key and optionally the number of shares as arguments." + exit 1 +fi + +if [ $# -eq 3 ]; then + shares="$3" +else + shares=0 +fi + +staker=$(cast wallet address $2) + +forge script script/unstake_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" $1 $shares --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) + +echo rewardsAfterUnstaking = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo taxedRewardsAfterUnstaking = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + +tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Unstaked(address,uint256,uint256)" --rpc-url http://localhost:4201 | grep "data") +if [[ "$tmp" != "" ]]; then + tmp=${tmp#*: } + tmp=$(cast abi-decode --input "x(uint256,uint256)" $tmp | sed 's/\[[^]]*\]//g') + tmp=(${tmp}) + d1=${tmp[0]} + d2=${tmp[1]} + #d1=$(echo $tmp | sed -n -e 1p | sed 's/\[[^]]*\]//g') + #d2=$(echo $tmp | sed -n -e 2p | 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) + +rewardsBeforeUnstaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +taxedRewardsBeforeUnstaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +echo rewardsBeforeUnstaking = $rewardsBeforeUnstaking +echo taxedRewardsBeforeUnstaking = $taxedRewardsBeforeUnstaking + +lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url http://localhost:4201) + +if [[ "$shares" == "0" ]]; then shares=$(cast call $lst "balanceOf(address)(uint256)" $staker --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g'); fi + +totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +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') +price=$(bc -l <<< "scale=36; ($stake+$rewardsBeforeUnstaking-($rewardsBeforeUnstaking-$taxedRewardsBeforeUnstaking)*$commissionNumerator/$denominator)/$totalSupply") +price0=$(cast call $1 "getPrice()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + +echo LST price: $price \~ $(cast to-unit $price0 ether) +echo unstaked LST value: $(bc -l <<< "scale=18; $shares*$price/10^18") ZIL + +if [[ "$tmp" != "" ]]; then echo event Unstaked\($staker, $d1, $d2\) emitted; fi + +echo $(date +"%T,%3N") $block_num \ No newline at end of file