From 680aa2fba6c7ffee92af2fb60d52ea19cab3ac2f Mon Sep 17 00:00:00 2001 From: DrZoltanFazekas Date: Wed, 16 Oct 2024 17:49:18 +0200 Subject: [PATCH] Implement claiming after unbonding period --- README.md | 48 ++- script/claim_Delegation.s.sol | 33 ++ script/commission_Delegation.s.sol | 20 +- script/deploy_Delegation.s.sol | 2 - script/deposit_Delegation.s.sol | 78 ----- script/stake_Delegation.s.sol | 15 +- script/unstake_Delegation.s.sol | 19 +- script/upgrade_Delegation.s.sol | 2 - src/Delegation.sol | 1 - src/DelegationV2.sol | 207 +++++++---- src/Deposit.sol | 223 ++++++++++++ test/Delegation.t.sol | 545 +++++++++++++++++++++++++++++ 12 files changed, 1004 insertions(+), 189 deletions(-) create mode 100644 script/claim_Delegation.s.sol delete mode 100644 script/deposit_Delegation.s.sol create mode 100644 src/Deposit.sol create mode 100644 test/Delegation.t.sol diff --git a/README.md b/README.md index 46558f3..48e5cbe 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,22 @@ The output will look like this: ## Contract Configuration -Now or at a later time you can set the commission on the rewards the validator earns to e.g. 10% and the wallet address the commission will be sent to e.g. the validator node's address: +Now or at a later time you can set the commission on the rewards the validator earns to e.g. 10% as follows: ```bash -forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint16, address)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000 0x15fc323DFE5D5DCfbeEdc25CEcbf57f676634d77 +forge script script/commission_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint16)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 1000 ``` The output will contain the following information: ``` Running version: 2 LST address: 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 - Current commission rate and commission address is: 0.0% 0x0000000000000000000000000000000000000000 - New commission rate and commission address is: 10.0% 0x15fc323DFE5D5DCfbeEdc25CEcbf57f676634d77 + Old commission rate: 0.0% + New commission rate: 10.0% +``` + +Note that the commission rate is specified as an integer to be devided by the `DENOMINATOR` which can be retrieved from the delegation contract: +```bash +cast call 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 "DENOMINATOR()(uint256)" --rpc-url http://localhost:4201 | sed 's/\[[^]]*\]//g' ``` ## Validator Activation @@ -82,12 +87,12 @@ with the private key of the delegator account. Make sure the account's balance c The output will look like this: ``` Running version: 2 - Current stake: 10000000000000000000000000 - Current rewards: 110314207650273223687 + Current stake: 10000000000000000000000000 ZIL + Current rewards: 110314207650273223687 ZIL LST address: 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 - Owner balance: 10000000000000000000000000 - Staker balance: 0 - Staker balance: 199993793908430833324 + Owner balance: 10000000000000000000000000 LST + Staker balance before: 99899145245801454561224 ZIL 0 LST + Staker balance after: 99699145245801454561224 ZIL 199993793908430833324 LST ``` Note that the staker LST balance in the output will be different from the actual LST balance which you can query by running @@ -98,7 +103,7 @@ This is due to the fact that the above output was generated based on the local s You can copy the LST address from the above output and add it to your wallet to transfer your liquid staking tokens to another account if you want to. -Last but not least, to unstake e.g. 100 LST, run +To unstake e.g. 100 LST, run ```bash forge script script/unstake_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable, uint256)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 100000000000000000000 --private-key 0x... ``` @@ -107,10 +112,23 @@ with the private key of an account that holds some LST. The output will look like this: ``` Running version: 2 - Current stake: 10000000000000000000000000 - Current rewards: 331912568306010928520 + Current stake: 10000000000000000000000000 ZIL + Current rewards: 331912568306010928520 ZIL LST address: 0x9e5c257D1c6dF74EaA54e58CdccaCb924669dc83 - Owner balance: 10000000000000000000000000 - Staker balance: 199993784619390291653 - Staker balance: 99993784619390291653 + Owner balance: 10000000000000000000000000 LST + Staker balance before: 99698814298179759361224 ZIL 199993784619390291653 LST + Staker balance after: 99698814298179759361224 ZIL 99993784619390291653 LST +``` + +Last but not least, to claim the amount that is available after the unbonding period, run +```bash +forge script script/claim_Delegation.s.sol --rpc-url http://localhost:4201 --broadcast --legacy --sig "run(address payable)" 0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2 --private-key 0x... ``` +with the private key of an account that unstaked some LST. + +The output will look like this: +``` + Running version: 2 + Staker balance before: 99698086421983460161224 ZIL + Staker balance after: 99798095485861371162343 ZIL +``` \ No newline at end of file diff --git a/script/claim_Delegation.s.sol b/script/claim_Delegation.s.sol new file mode 100644 index 0000000..78a75fc --- /dev/null +++ b/script/claim_Delegation.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {DelegationV2} from "src/DelegationV2.sol"; +import "forge-std/console.sol"; + +contract Claim is Script { + function run(address payable proxy) external { + + address staker = msg.sender; + + DelegationV2 delegation = DelegationV2( + proxy + ); + + console.log("Running version: %s", + delegation.version() + ); + + console.log("Staker balance before: %s ZIL", + staker.balance + ); + + vm.broadcast(); + + delegation.claim(); + + console.log("Staker balance after: %s ZIL", + staker.balance + ); + } +} \ No newline at end of file diff --git a/script/commission_Delegation.s.sol b/script/commission_Delegation.s.sol index 800daf0..fad63f1 100644 --- a/script/commission_Delegation.s.sol +++ b/script/commission_Delegation.s.sol @@ -7,10 +7,9 @@ import {DelegationV2} from "src/DelegationV2.sol"; import "forge-std/console.sol"; contract Stake is Script { - function run(address payable proxy, uint16 commissionNumerator, address commissionAddress) external { + function run(address payable proxy, uint16 commissionNumerator) external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - address owner = vm.addr(deployerPrivateKey); DelegationV2 delegation = DelegationV2( proxy @@ -25,23 +24,20 @@ contract Stake is Script { address(lst) ); - console.log("Current commission rate and commission address: %s.%s%% %s", + console.log("Old commission rate: %s.%s%%", uint256(delegation.getCommissionNumerator()) * 100 / uint256(delegation.DENOMINATOR()), - uint256(delegation.getCommissionNumerator()) % (uint256(delegation.DENOMINATOR()) / 100), - delegation.getCommissionAddress() + //TODO: check if the decimals are printed correctly e.g. 12.01% vs 12.1% + uint256(delegation.getCommissionNumerator()) % (uint256(delegation.DENOMINATOR()) / 100) ); - vm.startBroadcast(deployerPrivateKey); + vm.broadcast(deployerPrivateKey); delegation.setCommissionNumerator(commissionNumerator); - delegation.setCommissionAddress(commissionAddress); - vm.stopBroadcast(); - - console.log("New commission rate and commission address: %s.%s%% %s", + console.log("New commission rate: %s.%s%%", uint256(delegation.getCommissionNumerator()) * 100 / uint256(delegation.DENOMINATOR()), - uint256(delegation.getCommissionNumerator()) % (uint256(delegation.DENOMINATOR()) / 100), - delegation.getCommissionAddress() + //TODO: check if the decimals are printed correctly e.g. 12.01% vs 12.1% + uint256(delegation.getCommissionNumerator()) % (uint256(delegation.DENOMINATOR()) / 100) ); } } \ No newline at end of file diff --git a/script/deploy_Delegation.s.sol b/script/deploy_Delegation.s.sol index 9ad67ad..fde9d04 100644 --- a/script/deploy_Delegation.s.sol +++ b/script/deploy_Delegation.s.sol @@ -15,7 +15,6 @@ contract Deploy is Script { vm.startBroadcast(deployerPrivateKey); address implementation = address( - //new Delegation{salt: "zilliqa"}() new Delegation() ); @@ -25,7 +24,6 @@ contract Deploy is Script { ); address payable proxy = payable( - //new ERC1967Proxy{salt: "zilliqa"}(implementation, initializerCall) new ERC1967Proxy(implementation, initializerCall) ); diff --git a/script/deposit_Delegation.s.sol b/script/deposit_Delegation.s.sol deleted file mode 100644 index fa60f49..0000000 --- a/script/deposit_Delegation.s.sol +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.26; - -import {Script} from "forge-std/Script.sol"; -import {NonRebasingLST} from "src/NonRebasingLST.sol"; -import {DelegationV2} from "src/DelegationV2.sol"; -import "forge-std/console.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; - -contract Deposit is Script { - function run(address payable proxy) external { - - uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); - address owner = vm.addr(deployerPrivateKey); - - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); - - DelegationV2 delegation = DelegationV2( - proxy - ); -/* - console.log("Running version: %s", - delegation.version() - ); -*/ - //TODO: output the arguments to use with cast send since forge script will fail when it tries to execute the script locally and can't call the BLS signature verification precompile - /*vm.broadcast(deployerPrivateKey); - - delegation.deposit{ - value: 10_000_000 ether - }( - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), - bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), //"12D3KooWQDT1rcThrxoSmnCt9n35jrhy5wo4BHsM5JuVz8LstQpN" - bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") - ); - - console.log("Current stake: %s \r\n Current rewards: %s", - delegation.getStake(), - delegation.getRewards() - ); - */ - bytes memory input = abi.encodeWithSignature( - "deposit(bytes,bytes,bytes)", - bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), - bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), //"12D3KooWQDT1rcThrxoSmnCt9n35jrhy5wo4BHsM5JuVz8LstQpN" - bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") - ); - string memory output = 'cast send'; - output = string.concat(output, ' --legacy --value 10000000ether --rpc-url https://api.zq2-devnet.zilliqa.com --private-key '); - output = string.concat(output, Strings.toHexString(deployerPrivateKey)); - output = string.concat(output, ' '); - output = string.concat(output, Strings.toHexString(address(delegation))); - /*console.log("%s \\", output); - console.logBytes(input);*/ - output = string.concat(output, ' "deposit(bytes,bytes,bytes)"'); - output = string.concat(output, ' 0x92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c'); - output = string.concat(output, ' 0x002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f'); - output = string.concat(output, ' 0xb14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a'); - console.log(output); - - // use this only for testing if deposit transaction not possible (e.g. no fully synced node available) - /*delegation.setup( - bytes(hex"b0447d886f8499bc0fd4aa21da63d71a0175ddd005d217a00c5304e1272e4a79a7df0ecb878a343582c9f2ca78c8c17f"), - bytes(hex"0024080112203f260505ee97570cbc034831097eddf177c4a49151dffb129abdc209329cc7e0") - ); - */ -/* - NonRebasingLST lst = NonRebasingLST(delegation.getLST()); - console.log("LST address: %s", - address(lst) - ); - - console.log("Owner LST balance: %s", - lst.balanceOf(owner) - ); -*/ - } -} \ No newline at end of file diff --git a/script/stake_Delegation.s.sol b/script/stake_Delegation.s.sol index 7a1c400..79f2385 100644 --- a/script/stake_Delegation.s.sol +++ b/script/stake_Delegation.s.sol @@ -11,11 +11,7 @@ contract Stake is Script { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address owner = vm.addr(deployerPrivateKey); - //console.log("Owner is %s", owner); - - //address staker = 0xd819fFcE7A58b1E835c25617Db7b46a00888B013; address staker = msg.sender; - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); DelegationV2 delegation = DelegationV2( proxy @@ -25,7 +21,7 @@ contract Stake is Script { delegation.version() ); - console.log("Current stake: %s \r\n Current rewards: %s", + console.log("Current stake: %s ZIL \r\n Current rewards: %s ZIL", delegation.getStake(), delegation.getRewards() ); @@ -35,22 +31,23 @@ contract Stake is Script { address(lst) ); - console.log("Owner balance: %s", + console.log("Owner balance: %s LST", lst.balanceOf(owner) ); - console.log("Staker balance: %s", + console.log("Staker balance before: %s ZIL %s LST", + staker.balance, lst.balanceOf(staker) ); - //vm.broadcast(staker); vm.broadcast(); delegation.stake{ value: amount }(); - console.log("Staker balance: %s", + console.log("Staker balance after: %s ZIL %s LST", + staker.balance, lst.balanceOf(staker) ); } diff --git a/script/unstake_Delegation.s.sol b/script/unstake_Delegation.s.sol index 64cc109..f9fab0b 100644 --- a/script/unstake_Delegation.s.sol +++ b/script/unstake_Delegation.s.sol @@ -11,11 +11,7 @@ contract Unstake is Script { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address owner = vm.addr(deployerPrivateKey); - //console.log("Owner is %s", owner); - - //address staker = 0xd819fFcE7A58b1E835c25617Db7b46a00888B013; address staker = msg.sender; - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); DelegationV2 delegation = DelegationV2( proxy @@ -25,7 +21,7 @@ contract Unstake is Script { delegation.version() ); - console.log("Current stake: %s \r\n Current rewards: %s", + console.log("Current stake: %s ZIL \r\n Current rewards: %s ZIL", delegation.getStake(), delegation.getRewards() ); @@ -35,22 +31,27 @@ contract Unstake is Script { address(lst) ); - console.log("Owner balance: %s", + console.log("Owner LST balance: %s LST", lst.balanceOf(owner) ); - console.log("Staker balance: %s", + console.log("Staker balance before: %s ZIL %s LST", + staker.balance, lst.balanceOf(staker) ); - //vm.broadcast(staker); + if (amount == 0) { + amount = lst.balanceOf(staker); + } + vm.broadcast(); delegation.unstake( amount ); - console.log("Staker balance: %s", + console.log("Staker balance after: %s ZIL %s LST", + staker.balance, lst.balanceOf(staker) ); } diff --git a/script/upgrade_Delegation.s.sol b/script/upgrade_Delegation.s.sol index e0e886f..0ad8281 100644 --- a/script/upgrade_Delegation.s.sol +++ b/script/upgrade_Delegation.s.sol @@ -12,8 +12,6 @@ contract Upgrade is Script { address owner = vm.addr(deployerPrivateKey); console.log("Signer is %s", owner); - //address payable proxy = payable(0x7A0b7e6D24eDe78260c9ddBD98e828B0e11A8EA2); - Delegation oldDelegation = Delegation( proxy ); diff --git a/src/Delegation.sol b/src/Delegation.sol index 50d190c..aff774c 100644 --- a/src/Delegation.sol +++ b/src/Delegation.sol @@ -7,7 +7,6 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "src/NonRebasingLST.sol"; -// the contract is supposed to be deployed with the node's signer account contract Delegation is Initializable, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { /// @custom:storage-location erc7201:zilliqa.storage.Delegation diff --git a/src/DelegationV2.sol b/src/DelegationV2.sol index 251f693..df0748e 100644 --- a/src/DelegationV2.sol +++ b/src/DelegationV2.sol @@ -7,17 +7,52 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "src/NonRebasingLST.sol"; +library WithdrawalQueue { + + uint256 public constant UNBONDING_PERIOD = 30; //approx. 30s, used only for testing + + struct Item { + uint256 blockNumber; + uint256 amount; + } + + struct Fifo { + uint256 first; + uint256 last; + mapping(uint256 => Item) items; + } + + function queue(Fifo storage fifo, uint256 amount) internal { + fifo.items[fifo.last] = Item(block.number + UNBONDING_PERIOD, amount); + fifo.last++; + } + + function dequeue(Fifo storage fifo) internal returns(Item memory result) { + require(fifo.first < fifo.last, "queue empty"); + result = fifo.items[fifo.first]; + delete fifo.items[fifo.first]; + fifo.first++; + } + + function ready(Fifo storage fifo) internal view returns(bool) { + return fifo.first < fifo.last && fifo.items[fifo.first].blockNumber <= block.number; + } +} + // the contract is supposed to be deployed with the node's signer account -// TODO: add events contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { + using WithdrawalQueue for WithdrawalQueue.Fifo; + /// @custom:storage-location erc7201:zilliqa.storage.Delegation struct Storage { address lst; bytes blsPubKey; bytes peerId; - uint16 commissionNumerator; - address commissionAddress; + uint256 commissionNumerator; + uint256 taxedRewards; + mapping(address => WithdrawalQueue.Fifo) withdrawals; + uint256 totalWithdrawals; } // keccak256(abi.encode(uint256(keccak256("zilliqa.storage.Delegation")) - 1)) & ~bytes32(uint256(0xff)) @@ -31,7 +66,7 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade uint256 public constant MIN_DELEGATION = 100 ether; address public constant DEPOSIT_CONTRACT = 0x000000000000000000005a494C4445504F534954; - uint16 public constant DENOMINATOR = 10_000; + uint256 public constant DENOMINATOR = 10_000; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -48,27 +83,29 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade function _authorizeUpgrade(address newImplementation) internal onlyOwner override {} event Staked(address indexed delegator, uint256 amount, uint256 shares); - event UnStaked(address indexed delegator, uint256 amount, uint256 shares); + event Unstaked(address indexed delegator, uint256 amount, uint256 shares); + event Claimed(address indexed delegator, uint256 amount); - // currently not called as there is no transaction for issuing rewards + // not called as there is no transaction for issuing rewards receive() payable external { require (msg.sender == 0x0000000000000000000000000000000000000000, "rewards must be issues by zero address"); - // topup deposit by msg.value to restake the rewards + // we could deduct the commission from msg.value and + // topup the deposit to restake the rewards // or use them for instant stake withdrawals } - // called by the node's account that deployed this contract and is its owner - // with at least the minimum stake to request activation as a validator - function deposit( + function _deposit( bytes calldata blsPubKey, bytes calldata peerId, - bytes calldata signature - ) public payable onlyOwner { + bytes calldata signature, + uint256 depositAmount + ) internal { Storage storage $ = _getStorage(); + require($.blsPubKey.length == 0, "deposit already performed"); $.blsPubKey = blsPubKey; $.peerId = peerId; (bool success, bytes memory data) = DEPOSIT_CONTRACT.call{ - value: msg.value + value: depositAmount }( //abi.encodeWithSignature("deposit(bytes,bytes,bytes,address,address)", //TODO: replace next line with the previous one once the signer address is implemented @@ -81,72 +118,136 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade //owner() ) ); - NonRebasingLST($.lst).mint(owner(), msg.value); require(success, "deposit failed"); + } + + // called by the node's account that deployed this contract and is its owner + // to request the node's activation as a validator using the delegated stake + function deposit2( + bytes calldata blsPubKey, + bytes calldata peerId, + bytes calldata signature + ) public onlyOwner { + _deposit( + blsPubKey, + peerId, + signature, + address(this).balance + ); + } + + // called by the node's account that deployed this contract and is its owner + // with at least the minimum stake to request the node's activation as a validator + // before any stake is delegated to it + function deposit( + bytes calldata blsPubKey, + bytes calldata peerId, + bytes calldata signature + ) public payable onlyOwner { + _deposit( + blsPubKey, + peerId, + signature, + msg.value + ); + Storage storage $ = _getStorage(); + require(NonRebasingLST($.lst).totalSupply() == 0, "stake already delegated"); + NonRebasingLST($.lst).mint(owner(), msg.value); } function stake() public payable whenNotPaused { require(msg.value >= MIN_DELEGATION, "delegated amount too low"); - //TODO: topup the deposit by msg.value so that msg.value becomes part of getStake(), - // currently it's part of getRewards() since this contrac is the reward address + uint256 shares; Storage storage $ = _getStorage(); - uint256 shares = NonRebasingLST($.lst).totalSupply() * msg.value / (getStake() + getRewards()); + if ($.blsPubKey.length > 0) { + //TODO: topup the deposit by msg.value so that msg.value becomes part of getStake(), + // currently it's part of getRewards() since this contract is the reward address + } + taxRewards(); // before calculating the shares we must deduct the commission from the yet untaxed rewards + if (NonRebasingLST($.lst).totalSupply() == 0) + shares = msg.value; + else + shares = NonRebasingLST($.lst).totalSupply() * msg.value / (getStake() + $.taxedRewards); NonRebasingLST($.lst).mint(msg.sender, shares); emit Staked(msg.sender, msg.value, shares); } function unstake(uint256 shares) public whenNotPaused { + uint256 amount; Storage storage $ = _getStorage(); NonRebasingLST($.lst).burn(msg.sender, shares); - uint256 commission = (getRewards() * $.commissionNumerator / DENOMINATOR) * shares / NonRebasingLST($.lst).totalSupply(); - (bool success, bytes memory data) = $.commissionAddress.call{ - value: commission - }(""); - require(success, "transfer of commission failed"); - uint256 amount = (getStake() + getRewards()) * shares / NonRebasingLST($.lst).totalSupply() - commission; - //TODO: store but don't transfer the amount, msg.sender can claim it after the unbonding period - (success, data) = msg.sender.call{ - value: amount - }(""); - require(success, "transfer of funds failed"); - emit UnStaked(msg.sender, amount, shares); + taxRewards(); // before calculating the amount we must deduct the commission from the yet untaxed rewards + if (NonRebasingLST($.lst).totalSupply() == 0) + amount = shares; + else + amount = (getStake() + $.taxedRewards) * shares / NonRebasingLST($.lst).totalSupply(); + $.withdrawals[msg.sender].queue(amount); + $.totalWithdrawals += amount; + if ($.blsPubKey.length > 0) { + //TODO: if the contract's balance is smaller than totalWithdrawals + // then withdraw the difference from the deposit contract + } + emit Unstaked(msg.sender, amount, shares); } - function getCommissionNumerator() public view returns(uint16) { + function getCommissionNumerator() public view returns(uint256) { Storage storage $ = _getStorage(); return $.commissionNumerator; } - function setCommissionNumerator(uint16 _commissionNumerator) public onlyOwner { + function setCommissionNumerator(uint256 _commissionNumerator) public onlyOwner { require(_commissionNumerator < DENOMINATOR, "invalid commission"); Storage storage $ = _getStorage(); $.commissionNumerator = _commissionNumerator; } - function getCommissionAddress() public view returns(address) { + function taxRewards() internal { Storage storage $ = _getStorage(); - return $.commissionAddress; + uint256 rewards = getRewards(); + uint256 commission = (rewards - $.taxedRewards) * $.commissionNumerator / DENOMINATOR; + $.taxedRewards = rewards - commission; + if (commission == 0) + return; + // commissions are not subject to the unbonding period + (bool success, bytes memory data) = owner().call{ + value: commission + }(""); + require(success, "transfer of commission failed"); } - function setCommissionAddress(address _commissionAddress) public onlyOwner { + function claim() public whenNotPaused { Storage storage $ = _getStorage(); - $.commissionAddress = _commissionAddress; + uint256 total; + while ($.withdrawals[msg.sender].ready()) + total += $.withdrawals[msg.sender].dequeue().amount; + /*if (total == 0) + return;*/ + taxRewards(); // before the balance changes we must deduct the commission from the yet untaxed rewards + //TODO: claim funds withdrawn from the deposit contract + (bool success, bytes memory data) = msg.sender.call{ + value: total + }(""); + require(success, "transfer of funds failed"); + $.totalWithdrawals -= total; + emit Claimed(msg.sender, total); } - function claim() public whenNotPaused { - // + //TODO: call restake every time someone wants to stake, unstake or claim? + function restake() public onlyOwner { + taxRewards(); // before the balance changes, we must deduct the commission from the yet untaxed rewards + //TODO: topup the deposit by address(this).balance - $.totalWithdrawals + // i.e. the rewards accrued minus the amount needed for the pending withdrawals } - function restake() public onlyOwner{ - // + function getTaxedRewards() public view returns(uint256) { + Storage storage $ = _getStorage(); + return $.taxedRewards; } -/* function getRewards() public view returns(uint256){ - return 24391829365079365070369; - } -*/ function getRewards() public view returns(uint256) { Storage storage $ = _getStorage(); + if ($.blsPubKey.length == 0) + return 0; (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( abi.encodeWithSignature("getRewardAddress(bytes)", $.blsPubKey) ); @@ -155,13 +256,10 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade return rewardAddress.balance; } -/* //TODO: replace with the below getStake2() function once stake() tops up the deposit - function getStake() public view returns(uint256) { - return getStake2() + address(this).balance; - } -*/ function getStake() public view returns(uint256) { Storage storage $ = _getStorage(); + if ($.blsPubKey.length == 0) + return address(this).balance; (bool success, bytes memory data) = DEPOSIT_CONTRACT.staticcall( abi.encodeWithSignature("getStake(bytes)", $.blsPubKey) ); @@ -174,17 +272,4 @@ contract DelegationV2 is Initializable, PausableUpgradeable, Ownable2StepUpgrade return $.lst; } - // only for testing purposes, will be removed later - function setup(bytes calldata blsPubKey, bytes calldata peerId) public onlyOwner { - Storage storage $ = _getStorage(); - $.blsPubKey = blsPubKey; - $.peerId = peerId; - (bool success, bytes memory data) = owner().call{ - value: address(this).balance - }(""); - require(success, "transfer failed"); - $.lst = address(new NonRebasingLST(address(this))); - NonRebasingLST($.lst).mint(owner(), getStake()); - } - } \ No newline at end of file diff --git a/src/Deposit.sol b/src/Deposit.sol new file mode 100644 index 0000000..48f2f6e --- /dev/null +++ b/src/Deposit.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.20; + +struct Staker { + // The index of this staker's `blsPubKey` in the `_stakerKeys` array, plus 1. 0 is used for non-existing entries. + uint256 keyIndex; + // Invariant: `balance >= minimumStake` + uint256 balance; + address rewardAddress; + bytes peerId; +} + +contract Deposit { + bytes[] _stakerKeys; + mapping(bytes => Staker) _stakersMap; + uint256 public totalStake; + + uint256 public _minimumStake; + uint256 public _maximumStakers; + + constructor(uint256 minimumStake, uint256 maximumStakers) { + _minimumStake = minimumStake; + _maximumStakers = maximumStakers; + } + + function leaderFromRandomness( + uint256 randomness + ) private view returns (bytes memory) { + // Get a random number in the inclusive range of 0 to (totalStake - 1) + uint256 position = randomness % totalStake; + uint256 cummulative_stake = 0; + + for (uint256 i = 0; i < _stakerKeys.length; i++) { + bytes storage stakerKey = _stakerKeys[i]; + Staker storage staker = _stakersMap[stakerKey]; + + cummulative_stake += staker.balance; + + if (position < cummulative_stake) { + return stakerKey; + } + } + + revert("Unable to select next leader"); + } + + function leader() public view returns (bytes memory) { + return leaderFromRandomness(uint256(block.prevrandao)); + } + + function leaderAtView( + uint256 viewNumber + ) public view returns (bytes memory) { + uint256 randomness = uint256( + keccak256(bytes.concat(bytes32(viewNumber))) + ); + return leaderFromRandomness(randomness); + } + + // Temporary function to manually remove a staker. Can be called by the reward address of any staker with more than + // 10% stake. Will be removed later in development. + function tempRemoveStaker(bytes calldata blsPubKey) public { + require(blsPubKey.length == 48); + + // Inefficient, but its fine because this is temporary. + for (uint256 i = 0; i < _stakerKeys.length; i++) { + bytes storage stakerKey = _stakerKeys[i]; + Staker storage staker = _stakersMap[stakerKey]; + + // Check if the call is authorised. + if ( + msg.sender == staker.rewardAddress && + staker.balance > (totalStake / 10) + ) { + // The call is authorised, so we can delete the specified staker. + Staker storage stakerToDelete = _stakersMap[blsPubKey]; + + // Delete this staker's key from `_stakerKeys`. Swap the last element in the array into the deleted position. + bytes storage swappedStakerKey = _stakerKeys[ + _stakerKeys.length - 1 + ]; + Staker storage swappedStaker = _stakersMap[swappedStakerKey]; + _stakerKeys[stakerToDelete.keyIndex - 1] = swappedStakerKey; + swappedStaker.keyIndex = stakerToDelete.keyIndex; + + // The last element is now the element we want to delete. + _stakerKeys.pop(); + + // Reduce the total stake, but don't refund to the removed staker + totalStake -= stakerToDelete.balance; + + // Delete the staker from `_stakersMap` too. + delete _stakersMap[blsPubKey]; + + return; + } + } + revert( + "call must come from a reward address corresponding to a staker with more than 10% stake" + ); + } + + // keep in-sync with zilliqa/src/precompiles.rs + function _popVerify( + bytes memory pubkey, + bytes memory signature + ) private view returns (bool) { + // mocked to make tests work + return true; + bytes memory input = abi.encodeWithSelector( + hex"bfd24965", // bytes4(keccak256("popVerify(bytes,bytes)")) + signature, + pubkey + ); + uint inputLength = input.length; + bytes memory output = new bytes(32); + bool success; + assembly { + success := staticcall( + gas(), + 0x5a494c80, // "ZIL\x80" + add(input, 0x20), + inputLength, + add(output, 0x20), + 32 + ) + } + require(success, "popVerify"); + bool result = abi.decode(output, (bool)); + return result; + } + + function deposit( + bytes calldata blsPubKey, + bytes calldata peerId, + bytes calldata signature, + address rewardAddress + ) public payable { + require(blsPubKey.length == 48); + require(peerId.length == 38); + require(signature.length == 96); + + require(_stakerKeys.length < _maximumStakers, "too many stakers"); + + // Verify signature as a proof-of-possession of the private key. + bool pop = _popVerify(blsPubKey, signature); + require(pop, "rogue key check"); + + uint256 keyIndex = _stakersMap[blsPubKey].keyIndex; + if (keyIndex == 0) { + // The staker will be at index `_stakerKeys.length`. We also need to add 1 to avoid the 0 sentinel value. + _stakersMap[blsPubKey].keyIndex = _stakerKeys.length + 1; + _stakerKeys.push(blsPubKey); + } + + _stakersMap[blsPubKey].balance += msg.value; + totalStake += msg.value; + + if (_stakersMap[blsPubKey].balance < _minimumStake) { + revert("stake less than minimum stake"); + } + + _stakersMap[blsPubKey].rewardAddress = rewardAddress; + _stakersMap[blsPubKey].peerId = peerId; + } + + function setStake( + bytes calldata blsPubKey, + bytes calldata peerId, + address rewardAddress, + uint256 amount + ) public { + require(msg.sender == address(0)); + require(blsPubKey.length == 48); + require(peerId.length == 38); + + if (amount < _minimumStake) { + revert("stake less than minimum stake"); + } + + totalStake -= _stakersMap[blsPubKey].balance; + _stakersMap[blsPubKey].balance = amount; + totalStake += amount; + _stakersMap[blsPubKey].rewardAddress = rewardAddress; + _stakersMap[blsPubKey].peerId = peerId; + uint256 keyIndex = _stakersMap[blsPubKey].keyIndex; + if (keyIndex == 0) { + // The staker will be at index `_stakerKeys.length`. We also need to add 1 to avoid the 0 sentinel value. + _stakersMap[blsPubKey].keyIndex = _stakerKeys.length + 1; + _stakerKeys.push(blsPubKey); + } + } + + function getStake(bytes calldata blsPubKey) public view returns (uint256) { + require(blsPubKey.length == 48); + + return _stakersMap[blsPubKey].balance; + } + + function getRewardAddress( + bytes calldata blsPubKey + ) public view returns (address) { + require(blsPubKey.length == 48); + if (_stakersMap[blsPubKey].rewardAddress == address(0)) { + revert("not staked"); + } + return _stakersMap[blsPubKey].rewardAddress; + } + + function getStakers() public view returns (bytes[] memory) { + return _stakerKeys; + } + + function getPeerId( + bytes calldata blsPubKey + ) public view returns (bytes memory) { + require(blsPubKey.length == 48); + if (_stakersMap[blsPubKey].rewardAddress == address(0)) { + revert("not staked"); + } + return _stakersMap[blsPubKey].peerId; + } +} \ No newline at end of file diff --git a/test/Delegation.t.sol b/test/Delegation.t.sol new file mode 100644 index 0000000..966c766 --- /dev/null +++ b/test/Delegation.t.sol @@ -0,0 +1,545 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.26; + +import {Delegation} from "src/Delegation.sol"; +import {DelegationV2} from "src/DelegationV2.sol"; +import {NonRebasingLST} from "src/NonRebasingLST.sol"; +import {Deposit} from "src/Deposit.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Test, Vm} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +library Console { + function log(string memory format, uint256 amount) external { + string memory zeros = ""; + uint256 decimals = amount % 10**18; + while (decimals > 0 && decimals < 10**17) { + //console.log("%s %s", zeros, decimals); + zeros = string.concat(zeros, "0"); + decimals *= 10; + } + console.log( + format, + amount / 10**18, + zeros, + amount % 10**18 + ); + } +} + +contract DelegationTest is Test { + address payable proxy; + address owner; + address staker = 0xd819fFcE7A58b1E835c25617Db7b46a00888B013; + + function setUp() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + owner = vm.addr(deployerPrivateKey); + //console.log("Signer is %s", owner); + vm.deal(owner, 100_000 ether); + vm.startPrank(owner); + + address oldImplementation = address( + new Delegation() + ); + + bytes memory initializerCall = abi.encodeWithSelector( + Delegation.initialize.selector, + owner + ); + + proxy = payable( + new ERC1967Proxy(oldImplementation, initializerCall) + ); + /* + console.log( + "Proxy deployed: %s \r\n Implementation deployed: %s", + proxy, + oldImplementation + ); + //*/ + Delegation oldDelegation = Delegation( + proxy + ); + /* + console.log("Deployed version: %s", + oldDelegation.version() + ); + + console.log("Owner is %s", + oldDelegation.owner() + ); + //*/ + address payable newImplementation = payable( + new DelegationV2() + ); + /* + console.log("New implementation deployed: %s", + newImplementation + ); + //*/ + bytes memory reinitializerCall = abi.encodeWithSelector( + DelegationV2.reinitialize.selector + ); + + oldDelegation.upgradeToAndCall( + newImplementation, + reinitializerCall + ); + + DelegationV2 delegation = DelegationV2( + proxy + ); + /* + console.log("Upgraded to version: %s", + delegation.version() + ); + //*/ + NonRebasingLST lst = NonRebasingLST(delegation.getLST()); + /* + console.log("LST address: %s", + address(lst) + ); + + console.log("Old commission rate: %s.%s%%", + uint256(delegation.getCommissionNumerator()) * 100 / uint256(delegation.DENOMINATOR()), + //TODO: check if the decimals are printed correctly e.g. 12.01% vs 12.1% + uint256(delegation.getCommissionNumerator()) % (uint256(delegation.DENOMINATOR()) / 100) + ); + //*/ + uint256 commissionNumerator = 1_000; + delegation.setCommissionNumerator(commissionNumerator); + /* + console.log("New commission rate: %s.%s%%", + uint256(delegation.getCommissionNumerator()) * 100 / uint256(delegation.DENOMINATOR()), + //TODO: check if the decimals are printed correctly e.g. 12.01% vs 12.1% + uint256(delegation.getCommissionNumerator()) % (uint256(delegation.DENOMINATOR()) / 100) + ); + //*/ + + //vm.deployCodeTo("Deposit.sol", delegation.DEPOSIT_CONTRACT()); + vm.etch( + delegation.DEPOSIT_CONTRACT(), //0x000000000000000000005a494C4445504F534954, + address(new Deposit(10_000_000 ether, 256)).code + ); + vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(3)), bytes32(uint256(10_000_000 ether))); + vm.store(delegation.DEPOSIT_CONTRACT(), bytes32(uint256(4)), bytes32(uint256(256))); + /* + console.log("Deposit._minimimStake() =", Deposit(delegation.DEPOSIT_CONTRACT())._minimumStake()); + console.log("Deposit._maximumStakers() =", Deposit(delegation.DEPOSIT_CONTRACT())._maximumStakers()); + //*/ + } + + function run( + uint256 depositAmount, + uint256 rewardsBefore, + uint256 delegatedAmount, + uint256 rewardsAfter, + uint256 blocksUntil, + bool initialDeposit + ) public { + DelegationV2 delegation = DelegationV2(proxy); + NonRebasingLST lst = NonRebasingLST(delegation.getLST()); + + if (initialDeposit) { + vm.deal(owner, owner.balance + depositAmount); + vm.startPrank(owner); + + delegation.deposit{ + value: depositAmount + }( + bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), + bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), + bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") + ); + } else { + vm.deal(staker, staker.balance + depositAmount); + vm.startPrank(staker); + + vm.expectEmit( + true, + false, + false, + true, + address(delegation) + ); + emit DelegationV2.Staked( + staker, + depositAmount, + depositAmount + ); + + delegation.stake{ + value: depositAmount + }(); + + vm.startPrank(owner); + + delegation.deposit2( + bytes(hex"92fbe50544dce63cfdcc88301d7412f0edea024c91ae5d6a04c7cd3819edfc1b9d75d9121080af12e00f054d221f876c"), + bytes(hex"002408011220d5ed74b09dcbe84d3b32a56c01ab721cf82809848b6604535212a219d35c412f"), + bytes(hex"b14832a866a49ddf8a3104f8ee379d29c136f29aeb8fccec9d7fb17180b99e8ed29bee2ada5ce390cb704bc6fd7f5ce814f914498376c4b8bc14841a57ae22279769ec8614e2673ba7f36edc5a4bf5733aa9d70af626279ee2b2cde939b4bd8a") + ); + } + + vm.deal(address(delegation), rewardsBefore); + vm.deal(staker, 100_000 ether); + vm.startPrank(staker); + + Console.log("Stake deposited before staking: %s.%s%s ZIL", + delegation.getStake() + ); + + Console.log("Rewards before staking: %s.%s%s ZIL", + delegation.getRewards() + ); + + Console.log("Staker balance before staking: %s.%s%s ZIL", + staker.balance + ); + + Console.log("Staker balance before staking: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply before staking: %s.%s%s LST", + lst.totalSupply() + ); + + vm.recordLogs(); + + vm.expectEmit( + true, + false, + false, + false, + address(delegation) + ); + emit DelegationV2.Staked( + staker, + delegatedAmount, + lst.totalSupply() * delegatedAmount / (delegation.getStake() + delegation.getRewards()) + ); + + uint256 ownerZILBefore = delegation.owner().balance; + + delegation.stake{ + value: delegatedAmount + }(); + + uint256 ownerZILAfter = delegation.owner().balance; + + Vm.Log[] memory entries = vm.getRecordedLogs(); + uint256 loggedAmount; + uint256 loggedShares; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == keccak256("Staked(address,uint256,uint256)")) { + (loggedAmount, loggedShares) = abi.decode(entries[i].data, (uint256, uint256)); + //console.log(loggedAmount, loggedShares); + } + } + //console.log(delegatedAmount, (lst.totalSupply() - lst.balanceOf(staker)) * delegatedAmount / (delegation.getStake() + delegation.getTaxedRewards())); + //console.log(delegatedAmount, lst.balanceOf(staker)); + + Console.log("Owner commission after staking: %s.%s%s ZIL", + ownerZILAfter - ownerZILBefore + ); + + Console.log("Stake deposited after staking: %s.%s%s ZIL", + delegation.getStake() + ); + + Console.log("Rewards after staking: %s.%s%s ZIL", + delegation.getRewards() + ); + + Console.log("Staker balance after staking: %s.%s%s ZIL", + staker.balance + ); + + Console.log("Staker balance after staking: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply after staking: %s.%s%s LST", + lst.totalSupply() + ); + + vm.deal(address(delegation), address(delegation).balance + rewardsAfter); + + Console.log("LST price: %s.%s%s", + 10**18 * (delegation.getStake() + delegation.getRewards()) / lst.totalSupply() + ); + + Console.log("LST value: %s.%s%s", + lst.balanceOf(staker) * (delegation.getStake() + delegation.getRewards()) / lst.totalSupply() + ); + + vm.recordLogs(); + + vm.expectEmit( + true, + false, + false, + false, + address(delegation) + ); + emit DelegationV2.Unstaked( + staker, + (delegation.getStake() + delegation.getRewards()) * lst.balanceOf(staker) / lst.totalSupply(), + lst.balanceOf(staker) + ); + + uint256 stakerLSTBefore = lst.balanceOf(staker); + ownerZILBefore = delegation.owner().balance; + + delegation.unstake( + initialDeposit ? lst.balanceOf(staker) : lst.balanceOf(staker) - depositAmount + ); + + uint256 stakerLSTAfter = lst.balanceOf(staker); + ownerZILAfter = delegation.owner().balance; + + entries = vm.getRecordedLogs(); + + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == keccak256("Unstaked(address,uint256,uint256)")) { + (loggedAmount, loggedShares) = abi.decode(entries[i].data, (uint256, uint256)); + //console.log(loggedAmount, loggedShares); + } + } + //TODO: why is loggedAmount equal to the value below without adding back lst.totalSupply() + stakerLSTBefore although unstake() burns stakerLSTBefore before computing the amount? + //console.log((delegation.getStake() + delegation.getTaxedRewards()) * stakerLSTBefore / lst.totalSupply(), stakerLSTBefore - stakerLSTAfter); + + Console.log("Owner commission after unstaking: %s.%s%s ZIL", + ownerZILAfter - ownerZILBefore + ); + + Console.log("Stake deposited after unstaking: %s.%s%s ZIL", + delegation.getStake() + ); + + Console.log("Rewards after unstaking: %s.%s%s ZIL", + delegation.getRewards() + ); + + Console.log("Staker balance after unstaking: %s.%s%s ZIL", + staker.balance + ); + + Console.log("Staker balance after unstaking: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply after unstaking: %s.%s%s LST", + lst.totalSupply() + ); + + vm.roll(block.number + blocksUntil); + + vm.recordLogs(); + + uint256 unstakedAmount = loggedAmount; // the amount we logged on unstaking + vm.expectEmit( + true, + false, + false, + false, + address(delegation) + ); + emit DelegationV2.Claimed( + staker, + unstakedAmount + ); + + uint256 stakerZILBefore = staker.balance; + ownerZILBefore = delegation.owner().balance; + + delegation.claim(); + + uint256 stakerZILAfter = staker.balance; + ownerZILAfter = delegation.owner().balance; + + entries = vm.getRecordedLogs(); + + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == keccak256("Claimed(address,uint256)")) { + loggedAmount = abi.decode(entries[i].data, (uint256)); + //console.log(loggedAmount); + } + } + //console.log(stakerZILAfter - stakerZILBefore); + //console.log(unstakedAmount); + + Console.log("Owner commission after claiming: %s.%s%s ZIL", + ownerZILAfter - ownerZILBefore + ); + + Console.log("Stake deposited after claiming: %s.%s%s ZIL", + delegation.getStake() + ); + + Console.log("Rewards after claiming: %s.%s%s ZIL", + delegation.getRewards() + ); + + Console.log("Staker balance after claiming: %s.%s%s ZIL", + staker.balance + ); + + Console.log("Staker balance after claiming: %s.%s%s LST", + lst.balanceOf(staker) + ); + + Console.log("Total supply after claiming: %s.%s%s LST", + lst.totalSupply() + ); + + } + + function test_Real() public { + //TODO: how could the price fall below 1.00 when rewardsAfter was based on 9969126831808605271675? + // supply + rewards + 10k - tax < supply where tax = (rewards + 10k) / 10 + // supply + (rewards + 10k) * 9 / 10 < supply + // because we deducted 10% of 10k as commission and it reduced the left hand side + uint256 rewardsBefore = 9961644437442408088600; + uint256 rewardsAfter = (10003845141667760201143 - rewardsBefore) * uint256(10) / 9; + rewardsBefore = rewardsBefore * uint256(10) / 9 - 10_000 ether; + run( + 10_000_000 ether, + rewardsBefore, + 10_000 ether, // delegatedAmount + rewardsAfter, + 30, // blocksUntil claiming + true // initialDeposit + ); + // staker's ZIL after claiming minus before claiming plus 18-digit claiming transaction fee + Console.log("%s.%s%s", 99994156053341800951925 - 99894533133440243560633 + 377395241114400000); + } + + function test_ReproduceDevnet() public { + uint256 rewardsBefore = 500790859951588622934; + uint256 rewardsAfter = (532306705022011158106 - rewardsBefore) * uint256(10) / 9; + rewardsBefore = rewardsBefore * uint256(10) / 9 - 100 ether; + run( + 10_000_000 ether, + rewardsBefore, + 100 ether, // delegatedAmount + rewardsAfter, + 30, // blocksUntil claiming + true // initialDeposit + ); + // staker's ZIL after claiming minus before claiming plus 18-digit claiming transaction fee + Console.log("%s.%s%s", 99994156053341800951925 - 99894533133440243560633 + 377395241114400000); + } + + function test_NoRewardsUnstakeAll() public { + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + run( + 10_000_000 ether, // depositAmount + 365 * 24 * 51_000 ether * depositAmount / totalDeposit, // set rewardsBefore staking + 10_000 ether, // delegatedAmount + 0, // add rewardsAfter staking + 30, // wait blocksUntil claiming + true // initialDeposit + ); + } + + function test_SmallStakeSmallRewardsUnstakeAll() public { + run( + 10_000_000 ether, // depositAmount + 690 ether, // set rewardsBefore staking + 100 ether, // delegatedAmount + 100 ether, // add rewardsAfter staking + 30, // wait blocksUntil claiming + true // initialDeposit + ); + } + + function test_SmallStakeMediumRewardsUnstakeAll() public { + run( + 10_000_000 ether, // depositAmount + 690 ether, // set rewardsBefore staking + 100 ether, // delegatedAmount + 800 ether, // add rewardsAfter staking + 30, // wait blocksUntil claiming + true // initialDeposit + ); + } + + function test_SmallStakeOneYearUnstakeAll() public { + // 7.7318% APY + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 rewardsBefore = 690 ether; + uint256 rewardsAfter = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + Console.log("Rewards for 1 year: %s.%s%s", rewardsAfter); + run( + depositAmount, + rewardsBefore, + 100 ether, // delegatedAmount + rewardsAfter, + 30, // blocksUntil claiming + true // initialDeposit + ); + } + + function test_LargeStakeOneYearUnstakeAll() public { + // 7.6629% APY is lower than in SmallStakeOneYearUnstakeAll + // because the delegated amount is added to the rewards + // and the owner receives a commission on it + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 rewardsBefore = 690 ether; + uint256 rewardsAfter = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + Console.log("Rewards for 1 year: %s.%s%s", rewardsAfter); + run( + depositAmount, + rewardsBefore, + 100_000 ether, // delegatedAmount + rewardsAfter, + 30, // blocksUntil claiming + true // initialDeposit + ); + } + + function test_SmallStakeLaggardOneYearUnstakeAll() public { + // 7.1773% APY is lower than in SmallStakeOneYearUnstakeAll + // because ?????????? + uint256 depositAmount = 10_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 rewardsBefore = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + uint256 rewardsAfter = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + Console.log("Rewards for 1 year: %s.%s%s", rewardsAfter); + run( + depositAmount, + rewardsBefore, + 100 ether, // delegatedAmount + rewardsAfter, + 30, // blocksUntil claiming + true // initialDeposit + ); + } + + function test_SmallStakeMediumDepositOneYearUnstakeAll() public { + // 7.7323% APY is higher than in SmallStakeOneYearUnstakeAll + // because the delegated amount is not added to the deposit + // i.e. it doesn't earn rewards, but the missing rewards are + // more significant in case of a smaller deposit + uint256 depositAmount = 100_000_000 ether; + uint256 totalDeposit = 5_200_000_000 ether; + uint256 rewardsBefore = 690 ether; + uint256 rewardsAfter = 365 * 24 * 51_000 ether * depositAmount / totalDeposit; + Console.log("Rewards for 1 year: %s.%s%s", rewardsAfter); + run( + depositAmount, + rewardsBefore, + 100 ether, // delegatedAmount + rewardsAfter, + 30, // blocksUntil claiming + true // initialDeposit + ); + } + +} \ No newline at end of file