From 7ca62a513b3a5c2929253c7ab70d5f3862e39f4d Mon Sep 17 00:00:00 2001 From: DrZoltanFazekas <55164848+DrZoltanFazekas@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:57:27 +0100 Subject: [PATCH] Apply guidelines, improve tests and docs, support solo staker migration (#24) * Extend readme * Extend readme, fix RewardPaid event topic * Allow customizing LST name and symbol * Assert owner is not one of the stakers used in the tests * Display the liquid staking token symbol in the scripts * Mention the bash scripts in the readme * Replace occurances of localhost url from bash scripts * Add assertions to test reward calculation * Implement functions requested by staking portal * Implement migration of solo stakers to staking pools * Apply linter changes, rename forge scripts * Ensure compatibility with deposit v3 * Restructure the readme, add more solhint rules * Get rid of compiler warnings * Minor fixes requested by the reviewer --- .solhint.json | 26 ++ README.md | 183 ++++++-- claim.sh | 22 +- ...nt_Delegation.s.sol => CheckVariant.s.sol} | 7 +- .../{claim_Delegation.s.sol => Claim.s.sol} | 3 +- .../{deploy_Delegation.s.sol => Deploy.s.sol} | 27 +- ...elegation.s.sol => ManageCommission.s.sol} | 5 +- .../{stake_Delegation.s.sol => Stake.s.sol} | 13 +- ...ds_Delegation.s.sol => StakeRewards.s.sol} | 3 +- ...unstake_Delegation.s.sol => Unstake.s.sol} | 13 +- ...upgrade_Delegation.s.sol => Upgrade.s.sol} | 3 +- ...Delegation.s.sol => WithdrawRewards.s.sol} | 3 +- src/BaseDelegation.sol | 109 +++-- src/Console.sol | 19 +- src/{Delegation.sol => IDelegation.sol} | 9 +- src/LiquidDelegation.sol | 45 +- src/LiquidDelegationV2.sol | 41 +- src/NonLiquidDelegation.sol | 53 +-- src/NonLiquidDelegationV2.sol | 43 +- src/NonRebasingLST.sol | 8 +- src/WithdrawalQueue.sol | 50 +++ stake.sh | 39 +- stakeRewards.sh | 40 +- state.sh | 61 +-- test/BaseDelegation.t.sol | 196 +++++++-- test/BlsVerifyPrecompile.t.sol | 11 + test/LiquidDelegation.t.sol | 408 ++++++++++-------- test/NonLiquidDelegation.t.sol | 321 +++++++++----- unstake.sh | 32 +- withdrawRewards.sh | 36 +- 30 files changed, 1189 insertions(+), 640 deletions(-) create mode 100644 .solhint.json rename script/{variant_Delegation.s.sol => CheckVariant.s.sol} (79%) rename script/{claim_Delegation.s.sol => Claim.s.sol} (89%) rename script/{deploy_Delegation.s.sol => Deploy.s.sol} (68%) rename script/{commission_Delegation.s.sol => ManageCommission.s.sol} (91%) rename script/{stake_Delegation.s.sol => Stake.s.sol} (84%) rename script/{stakeRewards_Delegation.s.sol => StakeRewards.s.sol} (91%) rename script/{unstake_Delegation.s.sol => Unstake.s.sol} (86%) rename script/{upgrade_Delegation.s.sol => Upgrade.s.sol} (96%) rename script/{withdrawRewards_Delegation.s.sol => WithdrawRewards.s.sol} (94%) rename src/{Delegation.sol => IDelegation.sol} (59%) create mode 100644 src/WithdrawalQueue.sol create mode 100644 test/BlsVerifyPrecompile.t.sol diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..6bf4464 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,26 @@ +{ + "extends": "solhint:recommended", + "plugins": [], + "rules": { + "no-inline-assembly": "off", + "avoid-low-level-calls": "off", + "reason-string": ["warn", {"maxLength": 70}], + "func-visibility": ["warn", {"ignoreConstructors": true}], + "gas-custom-errors": "off", + "no-empty-blocks": "off", + "no-complex-fallback": "off", + "interface-starts-with-i": "warn", + "func-param-name-mixedcase": "warn", + "modifier-name-mixedcase": "warn", + "gas-calldata-parameters": "warn", + "gas-indexed-events": "warn", + "gas-length-in-loops": "warn", + "comprehensive-interface": "warn", + "gas-increment-by-one": "off", + "private-vars-leading-underscore": ["off", {"strict": false}], + "named-parameters-mapping": "off", + "foundry-test-functions": ["off"], + "imports-order": "off", + "max-line-length": ["off", 120] + } +} diff --git a/README.md b/README.md index 179bf98..d96fd3e 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@ # Delegated Staking -This repository contains the contracts and scripts needed to activate a validator that users can delegate stake to. Currently, there are two variants of the contracts: -1. When delegating stake to the liquid variant, users receive a non-rebasing **liquid staking token** (LST) that anyone can send to the validator's contract later on to withdraw the stake plus the corresponding share of the validator rewards. -1. When delegating stake to the non-liquid variant, the users can regularly withdraw their share of the rewards without withdrawing their stake. +This repository contains the contracts and scripts needed to create a staking pool that users can delegate to. Currently, the contracts exist in two variants: +1. When delegating stake to the **liquid variant**, users receive a non-rebasing liquid staking token (LST) that anyone can send to the validator's contract later on to withdraw the stake plus the corresponding share of the validator rewards. +1. When delegating stake to the **non-liquid variant**, the users can regularly withdraw their share of the rewards without withdrawing their stake. + ## Prerequisites -Install Foundry (https://book.getfoundry.sh/getting-started/installation) and the OpenZeppelin contracts before proceeding with the deployment: + +To deploy and interact with the contracts throught the CLI, use the Forge scripts provided in this repository and described further below. First, install Foundry (https://book.getfoundry.sh/getting-started/installation) and the OpenZeppelin contracts before proceeding with the deployment: ``` forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit forge install OpenZeppelin/openzeppelin-contracts --no-commit ``` -The Zilliqa 2.0 deposit contract must be compiled for the tests included in this repository to work. Specify the folder containing the `deposit.sol` file in `remappings.txt`: -``` -@zilliqa/zq2/=/home/user/zq2/zilliqa/src/contracts/ -``` ## Contract Deployment -The delegation contract is used by delegators to stake and unstake ZIL with the respective validator. It acts as the validator node's control address and interacts with the deposit contract. + +The delegation contract manages the stake delegated to the staking pool. It acts as the validator node's control address and interacts with the Zilliqa 2.0 protocol's deposit contract. `BaseDelegation` is an abstract contract that concrete implementations inherit from. `LiquidDelegation` is the initial version of the liquid staking variant of the delegation contract that creates a `NonRebasingLST` contract when it is initialized. `LiquidDelegationV2` contains the full implementation including the LST price calculation and other features. `NonLiquidDelegation` is the initial version of the non-liquid staking variant of the delegation contract. `NonLiquidDelegationV2` contains the full implementation that allows delegators to withdraw rewards. @@ -25,12 +24,13 @@ Before running the deployment script, set the `PRIVATE_KEY` environment variable To deploy `LiquidDelegation` run ```bash -forge script script/deploy_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(string)" LiquidDelegation +forge script script/Deploy.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(string,string,string)" LiquidDelegation Name Symbol ``` +using the `Name` and the `Symbol` of your LST. To deploy ``NonLiquidDelegation` run ```bash -forge script script/deploy_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(string)" NonLiquidDelegation +forge script script/Deploy.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(string,string,string)" NonLiquidDelegation "" "" ``` You will see an output like this: @@ -44,13 +44,13 @@ You will see an output like this: You and your delegators will need the proxy address from the above output in all commands below. If you know the address of a proxy contract but don't know which variant of staking it supports, run ```bash -forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 +forge script script/CheckVariant.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 ``` The output will be `ILiquidStaking`, `INonLiquidStaking` or none of them if the address is not a valid delegation contract. To use the delegation contract, upgrade it to the latest version of `LiquidDelegationV2` or `NonLiquidDelegationV2` depending on the staking model it implements, by running ```bash -forge script script/upgrade_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 +forge script script/Upgrade.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 ``` The output will look like this: @@ -69,7 +69,7 @@ To adapt the contract to your needs, create your own copy of `LiquidDelegationV2 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, string, bool)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000 false +forge script script/ManageCommission.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, bool)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000 false ``` The output will contain the following information: @@ -86,28 +86,48 @@ cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "DENOMINATOR()(uint256)" -- Once the validator is activated and starts earning rewards, commissions are transferred automatically to the validator node's account. Commissions of a non-liquid staking validator are deducted when delegators withdraw rewards. In case of the liquid staking variant, commissions are deducted each time delegators stake, unstake or claim what they unstaked, or when the node requests the outstanding commissions that haven't been transferred yet. To collect them, run ```bash -forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, bool)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 same true +forge script script/ManageCommission.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, bool)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 same true ``` using `same` for the second argument to leave the commission percentage unchanged and `true` for the third argument. Replacing the second argument with `same` and the third argument with `false` only displays the current commission rate. -## Validator Activation -If your node's account has enough ZIL for the minimum stake required, you can activate your node as a validator with a deposit of e.g. 10 million ZIL. Run +## Validator Activation or Migration + +If your node has already been activated as a validator i.e. solo staker, you can migrate it to a staking pool. Run +```bash +cast send --legacy --rpc-url http://localhost:4201 --private-key 0x... \ +0x00000000005a494c4445504f53495450524f5859 "setControlAddress(bytes,address)" \ +0x92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c \ +0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 +``` +using the private key you used to deposit your nodes, the BLS public key of your node and the address of your delegation contract. Afterwards run +```bash +cast send --legacy --rpc-url http://localhost:4201 --private-key $PRIVATE_KEY \ +0x7a0b7e6d24ede78260c9ddbd98e828b0e11a8ea2 "migrate(bytes)" \ +0x92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c +``` +using the BLS public key of your node. + +If your node hasn't been deposited yet but the owner 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)" \ +0x7a0b7e6d24ede78260c9ddbd98e828b0e11a8ea2 "depositFirst(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 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 hexadecimal format and you must provide the delegation contract address when generating the BLS signature: +```bash +echo '{"secret_key":"...", "chain_id":..., "control_address":"0x7a0b7e6d24ede78260c9ddbd98e828b0e11a8ea2"}' | cargo run --bin convert-key +``` +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 +Alternatively, you can proceed to the next section and accept delegated stake until the contract's balance reaches the 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)" \ +0x7a0b7e6d24ede78260c9ddbd98e828b0e11a8ea2 "depositLater(bytes,bytes,bytes)" \ 0x92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c \ 0x002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f \ 0xb14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a @@ -118,11 +138,12 @@ Note that the deposit will not take effect and the node will not start earning r ## Staking and Unstaking -Once the delegation contract has been deployed and upgraded to the latest version, your node can accept delegations. In order to stake e.g. 200 ZIL, your delegators must run + +Once the delegation contract has been deployed and upgraded to the latest version, your node can accept delegations. In order to stake e.g. 200 ZIL, run ```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... +forge script script/Stake.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 200000000000000000000 --private-key 0x... ``` -with the private key of their account. It's important to make sure the account's balance can cover the transaction fees plus the 200 ZIL to be delegated. +with the private key of delegator account. It's important to make sure the account's balance can cover the transaction fees plus the 200 ZIL to be delegated. The output will look like this for liquid staking: ``` @@ -147,7 +168,7 @@ Due to the fact that the above output was generated based on the local script ex cast call 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 "balanceOf(address)(uint256)" 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g' ``` -Your delegators can copy the LST address from the above output and add it to their wallet to transfer their liquid staking tokens to another account if they want to. +Copy the LST address from the above output and add it to your wallet if you want to transfer liquid staking tokens to another account. To query the current price of an LST, run ```bash @@ -156,7 +177,7 @@ cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "getPrice()( To unstake e.g. 100 LST (liquid variant) or 100 ZIL (non-liquid variant), 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... +forge script script/Unstake.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 100000000000000000000 --private-key 0x... ``` using the private key of an account that holds some LST in case of the liquid variant or using the private key of the delegator account in case of the non-liquid variant. @@ -178,11 +199,11 @@ and like this for the non-liquid variant: Staker balance after: 99698814298179759361224 wei ``` -The ZIL balance hasn't increased because the unstaked amount can not be transferred immediately. To claim the amount that is available after the unbonding period, run +The ZIL balance hasn't increased yet because the unstaked amount can not be transferred immediately. To claim the unstaked amount 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... +forge script script/Claim.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... ``` -with the private key of the account that unstaked in the previous step above. +with the private key of the account that unstaked in the previous step. The output will look like this: ``` @@ -191,25 +212,28 @@ The output will look like this: Staker balance after: 99798095485861371162343 wei ``` -To query how much ZIL a user can already claim, run +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 ``` -with the user's address as an argument. +with the address of the account that unstaked above as an argument. + +Of course, delegators will not be using the CLI to stake, unstake and claim their funds. To enable delegators to access your staking pool through the staking portal maintained by the Zilliqa team, get in touch and provide your delegation contract address once you have set up the validator node and delegation contract. If you want to integrate staking into your dapp, see the [Development and Testing](#development-and-testing) section below. + +## Withdrawing or Staking Rewards -## Staking and Withdrawing Rewards -In the liquid staking variant, only you as the node operator can stake the rewards accrued by the node. To do so, run +In the liquid staking variant, you as the node operator can stake the rewards accrued by the node. To do so, run ```bash -forge script script/stakeRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... +forge script script/StakeRewards.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... ``` -In the non-liquid variant of staking, your delegators can stake or withdraw their share of the rewards. To query the amount of rewards available, run +In the non-liquid variant of staking, delegators can stake or withdraw their share of the rewards. To query the amount of rewards available, run ```bash cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "rewards()(uint256)" --from 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --block latest --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') ether ``` -In case users haven't withdrawn rewards for a long time during which many delegators staked or unstaked, the gas used by the above function might hit the block limit. In this case rewards can be withdrawn from the period between the (un)staking until which they were withdrawn last time and the `n`th subsequent (un)staking. This can be repeated several times to withdraw all rewards using multiple transactions. To calculate the rewards that can be withdrawn in the next transaction, choose a number `0 <= n <= 11000` e.g. `100` and run +In case you haven't withdrawn rewards for a long time during which many delegators staked or unstaked, the gas used by the above function might hit the block limit. In this case rewards can be withdrawn from the period between the (un)staking until which they were withdrawn last time and the `n`th subsequent (un)staking. This can be repeated several times to withdraw all rewards using multiple transactions. To calculate the rewards that can be withdrawn in the next transaction, choose a number `0 <= n <= 11000` e.g. `100` and run ```bash cast to-unit $(cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "rewards(uint64)(uint256)" 100 --from 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 --block latest --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') ether ``` @@ -217,12 +241,93 @@ Note that `n` actually denotes the number of additional (un)stakings so that at To withdraw e.g. 1 ZIL of rewards using `n = 100`, run ```bash -forge script script/withdrawRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, string)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000000000000000000 100 --private-key 0x... +forge script script/WithdrawRewards.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, string)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000000000000000000 100 --private-key 0x... ``` with the private key of a delegator account. To withdraw as much as possible with the given value of `n` set the amount to `all`. To withdraw the chosen amount without setting `n` replace `n` with `all`. To withdraw all rewards replace both the amount and `n` with `all`. -Last but not least, in order to stake rewards instead of withdrawing them, your delegators can run +Last but not least, in order to stake rewards instead of withdrawing them, run ```bash -forge script script/stakeRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... +forge script script/StakeRewards.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... ``` using the private key of their account. + + +## Development and Testing + +Staking pool operators are encouraged to fork and adapt the above contracts to implement features such as instant unstaking for a premium fee, automated staking of rewards to achieve the best possible APR or issuing a rebasing liquid staking token with a constant price of 1 ZIL but holder balances adjusted according to the rewards accrued. + +The tests included in this repository should also be adjusted and extended accordingly. They can be executed by running +```bash +PRIVATE_KEY="0x$(openssl rand -hex 32)" forge test +``` + +To enable the tests to interact with the Zilliqa 2.0 deposit contract, the contract must be compiled along with the test contracts. Specify the folder containing the `deposit.sol` file in `remappings.txt`: +``` +@zilliqa/zq2/=/home/user/zq2/zilliqa/src/contracts/ +``` + +The following bash scripts with verbose output can be used to test staking, unstaking and claiming of unstaked funds as well as withdrawing and staking of rewards and to print the current state of a delegator's stake queried from the validator's local node. Their output is useful for checking the results of these operations. Here a few examples of how to use them (private key replaced with `0x...`): +```bash +# stake 200 ZIL +chmod +x stake.sh && ./stake.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... 200000000000000000000 + +# unstake 100 ZIL +chmod +x unstake.sh && ./unstake.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... 100000000000000000000 + +# unstake all staked ZIL +chmod +x unstake.sh && ./unstake.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... + +# claim the unstaked ZIL +chmod +x claim.sh && ./claim.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... + +# stake all rewards accrued so far +chmod +x stakeRewards.sh && ./stakeRewards.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... + +# withdraw all rewards accrued so far +chmod +x withdrawRewards.sh && ./withdrawRewards.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... + +# withdraw 10 ZIL from the rewards accrued so far +chmod +x withdrawRewards.sh && ./withdrawRewards.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... 10000000000000000000 + +# withdraw 10 ZIL from the rewards accrued during the next 1000 (un)stakings +chmod +x withdrawRewards.sh && ./withdrawRewards.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0x... 10000000000000000000 1000 + +# display the current state of the stake of the below delegator address +chmod +x state.sh && ./state.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 + +# display the state of the stake of the below delegator at block 4800000 +chmod +x state.sh && ./state.sh 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 0xd819fFcE7A58b1E835c25617Db7b46a00888B013 4800000 +``` + +Use the events and methods defined in the `IDeposit` interface and the `BaseDelegation` contract to integrate Zilliqa 2.0 staking into your dapp: +```solidity +event Staked(address indexed delegator, uint256 amount, bytes data); +event Unstaked(address indexed delegator, uint256 amount, bytes data); +event Claimed(address indexed delegator, uint256 amount, bytes data); + +function stake() external payable; +function unstake(uint256) external returns(uint256 unstakedZil); +function claim() external; + +function getClaimable() external virtual view returns(uint256 total); +function getPendingClaims() external virtual view returns(uint256[2][] memory blockNumbersAndAmounts); +function getMinDelegation() external view returns(uint256 amount); +function getCommission() external view returns(uint256 numerator, uint256 denominator); +function getStake() external view returns(uint256 validatorStake); +``` + +There are additional events and methods applicable only to a specific staking variant such as +```solidity +function getLST() external view returns(address erc20Contract); +function getPrice() external view returns(uint256 oneTokenToZil); +``` +for liquid staking and +```solidity +event RewardPaid(address indexed delegator, uint256 reward); + +function rewards() external view returns(uint256 total); +function withdrawAllRewards() external returns(uint256 taxedRewards); +function withdrawRewards(uint256 amount) external returns(uint256 taxedRewards); +function stakeRewards() external; +``` +and a few more for the non-liquid variant. diff --git a/claim.sh b/claim.sh index fa586bf..bbd826a 100755 --- a/claim.sh +++ b/claim.sh @@ -1,5 +1,7 @@ #!/bin/bash +url=http://localhost:4201 + if [ $# -ne 2 ]; then echo "Provide the delegation contract address and a staker private key as arguments." exit 1 @@ -7,26 +9,26 @@ fi staker=$(cast wallet address $2) -temp=$(forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" $1 | tail -n 1) +temp=$(forge script script/CheckVariant.s.sol --rpc-url $url --sig "run(address payable)" $1 | tail -n 1) variant=$(sed -E 's/\s\s([a-zA-Z0-9]+)/\1/' <<< "$temp") if [[ "$variant" == "$temp" ]]; then echo Incompatible delegation contract at $1 exit 1 fi -forge script script/claim_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" $1 --private-key $2 -vvvv +forge script script/Claim.s.sol --rpc-url $url --broadcast --legacy --sig "run(address payable)" $1 --private-key $2 -vvvv -block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) +block=$(cast rpc eth_blockNumber --rpc-url $url) 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 rewardsAfterClaiming = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ "$variant" == "ILiquidDelegation" ]]; then - echo taxedRewardsAfterClaiming = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + echo taxedRewardsAfterClaiming = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') fi -stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) -tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Claimed(address,uint256,bytes)" --rpc-url http://localhost:4201 | grep "data") +tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Claimed(address,uint256,bytes)" --rpc-url $url | grep "data") if [[ "$tmp" != "" ]]; then tmp=${tmp#*: } tmp=$(cast abi-decode --input "x(uint256,bytes)" $tmp | sed 's/\[[^]]*\]//g') @@ -43,12 +45,12 @@ 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 rewardsBeforeClaiming = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ "$variant" == "ILiquidDelegation" ]]; then - echo taxedRewardsBeforeClaiming = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + echo taxedRewardsBeforeClaiming = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') fi -stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) echo claimed amount - gas fee = $(bc -l <<< "scale=18; $stakerWeiAfter-$stakerWeiBefore") wei diff --git a/script/variant_Delegation.s.sol b/script/CheckVariant.s.sol similarity index 79% rename from script/variant_Delegation.s.sol rename to script/CheckVariant.s.sol index 4d13f8c..214a562 100644 --- a/script/variant_Delegation.s.sol +++ b/script/CheckVariant.s.sol @@ -1,16 +1,17 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {ILiquidDelegation} from "src/LiquidDelegation.sol"; import {INonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; -contract Upgrade is Script { +contract CheckVariant is Script { using ERC165Checker for address; - function run(address proxy) external { + function run(address proxy) external view { if (proxy.supportsInterface(type(ILiquidDelegation).interfaceId)) console.log("ILiquidDelegation"); diff --git a/script/claim_Delegation.s.sol b/script/Claim.s.sol similarity index 89% rename from script/claim_Delegation.s.sol rename to script/Claim.s.sol index e367bfa..c6c1e16 100644 --- a/script/claim_Delegation.s.sol +++ b/script/Claim.s.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; contract Claim is Script { function run(address payable proxy) external { diff --git a/script/deploy_Delegation.s.sol b/script/Deploy.s.sol similarity index 68% rename from script/deploy_Delegation.s.sol rename to script/Deploy.s.sol index bd3088e..aea663c 100644 --- a/script/deploy_Delegation.s.sol +++ b/script/Deploy.s.sol @@ -1,18 +1,19 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; import {LiquidDelegation} from "src/LiquidDelegation.sol"; import {NonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; contract Deploy is Script { using Strings for string; - function run(string calldata variant) external { + function run(string calldata variant, string calldata name, string calldata symbol) external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address owner = vm.addr(deployerPrivateKey); console.log("Signer is %s", owner); @@ -20,19 +21,25 @@ contract Deploy is Script { vm.startBroadcast(deployerPrivateKey); address implementation; + bytes memory initializerCall; - if (variant.equal("LiquidDelegation")) + if (variant.equal("LiquidDelegation")) { implementation = address(new LiquidDelegation()); - else if (variant.equal("NonLiquidDelegation")) + initializerCall = abi.encodeWithSignature( + "initialize(address,string,string)", + owner, + name, + symbol + ); + } else if (variant.equal("NonLiquidDelegation")) { implementation = address(new NonLiquidDelegation()); - else + initializerCall = abi.encodeWithSignature( + "initialize(address)", + owner + ); + } else return; - bytes memory initializerCall = abi.encodeWithSignature( - "initialize(address)", - owner - ); - address payable proxy = payable( new ERC1967Proxy(implementation, initializerCall) ); diff --git a/script/commission_Delegation.s.sol b/script/ManageCommission.s.sol similarity index 91% rename from script/commission_Delegation.s.sol rename to script/ManageCommission.s.sol index 349639d..b32d033 100644 --- a/script/commission_Delegation.s.sol +++ b/script/ManageCommission.s.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; import {Console} from "src/Console.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; -contract Commission is Script { +contract ManageCommission is Script { using Strings for string; function run(address payable proxy, string calldata commissionNumerator, bool collectCommission) external { diff --git a/script/stake_Delegation.s.sol b/script/Stake.s.sol similarity index 84% rename from script/stake_Delegation.s.sol rename to script/Stake.s.sol index 68674e9..2cc8e7b 100644 --- a/script/stake_Delegation.s.sol +++ b/script/Stake.s.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {NonRebasingLST} from "src/NonRebasingLST.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; import {ILiquidDelegation} from "src/LiquidDelegation.sol"; import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol"; import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; contract Stake is Script { using ERC165Checker for address; @@ -34,9 +35,10 @@ contract Stake is Script { address(lst) ); - console.log("Staker balance before: %s wei %s LST", + console.log("Staker balance before: %s wei %s %s", staker.balance, - lst.balanceOf(staker) + lst.balanceOf(staker), + lst.symbol() ); } else { console.log("Staker balance before: %s wei", @@ -52,9 +54,10 @@ contract Stake is Script { if (address(delegation).supportsInterface(type(ILiquidDelegation).interfaceId)) { NonRebasingLST lst = NonRebasingLST(LiquidDelegationV2(payable(address(delegation))).getLST()); - console.log("Staker balance after: %s wei %s LST", + console.log("Staker balance after: %s wei %s %s", staker.balance, - lst.balanceOf(staker) + lst.balanceOf(staker), + lst.symbol() ); } else { console.log("Staker balance after: %s wei", diff --git a/script/stakeRewards_Delegation.s.sol b/script/StakeRewards.s.sol similarity index 91% rename from script/stakeRewards_Delegation.s.sol rename to script/StakeRewards.s.sol index 097a12f..6add143 100644 --- a/script/stakeRewards_Delegation.s.sol +++ b/script/StakeRewards.s.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; contract StakeRewards is Script { diff --git a/script/unstake_Delegation.s.sol b/script/Unstake.s.sol similarity index 86% rename from script/unstake_Delegation.s.sol rename to script/Unstake.s.sol index bba5c25..617eee0 100644 --- a/script/unstake_Delegation.s.sol +++ b/script/Unstake.s.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {NonRebasingLST} from "src/NonRebasingLST.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; import {ILiquidDelegation} from "src/LiquidDelegation.sol"; import {INonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; contract Unstake is Script { using ERC165Checker for address; @@ -34,9 +35,10 @@ contract Unstake is Script { address(lst) ); - console.log("Staker balance before: %s wei %s LST", + console.log("Staker balance before: %s wei %s %s", staker.balance, - lst.balanceOf(staker) + lst.balanceOf(staker), + lst.symbol() ); if (amount == 0) { @@ -63,9 +65,10 @@ contract Unstake is Script { if (address(delegation).supportsInterface(type(ILiquidDelegation).interfaceId)) { NonRebasingLST lst = NonRebasingLST(ILiquidDelegation(payable(address(delegation))).getLST()); - console.log("Staker balance after: %s wei %s LST", + console.log("Staker balance after: %s wei %s %s", staker.balance, - lst.balanceOf(staker) + lst.balanceOf(staker), + lst.symbol() ); } else { console.log("Staker balance after: %s wei", diff --git a/script/upgrade_Delegation.s.sol b/script/Upgrade.s.sol similarity index 96% rename from script/upgrade_Delegation.s.sol rename to script/Upgrade.s.sol index de11ef2..62d69fd 100644 --- a/script/upgrade_Delegation.s.sol +++ b/script/Upgrade.s.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; import {ILiquidDelegation} from "src/LiquidDelegation.sol"; @@ -8,7 +9,7 @@ import {INonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol"; import {NonLiquidDelegationV2} from "src/NonLiquidDelegationV2.sol"; import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; contract Upgrade is Script { using ERC165Checker for address; diff --git a/script/withdrawRewards_Delegation.s.sol b/script/WithdrawRewards.s.sol similarity index 94% rename from script/withdrawRewards_Delegation.s.sol rename to script/WithdrawRewards.s.sol index 77ec220..fd284b3 100644 --- a/script/withdrawRewards_Delegation.s.sol +++ b/script/WithdrawRewards.s.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; import {NonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; contract WithdrawRewards is Script { using Strings for string; diff --git a/src/BaseDelegation.sol b/src/BaseDelegation.sol index cb61556..4c8f42a 100644 --- a/src/BaseDelegation.sol +++ b/src/BaseDelegation.sol @@ -1,58 +1,14 @@ // 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 "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import "src/Delegation.sol"; +import {IDelegation} from "src/IDelegation.sol"; +import {WithdrawalQueue} from "src/WithdrawalQueue.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -library WithdrawalQueue { - - address public constant DEPOSIT_CONTRACT = address(0x5A494C4445504F53495450524F5859); - - struct Item { - uint256 blockNumber; - uint256 amount; - } - - struct Fifo { - uint256 first; - uint256 last; - mapping(uint256 => Item) items; - } - - function unbondingPeriod() view internal returns(uint256) { - (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( - abi.encodeWithSignature("withdrawalPeriod()") - ); - require(success, "unbonding period unknown"); - return abi.decode(data, (uint256)); - } - - function enqueue(Fifo storage fifo, uint256 amount) internal { - fifo.items[fifo.last] = Item(block.number + unbondingPeriod(), 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); - } -} - -abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable, ERC165Upgradeable { +abstract contract BaseDelegation is IDelegation, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable, ERC165Upgradeable { using WithdrawalQueue for WithdrawalQueue.Fifo; @@ -66,6 +22,7 @@ abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2Ste } // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.BaseDelegation")) - 1)) & ~bytes32(uint256(0xff)) + // solhint-disable const-name-snakecase bytes32 private constant BaseDelegationStorageLocation = 0xc8ff0e571ef581b660c1651f85bbac921a40f9489bd04631c07fa723c13c6000; function _getBaseDelegationStorage() private pure returns (BaseDelegationStorage storage $) { @@ -82,6 +39,7 @@ abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2Ste return _getInitializedVersion(); } + // solhint-disable func-name-mixedcase function __BaseDelegation_init(address initialOwner) internal onlyInitializing { __Pausable_init_unchained(); __Ownable2Step_init_unchained(); @@ -96,6 +54,19 @@ abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2Ste function _authorizeUpgrade(address newImplementation) internal onlyOwner virtual override {} + function _migrate(bytes calldata blsPubKey) internal onlyOwner virtual { + BaseDelegationStorage storage $ = _getBaseDelegationStorage(); + require(!_isActivated() && address(this).balance == 0, "validator can not be migrated"); + $.blsPubKey = blsPubKey; + (bool success, bytes memory data) = DEPOSIT_CONTRACT.call(abi.encodeWithSignature("getPeerId(bytes)", blsPubKey)); + require(success, "peer id could not be retrieved"); + $.peerId = data; + (success, ) = DEPOSIT_CONTRACT.call(abi.encodeWithSignature("setRewardAddress(bytes,address)", blsPubKey, address(this))); + require(success, "reward address could not be changed"); + } + + function migrate(bytes calldata blsPubKey) public virtual; + function _deposit( bytes calldata blsPubKey, bytes calldata peerId, @@ -109,23 +80,24 @@ abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2Ste (bool success, ) = DEPOSIT_CONTRACT.call{ value: depositAmount }( - abi.encodeWithSignature("deposit(bytes,bytes,bytes,address)", + abi.encodeWithSignature("deposit(bytes,bytes,bytes,address,address)", blsPubKey, peerId, signature, - address(this) + address(this), + owner() ) ); require(success, "deposit failed"); } - function deposit( + function depositFirst( bytes calldata blsPubKey, bytes calldata peerId, bytes calldata signature ) public virtual payable; - function deposit2( + function depositLater( bytes calldata blsPubKey, bytes calldata peerId, bytes calldata signature @@ -181,9 +153,19 @@ abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2Ste $.commissionNumerator = _commissionNumerator; } + function getCommission() public virtual view returns(uint256 numerator, uint256 denominator) { + BaseDelegationStorage storage $ = _getBaseDelegationStorage(); + numerator = $.commissionNumerator; + denominator = DENOMINATOR; + } + + function getMinDelegation() public virtual view returns(uint256) { + return MIN_DELEGATION; + } + function stake() external virtual payable; - function unstake(uint256) external virtual; + function unstake(uint256) external virtual returns(uint256); function claim() external virtual; @@ -201,6 +183,21 @@ abstract contract BaseDelegation is Delegation, PausableUpgradeable, Ownable2Ste } } + function getPendingClaims() public virtual view returns(uint256[2][] memory claims) { + BaseDelegationStorage storage $ = _getBaseDelegationStorage(); + WithdrawalQueue.Fifo storage fifo = $.withdrawals[_msgSender()]; + uint256 index = fifo.first; + while (fifo.ready(index)) + index++; + uint256 firstPending = index; + claims = new uint256[2][](fifo.last - index); + while (fifo.notReady(index)) { + WithdrawalQueue.Item storage item = fifo.items[index]; + claims[index - firstPending] = [item.blockNumber, item.amount]; + index++; + } + } + function _dequeueWithdrawals() internal virtual returns (uint256 total) { BaseDelegationStorage storage $ = _getBaseDelegationStorage(); while ($.withdrawals[_msgSender()].ready()) diff --git a/src/Console.sol b/src/Console.sol index 639da8d..27f6262 100644 --- a/src/Console.sol +++ b/src/Console.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; library Console { - function convert(int256 amount, uint8 precision) pure internal returns ( + function convert(int256 amount, uint8 precision) internal pure returns ( int256 predecimal, string memory zeros, uint256 postdecimal @@ -24,35 +25,35 @@ library Console { postdecimal /= 10; } - function toString(uint256 amount, uint8 precision) pure internal returns (string memory result) { + function toString(uint256 amount, uint8 precision) internal pure returns (string memory result) { return toString(int256(amount), precision); } - function toString(int256 amount, uint8 precision) pure internal returns (string memory result) { + function toString(int256 amount, uint8 precision) internal pure returns (string memory result) { (int256 predecimal, string memory zeros, uint256 postdecimal) = convert(amount, precision); result = string.concat(Strings.toStringSigned(predecimal), "."); result = string.concat(result, zeros); result = string.concat(result, Strings.toString(postdecimal)); } - function log(string memory format, uint256 amount, uint8 precision) pure internal { + function log(string memory format, uint256 amount, uint8 precision) internal pure { log(format, int256(amount), precision); } - function log(string memory format, int256 amount, uint8 precision) pure internal { + function log(string memory format, int256 amount, uint8 precision) internal pure { (int256 predecimal, string memory zeros, uint256 postdecimal) = convert(amount, precision); console.log(format, Strings.toStringSigned(predecimal), zeros, postdecimal); } - function log(string memory format, uint256 amount) pure internal { + function log(string memory format, uint256 amount) internal pure { return log(format, amount, 18); } - function log(string memory format, int256 amount) pure internal { + function log(string memory format, int256 amount) internal pure { return log(format, amount, 18); } - function log(string memory format, uint64[] memory array) pure internal { + function log(string memory format, uint64[] memory array) internal pure { string memory s; for (uint256 i = 0; i < array.length; i++) { s = string.concat(s, Strings.toString(array[i])); diff --git a/src/Delegation.sol b/src/IDelegation.sol similarity index 59% rename from src/Delegation.sol rename to src/IDelegation.sol index 7cbb297..9678e15 100644 --- a/src/Delegation.sol +++ b/src/IDelegation.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -interface Delegation { +interface IDelegation { // data can store additional information e.g. liquid staking tokens event Staked(address indexed delegator, uint256 amount, bytes data); @@ -10,8 +10,13 @@ interface Delegation { event CommissionPaid(address indexed owner, uint256 commission); function stake() external payable; - function unstake(uint256) external; + function unstake(uint256) external returns(uint256); function claim() external; function collectCommission() external; function stakeRewards() external; + function getMinDelegation() external view returns(uint256); + function getCommission() external view returns(uint256, uint256); + function getStake() external view returns(uint256); + function getClaimable() external view returns(uint256); + function getPendingClaims() external view returns(uint256[2][] memory); } \ No newline at end of file diff --git a/src/LiquidDelegation.sol b/src/LiquidDelegation.sol index 3d7e068..bd4c297 100644 --- a/src/LiquidDelegation.sol +++ b/src/LiquidDelegation.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -import "src/BaseDelegation.sol"; -import "src/NonRebasingLST.sol"; +import {BaseDelegation} from "src/BaseDelegation.sol"; +import {NonRebasingLST} from "src/NonRebasingLST.sol"; // do not change this interface, it will break the detection of // the staking variant of an already deployed delegation contract @@ -20,6 +20,7 @@ contract LiquidDelegation is BaseDelegation, ILiquidDelegation { } // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.LiquidDelegation")) - 1)) & ~bytes32(uint256(0xff)) + // solhint-disable const-name-snakecase bytes32 private constant LiquidDelegationStorageLocation = 0xfa57cbed4b267d0bc9f2cbdae86b4d1d23ca818308f873af9c968a23afadfd00; function _getLiquidDelegationStorage() private pure returns (LiquidDelegationStorage storage $) { @@ -33,10 +34,10 @@ contract LiquidDelegation is BaseDelegation, ILiquidDelegation { _disableInitializers(); } - function initialize(address initialOwner) initializer public { + function initialize(address initialOwner, string calldata name, string calldata symbol) public initializer { __BaseDelegation_init(initialOwner); LiquidDelegationStorage storage $ = _getLiquidDelegationStorage(); - $.lst = address(new NonRebasingLST(address(this))); + $.lst = address(new NonRebasingLST(address(this), name, symbol)); } function getLST() public view returns(address) { @@ -44,19 +45,23 @@ contract LiquidDelegation is BaseDelegation, ILiquidDelegation { return $.lst; } - function deposit( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature + function depositFirst( + bytes calldata, + bytes calldata, + bytes calldata ) public override payable { revert("not implemented"); } - function deposit2( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature - ) public override { + function depositLater( + bytes calldata, + bytes calldata, + bytes calldata + ) public pure override { + revert("not implemented"); + } + + function migrate(bytes calldata) public pure override { revert("not implemented"); } @@ -64,28 +69,28 @@ contract LiquidDelegation is BaseDelegation, ILiquidDelegation { revert("not implemented"); } - function unstake(uint256) external override { + function unstake(uint256) external pure override returns(uint256) { revert("not implemented"); } - function claim() external override { + function claim() external pure override { revert("not implemented"); } - function collectCommission() public override { + function collectCommission() public pure override { revert("not implemented"); } - function stakeRewards() public override { + function stakeRewards() public pure override { revert("not implemented"); } - function getPrice() public view returns(uint256) { + function getPrice() public pure returns(uint256) { revert("not implemented"); } - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(ILiquidDelegation).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { + return _interfaceId == type(ILiquidDelegation).interfaceId || super.supportsInterface(_interfaceId); } function interfaceId() public pure returns (bytes4) { diff --git a/src/LiquidDelegationV2.sol b/src/LiquidDelegationV2.sol index 61bd05a..7a8f828 100644 --- a/src/LiquidDelegationV2.sol +++ b/src/LiquidDelegationV2.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -import "src/BaseDelegation.sol"; -import "src/LiquidDelegation.sol"; -import "src/NonRebasingLST.sol"; +import {BaseDelegation} from "src/BaseDelegation.sol"; +import {ILiquidDelegation} from "src/LiquidDelegation.sol"; +import {NonRebasingLST} from "src/NonRebasingLST.sol"; // the contract is supposed to be deployed with the node's signer account contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { @@ -15,6 +15,7 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { } // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.LiquidDelegation")) - 1)) & ~bytes32(uint256(0xff)) + // solhint-disable const-name-snakecase bytes32 private constant LiquidDelegationStorageLocation = 0xfa57cbed4b267d0bc9f2cbdae86b4d1d23ca818308f873af9c968a23afadfd00; function _getLiquidDelegationStorage() private pure returns (LiquidDelegationStorage storage $) { @@ -34,20 +35,29 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { // it won't be possible to identify the actual version of the // source file without a hardcoded version number, but storing // the file versions in separate folders would help - function reinitialize() reinitializer(version() + 1) public { + function reinitialize() public reinitializer(version() + 1) { } // called when stake withdrawn from the deposit contract is claimed // but not called when rewards are assigned to the reward address - receive() payable external { + receive() external payable { LiquidDelegationStorage storage $ = _getLiquidDelegationStorage(); // 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 - // to request the node's activation as a validator using the delegated stake - function deposit2( + // called by the node's owner who deployed this contract + // to turn the already deposited validator node into a staking pool + function migrate(bytes calldata blsPubKey) public override onlyOwner { + _migrate(blsPubKey); + LiquidDelegationStorage storage $ = _getLiquidDelegationStorage(); + require(NonRebasingLST($.lst).totalSupply() == 0, "stake already delegated"); + NonRebasingLST($.lst).mint(owner(), getStake()); + } + + // called by the node's owner who deployed this contract + // to deposit the node as a validator using the delegated stake + function depositLater( bytes calldata blsPubKey, bytes calldata peerId, bytes calldata signature @@ -60,10 +70,10 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { ); } - // 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( + // called by the node's owner who deployed this contract + // with at least the minimum stake to deposit the node + // as a validator before any stake is delegated to it + function depositFirst( bytes calldata blsPubKey, bytes calldata peerId, bytes calldata signature @@ -105,8 +115,7 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { emit Staked(_msgSender(), msg.value, abi.encode(shares)); } - function unstake(uint256 shares) public override whenNotPaused { - uint256 amount; + function unstake(uint256 shares) public override whenNotPaused returns(uint256 amount) { LiquidDelegationStorage storage $ = _getLiquidDelegationStorage(); // before calculating the amount deduct the commission from the yet untaxed rewards taxRewards(); @@ -201,8 +210,8 @@ contract LiquidDelegationV2 is BaseDelegation, ILiquidDelegation { return $.lst; } - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(ILiquidDelegation).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { + return _interfaceId == type(ILiquidDelegation).interfaceId || super.supportsInterface(_interfaceId); } function interfaceId() public pure returns (bytes4) { diff --git a/src/NonLiquidDelegation.sol b/src/NonLiquidDelegation.sol index efbd65b..a2f0b33 100644 --- a/src/NonLiquidDelegation.sol +++ b/src/NonLiquidDelegation.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -import "src/BaseDelegation.sol"; +import {BaseDelegation} from "src/BaseDelegation.sol"; // do not change this interface, it will break the detection of // the staking variant of an already deployed delegation contract @@ -20,6 +20,7 @@ contract NonLiquidDelegation is BaseDelegation, INonLiquidDelegation { */ // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.NonLiquidDelegation")) - 1)) & ~bytes32(uint256(0xff)) + // solhint-disable const-name-snakecase bytes32 private constant NonLiquidDelegationStorageLocation = 0x66c8dc4f9c8663296597cb1e39500488e05713d82a9122d4f548b19a70fc2000; /* commented out because defining empty structs is disallowed @@ -35,23 +36,27 @@ contract NonLiquidDelegation is BaseDelegation, INonLiquidDelegation { _disableInitializers(); } - function initialize(address initialOwner) initializer public { + function initialize(address initialOwner) public initializer { __BaseDelegation_init(initialOwner); } - function deposit( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature + function depositFirst( + bytes calldata, + bytes calldata, + bytes calldata ) public override payable { revert("not implemented"); } - function deposit2( - bytes calldata blsPubKey, - bytes calldata peerId, - bytes calldata signature - ) public override { + function depositLater( + bytes calldata, + bytes calldata, + bytes calldata + ) public pure override { + revert("not implemented"); + } + + function migrate(bytes calldata) public pure override { revert("not implemented"); } @@ -59,52 +64,52 @@ contract NonLiquidDelegation is BaseDelegation, INonLiquidDelegation { revert("not implemented"); } - function unstake(uint256) external override { + function unstake(uint256) external pure override returns(uint256) { revert("not implemented"); } - function claim() external override { + function claim() external pure override { revert("not implemented"); } - function collectCommission() public override { + function collectCommission() public pure override { revert("not implemented"); } - function stakeRewards() public override { + function stakeRewards() public pure override { revert("not implemented"); } - function rewards() public view returns(uint256) { + function rewards() public pure returns(uint256) { revert("not implemented"); } - function rewards(uint64) public view returns(uint256) { + function rewards(uint64) public pure returns(uint256) { revert("not implemented"); } - function getDelegatedStake() public view returns(uint256) { + function getDelegatedStake() public pure returns(uint256) { revert("not implemented"); } - function withdrawRewards(uint256, uint64) public returns(uint256) { + function withdrawRewards(uint256, uint64) public pure returns(uint256) { revert("not implemented"); } - function withdrawRewards(uint256) public returns(uint256) { + function withdrawRewards(uint256) public pure returns(uint256) { revert("not implemented"); } - function withdrawAllRewards(uint64) public returns(uint256) { + function withdrawAllRewards(uint64) public pure returns(uint256) { revert("not implemented"); } - function withdrawAllRewards() public returns(uint256) { + function withdrawAllRewards() public pure returns(uint256) { revert("not implemented"); } - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(INonLiquidDelegation).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { + return _interfaceId == type(INonLiquidDelegation).interfaceId || super.supportsInterface(_interfaceId); } function interfaceId() public pure returns (bytes4) { diff --git a/src/NonLiquidDelegationV2.sol b/src/NonLiquidDelegationV2.sol index f2f01a1..cc4fd17 100644 --- a/src/NonLiquidDelegationV2.sol +++ b/src/NonLiquidDelegationV2.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -import "src/BaseDelegation.sol"; -import "src/NonLiquidDelegation.sol"; +import {BaseDelegation} from "src/BaseDelegation.sol"; +import {INonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { @@ -50,6 +50,7 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { } // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.NonLiquidDelegation")) - 1)) & ~bytes32(uint256(0xff)) + // solhint-disable const-name-snakecase bytes32 private constant NonLiquidDelegationStorageLocation = 0x66c8dc4f9c8663296597cb1e39500488e05713d82a9122d4f548b19a70fc2000; function _getNonLiquidDelegationStorage() private pure returns (NonLiquidDelegationStorage storage $) { @@ -69,12 +70,12 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { // it won't be possible to identify the actual version of the // source file without a hardcoded version number, but storing // the file versions in separate folders would help - function reinitialize() reinitializer(version() + 1) public { + function reinitialize() public reinitializer(version() + 1) { } // called when stake withdrawn from the deposit contract is claimed // but not called when rewards are assigned to the reward address - receive() payable external { + receive() external payable { NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage(); // add the stake withdrawn from the deposit to the reward balance $.totalRewards += int256(msg.value); @@ -112,11 +113,22 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { result = $.stakings[stakingIndices[stakingIndices.length - 1]].amount; } - event RewardPaid(address indexed owner, uint256 reward); + event RewardPaid(address indexed delegator, uint256 reward); - // 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( + // called by the node's owner who deployed this contract + // to turn the already deposited validator node into a staking pool + function migrate(bytes calldata blsPubKey) public override onlyOwner { + _migrate(blsPubKey); + NonLiquidDelegationStorage storage $ = _getNonLiquidDelegationStorage(); + require($.stakings.length == 0, "stake already delegated"); + // the owner's deposit must also be recorded as staking otherwise + // the owner would not benefit from the rewards accrued by the deposit + _append(int256(getStake())); + } + + // called by the node's owner who deployed this contract + // to deposit the node as a validator using the delegated stake + function depositLater( bytes calldata blsPubKey, bytes calldata peerId, bytes calldata signature @@ -129,10 +141,10 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { ); } - // 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( + // called by the node's owner who deployed this contract + // with at least the minimum stake to deposit the node + // as a validator before any stake is delegated to it + function depositFirst( bytes calldata blsPubKey, bytes calldata peerId, bytes calldata signature @@ -171,11 +183,12 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { emit Staked(_msgSender(), msg.value, ""); } - function unstake(uint256 value) public override whenNotPaused { + function unstake(uint256 value) public override whenNotPaused returns(uint256 amount) { _append(-int256(value)); _decreaseDeposit(uint256(value)); _enqueueWithdrawal(value); emit Unstaked(_msgSender(), value, ""); + return value; } function _append(int256 value) internal { @@ -354,8 +367,8 @@ contract NonLiquidDelegationV2 is BaseDelegation, INonLiquidDelegation { function collectCommission() public override {} - function supportsInterface(bytes4 interfaceId) public view override returns (bool) { - return interfaceId == type(INonLiquidDelegation).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface(bytes4 _interfaceId) public view override returns (bool) { + return _interfaceId == type(INonLiquidDelegation).interfaceId || super.supportsInterface(_interfaceId); } function interfaceId() public pure returns (bytes4) { diff --git a/src/NonRebasingLST.sol b/src/NonRebasingLST.sol index 55e9496..4675fd6 100644 --- a/src/NonRebasingLST.sol +++ b/src/NonRebasingLST.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract NonRebasingLST is ERC20, Ownable { - constructor(address initialOwner) - ERC20("MyToken", "MTK") + constructor(address initialOwner, string memory name, string memory symbol) + ERC20(name, symbol) Ownable(initialOwner) {} diff --git a/src/WithdrawalQueue.sol b/src/WithdrawalQueue.sol new file mode 100644 index 0000000..53f0bd8 --- /dev/null +++ b/src/WithdrawalQueue.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +library WithdrawalQueue { + + address public constant DEPOSIT_CONTRACT = address(0x5A494C4445504F53495450524F5859); + + struct Item { + uint256 blockNumber; + uint256 amount; + } + + struct Fifo { + uint256 first; + uint256 last; + mapping(uint256 => Item) items; + } + + function unbondingPeriod() internal view returns(uint256) { + (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( + abi.encodeWithSignature("withdrawalPeriod()") + ); + require(success, "unbonding period unknown"); + return abi.decode(data, (uint256)); + } + + function enqueue(Fifo storage fifo, uint256 amount) internal { + fifo.items[fifo.last] = Item(block.number + unbondingPeriod(), 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 notReady(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); + } +} diff --git a/stake.sh b/stake.sh index 6dd17c2..90841f7 100755 --- a/stake.sh +++ b/stake.sh @@ -1,5 +1,7 @@ #!/bin/bash +url=http://localhost:4201 + if [ $# -ne 3 ]; then echo "Provide the delegation contract address, a staker private key and an amount in wei as arguments." exit 1 @@ -7,26 +9,26 @@ fi staker=$(cast wallet address $2) -temp=$(forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" $1 | tail -n 1) +temp=$(forge script script/CheckVariant.s.sol --rpc-url $url --sig "run(address payable)" $1 | tail -n 1) variant=$(sed -E 's/\s\s([a-zA-Z0-9]+)/\1/' <<< "$temp") if [[ "$variant" == "$temp" ]]; then echo Incompatible delegation contract at $1 exit 1 fi -forge script script/stake_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" $1 $3 --private-key $2 +forge script script/Stake.s.sol --rpc-url $url --broadcast --legacy --sig "run(address payable, uint256)" $1 $3 --private-key $2 -block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) +block=$(cast rpc eth_blockNumber --rpc-url $url) 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 rewardsAfterStaking = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ "$variant" == "ILiquidDelegation" ]]; then - echo taxedRewardsAfterStaking = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + echo taxedRewardsAfterStaking = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') fi -stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) -tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Staked(address,uint256,bytes)" --rpc-url http://localhost:4201 | grep "data") +tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Staked(address,uint256,bytes)" --rpc-url $url | grep "data") if [[ "$tmp" != "" ]]; then tmp=${tmp#*: } tmp=$(cast abi-decode --input "x(uint256,bytes)" $tmp | sed 's/\[[^]]*\]//g') @@ -42,27 +44,28 @@ 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') +rewardsBeforeStaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo rewardsBeforeStaking = $rewardsBeforeStaking -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') +stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ "$variant" == "ILiquidDelegation" ]]; then - taxedRewardsBeforeStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + taxedRewardsBeforeStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo taxedRewardsBeforeStaking = $taxedRewardsBeforeStaking - lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url http://localhost:4201) + lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url $url) + symbol=$(cast call $lst "symbol()(string)" --block $block_num --rpc-url $url | tr -d '"') - totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url $url | 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') + price0=$(cast call $1 "getPrice()(uint256)" --block $block_num --rpc-url $url | 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 + echo $symbol price: $price \~ $(cast to-unit $price0 ether) + echo staked ZIL shares: $(bc -l <<< "scale=18; $3/$price/10^18") $symbol fi -stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) echo staked amount + gas fee = $(bc -l <<< "scale=18; $stakerWeiBefore-$stakerWeiAfter") wei if [[ "$tmp" != "" ]]; then echo event Staked\($staker, $d1, $d2\) emitted; fi diff --git a/stakeRewards.sh b/stakeRewards.sh index a023f61..89d4f4a 100755 --- a/stakeRewards.sh +++ b/stakeRewards.sh @@ -1,42 +1,44 @@ #!/bin/bash +url=http://localhost:4201 + if [ $# -lt 2 ]; then - echo "Provide the delegation contract address and the validator or stakker private key." + echo "Provide the delegation contract address and the validator or staker private key." exit 1 fi staker=$(cast wallet address $2) -temp=$(forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" $1 | tail -n 1) +temp=$(forge script script/CheckVariant.s.sol --rpc-url $url --sig "run(address payable)" $1 | tail -n 1) variant=$(sed -E 's/\s\s([a-zA-Z0-9]+)/\1/' <<< "$temp") if [[ "$variant" == "$temp" ]]; then echo Incompatible delegation contract at $1 exit 1 fi -owner=$(cast call $1 "owner()(address)" --block latest --rpc-url http://localhost:4201) +owner=$(cast call $1 "owner()(address)" --block latest --rpc-url $url) if [ "$variant" == "ILiquidDelegation" ] && [ "$staker" != "$owner" ]; then echo Rewards must be staked by the validator and it is not $staker exit 1 fi -forge script script/stakeRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" $1 --private-key $2 +forge script script/StakeRewards.s.sol --rpc-url $url --broadcast --legacy --sig "run(address payable)" $1 --private-key $2 -block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) +block=$(cast rpc eth_blockNumber --rpc-url $url) block_num=$(echo $block | tr -d '"' | cast to-dec --base-in 16) -rewardsAfterStaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') -taxedRewardsAfterStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') -depositAfterStaking=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +rewardsAfterStaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +taxedRewardsAfterStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +depositAfterStaking=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo rewardsAfterStaking = $rewardsAfterStaking echo taxedRewardsAfterStaking = $taxedRewardsAfterStaking echo depositAfterStaking = $depositAfterStaking -stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -ownerWeiAfter=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) +ownerWeiAfter=$(cast rpc eth_getBalance $owner $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) -tmp1=$(cast logs --from-block $block_num --to-block $block_num --address $1 "CommissionPaid(address,uint256)" --rpc-url http://localhost:4201 | grep "data") +tmp1=$(cast logs --from-block $block_num --to-block $block_num --address $1 "CommissionPaid(address,uint256)" --rpc-url $url | grep "data") if [[ "$tmp1" != "" ]]; then tmp1=${tmp1#*: } tmp1=$(cast abi-decode --input "x(uint256)" $tmp1 | sed 's/\[[^]]*\]//g') @@ -50,16 +52,16 @@ echo $(date +"%T,%3N") $block_num block_num=$((block_num-1)) block=$(echo $block_num | cast to-hex --base-in 10) -stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') -commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') -denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') -stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -ownerWeiBefore=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) +ownerWeiBefore=$(cast rpc eth_getBalance $owner $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) -rewardsBeforeStaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') -taxedRewardsBeforeStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') -depositBeforeStaking=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +rewardsBeforeStaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +taxedRewardsBeforeStaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +depositBeforeStaking=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo rewardsBeforeStaking = $rewardsBeforeStaking echo taxedRewardsBeforeStaking = $taxedRewardsBeforeStaking echo depositBeforeStaking = $depositBeforeStaking diff --git a/state.sh b/state.sh index a917152..3926d22 100755 --- a/state.sh +++ b/state.sh @@ -1,11 +1,13 @@ #!/bin/bash +url=http://localhost:4201 + if [ $# -lt 2 ]; then echo "Provide the delegation contract address, a staker address and optionally, a block number as arguments." exit 1 fi -temp=$(forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" $1 | tail -n 1) +temp=$(forge script script/CheckVariant.s.sol --rpc-url $url --sig "run(address payable)" $1 | tail -n 1) variant=$(sed -E 's/\s\s([a-zA-Z0-9]+)/\1/' <<< "$temp") if [[ "$variant" == "$temp" ]]; then echo Incompatible delegation contract at $1 @@ -16,75 +18,76 @@ if [ $# -eq 3 ]; then block_num=$3 block=$(echo $block_num | cast to-hex --base-in 10) else - block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) + block=$(cast rpc eth_blockNumber --rpc-url $url) block_num=$(echo $block | tr -d '"' | cast to-dec --base-in 16) fi echo $(date +"%T,%3N") $block_num -owner=$(cast call $1 "owner()(address)" --block $block_num --rpc-url http://localhost:4201) +owner=$(cast call $1 "owner()(address)" --block $block_num --rpc-url $url) -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) +rewardsBeforeUnstaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +#rewardsBeforeUnstaking=$(cast rpc eth_getBalance $1 $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) echo rewardsBeforeUnstaking = $rewardsBeforeUnstaking -x=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +x=$(cast rpc eth_getBalance $owner $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) owner_zil=$(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) +x=$(cast rpc eth_getBalance $2 $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) staker_zil=$(cast to-unit $x ether) if [[ "$variant" == "ILiquidDelegation" ]]; then - taxedRewardsBeforeUnstaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + taxedRewardsBeforeUnstaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo taxedRewardsBeforeUnstaking = $taxedRewardsBeforeUnstaking - lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url http://localhost:4201) - x=$(cast call $lst "balanceOf(address)(uint256)" $owner --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url $url) + symbol=$(cast call $lst "symbol()(string)" --block $block_num --rpc-url $url | tr -d '"') + x=$(cast call $lst "balanceOf(address)(uint256)" $owner --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') owner_lst=$(cast to-unit $x ether) - x=$(cast call $lst "balanceOf(address)(uint256)" $2 --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + x=$(cast call $lst "balanceOf(address)(uint256)" $2 --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') staker_lst=$(cast to-unit $x ether) - echo owner: $owner_lst LST && echo owner: $owner_zil ZIL unstaked - echo staker: $staker_lst LST && echo staker: $staker_zil ZIL unstaked + echo owner: $owner_lst $symbol && echo owner: $owner_zil ZIL unstaked + echo staker: $staker_lst $symbol && echo staker: $staker_zil ZIL unstaked else - x=$(cast call $1 "getDelegatedStake()(uint256)" --from $owner --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + x=$(cast call $1 "getDelegatedStake()(uint256)" --from $owner --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') owner_staked=$(cast to-unit $x ether) - x=$(cast call $1 "getDelegatedStake()(uint256)" --from $2 --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + x=$(cast call $1 "getDelegatedStake()(uint256)" --from $2 --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') staker_staked=$(cast to-unit $x ether) echo owner: $owner_staked ZIL staked && echo owner: $owner_zil ZIL unstaked echo staker: $staker_staked ZIL staked && echo staker: $staker_zil ZIL unstaked fi -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') +stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ "$variant" == "ILiquidDelegation" ]]; then - totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ $totalSupply -ne 0 ]]; then price=$(bc -l <<< "scale=36; ($stake+$rewardsBeforeUnstaking-($rewardsBeforeUnstaking-$taxedRewardsBeforeUnstaking)*$commissionNumerator/$denominator)/$totalSupply") else price=$(bc -l <<< "scale=36; 1/1") fi - price0=$(cast call $1 "getPrice()(uint256)" --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 + price0=$(cast call $1 "getPrice()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') + echo $symbol supply: $(cast to-unit $totalSupply ether) + echo $symbol price: $price \~ $(cast to-unit $price0 ether) ZIL + echo staker $symbol value: $(bc -l <<< "scale=18; $staker_lst*$price") ZIL else - x=$(cast call $1 "rewards()(uint256)" --from $2 --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + x=$(cast call $1 "rewards()(uint256)" --from $2 --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') staker_rewards=$(cast to-unit $x ether) echo staker rewards: $staker_rewards ZIL fi -claimable=$(cast call $1 "getClaimable()(uint256)" --from $2 --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +claimable=$(cast call $1 "getClaimable()(uint256)" --from $2 --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo staker claimable: $(cast to-unit $claimable ether) ZIL echo validator deposit: $(cast to-unit $stake ether) ZIL -validatorBalance=$(cast rpc eth_getBalance $1 $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +validatorBalance=$(cast rpc eth_getBalance $1 $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) echo validator balance: $(cast to-unit $validatorBalance ether) ZIL -pendingWithdrawals=$(cast call $1 "getTotalWithdrawals()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +pendingWithdrawals=$(cast call $1 "getTotalWithdrawals()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo pending withdrawals: $(cast to-unit $pendingWithdrawals ether) ZIL -totalStake=$(cast call 0x00000000005A494C4445504F53495450524F5859 "getFutureTotalStake()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +totalStake=$(cast call 0x00000000005A494C4445504F53495450524F5859 "getFutureTotalStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo total stake: $(cast to-unit $totalStake ether) ZIL -depositBalance=$(cast rpc eth_getBalance 0x00000000005A494C4445504F53495450524F5859 $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +depositBalance=$(cast rpc eth_getBalance 0x00000000005A494C4445504F53495450524F5859 $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) echo deposit balance: $(cast to-unit $depositBalance ether) ZIL diff --git a/test/BaseDelegation.t.sol b/test/BaseDelegation.t.sol index 48d68ab..901579b 100644 --- a/test/BaseDelegation.t.sol +++ b/test/BaseDelegation.t.sol @@ -1,28 +1,25 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; +/* solhint-disable no-console */ +import {BlsVerifyPrecompile} from "test/BlsVerifyPrecompile.t.sol"; import {BaseDelegation} from "src/BaseDelegation.sol"; -import {Delegation} from "src/Delegation.sol"; -import {Deposit, InitialStaker} from "@zilliqa/zq2/deposit_v2.sol"; -import {Console} from "src/Console.sol"; +import {WithdrawalQueue} from "src/WithdrawalQueue.sol"; +import {IDelegation} from "src/IDelegation.sol"; +import {Deposit} from "@zilliqa/zq2/deposit_v3.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Test, Vm} from "forge-std/Test.sol"; -import "forge-std/console.sol"; - -contract PopVerifyPrecompile { - function popVerify(bytes memory, bytes memory) public pure returns(bool) { - return true; - } -} +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +/* solhint-disable one-contract-per-file */ abstract contract BaseDelegationTest is Test { - address payable proxy; - address oldImplementation; - bytes initializerCall; - address payable newImplementation; - bytes reinitializerCall; - address owner; - address[4] stakers = [ + address payable internal proxy; + address internal oldImplementation; + bytes internal initializerCall; + address payable internal newImplementation; + bytes internal reinitializerCall; + address internal owner; + address[4] internal stakers = [ 0xd819fFcE7A58b1E835c25617Db7b46a00888B013, 0x092E5E57955437876dA9Df998C96e2BE19341670, 0xeA78aAE5Be606D2D152F00760662ac321aB8F017, @@ -32,6 +29,8 @@ abstract contract BaseDelegationTest is Test { constructor() { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); owner = vm.addr(deployerPrivateKey); + for (uint256 i = 0; i < stakers.length; i++) + assertNotEq(owner, stakers[i], "owner and staker must be different"); //console.log("Signer is %s", owner); } @@ -102,31 +101,53 @@ abstract contract BaseDelegationTest is Test { ); //*/ - InitialStaker[] memory initialStakers = new InitialStaker[](0); //vm.deployCodeTo("Deposit.sol", delegation.DEPOSIT_CONTRACT()); vm.etch( delegation.DEPOSIT_CONTRACT(), - // since the deposit contract is upgradeable, the constructor has no parameters - //address(new Deposit(10_000_000 ether, 256, 10, initialStakers)).code address(new Deposit()).code ); vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(0x958a6cf6390bd7165e3519675caa670ab90f0161508a9ee714d3db7edc50740b)), bytes32(uint256(block.number / 10))); vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(0x958a6cf6390bd7165e3519675caa670ab90f0161508a9ee714d3db7edc50740c)), bytes32(uint256(10_000_000 ether))); vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(0x958a6cf6390bd7165e3519675caa670ab90f0161508a9ee714d3db7edc50740d)), bytes32(uint256(256))); vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(0x958a6cf6390bd7165e3519675caa670ab90f0161508a9ee714d3db7edc50740e)), bytes32(uint256(10))); - /* since the deposit contract is upgradeable, the storage locations changed too - vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(11)), bytes32(uint256(block.number / 10))); - vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(12)), bytes32(uint256(10_000_000 ether))); - vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(13)), bytes32(uint256(256))); - vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(14)), bytes32(uint256(10))); - */ /* console.log("Deposit.minimimStake() =", Deposit(delegation.DEPOSIT_CONTRACT()).minimumStake()); console.log("Deposit.maximumStakers() =", Deposit(delegation.DEPOSIT_CONTRACT()).maximumStakers()); console.log("Deposit.blocksPerEpoch() =", Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch()); //*/ - vm.etch(address(0x5a494c80), address(new PopVerifyPrecompile()).code); + vm.etch(address(0x5a494c81), address(new BlsVerifyPrecompile()).code); + + vm.stopPrank(); + } + + enum DepositMode {DepositThenStake, StakeThenDeposit, DepositThenMigrate} + + function migrate( + BaseDelegation delegation, + uint256 depositAmount + ) internal { + vm.deal(owner, owner.balance + depositAmount); + vm.startPrank(owner); + + Deposit(delegation.DEPOSIT_CONTRACT()).deposit{ + value: depositAmount + }( + bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), + bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), + bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a"), + address(0x0), + address(0x0) + ); + + Deposit(delegation.DEPOSIT_CONTRACT()).setControlAddress( + bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), + address(delegation) + ); + + delegation.migrate( + bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c") + ); vm.stopPrank(); } @@ -140,7 +161,7 @@ abstract contract BaseDelegationTest is Test { vm.deal(owner, owner.balance + depositAmount); vm.startPrank(owner); - delegation.deposit{ + delegation.depositFirst{ value: depositAmount }( bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), @@ -158,7 +179,7 @@ abstract contract BaseDelegationTest is Test { false, address(delegation) ); - emit Delegation.Staked( + emit IDelegation.Staked( stakers[0], depositAmount, "" @@ -170,7 +191,7 @@ abstract contract BaseDelegationTest is Test { vm.startPrank(owner); - delegation.deposit2( + delegation.depositLater( bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") @@ -179,4 +200,117 @@ abstract contract BaseDelegationTest is Test { // wait 2 epochs for the change to the deposit to take affect vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch() * 2); } + + function claimsAfterManyUnstakings(BaseDelegation delegation, uint64 steps) internal { + uint256 i; + uint256 x; + + deposit(BaseDelegation(delegation), 10_000_000 ether, true); + + // wait 2 epochs for the change to the deposit to take affect + vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch() * 2); + + for (i = 0; i < 4; i++) { + vm.deal(stakers[i], 100_000 ether); + console.log("staker %s: %s", i+1, stakers[i]); + } + + // rewards accrued so far + vm.deal(address(delegation), 50_000 ether); + x = 50; + i = 1; + + vm.startPrank(stakers[i-1]); + vm.recordLogs(); + vm.expectEmit( + true, + false, + false, + false, + address(delegation) + ); + emit IDelegation.Staked( + stakers[i-1], + steps * x * 1 ether, + "" + ); + delegation.stake{value: 2 * steps * x * 1 ether}(); + // wait 2 epochs for the change to the deposit to take affect + vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch() * 2); + vm.stopPrank(); + vm.deal(address(delegation), address(delegation).balance + 10_000 ether); + + uint256 totalUnstaked; + uint256 totalPending; + uint256[2][] memory claims; + + for (uint256 j = 0; j < steps; j++) { + console.log("--------------------------------------------------------------------"); + vm.startPrank(stakers[i-1]); + + uint256 amount = delegation.unstake(x * 1 ether); + console.log("%s unstaked %s in block %s", stakers[i-1], amount, block.number); + totalUnstaked += amount; + + //console.log("block number: %s", block.number); + console.log("claimable: %s", delegation.getClaimable()); + claims = delegation.getPendingClaims(); + console.log("%s pending claims:", claims.length); + totalPending = 0; + for (uint256 k = 0; k < claims.length; k++) { + console.log("%s can claim %s in block %s", stakers[i-1], claims[k][1], claims[k][0]); + totalPending += claims[k][1]; + } + assertEq(delegation.getClaimable() + totalPending, totalUnstaked, "claims must match unstaked amount"); + + // wait 2 epochs for the change to the deposit to take affect + vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch() * 2); + vm.stopPrank(); + vm.deal(address(delegation), address(delegation).balance + 10_000 ether); + } + + vm.startPrank(stakers[i-1]); + + console.log("--------------------------------------------------------------------"); + console.log("block number: %s", block.number); + console.log("claimable: %s", delegation.getClaimable()); + claims = delegation.getPendingClaims(); + console.log("%s pending claims:", claims.length); + totalPending = 0; + for (uint256 j = 0; j < claims.length; j++) { + console.log("%s can claim %s in block %s", stakers[i-1], claims[j][1], claims[j][0]); + totalPending += claims[j][1]; + } + assertEq(delegation.getClaimable() + totalPending, totalUnstaked, "claims must match unstaked amount"); + + vm.roll(block.number + 100); + + console.log("--------------------------------------------------------------------"); + console.log("block number: %s", block.number); + console.log("claimable: %s", delegation.getClaimable()); + claims = delegation.getPendingClaims(); + console.log("%s pending claims:", claims.length); + totalPending = 0; + for (uint256 j = 0; j < claims.length; j++) { + console.log("%s can claim %s in block %s", stakers[i-1], claims[j][1], claims[j][0]); + totalPending += claims[j][1]; + } + assertEq(delegation.getClaimable() + totalPending, totalUnstaked, "claims must match unstaked amount"); + + vm.roll(block.number + WithdrawalQueue.unbondingPeriod()); + + console.log("--------------------------------------------------------------------"); + console.log("block number: %s", block.number); + console.log("claimable: %s", delegation.getClaimable()); + claims = delegation.getPendingClaims(); + console.log("%s pending claims:", claims.length); + totalPending = 0; + for (uint256 j = 0; j < claims.length; j++) { + console.log("%s can claim %s in block %s", stakers[i-1], claims[j][1], claims[j][0]); + totalPending += claims[j][1]; + } + assertEq(delegation.getClaimable() + totalPending, totalUnstaked, "claims must match unstaked amount"); + + vm.stopPrank(); + } } \ No newline at end of file diff --git a/test/BlsVerifyPrecompile.t.sol b/test/BlsVerifyPrecompile.t.sol new file mode 100644 index 0000000..d244127 --- /dev/null +++ b/test/BlsVerifyPrecompile.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +contract BlsVerifyPrecompile { + + // solhint-disable foundry-test-functions + function blsVerify(bytes memory, bytes memory, bytes memory) public pure returns(bool) { + return true; + } + +} \ No newline at end of file diff --git a/test/LiquidDelegation.t.sol b/test/LiquidDelegation.t.sol index 51a453f..cadc807 100644 --- a/test/LiquidDelegation.t.sol +++ b/test/LiquidDelegation.t.sol @@ -1,27 +1,32 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -import {BaseDelegationTest, PopVerifyPrecompile} from "test/BaseDelegation.t.sol"; +/* solhint-disable no-console */ +import {BaseDelegationTest} from "test/BaseDelegation.t.sol"; import {LiquidDelegation} from "src/LiquidDelegation.sol"; import {LiquidDelegationV2} from "src/LiquidDelegationV2.sol"; import {NonRebasingLST} from "src/NonRebasingLST.sol"; -import {BaseDelegation, WithdrawalQueue} from "src/BaseDelegation.sol"; -import {Delegation} from "src/Delegation.sol"; -import {Deposit} from "@zilliqa/zq2/deposit_v2.sol"; +import {BaseDelegation} from "src/BaseDelegation.sol"; +import {WithdrawalQueue} from "src/WithdrawalQueue.sol"; +import {IDelegation} from "src/IDelegation.sol"; +import {Deposit} from "@zilliqa/zq2/deposit_v3.sol"; import {Console} from "src/Console.sol"; import {Vm} from "forge-std/Test.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; +/* solhint-disable func-name-mixedcase */ contract LiquidDelegationTest is BaseDelegationTest { - LiquidDelegationV2 delegation; - NonRebasingLST lst; + LiquidDelegationV2 internal delegation; + NonRebasingLST internal lst; constructor() BaseDelegationTest() { oldImplementation = address(new LiquidDelegation()); newImplementation = payable(new LiquidDelegationV2()); initializerCall = abi.encodeWithSelector( LiquidDelegation.initialize.selector, - owner + owner, + "LiquidStakingToken", + "LST" ); reinitializerCall = abi.encodeWithSelector( LiquidDelegationV2.reinitialize.selector @@ -49,12 +54,15 @@ contract LiquidDelegationTest is BaseDelegationTest { uint256 rewardsAccruedAfterEach, uint256 rewardsBeforeUnstaking, uint256 blocksUntil, - bool initialDeposit - ) public { + DepositMode mode + ) internal { delegation = LiquidDelegationV2(proxy); lst = NonRebasingLST(delegation.getLST()); - deposit(BaseDelegation(delegation), depositAmount, initialDeposit); + if (mode == DepositMode.DepositThenMigrate) + migrate(BaseDelegation(delegation), depositAmount); + else + deposit(BaseDelegation(delegation), depositAmount, mode == DepositMode.DepositThenStake); vm.store(address(delegation), 0xfa57cbed4b267d0bc9f2cbdae86b4d1d23ca818308f873af9c968a23afadfd01, bytes32(taxedRewardsBeforeStaking)); vm.deal(address(delegation), rewardsBeforeStaking); @@ -106,7 +114,7 @@ contract LiquidDelegationTest is BaseDelegationTest { false, address(delegation) ); - emit Delegation.Staked( + emit IDelegation.Staked( stakers[0], delegatedAmount, abi.encode(lst.totalSupply() * delegatedAmount / (delegation.getStake() + delegation.getRewards())) @@ -190,7 +198,7 @@ contract LiquidDelegationTest is BaseDelegationTest { false, address(delegation) ); - emit Delegation.Unstaked( + emit IDelegation.Unstaked( stakers[0], (delegation.getStake() + delegation.getRewards()) * lst.balanceOf(stakers[0]) / lst.totalSupply(), abi.encode(lst.balanceOf(stakers[0])) @@ -199,7 +207,7 @@ contract LiquidDelegationTest is BaseDelegationTest { uint256[2] memory stakerLST = [lst.balanceOf(stakers[0]), 0]; ownerZIL[0] = delegation.owner().balance; - uint256 shares = initialDeposit ? lst.balanceOf(stakers[0]) : lst.balanceOf(stakers[0]) - depositAmount; + uint256 shares = mode != DepositMode.StakeThenDeposit ? lst.balanceOf(stakers[0]) : lst.balanceOf(stakers[0]) - depositAmount; assertEq(totalShares, shares, "staked shares balance mismatch"); delegation.unstake( @@ -259,8 +267,6 @@ contract LiquidDelegationTest is BaseDelegationTest { ); vm.roll(block.number + blocksUntil); - //TODO: remove the next line once https://github.com/Zilliqa/zq2/issues/1761 is fixed - vm.warp(block.timestamp + blocksUntil); vm.recordLogs(); @@ -274,7 +280,7 @@ contract LiquidDelegationTest is BaseDelegationTest { false, address(delegation) ); - emit Delegation.Claimed( + emit IDelegation.Claimed( stakers[0], unstakedAmount, "" @@ -330,7 +336,9 @@ contract LiquidDelegationTest is BaseDelegationTest { } - function test_1a_LargeStake_Late_NoRewards_UnstakeAll() public { + // Test cases of depositing first and staking afterwards start here + + function test_DepositThenStake_LargeStake_Late_NoRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -349,67 +357,103 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake ); - } + } - //TODO: remove the test once https://github.com/Zilliqa/zq2/issues/1761 is fixed - function test_DepositContract() public { - vm.deal(owner, 10_000_000 ether + 1_000_000 ether + 0 ether); - vm.deal(stakers[0], 0); - vm.startPrank(owner); - Deposit(delegation.DEPOSIT_CONTRACT()).deposit{ - value: 10_000_000 ether - }( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), - bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), - bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a"), - address(stakers[0]) - ); - console.log("validator deposited"); - console.log("validator stake: %s", Deposit(delegation.DEPOSIT_CONTRACT()).getStake( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c") - )); - vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch() * 2); - console.log("validator stake: %s", Deposit(delegation.DEPOSIT_CONTRACT()).getStake( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c") - )); - Deposit(delegation.DEPOSIT_CONTRACT()).depositTopup{ - value: 1_000_000 ether - }(); - console.log("validator staked"); - console.log("validator stake: %s", Deposit(delegation.DEPOSIT_CONTRACT()).getStake( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c") - )); - vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch() * 2); - console.log("validator stake: %s", Deposit(delegation.DEPOSIT_CONTRACT()).getStake( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c") - )); - Deposit(delegation.DEPOSIT_CONTRACT()).unstake( - 500_000 ether - ); - console.log("validator unstaked"); - console.log("validator stake: %s", Deposit(delegation.DEPOSIT_CONTRACT()).getStake( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c") - )); - vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).blocksPerEpoch() * 2); - console.log("validator stake: %s", Deposit(delegation.DEPOSIT_CONTRACT()).getStake( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c") - )); - console.log("validator balance: %s", owner.balance); - Deposit(delegation.DEPOSIT_CONTRACT()).withdraw(); - console.log("validator withdrew"); - console.log("validator balance: %s", owner.balance); - //vm.roll(block.number + Deposit(delegation.DEPOSIT_CONTRACT()).withdrawalPeriod()); - //TODO: remove the next line and uncomment the previous once https://github.com/Zilliqa/zq2/issues/1761 is fixed - vm.warp(block.timestamp + Deposit(delegation.DEPOSIT_CONTRACT()).withdrawalPeriod()); // skip(WithdrawalQueue.unbondingPeriod()); - Deposit(delegation.DEPOSIT_CONTRACT()).withdraw(); - console.log("validator withdrew again"); - console.log("validator balance: %s", owner.balance); - vm.stopPrank(); + function test_DepositThenStake_LargeStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + stakers[0] = 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 + WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming + DepositMode.DepositThenStake + ); + } + + function test_DepositThenStake_SmallStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + stakers[0] = 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 + WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming + DepositMode.DepositThenStake + ); + } + + function test_DepositThenStake_LargeStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + stakers[0] = 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 + WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming + DepositMode.DepositThenStake + ); + } + + function test_DepositThenStake_SmallStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + stakers[0] = 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 + WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming + DepositMode.DepositThenStake + ); } - function test_1b_LargeStake_Early_NoRewards_UnstakeAll() public { + function test_DepositThenStake_LargeStake_Early_NoRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -428,11 +472,59 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake + ); + } + + function test_DepositThenStake_LargeStake_Late_SmallValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + stakers[0] = 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 + WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming + DepositMode.DepositThenStake ); } - function test_2a_LargeStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + // Test cases of migrating a solo staker to a staking pool start here + + function test_DepositThenMigrate_LargeStake_Late_NoRewards_UnstakeAll() public { + stakers[0] = 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 + WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming + DepositMode.DepositThenMigrate + ); + } + + function test_DepositThenMigrate_LargeStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -451,11 +543,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenMigrate ); } - function test_3a_SmallStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + function test_DepositThenMigrate_SmallStake_Late_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -474,11 +566,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenMigrate ); } - function test_4a_LargeStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + function test_DepositThenMigrate_LargeStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 100_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -497,11 +589,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenMigrate ); } - function test_5a_SmallStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + function test_DepositThenMigrate_SmallStake_Late_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 100_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -520,11 +612,34 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenMigrate ); } - function test_2b_LargeStake_Late_SmallValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + function test_DepositThenMigrate_LargeStake_Early_NoRewards_UnstakeAll() public { + stakers[0] = 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 + WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming + DepositMode.DepositThenMigrate + ); + } + + function test_DepositThenMigrate_LargeStake_Late_SmallValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -543,11 +658,13 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenMigrate ); - } + } - function test_3b_SmallStake_Late_SmallValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + // Test cases of staking first and depositing later start here + + function test_StakeThenDeposit_SmallStake_Late_SmallValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -566,11 +683,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - false // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.StakeThenDeposit ); - } + } - function test_4b_LargeStake_Late_LargeValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + function test_StakeThenDeposit_LargeStake_Late_LargeValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 100_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -589,11 +706,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - false // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.StakeThenDeposit ); } - function test_5b_SmallStake_Late_LargeValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { + function test_StakeThenDeposit_SmallStake_Late_LargeValidator_DelegatedDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 100_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -612,11 +729,13 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - false // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.StakeThenDeposit ); } - function test_2c_LargeStake_Early_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + // Test cases of early staking start here + + function test_DepositThenStake_LargeStake_Early_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -635,11 +754,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake ); } - function test_3c_SmallStake_Early_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + function test_DepositThenStake_SmallStake_Early_SmallValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -658,11 +777,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - false // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake ); } - function test_4c_LargeStake_Early_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + function test_DepositThenStake_LargeStake_Early_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 100_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -681,11 +800,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - false // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake ); } - function test_5c_SmallStake_Early_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { + function test_DepositThenStake_SmallStake_Early_LargeValidator_OwnDeposit_OneYearOfRewards_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 100_000_000 ether; uint256 totalDeposit = 5_200_000_000 ether; @@ -704,11 +823,13 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - false // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake ); } - function test_6a_ManyVsOneStake_UnstakeAll() public { + // Additional test cases start here + + function test_DepositThenStake_ManyVsOneStake_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 110_000_000 ether; @@ -729,11 +850,11 @@ contract LiquidDelegationTest is BaseDelegationTest { 5 * 51_000 ether / uint256(3600) * depositAmount / totalDeposit, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 51_000 ether / uint256(60) * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake ); } - function test_6b_OneVsManyStakes_UnstakeAll() public { + function test_DepositThenStake_OneVsManyStakes_UnstakeAll() public { stakers[0] = 0x092E5E57955437876dA9Df998C96e2BE19341670; uint256 depositAmount = 10_000_000 ether; uint256 totalDeposit = 110_000_000 ether; @@ -752,80 +873,15 @@ contract LiquidDelegationTest is BaseDelegationTest { 0, // rewardsAccruedAfterEach taxedRewardsAfterStaking + 51_000 ether / uint256(60) * depositAmount / totalDeposit, // rewardsBeforeUnstaking WithdrawalQueue.unbondingPeriod(), // after unstaking wait blocksUntil claiming - true // initialDeposit using funds held by the node, otherwise delegated by a staker + DepositMode.DepositThenStake ); } - /* - 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 - */ - //TODO: update the values based on the devnet and fix the failing test (typo intentional) - function est_0_ReproduceRealNetwork() public { - stakers[0] = 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, - WithdrawalQueue.unbondingPeriod(), // 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", - uint256( - 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 + function test_claimsAfterManyUnstakings() public { + claimsAfterManyUnstakings( + LiquidDelegationV2(proxy), //delegation + 20 //steps + ); } } \ No newline at end of file diff --git a/test/NonLiquidDelegation.t.sol b/test/NonLiquidDelegation.t.sol index 504afff..4316f56 100644 --- a/test/NonLiquidDelegation.t.sol +++ b/test/NonLiquidDelegation.t.sol @@ -1,19 +1,24 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.26; -import {BaseDelegationTest, PopVerifyPrecompile} from "test/BaseDelegation.t.sol"; +/* solhint-disable no-console */ +import {BaseDelegationTest} from "test/BaseDelegation.t.sol"; import {NonLiquidDelegation} from "src/NonLiquidDelegation.sol"; import {NonLiquidDelegationV2} from "src/NonLiquidDelegationV2.sol"; -import {BaseDelegation, WithdrawalQueue} from "src/BaseDelegation.sol"; -import {Delegation} from "src/Delegation.sol"; -import {Deposit} from "@zilliqa/zq2/deposit_v2.sol"; +import {BaseDelegation} from "src/BaseDelegation.sol"; +import {WithdrawalQueue} from "src/WithdrawalQueue.sol"; +import {IDelegation} from "src/IDelegation.sol"; +import {Deposit} from "@zilliqa/zq2/deposit_v3.sol"; import {Console} from "src/Console.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {Vm} from "forge-std/Test.sol"; -import "forge-std/console.sol"; +import {console} from "forge-std/console.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +/* solhint-disable func-name-mixedcase */ contract NonLiquidDelegationTest is BaseDelegationTest { - NonLiquidDelegationV2 delegation; + using SafeCast for int256; + + NonLiquidDelegationV2 internal delegation; constructor() BaseDelegationTest() { oldImplementation = address(new NonLiquidDelegation()); @@ -40,35 +45,37 @@ contract NonLiquidDelegationTest is BaseDelegationTest { revert("staker not found"); } - function snapshot(string memory s, uint256 i, uint256 x) internal view { + function snapshot(string memory s, uint256 i, uint256 x) internal view returns(uint256 calculatedRewards) { console.log("-----------------------------------------------"); console.log(s, i, x); uint256[] memory shares = new uint256[](stakers.length); NonLiquidDelegationV2.Staking[] memory stakings = delegation.getStakingHistory(); - for (i = 0; i < stakings.length; i++) - //i = stakings.length - 1; + for (uint256 k = 0; k < stakings.length; k++) + //k = stakings.length - 1; { - uint256 stakerIndex = findStaker(stakings[i].staker); - shares[stakerIndex] = stakings[i].amount; - s = string.concat("index: ", Strings.toString(i)); + uint256 stakerIndex = findStaker(stakings[k].staker); + shares[stakerIndex] = stakings[k].amount; + s = string.concat("index: ", Strings.toString(k)); s = string.concat(s, "\tstaker "); - assertEq(stakings[i].staker, stakers[stakerIndex], "found staker mismatch"); + assertEq(stakings[k].staker, stakers[stakerIndex], "found staker mismatch"); s = string.concat(s, Strings.toString(stakerIndex + 1)); s = string.concat(s, ": "); - s = string.concat(s, Strings.toHexString(stakings[i].staker)); + s = string.concat(s, Strings.toHexString(stakings[k].staker)); s = string.concat(s, " amount: "); - s = string.concat(s, Strings.toString(stakings[i].amount / 1 ether)); + s = string.concat(s, Strings.toString(stakings[k].amount / 1 ether)); s = string.concat(s, "\ttotal: "); - s = string.concat(s, Strings.toString(stakings[i].total / 1 ether)); - if (stakings[i].total < 100_000_000 ether) + s = string.concat(s, Strings.toString(stakings[k].total / 1 ether)); + if (stakings[k].total < 100_000_000 ether) s = string.concat(s, "\t"); s = string.concat(s, "\trewards: "); - s = string.concat(s, Strings.toString(stakings[i].rewards / 1 ether)); - s = string.concat(s, "\tshares: "); + s = string.concat(s, Strings.toString(stakings[k].rewards / 1 ether)); + if (stakings[k].rewards < 10_000 ether) + s = string.concat(s, "\t"); + s = string.concat(s, "\t\tshares:\t"); for (uint256 j = 0; j < shares.length; j++) - if (stakings[i].total != 0) { - string memory s0 = string.concat(Console.toString(10**6 * shares[j] / stakings[i].total, 4), "%"); - if (bytes(s0).length <= 7) + if (stakings[k].total != 0) { + string memory s0 = string.concat(Console.toString(10**6 * shares[j] / stakings[k].total, 4), "%"); + if (bytes(s0).length < 8) s0 = string.concat(s0, "\t\t"); else s0 = string.concat(s0, "\t"); @@ -76,6 +83,10 @@ contract NonLiquidDelegationTest is BaseDelegationTest { } else s = string.concat(s, "0.0%\t\t"); console.log(s); + if (k < stakings.length - 1) + calculatedRewards += stakings[k+1].rewards * shares[i-1] / stakings[k].total; + else + calculatedRewards += (int256(delegation.getRewards()) - delegation.getTotalRewards()).toUint256() * shares[i-1] / stakings[k].total; } ( uint64[] memory stakingIndices, @@ -85,11 +96,10 @@ contract NonLiquidDelegationTest is BaseDelegationTest { uint256 withdrawnAfterLastStaking ) = delegation.getStakingData(); Console.log("stakingIndices = [ %s]", stakingIndices); - console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), uint(lastWithdrawnRewardIndex)); + console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint256(firstStakingIndex), uint256(lastWithdrawnRewardIndex)); console.log("allWithdrawnRewards = %s withdrawnAfterLastStaking = %s", allWithdrawnRewards, withdrawnAfterLastStaking); } - //TODO: add assertions function run ( bytes memory _stakerIndicesBeforeWithdrawals, // each element in the interval (-100, 100) @@ -103,8 +113,8 @@ contract NonLiquidDelegationTest is BaseDelegationTest { uint256 depositAmount, uint256 rewardsBeforeStaking, uint256 rewardsAccruedAfterEach, - bool initialDeposit - ) public { + DepositMode mode + ) internal { uint64 steps = withdrawalInSteps; uint256[] memory stakerIndicesBeforeWithdrawals = abi.decode(_stakerIndicesBeforeWithdrawals, (uint256[])); int256[] memory relativeAmountsBeforeWithdrawals = abi.decode(_relativeAmountsBeforeWithdrawals, (int256[])); @@ -113,10 +123,14 @@ contract NonLiquidDelegationTest is BaseDelegationTest { int256[] memory relativeAmountsAfterWithdrawals = abi.decode(_relativeAmountsAfterWithdrawals, (int256[])); require(stakerIndicesAfterWithdrawals.length == relativeAmountsAfterWithdrawals.length, "array length mismatch"); - if (initialDeposit) + if (mode == DepositMode.DepositThenMigrate) + migrate(BaseDelegation(delegation), depositAmount); + else + deposit(BaseDelegation(delegation), depositAmount, mode == DepositMode.DepositThenStake); + + if (mode != DepositMode.StakeThenDeposit) // otherwise snapshot() doesn't find the staker and reverts stakers[0] = owner; - deposit(BaseDelegation(delegation), depositAmount, initialDeposit); for (uint256 i = 0; i < stakers.length; i++) { vm.deal(stakers[i], 10 * depositAmount); @@ -147,20 +161,32 @@ contract NonLiquidDelegationTest is BaseDelegationTest { //no rewards if we withdraw in the same block as the last staking //vm.deal(address(delegation), address(delegation).balance + rewardsAccruedAfterEach); + uint256[] memory calculatedRewards = new uint256[](stakers.length); + uint256[] memory availableRewards = new uint256[](stakers.length); + uint256[] memory withdrawnRewards = new uint256[](stakers.length); for (uint256 i = 1; i <= 2; i++) { vm.startPrank(stakers[i-1]); - if (steps == 123_456_789) - snapshot("staker %s withdrawing all, remaining rewards:", i, 0); - else + calculatedRewards[i-1] = + steps == 123_456_789 ? + snapshot("staker %s withdrawing all, remaining rewards:", i, 0) : snapshot("staker %s withdrawing 1+%s times", i, steps); Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); - Console.log("staker rewards: %s.%s%s", delegation.rewards()); - if (steps == 123_456_789) - Console.log("staker withdrew: %s.%s%s", delegation.withdrawAllRewards()); - else - Console.log("staker withdrew: %s.%s%s", delegation.withdrawRewards(delegation.rewards(steps), steps)); + Console.log("calculated rewards: %s.%s%s", calculatedRewards[i-1] * (delegation.DENOMINATOR() - delegation.getCommissionNumerator()) / delegation.DENOMINATOR()); + availableRewards[i-1] = delegation.rewards(); + Console.log("staker rewards: %s.%s%s", availableRewards[i-1]); + withdrawnRewards[i-1] = + steps == 123_456_789 ? + delegation.withdrawAllRewards() : + delegation.withdrawRewards(delegation.rewards(steps), steps); + Console.log("staker withdrew: %s.%s%s", withdrawnRewards[i-1]); + assertApproxEqAbs( + calculatedRewards[i-1] * (delegation.DENOMINATOR() - delegation.getCommissionNumerator()) / delegation.DENOMINATOR(), + availableRewards[i-1], + 9, + "rewards differ from calculated value" + ); Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); @@ -186,54 +212,119 @@ contract NonLiquidDelegationTest is BaseDelegationTest { //further rewards accrued since the last staking vm.deal(address(delegation), address(delegation).balance + rewardsAccruedAfterEach); - for (uint256 i = 1; i <= stakers.length; i++) { - vm.startPrank(stakers[i-1]); - if (steps == 123_456_789) - snapshot("staker %s withdrawing all, remaining rewards:", i, 0); - else - snapshot("staker %s withdrawing 1+%s times", i, steps); - Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); - Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); - //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); - Console.log("staker rewards: %s.%s%s", delegation.rewards()); - if (steps == 123_456_789) - Console.log("staker withdrew: %s.%s%s", delegation.withdrawAllRewards()); - else + // withdraw rewards 5 times in the same block + // i.e. without additional reward accrual + for (uint256 r = 0; r < 5; r++) + for (uint256 i = 1; i <= stakers.length; i++) { + vm.startPrank(stakers[i-1]); + calculatedRewards[i-1] = + steps == 123_456_789 ? + snapshot("staker %s withdrawing all, remaining rewards:", i, 0) : + snapshot("staker %s withdrawing 1+%s times", i, steps); + int256 temp = int256(calculatedRewards[i-1]) - int256(withdrawnRewards[i-1] * delegation.DENOMINATOR() / (delegation.DENOMINATOR() - delegation.getCommissionNumerator())); + calculatedRewards[i-1] = (temp > 0 ? temp : -temp).toUint256(); + Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); + Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); + //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); + Console.log("calculated rewards: %s.%s%s", calculatedRewards[i-1] * (delegation.DENOMINATOR() - delegation.getCommissionNumerator()) / delegation.DENOMINATOR()); + availableRewards[i-1] = delegation.rewards(); + Console.log("staker rewards: %s.%s%s", availableRewards[i-1]); + uint256 withdrawnReward = + steps == 123_456_789 ? + delegation.withdrawAllRewards() : + delegation.withdrawRewards(delegation.rewards(steps), steps); + Console.log("staker withdrew now: %s.%s%s", withdrawnReward); + withdrawnRewards[i-1] += withdrawnReward; + Console.log("staker withdrew altogether: %s.%s%s", withdrawnRewards[i-1]); + assertApproxEqAbs( + calculatedRewards[i-1] * (delegation.DENOMINATOR() - delegation.getCommissionNumerator()) / delegation.DENOMINATOR(), + availableRewards[i-1], + 9, + "rewards differ from calculated value" + ); //TODO: add a test that withdraws a fixed amount < delegation.rewards(step) - Console.log("staker withdrew: %s.%s%s", delegation.withdrawRewards(delegation.rewards(steps), steps)); - Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); - Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); - //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); - vm.stopPrank(); - } + Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); + Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); + //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); + vm.stopPrank(); + } + } - // if we try to withdraw again immediately (in the same block), - // the amount withdrawn must equal zero - //* - for (uint256 i = 1; i <= stakers.length; i++) { - vm.startPrank(stakers[i-1]); - if (steps == 123_456_789) - snapshot("staker %s withdrawing all, remaining rewards:", i, 0); - else - snapshot("staker %s withdrawing 1+%s times", i, steps); - Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); - Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); - //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); - Console.log("staker rewards: %s.%s%s", delegation.rewards()); - if (steps == 123_456_789) - Console.log("staker withdrew: %s.%s%s", delegation.withdrawAllRewards()); - else - //TODO: add a test that withdraws a fixed amount < delegation.rewards(step) - Console.log("staker withdrew: %s.%s%s", delegation.withdrawRewards(delegation.rewards(steps), steps)); - Console.log("rewards accrued until last staking: %s.%s%s", delegation.getTotalRewards()); - Console.log("delegation contract balance: %s.%s%s", address(delegation).balance); - //Console.log("staker balance: %s.%s%s", stakers[i-1].balance); - vm.stopPrank(); - } - //*/ + // Test cases of depositing first and staking afterwards start here + + function test_DepositThenStake_withdrawAllRewards() public { + run( + abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, + abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, + abi.encode([uint256(0x20), 1, 4]), //bytes -> uint256[] memory stakerIndicesAfterWithdrawals, + abi.encode([int256(0x20), 1, 40]), //bytes -> int256[] memory relativeAmountsAfterWithdrawals, + 123_456_789, //uint256 withdrawalInSteps, + 10_000_000 ether, //uint256 depositAmount, + 50_000 ether, //uint256 rewardsBeforeStaking, + 10_000 ether, //uint256 rewardsAccruedAfterEach, + DepositMode.DepositThenStake + ); } - function test_withdrawAllRewards_OwnDeposit() public { + function test_DepositThenStake_withdraw1Plus0Rewards () public { + run( + abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, + abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, + abi.encode([uint256(0x20), 1, 4]), //bytes -> uint256[] memory stakerIndicesAfterWithdrawals, + abi.encode([int256(0x20), 1, 40]), //bytes -> int256[] memory relativeAmountsAfterWithdrawals, + 0, //uint256 withdrawalInSteps, + 10_000_000 ether, //uint256 depositAmount, + 50_000 ether, //uint256 rewardsBeforeStaking, + 10_000 ether, //uint256 rewardsAccruedAfterEach, + DepositMode.DepositThenStake + ); + } + + function test_DepositThenStake_withdraw1Plus1Rewards () public { + run( + abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, + abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, + abi.encode([uint256(0x20), 1, 4]), //bytes -> uint256[] memory stakerIndicesAfterWithdrawals, + abi.encode([int256(0x20), 1, 40]), //bytes -> int256[] memory relativeAmountsAfterWithdrawals, + 1, //uint256 withdrawalInSteps, + 10_000_000 ether, //uint256 depositAmount, + 50_000 ether, //uint256 rewardsBeforeStaking, + 10_000 ether, //uint256 rewardsAccruedAfterEach, + DepositMode.DepositThenStake + ); + } + + function test_DepositThenStake_withdraw1Plus2Rewards () public { + run( + abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, + abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, + abi.encode([uint256(0x20), 1, 4]), //bytes -> uint256[] memory stakerIndicesAfterWithdrawals, + abi.encode([int256(0x20), 1, 40]), //bytes -> int256[] memory relativeAmountsAfterWithdrawals, + 2, //uint256 withdrawalInSteps, + 10_000_000 ether, //uint256 depositAmount, + 50_000 ether, //uint256 rewardsBeforeStaking, + 10_000 ether, //uint256 rewardsAccruedAfterEach, + DepositMode.DepositThenStake + ); + } + + function test_DepositThenStake_withdraw1Plus3Rewards () public { + run( + abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, + abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, + abi.encode([uint256(0x20), 1, 4]), //bytes -> uint256[] memory stakerIndicesAfterWithdrawals, + abi.encode([int256(0x20), 1, 40]), //bytes -> int256[] memory relativeAmountsAfterWithdrawals, + 3, //uint256 withdrawalInSteps, + 10_000_000 ether, //uint256 depositAmount, + 50_000 ether, //uint256 rewardsBeforeStaking, + 10_000 ether, //uint256 rewardsAccruedAfterEach, + DepositMode.DepositThenStake + ); + } + + // Test cases of migrating a solo staker to a staking pool start here + + function test_DepositThenMigrate_withdrawAllRewards() public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -243,11 +334,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - true //bool initialDeposit + DepositMode.DepositThenMigrate ); } - function test_withdraw1Plus0Rewards_OwnDeposit () public { + function test_DepositThenMigrate_withdraw1Plus0Rewards () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -257,11 +348,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - true //bool initialDeposit + DepositMode.DepositThenMigrate ); } - function test_withdraw1Plus1Rewards_OwnDeposit () public { + function test_DepositThenMigrate_withdraw1Plus1Rewards () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -271,11 +362,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - true //bool initialDeposit + DepositMode.DepositThenMigrate ); } - function test_withdraw1Plus2Rewards_OwnDeposit () public { + function test_DepositThenMigrate_withdraw1Plus2Rewards () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -285,11 +376,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - true //bool initialDeposit + DepositMode.DepositThenMigrate ); } - function test_withdraw1Plus3Rewards_OwnDeposit () public { + function test_DepositThenMigrate_withdraw1Plus3Rewards () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -299,11 +390,13 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - true //bool initialDeposit + DepositMode.DepositThenMigrate ); } - function test_withdrawAllRewards_DelegatedDeposit() public { + // Test cases of staking first and depositing later start here + + function test_StakeThenDeposit_withdrawAllRewards() public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -313,11 +406,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - false //bool initialDeposit + DepositMode.StakeThenDeposit ); } - function test_withdraw1Plus0Rewards_DelegatedDeposit () public { + function test_StakeThenDeposit_withdraw1Plus0Rewards () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -327,11 +420,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - false //bool initialDeposit + DepositMode.StakeThenDeposit ); } - function test_withdraw1Plus1Rewards_DelegatedDeposit () public { + function test_StakeThenDeposit_withdraw1Plus1Rewards () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -341,11 +434,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - false //bool initialDeposit + DepositMode.StakeThenDeposit ); } - function test_withdraw1Plus2Rewards_DelegatedDeposit () public { + function test_StakeThenDeposit_withdraw1Plus2Rewards () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -355,11 +448,11 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - false //bool initialDeposit + DepositMode.StakeThenDeposit ); } - function test_withdraw1Plus3Rewards_DelegatedDeposit () public { + function test_StakeThenDeposit_withdraw1Plus3Rewards_DelegatedDeposit () public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -369,10 +462,12 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - false //bool initialDeposit + DepositMode.StakeThenDeposit ); } + // Additional test cases start here + // run with // forge test -vv --via-ir --gas-report --gas-limit 10000000000 --block-gas-limit 10000000000 --match-test AfterMany function test_withdrawAfterManyStakings() public { @@ -406,7 +501,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { false, address(delegation) ); - emit Delegation.Staked( + emit IDelegation.Staked( stakers[i-1], x * 1 ether, "" @@ -428,7 +523,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { false, address(delegation) ); - emit Delegation.Unstaked( + emit IDelegation.Unstaked( stakers[i-1], x * 1 ether, "" @@ -462,9 +557,6 @@ contract NonLiquidDelegationTest is BaseDelegationTest { vm.stopPrank(); vm.roll(block.number + WithdrawalQueue.unbondingPeriod()); - //TODO: remove the next line once https://github.com/Zilliqa/zq2/issues/1761 is fixed - vm.warp(block.timestamp + WithdrawalQueue.unbondingPeriod()); - i = 1; vm.startPrank(stakers[i-1]); @@ -476,7 +568,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { false, address(delegation) ); - emit Delegation.Claimed( + emit IDelegation.Claimed( stakers[i-1], steps / 8 * x * 1 ether, "" @@ -485,6 +577,13 @@ contract NonLiquidDelegationTest is BaseDelegationTest { vm.stopPrank(); } + function test_claimsAfterManyUnstakings() public { + claimsAfterManyUnstakings( + NonLiquidDelegationV2(proxy), //delegation + 20 //steps + ); + } + function test_rewardsAfterWithdrawalLessThanBeforeWithdrawal() public { uint256 i; uint256 x; @@ -515,7 +614,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { false, address(delegation) ); - emit Delegation.Staked( + emit IDelegation.Staked( stakers[i-1], x * 1 ether, "" @@ -544,7 +643,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { uint256 withdrawnAfterLastStaking ) = delegation.getStakingData(); Console.log("stakingIndices = [ %s]", stakingIndices); - console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), uint(lastWithdrawnRewardIndex)); + console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint256(firstStakingIndex), uint256(lastWithdrawnRewardIndex)); console.log("allWithdrawnRewards = %s withdrawnAfterLastStaking = %s", allWithdrawnRewards, withdrawnAfterLastStaking); vm.recordLogs(); @@ -569,7 +668,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { withdrawnAfterLastStaking ) = delegation.getStakingData(); Console.log("stakingIndices = [ %s]", stakingIndices); - console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint(firstStakingIndex), uint(lastWithdrawnRewardIndex)); + console.log("firstStakingIndex = %s lastWithdrawnRewardIndex = %s", uint256(firstStakingIndex), uint256(lastWithdrawnRewardIndex)); console.log("allWithdrawnRewards = %s withdrawnAfterLastStaking = %s", allWithdrawnRewards, withdrawnAfterLastStaking); Console.log("contract balance: %s.%s%s", address(delegation).balance); @@ -579,7 +678,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { vm.stopPrank(); } - function test_withdrawAllRewardsThenNoMoreStakings_DelegatedDeposit() public { + function test_StakeThenDeposit_withdrawAllRewardsThenNoMoreStakings() public { run( abi.encode([uint256(0x20), 5, 1, 2, 3, 1, 2]), //bytes -> uint256[] memory stakerIndicesBeforeWithdrawals, abi.encode([int256(0x20), 5, 50, 50, 25, 35, -35]), //bytes -> int256[] memory relativeAmountsBeforeWithdrawals, @@ -589,7 +688,7 @@ contract NonLiquidDelegationTest is BaseDelegationTest { 10_000_000 ether, //uint256 depositAmount, 50_000 ether, //uint256 rewardsBeforeStaking, 10_000 ether, //uint256 rewardsAccruedAfterEach, - false //bool initialDeposit + DepositMode.StakeThenDeposit ); } diff --git a/unstake.sh b/unstake.sh index 13f0513..47ca435 100755 --- a/unstake.sh +++ b/unstake.sh @@ -1,5 +1,7 @@ #!/bin/bash +url=http://localhost:4201 + if [ $# -lt 2 ]; then echo "Provide the delegation contract address, a staker private key and optionally how much to unstake as arguments." exit 1 @@ -13,24 +15,24 @@ fi staker=$(cast wallet address $2) -temp=$(forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" $1 | tail -n 1) +temp=$(forge script script/CheckVariant.s.sol --rpc-url $url --sig "run(address payable)" $1 | tail -n 1) variant=$(sed -E 's/\s\s([a-zA-Z0-9]+)/\1/' <<< "$temp") if [[ "$variant" == "$temp" ]]; then echo Incompatible delegation contract at $1 exit 1 fi -forge script script/unstake_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" $1 $shares --private-key $2 +forge script script/Unstake.s.sol --rpc-url $url --broadcast --legacy --sig "run(address payable, uint256)" $1 $shares --private-key $2 -block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) +block=$(cast rpc eth_blockNumber --rpc-url $url) 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 rewardsAfterUnstaking = $(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ "$variant" == "ILiquidDelegation" ]]; then - echo taxedRewardsAfterUnstaking = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + echo taxedRewardsAfterUnstaking = $(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') fi -tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Unstaked(address,uint256,bytes)" --rpc-url http://localhost:4201 | grep "data") +tmp=$(cast logs --from-block $block_num --to-block $block_num --address $1 "Unstaked(address,uint256,bytes)" --rpc-url $url | grep "data") if [[ "$tmp" != "" ]]; then tmp=${tmp#*: } tmp=$(cast abi-decode --input "x(uint256,bytes)" $tmp | sed 's/\[[^]]*\]//g') @@ -46,20 +48,20 @@ 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') +rewardsBeforeUnstaking=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo rewardsBeforeUnstaking = $rewardsBeforeUnstaking -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') +stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') if [[ "$variant" == "ILiquidDelegation" ]]; then - taxedRewardsBeforeUnstaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') + taxedRewardsBeforeUnstaking=$(cast call $1 "getTaxedRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') 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') + lst=$(cast call $1 "getLST()(address)" --block $block_num --rpc-url $url) + if [[ "$shares" == "0" ]]; then shares=$(cast call $lst "balanceOf(address)(uint256)" $staker --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g'); fi + totalSupply=$(cast call $lst "totalSupply()(uint256)" --block $block_num --rpc-url $url | 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') + price0=$(cast call $1 "getPrice()(uint256)" --block $block_num --rpc-url $url | 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 fi diff --git a/withdrawRewards.sh b/withdrawRewards.sh index 6d3bb5b..a1f68f7 100755 --- a/withdrawRewards.sh +++ b/withdrawRewards.sh @@ -1,5 +1,7 @@ #!/bin/bash +url=http://localhost:4201 + if [ $# -lt 2 ]; then echo "Provide the delegation contract address, a staker private key and optionally an amount and number of steps as arguments." exit 1 @@ -19,7 +21,7 @@ fi staker=$(cast wallet address $2) -temp=$(forge script script/variant_Delegation.s.sol --rpc-url http://localhost:4201 --sig "run(address payable)" $1 | tail -n 1) +temp=$(forge script script/CheckVariant.s.sol --rpc-url $url --sig "run(address payable)" $1 | tail -n 1) variant=$(sed -E 's/\s\s([a-zA-Z0-9]+)/\1/' <<< "$temp") if [[ "$variant" == "$temp" ]]; then echo Incompatible delegation contract at $1 @@ -30,20 +32,20 @@ if [[ "$variant" != "INonLiquidDelegation" ]]; then exit 1 fi -forge script script/withdrawRewards_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, string, string)" $1 $amount $steps --private-key $2 +forge script script/WithdrawRewards.s.sol --rpc-url $url --broadcast --legacy --sig "run(address payable, string, string)" $1 $amount $steps --private-key $2 -block=$(cast rpc eth_blockNumber --rpc-url http://localhost:4201) +block=$(cast rpc eth_blockNumber --rpc-url $url) block_num=$(echo $block | tr -d '"' | cast to-dec --base-in 16) -owner=$(cast call $1 "owner()(address)" --block $block_num --rpc-url http://localhost:4201) +owner=$(cast call $1 "owner()(address)" --block $block_num --rpc-url $url) -rewardsAfterWithdrawal=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +rewardsAfterWithdrawal=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo rewardsAfterWithdrawal = $rewardsAfterWithdrawal -stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -ownerWeiAfter=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiAfter=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) +ownerWeiAfter=$(cast rpc eth_getBalance $owner $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) -tmp1=$(cast logs --from-block $block_num --to-block $block_num --address $1 "RewardPaid(address,uint256)" --rpc-url http://localhost:4201 | grep "data") +tmp1=$(cast logs --from-block $block_num --to-block $block_num --address $1 "RewardPaid(address,uint256)" --rpc-url $url | grep "data") if [[ "$tmp1" != "" ]]; then tmp1=${tmp1#*: } tmp1=$(cast abi-decode --input "x(uint256)" $tmp1 | sed 's/\[[^]]*\]//g') @@ -52,7 +54,7 @@ if [[ "$tmp1" != "" ]]; then #d1=$(echo $tmp | sed -n -e 1p | sed 's/\[[^]]*\]//g') fi -tmp2=$(cast logs --from-block $block_num --to-block $block_num --address $1 "CommissionPaid(address,uint256)" --rpc-url http://localhost:4201 | grep "data") +tmp2=$(cast logs --from-block $block_num --to-block $block_num --address $1 "CommissionPaid(address,uint256)" --rpc-url $url | grep "data") if [[ "$tmp2" != "" ]]; then tmp2=${tmp2#*: } tmp2=$(cast abi-decode --input "x(uint256)" $tmp2 | sed 's/\[[^]]*\]//g') @@ -61,7 +63,7 @@ if [[ "$tmp2" != "" ]]; then #d2=$(echo $tmp2 | sed -n -e 1p | sed 's/\[[^]]*\]//g') fi -x=$(cast call $1 "rewards()(uint256)" --from $staker --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +x=$(cast call $1 "rewards()(uint256)" --from $staker --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') staker_rewards_after_withdrawal=$(cast to-unit $x ether) echo $(date +"%T,%3N") $block_num @@ -69,17 +71,17 @@ echo $(date +"%T,%3N") $block_num block_num=$((block_num-1)) block=$(echo $block_num | cast to-hex --base-in 10) -rewardsBeforeWithdrawal=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +rewardsBeforeWithdrawal=$(cast call $1 "getRewards()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') echo rewardsBeforeWithdrawal = $rewardsBeforeWithdrawal -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') +stake=$(cast call $1 "getStake()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +commissionNumerator=$(cast call $1 "getCommissionNumerator()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') +denominator=$(cast call $1 "DENOMINATOR()(uint256)" --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') -stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) -ownerWeiBefore=$(cast rpc eth_getBalance $owner $block --rpc-url http://localhost:4201 | tr -d '"' | cast to-dec --base-in 16) +stakerWeiBefore=$(cast rpc eth_getBalance $staker $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) +ownerWeiBefore=$(cast rpc eth_getBalance $owner $block --rpc-url $url | tr -d '"' | cast to-dec --base-in 16) -x=$(cast call $1 "rewards()(uint256)" --from $staker --block $block_num --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g') +x=$(cast call $1 "rewards()(uint256)" --from $staker --block $block_num --rpc-url $url | sed 's/\[[^]]*\]//g') staker_rewards_before_withdrawal=$(cast to-unit $x ether) echo staker rewards before withdrawal: $staker_rewards_before_withdrawal ZIL