From afc5fa1f826c9abcae2c1a9ac52c47bfecfc61ab Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 10 Jan 2025 17:40:41 +0300 Subject: [PATCH 01/22] feat: test shareRate with fuzzing --- package.json | 3 +- .../contracts/Protocol__Deployment.t.sol | 126 ++++++++++++++++++ test/0.8.25/vaults/contracts/ShareRate.t.sol | 103 ++++++++++++++ 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol create mode 100644 test/0.8.25/vaults/contracts/ShareRate.t.sol diff --git a/package.json b/package.json index 971ae0d99..0021fd60e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "typecheck": "tsc --noEmit", "prepare": "husky", "abis:extract": "hardhat abis:extract", - "verify:deployed": "hardhat verify:deployed" + "verify:deployed": "hardhat verify:deployed", + "test:custom": "forge test -vvvv --match-path \"test/0.8.25/vaults/contracts/ShareRate.t.sol\"" }, "lint-staged": { "./**/*.ts": [ diff --git a/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol b/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol new file mode 100644 index 000000000..6c5272922 --- /dev/null +++ b/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../../../contracts/0.8.9/EIP712StETH.sol"; +import "forge-std/Test.sol"; + +import {CommonBase} from "forge-std/Base.sol"; +import {LidoLocator} from "../../../../contracts/0.8.9/LidoLocator.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console2} from "forge-std/console2.sol"; + +interface ILido { + function getTotalShares() external view returns (uint256); + function mintExternalShares(address _recipient, uint256 _amountOfShares) external; + function burnExternalShares(uint256 _amountOfShares) external; + function setMaxExternalRatioBP(uint256 _maxExternalRatioBP) external; + function initialize(address _lidoLocator, address _eip712StETH) external payable; + function resumeStaking() external; + function resume() external; + function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) external; +} + +interface IKernel { + function acl() external view returns (IACL); + function newAppInstance(bytes32 _appId, address _appBase, bytes calldata _initializePayload, bool _setDefault) external; +} + +interface IACL { + function initialize(address _permissionsCreator) external; + function createPermission(address _entity, address _app, bytes32 _role, address _manager) external; + function hasPermission(address _who, address _where, bytes32 _what) external view returns (bool); +} + +interface IDaoFactory { + function newDAO(address _root) external returns (IKernel); +} + +contract Protocol__Deployment is Test { + ILido public lidoContract; + + address private rootAccount; + address private userAccount; + + address public kernelBase; + address public aclBase; + address public evmScriptRegistryFactory; + address public daoFactoryAdr; + + address public accounting = makeAddr("dummy-locator:accounting"); + + function prepareLidoContract(uint256 _startBalance, address _rootAccount, address _userAccount) public { + rootAccount = _rootAccount; + userAccount = _userAccount; + + vm.startPrank(rootAccount); + kernelBase = deployCode("Kernel.sol:Kernel", abi.encode(true)); + aclBase = deployCode("ACL.sol:ACL"); + evmScriptRegistryFactory = deployCode("EVMScriptRegistryFactory.sol:EVMScriptRegistryFactory"); + daoFactoryAdr = deployCode("DAOFactory.sol:DAOFactory", + abi.encode(kernelBase, aclBase ,evmScriptRegistryFactory) + ); + vm.stopPrank(); + + IDaoFactory daoFactory = IDaoFactory(daoFactoryAdr); + + vm.recordLogs(); + daoFactory.newDAO(rootAccount); + Vm.Log[] memory logs = vm.getRecordedLogs(); + (address daoAddress) = abi.decode(logs[logs.length - 1].data, (address)); + + vm.startPrank(rootAccount); + IKernel dao = IKernel(address(daoAddress)); + IACL acl = IACL(address(dao.acl())); + acl.createPermission(rootAccount, daoAddress, keccak256("APP_MANAGER_ROLE"), rootAccount); + vm.stopPrank(); + + vm.startPrank(rootAccount); + address impl = deployCode("Lido.sol:Lido"); + vm.recordLogs(); + dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), impl, "", false); + logs = vm.getRecordedLogs(); + vm.stopPrank(); + + (address lidoProxyAddress) = abi.decode(logs[logs.length - 1].data, (address)); + + vm.startPrank(rootAccount); + lidoContract = ILido(lidoProxyAddress); + vm.stopPrank(); + + vm.startPrank(rootAccount); + acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_CONTROL_ROLE"), rootAccount); + acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_PAUSE_ROLE"), rootAccount); + acl.createPermission(userAccount, address(lidoContract), keccak256("RESUME_ROLE"), rootAccount); + acl.createPermission(userAccount, address(lidoContract), keccak256("PAUSE_ROLE"), rootAccount); + assertTrue(acl.hasPermission(userAccount, address(lidoContract), keccak256("STAKING_CONTROL_ROLE"))); + vm.stopPrank(); + + vm.startPrank(rootAccount); + LidoLocator locator = new LidoLocator( LidoLocator.Config({ + accountingOracle: makeAddr("dummy-locator:accountingOracle"), + depositSecurityModule: makeAddr("dummy-locator:burner"), + elRewardsVault: makeAddr("dummy-locator:depositSecurityModule"), + legacyOracle: makeAddr("dummy-locator:elRewardsVault"), + lido: address(lidoContract), + oracleReportSanityChecker: makeAddr("dummy-locator:lido"), + postTokenRebaseReceiver: makeAddr("dummy-locator:oracleDaemonConfig"), + burner: makeAddr("dummy-locator:oracleReportSanityChecker"), + stakingRouter: makeAddr("dummy-locator:postTokenRebaseReceiver"), + treasury: makeAddr("dummy-locator:stakingRouter"), + validatorsExitBusOracle: makeAddr("dummy-locator:treasury"), + withdrawalQueue: makeAddr("dummy-locator:validatorsExitBusOracle"), + withdrawalVault: makeAddr("dummy-locator:withdrawalQueue"), + oracleDaemonConfig: makeAddr("dummy-locator:withdrawalVault"), + accounting: accounting, + wstETH: makeAddr("dummy-locator:wstETH") + })); + + EIP712StETH eip712steth = new EIP712StETH(address(lidoContract)); + + vm.deal(address(lidoContract), _startBalance); + lidoContract.initialize(address(locator), address(eip712steth)); + vm.stopPrank(); + } +} diff --git a/test/0.8.25/vaults/contracts/ShareRate.t.sol b/test/0.8.25/vaults/contracts/ShareRate.t.sol new file mode 100644 index 000000000..0a90c7380 --- /dev/null +++ b/test/0.8.25/vaults/contracts/ShareRate.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../../../contracts/0.8.9/EIP712StETH.sol"; +import "forge-std/Test.sol"; + +import {CommonBase} from "forge-std/Base.sol"; +import {LidoLocator} from "../../../../contracts/0.8.9/LidoLocator.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console2} from "forge-std/console2.sol"; +import {Protocol__Deployment, ILido} from "./Protocol__Deployment.t.sol"; + +contract ShareRateHandler is CommonBase, StdCheats, StdUtils { + ILido public lidoContract; + address public accounting; + address public userAccount; + + uint256 public maxAmountOfShares; + + constructor(ILido _lido, address _accounting, address _userAccount, uint256 _maxAmountOfShares) { + lidoContract = _lido; + accounting = _accounting; + userAccount = _userAccount; + maxAmountOfShares = _maxAmountOfShares; + } + + function mintExternalShares(address _recipient, uint256 _amountOfShares) external { + _amountOfShares = bound(_amountOfShares, 1, maxAmountOfShares); + + vm.startPrank(userAccount); + lidoContract.resumeStaking(); + vm.stopPrank(); + + vm.startPrank(accounting); + lidoContract.mintExternalShares(_recipient, _amountOfShares); + vm.stopPrank(); + } + + function burnExternalShares(uint256 _amountOfShares) external { + _amountOfShares = bound(_amountOfShares, 1, maxAmountOfShares); + vm.startPrank(userAccount); + lidoContract.resumeStaking(); + vm.stopPrank(); + + vm.startPrank(accounting); + lidoContract.burnExternalShares(_amountOfShares); + vm.stopPrank(); + } + + function getTotalShares() external view returns (uint256) { + return lidoContract.getTotalShares(); + } +} + +contract ShareRate is Protocol__Deployment { + ShareRateHandler public shareRateHandler; + + uint256 private _maxExternalRatioBP = 10_000; + uint256 private _maxStakeLimit = 15_000 ether; + uint256 private _stakeLimitIncreasePerBlock = 20 ether; + uint256 private _maxAmountOfShares = 100; + uint256 private protocolStartBalance = 15_000 ether; + + address private rootAccount = address(0x123); + address private userAccount = address(0x321); + + function setUp() public { + Protocol__Deployment.prepareLidoContract( + protocolStartBalance, + rootAccount, + userAccount + ); + + vm.startPrank(userAccount); + lidoContract.setMaxExternalRatioBP(_maxExternalRatioBP); + lidoContract.setStakingLimit(_maxStakeLimit, _stakeLimitIncreasePerBlock); + lidoContract.resume(); + vm.stopPrank(); + + shareRateHandler = new ShareRateHandler(lidoContract, accounting, userAccount, _maxAmountOfShares); + targetContract(address(shareRateHandler)); + + bytes4[] memory selectors = new bytes4[](2); + selectors[0] = shareRateHandler.mintExternalShares.selector; + selectors[1] = shareRateHandler.burnExternalShares.selector; + + targetSelector( + FuzzSelector({addr: address(shareRateHandler), selectors: selectors}) + ); + } + + /** + * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs + * forge-config: default.invariant.runs = 32 + * forge-config: default.invariant.depth = 16 + * forge-config: default.invariant.fail-on-revert = true + */ + function invariant_totalShares() public { + assertEq(lidoContract.getTotalShares(), shareRateHandler.getTotalShares()); + } +} From 0b6fc5c2f389a05931b16a022eb2c09b348f11be Mon Sep 17 00:00:00 2001 From: Sergey White Date: Mon, 13 Jan 2025 18:41:05 +0300 Subject: [PATCH 02/22] feat: test shareRate with fuzzing --- .../contracts/Protocol__Deployment.t.sol | 135 ++++++++++-------- test/0.8.25/vaults/contracts/ShareRate.t.sol | 9 +- 2 files changed, 82 insertions(+), 62 deletions(-) diff --git a/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol b/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol index 6c5272922..d6cc55685 100644 --- a/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol +++ b/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol @@ -1,11 +1,13 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only pragma solidity ^0.8.0; -import "../../../../contracts/0.8.9/EIP712StETH.sol"; +import "contracts/0.8.9/EIP712StETH.sol"; import "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; -import {LidoLocator} from "../../../../contracts/0.8.9/LidoLocator.sol"; +import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; @@ -13,23 +15,33 @@ import {console2} from "forge-std/console2.sol"; interface ILido { function getTotalShares() external view returns (uint256); + function mintExternalShares(address _recipient, uint256 _amountOfShares) external; + function burnExternalShares(uint256 _amountOfShares) external; + function setMaxExternalRatioBP(uint256 _maxExternalRatioBP) external; + function initialize(address _lidoLocator, address _eip712StETH) external payable; + function resumeStaking() external; + function resume() external; + function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) external; } interface IKernel { function acl() external view returns (IACL); + function newAppInstance(bytes32 _appId, address _appBase, bytes calldata _initializePayload, bool _setDefault) external; } interface IACL { function initialize(address _permissionsCreator) external; + function createPermission(address _entity, address _app, bytes32 _role, address _manager) external; + function hasPermission(address _who, address _where, bytes32 _what) external view returns (bool); } @@ -39,6 +51,8 @@ interface IDaoFactory { contract Protocol__Deployment is Test { ILido public lidoContract; + ILidoLocator public lidoLocator; + IACL private acl; address private rootAccount; address private userAccount; @@ -48,20 +62,40 @@ contract Protocol__Deployment is Test { address public evmScriptRegistryFactory; address public daoFactoryAdr; - address public accounting = makeAddr("dummy-locator:accounting"); - function prepareLidoContract(uint256 _startBalance, address _rootAccount, address _userAccount) public { rootAccount = _rootAccount; userAccount = _userAccount; vm.startPrank(rootAccount); - kernelBase = deployCode("Kernel.sol:Kernel", abi.encode(true)); - aclBase = deployCode("ACL.sol:ACL"); - evmScriptRegistryFactory = deployCode("EVMScriptRegistryFactory.sol:EVMScriptRegistryFactory"); - daoFactoryAdr = deployCode("DAOFactory.sol:DAOFactory", - abi.encode(kernelBase, aclBase ,evmScriptRegistryFactory) - ); + + (IKernel dao, IACL acl) = createAragonDao(); + + address impl = deployCode("Lido.sol:Lido"); + + address lidoProxyAddress = addAragonApp(dao, impl); + + lidoContract = ILido(lidoProxyAddress); + + acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_CONTROL_ROLE"), rootAccount); + acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_PAUSE_ROLE"), rootAccount); + acl.createPermission(userAccount, address(lidoContract), keccak256("RESUME_ROLE"), rootAccount); + acl.createPermission(userAccount, address(lidoContract), keccak256("PAUSE_ROLE"), rootAccount); + + lidoLocator = deployLidoLocator(address(lidoContract)); + EIP712StETH eip712steth = new EIP712StETH(address(lidoContract)); + + vm.deal(address(lidoContract), _startBalance); + lidoContract.initialize(address(lidoLocator), address(eip712steth)); vm.stopPrank(); + } + + function createAragonDao() private returns (IKernel, IACL) { + kernelBase = deployCode("Kernel.sol:Kernel", abi.encode(true)); + aclBase = deployCode("ACL.sol:ACL"); + evmScriptRegistryFactory = deployCode("EVMScriptRegistryFactory.sol:EVMScriptRegistryFactory"); + daoFactoryAdr = deployCode("DAOFactory.sol:DAOFactory", + abi.encode(kernelBase, aclBase, evmScriptRegistryFactory) + ); IDaoFactory daoFactory = IDaoFactory(daoFactoryAdr); @@ -70,57 +104,42 @@ contract Protocol__Deployment is Test { Vm.Log[] memory logs = vm.getRecordedLogs(); (address daoAddress) = abi.decode(logs[logs.length - 1].data, (address)); - vm.startPrank(rootAccount); - IKernel dao = IKernel(address(daoAddress)); - IACL acl = IACL(address(dao.acl())); - acl.createPermission(rootAccount, daoAddress, keccak256("APP_MANAGER_ROLE"), rootAccount); - vm.stopPrank(); + IKernel dao = IKernel(address(daoAddress)); + acl = IACL(address(dao.acl())); - vm.startPrank(rootAccount); - address impl = deployCode("Lido.sol:Lido"); - vm.recordLogs(); - dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), impl, "", false); - logs = vm.getRecordedLogs(); - vm.stopPrank(); + acl.createPermission(rootAccount, daoAddress, keccak256("APP_MANAGER_ROLE"), rootAccount); - (address lidoProxyAddress) = abi.decode(logs[logs.length - 1].data, (address)); + return (dao, acl); + } - vm.startPrank(rootAccount); - lidoContract = ILido(lidoProxyAddress); - vm.stopPrank(); + function addAragonApp(IKernel dao, address lidoImpl) private returns (address) { + vm.recordLogs(); + dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), lidoImpl, "", false); + Vm.Log[] memory logs = vm.getRecordedLogs(); - vm.startPrank(rootAccount); - acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_CONTROL_ROLE"), rootAccount); - acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_PAUSE_ROLE"), rootAccount); - acl.createPermission(userAccount, address(lidoContract), keccak256("RESUME_ROLE"), rootAccount); - acl.createPermission(userAccount, address(lidoContract), keccak256("PAUSE_ROLE"), rootAccount); - assertTrue(acl.hasPermission(userAccount, address(lidoContract), keccak256("STAKING_CONTROL_ROLE"))); - vm.stopPrank(); + (address lidoProxyAddress) = abi.decode(logs[logs.length - 1].data, (address)); - vm.startPrank(rootAccount); - LidoLocator locator = new LidoLocator( LidoLocator.Config({ - accountingOracle: makeAddr("dummy-locator:accountingOracle"), - depositSecurityModule: makeAddr("dummy-locator:burner"), - elRewardsVault: makeAddr("dummy-locator:depositSecurityModule"), - legacyOracle: makeAddr("dummy-locator:elRewardsVault"), - lido: address(lidoContract), - oracleReportSanityChecker: makeAddr("dummy-locator:lido"), - postTokenRebaseReceiver: makeAddr("dummy-locator:oracleDaemonConfig"), - burner: makeAddr("dummy-locator:oracleReportSanityChecker"), - stakingRouter: makeAddr("dummy-locator:postTokenRebaseReceiver"), - treasury: makeAddr("dummy-locator:stakingRouter"), - validatorsExitBusOracle: makeAddr("dummy-locator:treasury"), - withdrawalQueue: makeAddr("dummy-locator:validatorsExitBusOracle"), - withdrawalVault: makeAddr("dummy-locator:withdrawalQueue"), - oracleDaemonConfig: makeAddr("dummy-locator:withdrawalVault"), - accounting: accounting, - wstETH: makeAddr("dummy-locator:wstETH") - })); - - EIP712StETH eip712steth = new EIP712StETH(address(lidoContract)); - - vm.deal(address(lidoContract), _startBalance); - lidoContract.initialize(address(locator), address(eip712steth)); - vm.stopPrank(); + return lidoProxyAddress; + } + + function deployLidoLocator(address lido) private returns (ILidoLocator) { + return new LidoLocator(LidoLocator.Config({ + accountingOracle: makeAddr("dummy-locator:accountingOracle"), + depositSecurityModule: makeAddr("dummy-locator:burner"), + elRewardsVault: makeAddr("dummy-locator:depositSecurityModule"), + legacyOracle: makeAddr("dummy-locator:elRewardsVault"), + lido: lido, + oracleReportSanityChecker: makeAddr("dummy-locator:lido"), + postTokenRebaseReceiver: makeAddr("dummy-locator:oracleDaemonConfig"), + burner: makeAddr("dummy-locator:oracleReportSanityChecker"), + stakingRouter: makeAddr("dummy-locator:postTokenRebaseReceiver"), + treasury: makeAddr("dummy-locator:stakingRouter"), + validatorsExitBusOracle: makeAddr("dummy-locator:treasury"), + withdrawalQueue: makeAddr("dummy-locator:validatorsExitBusOracle"), + withdrawalVault: makeAddr("dummy-locator:withdrawalQueue"), + oracleDaemonConfig: makeAddr("dummy-locator:withdrawalVault"), + accounting: makeAddr("dummy-locator:accounting"), + wstETH: makeAddr("dummy-locator:wstETH") + })); } } diff --git a/test/0.8.25/vaults/contracts/ShareRate.t.sol b/test/0.8.25/vaults/contracts/ShareRate.t.sol index 0a90c7380..621b27cfe 100644 --- a/test/0.8.25/vaults/contracts/ShareRate.t.sol +++ b/test/0.8.25/vaults/contracts/ShareRate.t.sol @@ -1,11 +1,12 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only pragma solidity ^0.8.0; -import "../../../../contracts/0.8.9/EIP712StETH.sol"; +import "contracts/0.8.9/EIP712StETH.sol"; import "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; -import {LidoLocator} from "../../../../contracts/0.8.9/LidoLocator.sol"; +import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; @@ -79,7 +80,7 @@ contract ShareRate is Protocol__Deployment { lidoContract.resume(); vm.stopPrank(); - shareRateHandler = new ShareRateHandler(lidoContract, accounting, userAccount, _maxAmountOfShares); + shareRateHandler = new ShareRateHandler(lidoContract, lidoLocator.accounting(), userAccount, _maxAmountOfShares); targetContract(address(shareRateHandler)); bytes4[] memory selectors = new bytes4[](2); From c289fb8b3e9dff2df85c1c1b644a0ebe18604cc4 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 13 Jan 2025 16:49:47 +0000 Subject: [PATCH 03/22] chore: some refactoring --- package.json | 2 +- .../contracts => }/Protocol__Deployment.t.sol | 67 +++++++++++-------- .../{vaults/contracts => }/ShareRate.t.sol | 56 +++++++++------- 3 files changed, 74 insertions(+), 51 deletions(-) rename test/0.8.25/{vaults/contracts => }/Protocol__Deployment.t.sol (66%) rename test/0.8.25/{vaults/contracts => }/ShareRate.t.sol (63%) diff --git a/package.json b/package.json index 5e0ad6b15..a34b4fafa 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "prepare": "husky", "abis:extract": "hardhat abis:extract", "verify:deployed": "hardhat verify:deployed", - "test:custom": "forge test -vvvv --match-path \"test/0.8.25/vaults/contracts/ShareRate.t.sol\"" + "test:custom": "forge test --match-path \"test/0.8.25/ShareRate.t.sol\"" }, "lint-staged": { "./**/*.ts": [ diff --git a/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol similarity index 66% rename from test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol rename to test/0.8.25/Protocol__Deployment.t.sol index d6cc55685..1ffa29fdd 100644 --- a/test/0.8.25/vaults/contracts/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -1,21 +1,25 @@ // SPDX-License-Identifier: UNLICENSED // for testing purposes only + pragma solidity ^0.8.0; -import "contracts/0.8.9/EIP712StETH.sol"; import "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; -import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; -import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; +import {EIP712StETH} from "contracts/0.8.9/EIP712StETH.sol"; +import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; + interface ILido { function getTotalShares() external view returns (uint256); + function getExternalShares() external view returns (uint256); + function mintExternalShares(address _recipient, uint256 _amountOfShares) external; function burnExternalShares(uint256 _amountOfShares) external; @@ -34,7 +38,12 @@ interface ILido { interface IKernel { function acl() external view returns (IACL); - function newAppInstance(bytes32 _appId, address _appBase, bytes calldata _initializePayload, bool _setDefault) external; + function newAppInstance( + bytes32 _appId, + address _appBase, + bytes calldata _initializePayload, + bool _setDefault + ) external; } interface IACL { @@ -49,7 +58,7 @@ interface IDaoFactory { function newDAO(address _root) external returns (IKernel); } -contract Protocol__Deployment is Test { +contract BaseProtocolTest is Test { ILido public lidoContract; ILidoLocator public lidoLocator; IACL private acl; @@ -62,7 +71,7 @@ contract Protocol__Deployment is Test { address public evmScriptRegistryFactory; address public daoFactoryAdr; - function prepareLidoContract(uint256 _startBalance, address _rootAccount, address _userAccount) public { + function setUpProtocol(uint256 _startBalance, address _rootAccount, address _userAccount) public { rootAccount = _rootAccount; userAccount = _userAccount; @@ -93,7 +102,8 @@ contract Protocol__Deployment is Test { kernelBase = deployCode("Kernel.sol:Kernel", abi.encode(true)); aclBase = deployCode("ACL.sol:ACL"); evmScriptRegistryFactory = deployCode("EVMScriptRegistryFactory.sol:EVMScriptRegistryFactory"); - daoFactoryAdr = deployCode("DAOFactory.sol:DAOFactory", + daoFactoryAdr = deployCode( + "DAOFactory.sol:DAOFactory", abi.encode(kernelBase, aclBase, evmScriptRegistryFactory) ); @@ -102,7 +112,7 @@ contract Protocol__Deployment is Test { vm.recordLogs(); daoFactory.newDAO(rootAccount); Vm.Log[] memory logs = vm.getRecordedLogs(); - (address daoAddress) = abi.decode(logs[logs.length - 1].data, (address)); + address daoAddress = abi.decode(logs[logs.length - 1].data, (address)); IKernel dao = IKernel(address(daoAddress)); acl = IACL(address(dao.acl())); @@ -117,29 +127,32 @@ contract Protocol__Deployment is Test { dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), lidoImpl, "", false); Vm.Log[] memory logs = vm.getRecordedLogs(); - (address lidoProxyAddress) = abi.decode(logs[logs.length - 1].data, (address)); + address lidoProxyAddress = abi.decode(logs[logs.length - 1].data, (address)); return lidoProxyAddress; } function deployLidoLocator(address lido) private returns (ILidoLocator) { - return new LidoLocator(LidoLocator.Config({ - accountingOracle: makeAddr("dummy-locator:accountingOracle"), - depositSecurityModule: makeAddr("dummy-locator:burner"), - elRewardsVault: makeAddr("dummy-locator:depositSecurityModule"), - legacyOracle: makeAddr("dummy-locator:elRewardsVault"), - lido: lido, - oracleReportSanityChecker: makeAddr("dummy-locator:lido"), - postTokenRebaseReceiver: makeAddr("dummy-locator:oracleDaemonConfig"), - burner: makeAddr("dummy-locator:oracleReportSanityChecker"), - stakingRouter: makeAddr("dummy-locator:postTokenRebaseReceiver"), - treasury: makeAddr("dummy-locator:stakingRouter"), - validatorsExitBusOracle: makeAddr("dummy-locator:treasury"), - withdrawalQueue: makeAddr("dummy-locator:validatorsExitBusOracle"), - withdrawalVault: makeAddr("dummy-locator:withdrawalQueue"), - oracleDaemonConfig: makeAddr("dummy-locator:withdrawalVault"), - accounting: makeAddr("dummy-locator:accounting"), - wstETH: makeAddr("dummy-locator:wstETH") - })); + return + new LidoLocator( + LidoLocator.Config({ + accountingOracle: makeAddr("dummy-locator:accountingOracle"), + depositSecurityModule: makeAddr("dummy-locator:burner"), + elRewardsVault: makeAddr("dummy-locator:depositSecurityModule"), + legacyOracle: makeAddr("dummy-locator:elRewardsVault"), + lido: lido, + oracleReportSanityChecker: makeAddr("dummy-locator:lido"), + postTokenRebaseReceiver: makeAddr("dummy-locator:oracleDaemonConfig"), + burner: makeAddr("dummy-locator:oracleReportSanityChecker"), + stakingRouter: makeAddr("dummy-locator:postTokenRebaseReceiver"), + treasury: makeAddr("dummy-locator:stakingRouter"), + validatorsExitBusOracle: makeAddr("dummy-locator:treasury"), + withdrawalQueue: makeAddr("dummy-locator:validatorsExitBusOracle"), + withdrawalVault: makeAddr("dummy-locator:withdrawalQueue"), + oracleDaemonConfig: makeAddr("dummy-locator:withdrawalVault"), + accounting: makeAddr("dummy-locator:accounting"), + wstETH: makeAddr("dummy-locator:wstETH") + }) + ); } } diff --git a/test/0.8.25/vaults/contracts/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol similarity index 63% rename from test/0.8.25/vaults/contracts/ShareRate.t.sol rename to test/0.8.25/ShareRate.t.sol index 621b27cfe..8d6612345 100644 --- a/test/0.8.25/vaults/contracts/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "contracts/0.8.9/EIP712StETH.sol"; -import "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; @@ -11,7 +10,8 @@ import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; -import {Protocol__Deployment, ILido} from "./Protocol__Deployment.t.sol"; + +import {BaseProtocolTest, ILido} from "./Protocol__Deployment.t.sol"; contract ShareRateHandler is CommonBase, StdCheats, StdUtils { ILido public lidoContract; @@ -28,26 +28,33 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { } function mintExternalShares(address _recipient, uint256 _amountOfShares) external { + // we don't want to test the zero address case, as it would revert + vm.assume(_recipient != address(0)); + _amountOfShares = bound(_amountOfShares, 1, maxAmountOfShares); + // TODO: We need to make this condition work + // _amountOfShares = bound(_amountOfShares, 1, _amountOfShares); - vm.startPrank(userAccount); + vm.prank(userAccount); lidoContract.resumeStaking(); - vm.stopPrank(); - vm.startPrank(accounting); + vm.prank(accounting); lidoContract.mintExternalShares(_recipient, _amountOfShares); - vm.stopPrank(); } function burnExternalShares(uint256 _amountOfShares) external { - _amountOfShares = bound(_amountOfShares, 1, maxAmountOfShares); - vm.startPrank(userAccount); + uint256 totalShares = lidoContract.getExternalShares(); + if (totalShares != 0) { + _amountOfShares = bound(_amountOfShares, 2, maxAmountOfShares); + } else { + _amountOfShares = 1; + } + + vm.prank(userAccount); lidoContract.resumeStaking(); - vm.stopPrank(); - vm.startPrank(accounting); + vm.prank(accounting); lidoContract.burnExternalShares(_amountOfShares); - vm.stopPrank(); } function getTotalShares() external view returns (uint256) { @@ -55,24 +62,24 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { } } -contract ShareRate is Protocol__Deployment { +contract ShareRateTest is BaseProtocolTest { ShareRateHandler public shareRateHandler; uint256 private _maxExternalRatioBP = 10_000; uint256 private _maxStakeLimit = 15_000 ether; uint256 private _stakeLimitIncreasePerBlock = 20 ether; uint256 private _maxAmountOfShares = 100; + uint256 private protocolStartBalance = 15_000 ether; + uint256 private protocolStartExternalShares = 10_000; address private rootAccount = address(0x123); address private userAccount = address(0x321); function setUp() public { - Protocol__Deployment.prepareLidoContract( - protocolStartBalance, - rootAccount, - userAccount - ); + BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); + + address accountingContract = lidoLocator.accounting(); vm.startPrank(userAccount); lidoContract.setMaxExternalRatioBP(_maxExternalRatioBP); @@ -80,22 +87,25 @@ contract ShareRate is Protocol__Deployment { lidoContract.resume(); vm.stopPrank(); - shareRateHandler = new ShareRateHandler(lidoContract, lidoLocator.accounting(), userAccount, _maxAmountOfShares); + shareRateHandler = new ShareRateHandler(lidoContract, accountingContract, userAccount, _maxAmountOfShares); targetContract(address(shareRateHandler)); bytes4[] memory selectors = new bytes4[](2); selectors[0] = shareRateHandler.mintExternalShares.selector; selectors[1] = shareRateHandler.burnExternalShares.selector; - targetSelector( - FuzzSelector({addr: address(shareRateHandler), selectors: selectors}) - ); + targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: selectors})); + + // @dev mint 10000 external shares to simulate some shares already minted, so + // burnExternalShares will be able to actually burn some shares + vm.prank(accountingContract); + lidoContract.mintExternalShares(accountingContract, protocolStartExternalShares); } /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 32 - * forge-config: default.invariant.depth = 16 + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true */ function invariant_totalShares() public { From d79df96534da5cfff41d28cb4782326db47d458b Mon Sep 17 00:00:00 2001 From: Sergey White Date: Tue, 14 Jan 2025 11:37:07 +0300 Subject: [PATCH 04/22] feat: test shareRate with fuzzing --- test/0.8.25/Protocol__Deployment.t.sol | 18 +++++++++--------- test/0.8.25/ShareRate.t.sol | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 1ffa29fdd..4ed3f4cec 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -61,7 +61,8 @@ interface IDaoFactory { contract BaseProtocolTest is Test { ILido public lidoContract; ILidoLocator public lidoLocator; - IACL private acl; + IACL public acl; + IKernel private dao; address private rootAccount; address private userAccount; @@ -76,8 +77,7 @@ contract BaseProtocolTest is Test { userAccount = _userAccount; vm.startPrank(rootAccount); - - (IKernel dao, IACL acl) = createAragonDao(); + (dao, acl) = createAragonDao(); address impl = deployCode("Lido.sol:Lido"); @@ -114,17 +114,17 @@ contract BaseProtocolTest is Test { Vm.Log[] memory logs = vm.getRecordedLogs(); address daoAddress = abi.decode(logs[logs.length - 1].data, (address)); - IKernel dao = IKernel(address(daoAddress)); - acl = IACL(address(dao.acl())); + IKernel _dao = IKernel(address(daoAddress)); + IACL _acl = IACL(address(_dao.acl())); - acl.createPermission(rootAccount, daoAddress, keccak256("APP_MANAGER_ROLE"), rootAccount); + _acl.createPermission(rootAccount, daoAddress, keccak256("APP_MANAGER_ROLE"), rootAccount); - return (dao, acl); + return (_dao, _acl); } - function addAragonApp(IKernel dao, address lidoImpl) private returns (address) { + function addAragonApp(IKernel _dao, address lidoImpl) private returns (address) { vm.recordLogs(); - dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), lidoImpl, "", false); + _dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), lidoImpl, "", false); Vm.Log[] memory logs = vm.getRecordedLogs(); address lidoProxyAddress = abi.decode(logs[logs.length - 1].data, (address)); diff --git a/test/0.8.25/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol index 8d6612345..052d1da53 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -108,7 +108,7 @@ contract ShareRateTest is BaseProtocolTest { * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true */ - function invariant_totalShares() public { + function invariant_totalShares() public view { assertEq(lidoContract.getTotalShares(), shareRateHandler.getTotalShares()); } } From 167c38701a7f6a5bb2b96bbbf12716ac9656ef05 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Thu, 16 Jan 2025 15:26:40 +0300 Subject: [PATCH 05/22] feat: fuzz oracleReport --- package.json | 3 +- test/0.8.25/Accounting.t.sol | 126 +++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 test/0.8.25/Accounting.t.sol diff --git a/package.json b/package.json index a34b4fafa..87cf88be4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "prepare": "husky", "abis:extract": "hardhat abis:extract", "verify:deployed": "hardhat verify:deployed", - "test:custom": "forge test --match-path \"test/0.8.25/ShareRate.t.sol\"" + "test:fuzzShateRate": "forge test --match-path \"test/0.8.25/ShareRate.t.sol\"", + "test:fuzzOracleReport": "forge test --match-path \"test/0.8.25/Accounting.t.sol\"" }, "lint-staged": { "./**/*.ts": [ diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol new file mode 100644 index 000000000..0d5835d03 --- /dev/null +++ b/test/0.8.25/Accounting.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only +pragma solidity ^0.8.0; + +import "../../contracts/common/interfaces/ReportValues.sol"; +import {CommonBase} from "forge-std/Base.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; + +import {StdUtils} from "forge-std/StdUtils.sol"; +import {Test} from "../../foundry/lib/forge-std/src/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console2} from "forge-std/console2.sol"; + +contract AccountingMock { + function handleOracleReport(ReportValues memory _report) external { + /*timestamp = _timestamp; + timeElapsed = _timeElapsed; + clValidators = _clValidators; + clBalance = _clValidators * 32 ether; + + withdrawalVaultBalance = _withdrawalVaultBalance; + elRewardsVaultBalance = _elRewardsVaultBalance; + elRewardsVaultBalance = _elRewardsVaultBalance; + sharesRequestedToBurn = _sharesRequestedToBurn; + + withdrawalFinalizationBatches = _withdrawalFinalizationBatches; + vaultValues = _vaultValues; + netCashFlows = _netCashFlows;*/ + } + + function check() public pure returns (bool) { + return true; + } +} + +contract AccountingHandler is CommonBase, StdCheats, StdUtils { + AccountingMock private accounting; + ReportValues[] public reports; + + constructor(AccountingMock _accounting, ReportValues memory _refReport) { + accounting = _accounting; + reports.push(_refReport); + } + + function length() public view returns (uint256) { + return reports.length; + } + + function handleOracleReport( + uint256 _clValidators, + uint256 _withdrawalVaultBalance, + uint256 _elRewardsVaultBalance, + // TODO When adding lido.accounting contract - to use this limitation + // sharesRequestedToBurn - [0, lido.getTotalShares()] + uint256 _sharesRequestedToBurn + ) external { + ReportValues memory lastReport = reports[reports.length - 1]; + + uint256 _timeElapsed = 86_400; + uint256 _timestamp = lastReport.timestamp + _timeElapsed; + + _clValidators = bound(_clValidators, lastReport.clValidators, type(uint32).max); + _withdrawalVaultBalance = bound(_withdrawalVaultBalance, 0, type(uint32).max); + _elRewardsVaultBalance = bound(_elRewardsVaultBalance, 0, type(uint32).max); + // _clValidators = Math.floor(_clValidators); + uint256 clBalance = _clValidators * 32 ether; + + ReportValues memory currentReport = ReportValues({ + timestamp: _timestamp, + timeElapsed: _timeElapsed, + clValidators: _clValidators, + clBalance: clBalance, + withdrawalVaultBalance: _withdrawalVaultBalance, + elRewardsVaultBalance: _elRewardsVaultBalance, + sharesRequestedToBurn: _sharesRequestedToBurn, + withdrawalFinalizationBatches: new uint256[](0), + vaultValues: new uint256[](0), + netCashFlows: new int256[](0) + }); + + accounting.handleOracleReport(currentReport); + + reports.push(currentReport); + } +} + +contract AccountingTest is Test { + AccountingMock private accounting; + AccountingHandler private accountingHlr; + + function setUp() public { + ReportValues memory refReport = ReportValues({ + timestamp: 1705312150, + timeElapsed: 0, + clValidators: 0, + clBalance: 0, + withdrawalVaultBalance: 0, + elRewardsVaultBalance: 0, + sharesRequestedToBurn: 0, + withdrawalFinalizationBatches: new uint256[](0), + vaultValues: new uint256[](0), + netCashFlows: new int256[](0) + }); + + accounting = new AccountingMock(); + accountingHlr = new AccountingHandler(accounting, refReport); + + targetContract(address(accountingHlr)); + + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = accountingHlr.handleOracleReport.selector; + + targetSelector(FuzzSelector({addr: address(accountingHlr), selectors: selectors})); + } + + /** + * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 + * forge-config: default.invariant.fail-on-revert = true + */ + function invariant_fuzzTotalShares() public { + assertEq(accounting.check(), true); + console2.log("Reports count:", accountingHlr.length()); + } +} From 2386b48c04541d1b7216685440a07d7d80d1cec4 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Thu, 16 Jan 2025 16:03:49 +0000 Subject: [PATCH 06/22] chore: add oracle report related contracts --- test/0.8.25/Accounting.t.sol | 110 ++++++++++++++++--------- test/0.8.25/Protocol__Deployment.t.sol | 100 +++++++++++++--------- 2 files changed, 135 insertions(+), 75 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 0d5835d03..d74a1bf10 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -2,44 +2,33 @@ // for testing purposes only pragma solidity ^0.8.0; -import "../../contracts/common/interfaces/ReportValues.sol"; import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; - import {StdUtils} from "forge-std/StdUtils.sol"; -import {Test} from "../../foundry/lib/forge-std/src/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; -contract AccountingMock { - function handleOracleReport(ReportValues memory _report) external { - /*timestamp = _timestamp; - timeElapsed = _timeElapsed; - clValidators = _clValidators; - clBalance = _clValidators * 32 ether; - - withdrawalVaultBalance = _withdrawalVaultBalance; - elRewardsVaultBalance = _elRewardsVaultBalance; - elRewardsVaultBalance = _elRewardsVaultBalance; - sharesRequestedToBurn = _sharesRequestedToBurn; - - withdrawalFinalizationBatches = _withdrawalFinalizationBatches; - vaultValues = _vaultValues; - netCashFlows = _netCashFlows;*/ - } +import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; - function check() public pure returns (bool) { - return true; - } +import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; + +interface IAccounting { + function initialize(address _admin) external; + + function handleOracleReport(ReportValues memory _report) external; + + function simulateOracleReport(ReportValues memory _report, uint256 _withdrawalShareRate) external; } contract AccountingHandler is CommonBase, StdCheats, StdUtils { - AccountingMock private accounting; + IAccounting private accounting; ReportValues[] public reports; + address private accountingOracle; - constructor(AccountingMock _accounting, ReportValues memory _refReport) { - accounting = _accounting; + constructor(address _accounting, address _accountingOracle, ReportValues memory _refReport) { + accounting = IAccounting(_accounting); reports.push(_refReport); + accountingOracle = _accountingOracle; } function length() public view returns (uint256) { @@ -78,17 +67,25 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { netCashFlows: new int256[](0) }); + vm.prank(accountingOracle); accounting.handleOracleReport(currentReport); reports.push(currentReport); } } -contract AccountingTest is Test { - AccountingMock private accounting; - AccountingHandler private accountingHlr; +contract AccountingTest is BaseProtocolTest { + AccountingHandler private accountingHandler; + uint256 private protocolStartBalance = 15_000 ether; + + address private rootAccount = address(0x123); + address private userAccount = address(0x321); + + address private depositContract = address(0x4242424242424242424242424242424242424242); function setUp() public { + BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); + ReportValues memory refReport = ReportValues({ timestamp: 1705312150, timeElapsed: 0, @@ -102,15 +99,55 @@ contract AccountingTest is Test { netCashFlows: new int256[](0) }); - accounting = new AccountingMock(); - accountingHlr = new AccountingHandler(accounting, refReport); - - targetContract(address(accountingHlr)); - + // Add accounting contract with handler to the protocol + address accountingImpl = deployCode( + "Accounting.sol:Accounting", + abi.encode([address(lidoLocator), lidoLocator.lido()]) + ); + accountingHandler = new AccountingHandler(accountingImpl, lidoLocator.accountingOracle(), refReport); + + deployCodeTo( + "AccountingOracle.sol:AccountingOracle", + abi.encode( + address(lidoLocator), + lidoLocator.legacyOracle(), + 12, // secondsPerSlot + 1695902400 // genesisTime + ), + lidoLocator.accountingOracle() + ); + + deployCodeTo( + "OssifiableProxy.sol:OssifiableProxy", + abi.encode(accountingHandler, rootAccount, new bytes(0)), + lidoLocator.accounting() + ); + + // Add burner contract to the protocol + deployCodeTo( + "Burner.sol:Burner", + abi.encode(rootAccount, address(lidoLocator), lidoLocator.lido(), 0, 0), + lidoLocator.burner() + ); + + // Add staking router contract to the protocol + deployCodeTo("StakingRouter.sol:StakingRouter", abi.encode(depositContract), lidoLocator.stakingRouter()); + + // Add oracle report sanity checker contract to the protocol + deployCodeTo( + "OracleReportSanityChecker.sol:OracleReportSanityChecker", + abi.encode(address(lidoLocator), rootAccount, [1500, 1500, 1000, 2000, 8, 24, 128, 5000000, 1000, 101, 50]), + lidoLocator.oracleReportSanityChecker() + ); + + // Set target contract to the accounting handler + targetContract(address(accountingHandler)); + + // Set target selectors to the accounting handler bytes4[] memory selectors = new bytes4[](1); - selectors[0] = accountingHlr.handleOracleReport.selector; + selectors[0] = accountingHandler.handleOracleReport.selector; - targetSelector(FuzzSelector({addr: address(accountingHlr), selectors: selectors})); + targetSelector(FuzzSelector({addr: address(accountingHandler), selectors: selectors})); } /** @@ -120,7 +157,6 @@ contract AccountingTest is Test { * forge-config: default.invariant.fail-on-revert = true */ function invariant_fuzzTotalShares() public { - assertEq(accounting.check(), true); - console2.log("Reports count:", accountingHlr.length()); + assertEq(accountingHandler.length(), 1); // TODO: add real invariant, this is just a placeholder } } diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 4ed3f4cec..43c7f5ee5 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -11,8 +11,6 @@ import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; -import {EIP712StETH} from "contracts/0.8.9/EIP712StETH.sol"; -import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; interface ILido { @@ -58,6 +56,25 @@ interface IDaoFactory { function newDAO(address _root) external returns (IKernel); } +struct LidoLocatorConfig { + address accountingOracle; + address depositSecurityModule; + address elRewardsVault; + address legacyOracle; + address lido; + address oracleReportSanityChecker; + address postTokenRebaseReceiver; + address burner; + address stakingRouter; + address treasury; + address validatorsExitBusOracle; + address withdrawalQueue; + address withdrawalVault; + address oracleDaemonConfig; + address accounting; + address wstETH; +} + contract BaseProtocolTest is Test { ILido public lidoContract; ILidoLocator public lidoLocator; @@ -76,28 +93,34 @@ contract BaseProtocolTest is Test { rootAccount = _rootAccount; userAccount = _userAccount; - vm.startPrank(rootAccount); - (dao, acl) = createAragonDao(); - address impl = deployCode("Lido.sol:Lido"); + vm.startPrank(rootAccount); + (dao, acl) = createAragonDao(); address lidoProxyAddress = addAragonApp(dao, impl); lidoContract = ILido(lidoProxyAddress); - acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_CONTROL_ROLE"), rootAccount); - acl.createPermission(userAccount, address(lidoContract), keccak256("STAKING_PAUSE_ROLE"), rootAccount); - acl.createPermission(userAccount, address(lidoContract), keccak256("RESUME_ROLE"), rootAccount); - acl.createPermission(userAccount, address(lidoContract), keccak256("PAUSE_ROLE"), rootAccount); + /// @dev deal lido contract with start balance + vm.deal(lidoProxyAddress, _startBalance); + + acl.createPermission(userAccount, lidoProxyAddress, keccak256("STAKING_CONTROL_ROLE"), rootAccount); + acl.createPermission(userAccount, lidoProxyAddress, keccak256("STAKING_PAUSE_ROLE"), rootAccount); + acl.createPermission(userAccount, lidoProxyAddress, keccak256("RESUME_ROLE"), rootAccount); + acl.createPermission(userAccount, lidoProxyAddress, keccak256("PAUSE_ROLE"), rootAccount); - lidoLocator = deployLidoLocator(address(lidoContract)); - EIP712StETH eip712steth = new EIP712StETH(address(lidoContract)); + /// @dev deploy lido locator with dummy default values + lidoLocator = _deployLidoLocator(lidoProxyAddress); + + /// @dev deploy eip712steth + address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress)); - vm.deal(address(lidoContract), _startBalance); lidoContract.initialize(address(lidoLocator), address(eip712steth)); + vm.stopPrank(); } + /// @dev create aragon dao and return kernel and acl function createAragonDao() private returns (IKernel, IACL) { kernelBase = deployCode("Kernel.sol:Kernel", abi.encode(true)); aclBase = deployCode("ACL.sol:ACL"); @@ -122,37 +145,38 @@ contract BaseProtocolTest is Test { return (_dao, _acl); } - function addAragonApp(IKernel _dao, address lidoImpl) private returns (address) { + /// @dev add aragon app to dao and return proxy address + function addAragonApp(IKernel _dao, address _impl) private returns (address) { vm.recordLogs(); - _dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), lidoImpl, "", false); + _dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), _impl, "", false); Vm.Log[] memory logs = vm.getRecordedLogs(); - address lidoProxyAddress = abi.decode(logs[logs.length - 1].data, (address)); + address proxyAddress = abi.decode(logs[logs.length - 1].data, (address)); - return lidoProxyAddress; + return proxyAddress; } - function deployLidoLocator(address lido) private returns (ILidoLocator) { - return - new LidoLocator( - LidoLocator.Config({ - accountingOracle: makeAddr("dummy-locator:accountingOracle"), - depositSecurityModule: makeAddr("dummy-locator:burner"), - elRewardsVault: makeAddr("dummy-locator:depositSecurityModule"), - legacyOracle: makeAddr("dummy-locator:elRewardsVault"), - lido: lido, - oracleReportSanityChecker: makeAddr("dummy-locator:lido"), - postTokenRebaseReceiver: makeAddr("dummy-locator:oracleDaemonConfig"), - burner: makeAddr("dummy-locator:oracleReportSanityChecker"), - stakingRouter: makeAddr("dummy-locator:postTokenRebaseReceiver"), - treasury: makeAddr("dummy-locator:stakingRouter"), - validatorsExitBusOracle: makeAddr("dummy-locator:treasury"), - withdrawalQueue: makeAddr("dummy-locator:validatorsExitBusOracle"), - withdrawalVault: makeAddr("dummy-locator:withdrawalQueue"), - oracleDaemonConfig: makeAddr("dummy-locator:withdrawalVault"), - accounting: makeAddr("dummy-locator:accounting"), - wstETH: makeAddr("dummy-locator:wstETH") - }) - ); + /// @dev deploy lido locator with dummy default values + function _deployLidoLocator(address lido) internal returns (ILidoLocator) { + LidoLocatorConfig memory config = LidoLocatorConfig({ + accountingOracle: makeAddr("dummy-locator:accountingOracle"), + depositSecurityModule: makeAddr("dummy-locator:depositSecurityModule"), + elRewardsVault: makeAddr("dummy-locator:elRewardsVault"), + legacyOracle: makeAddr("dummy-locator:legacyOracle"), + lido: lido, + oracleReportSanityChecker: makeAddr("dummy-locator:oracleReportSanityChecker"), + postTokenRebaseReceiver: address(0), + burner: makeAddr("dummy-locator:burner"), + stakingRouter: makeAddr("dummy-locator:stakingRouter"), + treasury: makeAddr("dummy-locator:treasury"), + validatorsExitBusOracle: makeAddr("dummy-locator:validatorsExitBusOracle"), + withdrawalQueue: makeAddr("dummy-locator:withdrawalQueue"), + withdrawalVault: makeAddr("dummy-locator:withdrawalVault"), + oracleDaemonConfig: makeAddr("dummy-locator:oracleDaemonConfig"), + accounting: makeAddr("dummy-locator:accounting"), + wstETH: makeAddr("dummy-locator:wstETH") + }); + + return ILidoLocator(deployCode("LidoLocator.sol:LidoLocator", abi.encode(config))); } } From 79ecbbb15cf7f3276851a8b62bafac0159b7c375 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Thu, 16 Jan 2025 16:40:50 +0000 Subject: [PATCH 07/22] fix: accounting initialization --- test/0.8.25/Accounting.t.sol | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index d74a1bf10..37abaea03 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -20,13 +20,20 @@ interface IAccounting { function simulateOracleReport(ReportValues memory _report, uint256 _withdrawalShareRate) external; } +interface ILido { + function getTotalShares() external view returns (uint256); +} + contract AccountingHandler is CommonBase, StdCheats, StdUtils { IAccounting private accounting; + ILido private lido; + ReportValues[] public reports; address private accountingOracle; - constructor(address _accounting, address _accountingOracle, ReportValues memory _refReport) { + constructor(address _accounting, address _lido, address _accountingOracle, ReportValues memory _refReport) { accounting = IAccounting(_accounting); + lido = ILido(_lido); reports.push(_refReport); accountingOracle = _accountingOracle; } @@ -39,8 +46,6 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { uint256 _clValidators, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, - // TODO When adding lido.accounting contract - to use this limitation - // sharesRequestedToBurn - [0, lido.getTotalShares()] uint256 _sharesRequestedToBurn ) external { ReportValues memory lastReport = reports[reports.length - 1]; @@ -51,6 +56,7 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { _clValidators = bound(_clValidators, lastReport.clValidators, type(uint32).max); _withdrawalVaultBalance = bound(_withdrawalVaultBalance, 0, type(uint32).max); _elRewardsVaultBalance = bound(_elRewardsVaultBalance, 0, type(uint32).max); + _sharesRequestedToBurn = bound(_sharesRequestedToBurn, 0, lido.getTotalShares()); // _clValidators = Math.floor(_clValidators); uint256 clBalance = _clValidators * 32 ether; @@ -104,7 +110,19 @@ contract AccountingTest is BaseProtocolTest { "Accounting.sol:Accounting", abi.encode([address(lidoLocator), lidoLocator.lido()]) ); - accountingHandler = new AccountingHandler(accountingImpl, lidoLocator.accountingOracle(), refReport); + + deployCodeTo( + "OssifiableProxy.sol:OssifiableProxy", + abi.encode(accountingImpl, rootAccount, new bytes(0)), + lidoLocator.accounting() + ); + + accountingHandler = new AccountingHandler( + lidoLocator.accounting(), + lidoLocator.lido(), + lidoLocator.accountingOracle(), + refReport + ); deployCodeTo( "AccountingOracle.sol:AccountingOracle", @@ -117,11 +135,7 @@ contract AccountingTest is BaseProtocolTest { lidoLocator.accountingOracle() ); - deployCodeTo( - "OssifiableProxy.sol:OssifiableProxy", - abi.encode(accountingHandler, rootAccount, new bytes(0)), - lidoLocator.accounting() - ); + IAccounting(lidoLocator.accounting()).initialize(rootAccount); // Add burner contract to the protocol deployCodeTo( @@ -157,6 +171,6 @@ contract AccountingTest is BaseProtocolTest { * forge-config: default.invariant.fail-on-revert = true */ function invariant_fuzzTotalShares() public { - assertEq(accountingHandler.length(), 1); // TODO: add real invariant, this is just a placeholder + assertGt(accountingHandler.length(), 0); // TODO: add real invariant, this is just a placeholder } } From 7fc5b7fc78d85f3b1a3caf77297fa772dd3e3a53 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Thu, 16 Jan 2025 19:45:45 +0300 Subject: [PATCH 08/22] feat: added _sharesRequestedToBurn to fuzzing --- test/0.8.25/Accounting.t.sol | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index d74a1bf10..e6892df5c 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -12,6 +12,10 @@ import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; +interface ILido { + function getTotalShares() external view returns (uint256); +} + interface IAccounting { function initialize(address _admin) external; @@ -22,10 +26,12 @@ interface IAccounting { contract AccountingHandler is CommonBase, StdCheats, StdUtils { IAccounting private accounting; + ILido private lido; ReportValues[] public reports; address private accountingOracle; - constructor(address _accounting, address _accountingOracle, ReportValues memory _refReport) { + constructor(address _lido, address _accounting, address _accountingOracle, ReportValues memory _refReport) { + lido = ILido(_lido); accounting = IAccounting(_accounting); reports.push(_refReport); accountingOracle = _accountingOracle; @@ -54,6 +60,8 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { // _clValidators = Math.floor(_clValidators); uint256 clBalance = _clValidators * 32 ether; + _sharesRequestedToBurn = bound(_sharesRequestedToBurn, 0, lido.getTotalShares()); + ReportValues memory currentReport = ReportValues({ timestamp: _timestamp, timeElapsed: _timeElapsed, @@ -104,7 +112,12 @@ contract AccountingTest is BaseProtocolTest { "Accounting.sol:Accounting", abi.encode([address(lidoLocator), lidoLocator.lido()]) ); - accountingHandler = new AccountingHandler(accountingImpl, lidoLocator.accountingOracle(), refReport); + accountingHandler = new AccountingHandler( + address(lidoContract), + accountingImpl, + lidoLocator.accountingOracle(), + refReport + ); deployCodeTo( "AccountingOracle.sol:AccountingOracle", From 166ef4b9087e42d7390ccced449821cf020b5b98 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 17 Jan 2025 16:06:41 +0300 Subject: [PATCH 09/22] feat: refactor fuzz.ProtocolDeployment --- test/0.8.25/Accounting.t.sol | 56 ++++---------------------- test/0.8.25/Protocol__Deployment.t.sol | 49 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 37abaea03..e85d4e274 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -11,10 +11,9 @@ import {console2} from "forge-std/console2.sol"; import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; +import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; interface IAccounting { - function initialize(address _admin) external; - function handleOracleReport(ReportValues memory _report) external; function simulateOracleReport(ReportValues memory _report, uint256 _withdrawalShareRate) external; @@ -74,9 +73,11 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { }); vm.prank(accountingOracle); - accounting.handleOracleReport(currentReport); - - reports.push(currentReport); + try accounting.handleOracleReport(currentReport) { + reports.push(currentReport); + } catch { + console2.log("Could not store report"); + } } } @@ -88,12 +89,11 @@ contract AccountingTest is BaseProtocolTest { address private rootAccount = address(0x123); address private userAccount = address(0x321); - address private depositContract = address(0x4242424242424242424242424242424242424242); function setUp() public { BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); ReportValues memory refReport = ReportValues({ - timestamp: 1705312150, + timestamp: genesisTimestamp, timeElapsed: 0, clValidators: 0, clBalance: 0, @@ -105,18 +105,6 @@ contract AccountingTest is BaseProtocolTest { netCashFlows: new int256[](0) }); - // Add accounting contract with handler to the protocol - address accountingImpl = deployCode( - "Accounting.sol:Accounting", - abi.encode([address(lidoLocator), lidoLocator.lido()]) - ); - - deployCodeTo( - "OssifiableProxy.sol:OssifiableProxy", - abi.encode(accountingImpl, rootAccount, new bytes(0)), - lidoLocator.accounting() - ); - accountingHandler = new AccountingHandler( lidoLocator.accounting(), lidoLocator.lido(), @@ -124,36 +112,6 @@ contract AccountingTest is BaseProtocolTest { refReport ); - deployCodeTo( - "AccountingOracle.sol:AccountingOracle", - abi.encode( - address(lidoLocator), - lidoLocator.legacyOracle(), - 12, // secondsPerSlot - 1695902400 // genesisTime - ), - lidoLocator.accountingOracle() - ); - - IAccounting(lidoLocator.accounting()).initialize(rootAccount); - - // Add burner contract to the protocol - deployCodeTo( - "Burner.sol:Burner", - abi.encode(rootAccount, address(lidoLocator), lidoLocator.lido(), 0, 0), - lidoLocator.burner() - ); - - // Add staking router contract to the protocol - deployCodeTo("StakingRouter.sol:StakingRouter", abi.encode(depositContract), lidoLocator.stakingRouter()); - - // Add oracle report sanity checker contract to the protocol - deployCodeTo( - "OracleReportSanityChecker.sol:OracleReportSanityChecker", - abi.encode(address(lidoLocator), rootAccount, [1500, 1500, 1000, 2000, 8, 24, 128, 5000000, 1000, 101, 50]), - lidoLocator.oracleReportSanityChecker() - ); - // Set target contract to the accounting handler targetContract(address(accountingHandler)); diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 43c7f5ee5..44a6c1743 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -13,6 +13,10 @@ import {console2} from "forge-std/console2.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +interface IAccounting { + function initialize(address _admin) external; +} + interface ILido { function getTotalShares() external view returns (uint256); @@ -89,6 +93,9 @@ contract BaseProtocolTest is Test { address public evmScriptRegistryFactory; address public daoFactoryAdr; + uint256 public genesisTimestamp = 1695902400; + address private depositContract = address(0x4242424242424242424242424242424242424242); + function setUpProtocol(uint256 _startBalance, address _rootAccount, address _userAccount) public { rootAccount = _rootAccount; userAccount = _userAccount; @@ -112,6 +119,48 @@ contract BaseProtocolTest is Test { /// @dev deploy lido locator with dummy default values lidoLocator = _deployLidoLocator(lidoProxyAddress); + // Add accounting contract with handler to the protocol + address accountingImpl = deployCode( + "Accounting.sol:Accounting", + abi.encode([address(lidoLocator), lidoProxyAddress]) + ); + + deployCodeTo( + "OssifiableProxy.sol:OssifiableProxy", + abi.encode(accountingImpl, rootAccount, new bytes(0)), + lidoLocator.accounting() + ); + + deployCodeTo( + "AccountingOracle.sol:AccountingOracle", + abi.encode( + address(lidoLocator), + lidoLocator.legacyOracle(), + 12, // secondsPerSlot + genesisTimestamp + ), + lidoLocator.accountingOracle() + ); + + // Add burner contract to the protocol + deployCodeTo( + "Burner.sol:Burner", + abi.encode(rootAccount, address(lidoLocator), lidoProxyAddress, 0, 0), + lidoLocator.burner() + ); + + // Add staking router contract to the protocol + deployCodeTo("StakingRouter.sol:StakingRouter", abi.encode(depositContract), lidoLocator.stakingRouter()); + + // Add oracle report sanity checker contract to the protocol + deployCodeTo( + "OracleReportSanityChecker.sol:OracleReportSanityChecker", + abi.encode(address(lidoLocator), rootAccount, [1500, 1500, 1000, 2000, 8, 24, 128, 5000000, 1000, 101, 50]), + lidoLocator.oracleReportSanityChecker() + ); + + IAccounting(lidoLocator.accounting()).initialize(rootAccount); + /// @dev deploy eip712steth address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress)); From c39aeeac589f88a1fb541367598869469f170262 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 17 Jan 2025 16:38:57 +0300 Subject: [PATCH 10/22] feat: fix incorrect timestamp for Report --- test/0.8.25/Accounting.t.sol | 8 ++++++-- test/0.8.25/Protocol__Deployment.t.sol | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index e85d4e274..482a1527e 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -52,6 +52,10 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { uint256 _timeElapsed = 86_400; uint256 _timestamp = lastReport.timestamp + _timeElapsed; + // cheatCode for + // if (_report.timestamp >= block.timestamp) revert IncorrectReportTimestamp(_report.timestamp, block.timestamp); + vm.warp(_timestamp + 1); + _clValidators = bound(_clValidators, lastReport.clValidators, type(uint32).max); _withdrawalVaultBalance = bound(_withdrawalVaultBalance, 0, type(uint32).max); _elRewardsVaultBalance = bound(_elRewardsVaultBalance, 0, type(uint32).max); @@ -124,8 +128,8 @@ contract AccountingTest is BaseProtocolTest { /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 256 - * forge-config: default.invariant.depth = 256 + * forge-config: default.invariant.runs = 2 + * forge-config: default.invariant.depth = 2 * forge-config: default.invariant.fail-on-revert = true */ function invariant_fuzzTotalShares() public { diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 44a6c1743..6e520c390 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -12,6 +12,7 @@ import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; interface IAccounting { function initialize(address _admin) external; @@ -93,7 +94,7 @@ contract BaseProtocolTest is Test { address public evmScriptRegistryFactory; address public daoFactoryAdr; - uint256 public genesisTimestamp = 1695902400; + uint256 public genesisTimestamp = 1_695_902_400; address private depositContract = address(0x4242424242424242424242424242424242424242); function setUpProtocol(uint256 _startBalance, address _rootAccount, address _userAccount) public { From 7d05ee2f5a0c1e376d8504b204e67ca242328748 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 20 Jan 2025 11:58:52 +0000 Subject: [PATCH 11/22] chore: add todos --- test/0.8.25/Accounting.t.sol | 6 +++++- test/0.8.25/ShareRate.t.sol | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 482a1527e..513ebd156 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -11,7 +11,6 @@ import {console2} from "forge-std/console2.sol"; import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; -import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; interface IAccounting { function handleOracleReport(ReportValues memory _report) external; @@ -133,6 +132,11 @@ contract AccountingTest is BaseProtocolTest { * forge-config: default.invariant.fail-on-revert = true */ function invariant_fuzzTotalShares() public { + // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) + // - user tokens must not be used except burner contract (from Zero / to Zero) + // - should not be able to decrease validator number + // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal + // - vault params do not affect protocol share rate assertGt(accountingHandler.length(), 0); // TODO: add real invariant, this is just a placeholder } } diff --git a/test/0.8.25/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol index 052d1da53..15c2b823d 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -93,6 +93,10 @@ contract ShareRateTest is BaseProtocolTest { bytes4[] memory selectors = new bytes4[](2); selectors[0] = shareRateHandler.mintExternalShares.selector; selectors[1] = shareRateHandler.burnExternalShares.selector; + // TODO: transfers + // TODO: submit + // TODO: withdrawals request + // TODO: claim targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: selectors})); From 1203e29a5f862e299fc0759440875b9073748859 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Wed, 22 Jan 2025 14:45:43 +0300 Subject: [PATCH 12/22] feat: wip fuzz handleOracleReport --- test/0.8.25/Accounting.t.sol | 142 +++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 513ebd156..239cb3fd6 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -2,15 +2,18 @@ // for testing purposes only pragma solidity ^0.8.0; +import "../../foundry/lib/forge-std/src/Vm.sol"; +import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; import {CommonBase} from "forge-std/Base.sol"; +import {Math} from "../../contracts/0.8.9/lib/Math.sol"; + +import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; + import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; - -import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; - -import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; +import {Math256} from "../../contracts/common/lib/Math256.sol"; interface IAccounting { function handleOracleReport(ReportValues memory _report) external; @@ -20,67 +23,113 @@ interface IAccounting { interface ILido { function getTotalShares() external view returns (uint256); + + function getBeaconStat() + external + view + returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance); } contract AccountingHandler is CommonBase, StdCheats, StdUtils { IAccounting private accounting; ILido private lido; - ReportValues[] public reports; + uint256 public ghost_clValidators; address private accountingOracle; - constructor(address _accounting, address _lido, address _accountingOracle, ReportValues memory _refReport) { + constructor(address _accounting, address _lido, address _accountingOracle) { accounting = IAccounting(_accounting); lido = ILido(_lido); - reports.push(_refReport); accountingOracle = _accountingOracle; + ghost_clValidators = 0; } - function length() public view returns (uint256) { - return reports.length; + function getClValidators() public pure returns (uint256) { + return 1; } function handleOracleReport( + uint256 _preClValidators, + uint256 _preClBalance, uint256 _clValidators, + uint256 _clBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, uint256 _sharesRequestedToBurn ) external { - ReportValues memory lastReport = reports[reports.length - 1]; - uint256 _timeElapsed = 86_400; - uint256 _timestamp = lastReport.timestamp + _timeElapsed; + uint256 _timestamp = 1_737_366_566 + _timeElapsed; + + /** + ReportValues memory refReport = ReportValues({ + timestamp: genesisTimestamp, + timeElapsed: 0, + clValidators: 100, + clBalance: 100 * 32 ether, + withdrawalVaultBalance: 0, + elRewardsVaultBalance: 0, + sharesRequestedToBurn: 0, + withdrawalFinalizationBatches: new uint256[](0), + vaultValues: new uint256[](0), + netCashFlows: new int256[](0) + }); + + vm.store(lidoLocator.lido(), keccak256("lido.Lido.depositedValidators"), bytes32(refReport.clValidators)); + vm.store(lidoLocator.lido(), keccak256("lido.Lido.beaconBalance"), bytes32(refReport.clBalance)); + */ // cheatCode for // if (_report.timestamp >= block.timestamp) revert IncorrectReportTimestamp(_report.timestamp, block.timestamp); vm.warp(_timestamp + 1); - _clValidators = bound(_clValidators, lastReport.clValidators, type(uint32).max); - _withdrawalVaultBalance = bound(_withdrawalVaultBalance, 0, type(uint32).max); - _elRewardsVaultBalance = bound(_elRewardsVaultBalance, 0, type(uint32).max); - _sharesRequestedToBurn = bound(_sharesRequestedToBurn, 0, lido.getTotalShares()); + // How to determinate max possible balance of validator + // + // APR ~ 4-6 % + // BalVal = 32 ETH + // after 10 years staking 32 x (1 + 0.06)^10 ~= 57.4 + // after 20 years staking 32 x (1 + 0.06)^20 ~= 114.8 + // + // Min Balance = 16. If balVal < 16, then validator is deactivated + uint256 minBalance = 16; + uint256 maxBalance = 100; + + // _withdrawalVaultBalance = bound(_withdrawalVaultBalance, 0, type(uint32).max); + // _elRewardsVaultBalance = bound(_elRewardsVaultBalance, 0, type(uint32).max); + // _sharesRequestedToBurn = bound(_sharesRequestedToBurn, 0, lido.getTotalShares()); // _clValidators = Math.floor(_clValidators); - uint256 clBalance = _clValidators * 32 ether; + _clValidators = bound(_clValidators, 1, type(uint32).max); + _clBalance = bound(_clBalance, _clValidators * minBalance, _clValidators * maxBalance); + + _preClValidators = bound(_preClValidators, 1, type(uint32).max); + _preClBalance = bound(_preClBalance, _preClValidators * minBalance, _preClValidators * maxBalance); + + vm.store(address(lido), keccak256("lido.Lido.depositedValidators"), bytes32(_preClValidators)); + vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(_preClValidators)); + vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(_preClBalance * 1 ether)); + + ghost_clValidators = _preClValidators; ReportValues memory currentReport = ReportValues({ timestamp: _timestamp, timeElapsed: _timeElapsed, clValidators: _clValidators, - clBalance: clBalance, - withdrawalVaultBalance: _withdrawalVaultBalance, - elRewardsVaultBalance: _elRewardsVaultBalance, - sharesRequestedToBurn: _sharesRequestedToBurn, + clBalance: _clBalance * 1 ether, + withdrawalVaultBalance: 0, + elRewardsVaultBalance: 0, + sharesRequestedToBurn: 0, withdrawalFinalizationBatches: new uint256[](0), vaultValues: new uint256[](0), netCashFlows: new int256[](0) }); vm.prank(accountingOracle); - try accounting.handleOracleReport(currentReport) { - reports.push(currentReport); - } catch { - console2.log("Could not store report"); - } + accounting.handleOracleReport(currentReport); + + /*try { + console2.log("success"); + } catch (bytes memory reason) { + console2.log(string(reason)); + }*/ } } @@ -95,24 +144,10 @@ contract AccountingTest is BaseProtocolTest { function setUp() public { BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); - ReportValues memory refReport = ReportValues({ - timestamp: genesisTimestamp, - timeElapsed: 0, - clValidators: 0, - clBalance: 0, - withdrawalVaultBalance: 0, - elRewardsVaultBalance: 0, - sharesRequestedToBurn: 0, - withdrawalFinalizationBatches: new uint256[](0), - vaultValues: new uint256[](0), - netCashFlows: new int256[](0) - }); - accountingHandler = new AccountingHandler( lidoLocator.accounting(), lidoLocator.lido(), - lidoLocator.accountingOracle(), - refReport + lidoLocator.accountingOracle() ); // Set target contract to the accounting handler @@ -125,18 +160,25 @@ contract AccountingTest is BaseProtocolTest { targetSelector(FuzzSelector({addr: address(accountingHandler), selectors: selectors})); } + //function invariant_fuzzTotalShares() public { + // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) + // - user tokens must not be used except burner contract (from Zero / to Zero) + // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal + // - vault params do not affect protocol share rate + // assertGt(accountingHandler.length(), 0); // TODO: add real invariant, this is just a placeholder + //} + + // // - should not be able to decrease validator number /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 2 - * forge-config: default.invariant.depth = 2 + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true */ - function invariant_fuzzTotalShares() public { - // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) - // - user tokens must not be used except burner contract (from Zero / to Zero) - // - should not be able to decrease validator number - // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal - // - vault params do not affect protocol share rate - assertGt(accountingHandler.length(), 0); // TODO: add real invariant, this is just a placeholder + function invariant_clValidators() public { + ILido lido = ILido(lidoLocator.lido()); + (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); + + assertEq(accountingHandler.ghost_clValidators(), clValidators); } } From 6ae188879f21fde2c5287d2199e38a7b3a5b887b Mon Sep 17 00:00:00 2001 From: Sergey White Date: Thu, 23 Jan 2025 17:07:35 +0300 Subject: [PATCH 13/22] feat: fuzz clValidators after report --- test/0.8.25/Accounting.t.sol | 95 +++++++++++++------------- test/0.8.25/Protocol__Deployment.t.sol | 43 +++++++++++- 2 files changed, 86 insertions(+), 52 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 239cb3fd6..23b6db93c 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -2,18 +2,16 @@ // for testing purposes only pragma solidity ^0.8.0; -import "../../foundry/lib/forge-std/src/Vm.sol"; -import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; +import "foundry/lib/forge-std/src/Vm.sol"; import {CommonBase} from "forge-std/Base.sol"; -import {Math} from "../../contracts/0.8.9/lib/Math.sol"; - -import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; - import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; -import {Math256} from "../../contracts/common/lib/Math256.sol"; + +import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; +import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; +import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; interface IAccounting { function handleOracleReport(ReportValues memory _report) external; @@ -24,6 +22,8 @@ interface IAccounting { interface ILido { function getTotalShares() external view returns (uint256); + function resume() external; + function getBeaconStat() external view @@ -35,17 +35,16 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { ILido private lido; uint256 public ghost_clValidators; + uint256 public ghost_depositedValidators; address private accountingOracle; + LimitsList public limitList; - constructor(address _accounting, address _lido, address _accountingOracle) { + constructor(address _accounting, address _lido, address _accountingOracle, LimitsList memory _limitList) { accounting = IAccounting(_accounting); lido = ILido(_lido); accountingOracle = _accountingOracle; ghost_clValidators = 0; - } - - function getClValidators() public pure returns (uint256) { - return 1; + limitList = _limitList; } function handleOracleReport( @@ -60,24 +59,6 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { uint256 _timeElapsed = 86_400; uint256 _timestamp = 1_737_366_566 + _timeElapsed; - /** - ReportValues memory refReport = ReportValues({ - timestamp: genesisTimestamp, - timeElapsed: 0, - clValidators: 100, - clBalance: 100 * 32 ether, - withdrawalVaultBalance: 0, - elRewardsVaultBalance: 0, - sharesRequestedToBurn: 0, - withdrawalFinalizationBatches: new uint256[](0), - vaultValues: new uint256[](0), - netCashFlows: new int256[](0) - }); - - vm.store(lidoLocator.lido(), keccak256("lido.Lido.depositedValidators"), bytes32(refReport.clValidators)); - vm.store(lidoLocator.lido(), keccak256("lido.Lido.beaconBalance"), bytes32(refReport.clBalance)); - */ - // cheatCode for // if (_report.timestamp >= block.timestamp) revert IncorrectReportTimestamp(_report.timestamp, block.timestamp); vm.warp(_timestamp + 1); @@ -90,25 +71,40 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { // after 20 years staking 32 x (1 + 0.06)^20 ~= 114.8 // // Min Balance = 16. If balVal < 16, then validator is deactivated - uint256 minBalance = 16; - uint256 maxBalance = 100; + // uint256 minBalance = 16; + // uint256 maxBalance = 100; + uint256 stableBalance = 32; // _withdrawalVaultBalance = bound(_withdrawalVaultBalance, 0, type(uint32).max); // _elRewardsVaultBalance = bound(_elRewardsVaultBalance, 0, type(uint32).max); // _sharesRequestedToBurn = bound(_sharesRequestedToBurn, 0, lido.getTotalShares()); // _clValidators = Math.floor(_clValidators); - _clValidators = bound(_clValidators, 1, type(uint32).max); - _clBalance = bound(_clBalance, _clValidators * minBalance, _clValidators * maxBalance); - _preClValidators = bound(_preClValidators, 1, type(uint32).max); - _preClBalance = bound(_preClBalance, _preClValidators * minBalance, _preClValidators * maxBalance); + _preClValidators = bound(_preClValidators, 250_000, type(uint32).max); + _preClBalance = bound(_preClBalance, _preClValidators * stableBalance, _preClValidators * stableBalance); + ghost_clValidators = _preClValidators; + + // _clValidators = bound(_clValidators, _preClValidators, _preClValidators + 900); + _clValidators = bound( + _clValidators, + _preClValidators, + _preClValidators + limitList.appearedValidatorsPerDayLimit + ); + _clBalance = bound(_clBalance, _clValidators * stableBalance, _clValidators * stableBalance); + + // depositedValidators is always greater or equal to beaconValidators + // Todo: Upper extremum ? + uint256 depositedValidators = bound( + _preClValidators, + _clValidators, + _clValidators + limitList.appearedValidatorsPerDayLimit + ); + ghost_depositedValidators = depositedValidators; - vm.store(address(lido), keccak256("lido.Lido.depositedValidators"), bytes32(_preClValidators)); + vm.store(address(lido), keccak256("lido.Lido.depositedValidators"), bytes32(depositedValidators)); vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(_preClValidators)); vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(_preClBalance * 1 ether)); - ghost_clValidators = _preClValidators; - ReportValues memory currentReport = ReportValues({ timestamp: _timestamp, timeElapsed: _timeElapsed, @@ -124,12 +120,6 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { vm.prank(accountingOracle); accounting.handleOracleReport(currentReport); - - /*try { - console2.log("success"); - } catch (bytes memory reason) { - console2.log(string(reason)); - }*/ } } @@ -147,12 +137,16 @@ contract AccountingTest is BaseProtocolTest { accountingHandler = new AccountingHandler( lidoLocator.accounting(), lidoLocator.lido(), - lidoLocator.accountingOracle() + lidoLocator.accountingOracle(), + limitList ); // Set target contract to the accounting handler targetContract(address(accountingHandler)); + vm.prank(userAccount); + lidoContract.resume(); + // Set target selectors to the accounting handler bytes4[] memory selectors = new bytes4[](1); selectors[0] = accountingHandler.handleOracleReport.selector; @@ -165,20 +159,23 @@ contract AccountingTest is BaseProtocolTest { // - user tokens must not be used except burner contract (from Zero / to Zero) // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal // - vault params do not affect protocol share rate - // assertGt(accountingHandler.length(), 0); // TODO: add real invariant, this is just a placeholder //} - // // - should not be able to decrease validator number /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs * forge-config: default.invariant.runs = 256 * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true + * + * Should not be able to decrease validator number */ function invariant_clValidators() public { ILido lido = ILido(lidoLocator.lido()); (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); - assertEq(accountingHandler.ghost_clValidators(), clValidators); + assertGe(clValidators, accountingHandler.ghost_clValidators()); + assertEq(depositedValidators, accountingHandler.ghost_depositedValidators()); + + // console2.log(depositedValidators); } } diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 6e520c390..9f1fcc731 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; - import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; @@ -12,7 +11,7 @@ import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; -import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; +import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; interface IAccounting { function initialize(address _admin) external; @@ -97,6 +96,21 @@ contract BaseProtocolTest is Test { uint256 public genesisTimestamp = 1_695_902_400; address private depositContract = address(0x4242424242424242424242424242424242424242); + LimitsList public limitList = + LimitsList({ + exitedValidatorsPerDayLimit: 9000, + appearedValidatorsPerDayLimit: 43200, + annualBalanceIncreaseBPLimit: 10_00, + maxValidatorExitRequestsPerReport: 600, + maxItemsPerExtraDataTransaction: 8, + maxNodeOperatorsPerExtraDataItem: 24, + requestTimestampMargin: 7680, + maxPositiveTokenRebase: 750000, + initialSlashingAmountPWei: 1000, + inactivityPenaltiesAmountPWei: 101, + clBalanceOraclesErrorUpperBPLimit: 50 + }); + function setUpProtocol(uint256 _startBalance, address _rootAccount, address _userAccount) public { rootAccount = _rootAccount; userAccount = _userAccount; @@ -156,10 +170,33 @@ contract BaseProtocolTest is Test { // Add oracle report sanity checker contract to the protocol deployCodeTo( "OracleReportSanityChecker.sol:OracleReportSanityChecker", - abi.encode(address(lidoLocator), rootAccount, [1500, 1500, 1000, 2000, 8, 24, 128, 5000000, 1000, 101, 50]), + abi.encode( + address(lidoLocator), + rootAccount, + [ + limitList.exitedValidatorsPerDayLimit, + limitList.appearedValidatorsPerDayLimit, + limitList.annualBalanceIncreaseBPLimit, + limitList.maxValidatorExitRequestsPerReport, + limitList.maxItemsPerExtraDataTransaction, + limitList.maxNodeOperatorsPerExtraDataItem, + limitList.requestTimestampMargin, + limitList.maxPositiveTokenRebase, + limitList.initialSlashingAmountPWei, + limitList.inactivityPenaltiesAmountPWei, + limitList.clBalanceOraclesErrorUpperBPLimit + ] + ), lidoLocator.oracleReportSanityChecker() ); + address secondOpinionOracle = makeAddr("dummy-OracleReportSanityChecker:secondOpinionOracle"); + vm.store( + lidoLocator.oracleReportSanityChecker(), + bytes32(uint256(2)), + bytes32(uint256(uint160(secondOpinionOracle))) + ); + IAccounting(lidoLocator.accounting()).initialize(rootAccount); /// @dev deploy eip712steth From ea08979280798b929d0d8b195e394ce1827bb3ad Mon Sep 17 00:00:00 2001 From: Sergey White Date: Thu, 23 Jan 2025 18:20:06 +0300 Subject: [PATCH 14/22] feat: try to get elRewards --- test/0.8.25/Accounting.t.sol | 30 ++++++++++++++++++++------ test/0.8.25/Protocol__Deployment.t.sol | 10 +++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 23b6db93c..2d2476d8f 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -36,15 +36,24 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { uint256 public ghost_clValidators; uint256 public ghost_depositedValidators; + address private accountingOracle; + address private lidoExecutionLayerRewardVault; LimitsList public limitList; - constructor(address _accounting, address _lido, address _accountingOracle, LimitsList memory _limitList) { + constructor( + address _accounting, + address _lido, + address _accountingOracle, + LimitsList memory _limitList, + address _lidoExecutionLayerRewardVault + ) { accounting = IAccounting(_accounting); lido = ILido(_lido); accountingOracle = _accountingOracle; ghost_clValidators = 0; limitList = _limitList; + lidoExecutionLayerRewardVault = _lidoExecutionLayerRewardVault; } function handleOracleReport( @@ -90,7 +99,7 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { _preClValidators, _preClValidators + limitList.appearedValidatorsPerDayLimit ); - _clBalance = bound(_clBalance, _clValidators * stableBalance, _clValidators * stableBalance); + _clBalance = bound(_clBalance, _clValidators * stableBalance, _clValidators * stableBalance + 1_000); // depositedValidators is always greater or equal to beaconValidators // Todo: Upper extremum ? @@ -105,13 +114,17 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(_preClValidators)); vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(_preClBalance * 1 ether)); + vm.deal(lidoExecutionLayerRewardVault, 1000 * 1 ether); + // IncorrectELRewardsVaultBalance(0) + // sharesToMintAsFees + ReportValues memory currentReport = ReportValues({ timestamp: _timestamp, timeElapsed: _timeElapsed, clValidators: _clValidators, clBalance: _clBalance * 1 ether, withdrawalVaultBalance: 0, - elRewardsVaultBalance: 0, + elRewardsVaultBalance: 1_000 * 1 ether, sharesRequestedToBurn: 0, withdrawalFinalizationBatches: new uint256[](0), vaultValues: new uint256[](0), @@ -138,7 +151,8 @@ contract AccountingTest is BaseProtocolTest { lidoLocator.accounting(), lidoLocator.lido(), lidoLocator.accountingOracle(), - limitList + limitList, + lidoLocator.elRewardsVault() ); // Set target contract to the accounting handler @@ -156,6 +170,8 @@ contract AccountingTest is BaseProtocolTest { //function invariant_fuzzTotalShares() public { // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) + // CLb + ELr <= 10% + // - user tokens must not be used except burner contract (from Zero / to Zero) // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal // - vault params do not affect protocol share rate @@ -163,13 +179,13 @@ contract AccountingTest is BaseProtocolTest { /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 256 - * forge-config: default.invariant.depth = 256 + * forge-config: default.invariant.runs = 1 + * forge-config: default.invariant.depth = 1 * forge-config: default.invariant.fail-on-revert = true * * Should not be able to decrease validator number */ - function invariant_clValidators() public { + function invariant_clValidators() public view { ILido lido = ILido(lidoLocator.lido()); (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 9f1fcc731..97f76ee80 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -95,6 +95,7 @@ contract BaseProtocolTest is Test { uint256 public genesisTimestamp = 1_695_902_400; address private depositContract = address(0x4242424242424242424242424242424242424242); + address public lidoTreasury = makeAddr("dummy-lido:treasury"); LimitsList public limitList = LimitsList({ @@ -164,6 +165,13 @@ contract BaseProtocolTest is Test { lidoLocator.burner() ); + // Add burner contract to the protocol + deployCodeTo( + "LidoExecutionLayerRewardsVault.sol:LidoExecutionLayerRewardsVault", + abi.encode(rootAccount, lidoProxyAddress, lidoTreasury), + lidoLocator.elRewardsVault() + ); + // Add staking router contract to the protocol deployCodeTo("StakingRouter.sol:StakingRouter", abi.encode(depositContract), lidoLocator.stakingRouter()); @@ -199,6 +207,8 @@ contract BaseProtocolTest is Test { IAccounting(lidoLocator.accounting()).initialize(rootAccount); + // contracts/0.8.9/LidoExecutionLayerRewardsVault.sol + /// @dev deploy eip712steth address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress)); From f7870a8e0e74ca59d9fb2645eca3880b4fac787f Mon Sep 17 00:00:00 2001 From: Sergey White Date: Wed, 29 Jan 2025 12:40:24 +0300 Subject: [PATCH 15/22] feat: invariant_handleOracleReport --- .../StakingRouter__MockForLidoAccounting.sol | 77 +++++++++++++++++++ test/0.8.25/Accounting.t.sol | 68 +++++++++++++--- test/0.8.25/Protocol__Deployment.t.sol | 45 ++++++++--- 3 files changed, 166 insertions(+), 24 deletions(-) diff --git a/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol b/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol index 8cfcd10dc..5339081f9 100644 --- a/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol +++ b/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol @@ -3,8 +3,11 @@ pragma solidity 0.8.9; +import {StakingRouter} from "contracts/0.8.9/StakingRouter.sol"; + contract StakingRouter__MockForLidoAccounting { event Mock__MintedRewardsReported(); + event Mock__MintedTotalShares(uint256 indexed _totalShares); address[] private recipients__mocked; uint256[] private stakingModuleIds__mocked; @@ -32,6 +35,13 @@ contract StakingRouter__MockForLidoAccounting { function reportRewardsMinted(uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares) external { emit Mock__MintedRewardsReported(); + + uint256 totalShares = 0; + for (uint256 i = 0; i < _totalShares.length; i++) { + totalShares += _totalShares[i]; + } + + emit Mock__MintedTotalShares(totalShares); } function mock__getStakingRewardsDistribution( @@ -47,4 +57,71 @@ contract StakingRouter__MockForLidoAccounting { totalFee__mocked = _totalFee; precisionPoint__mocked = _precisionPoints; } + + function getStakingModuleIds() public view returns (uint256[] memory) { + return stakingModuleIds__mocked; + } + + function getStakingModule(uint256 _stakingModuleId) public view returns (StakingRouter.StakingModule memory) { + if (_stakingModuleId >= 4) { + revert("Staking module does not exist"); + } + + if (_stakingModuleId == 1) { + return + StakingRouter.StakingModule({ + id: 1, + stakingModuleAddress: 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5, + stakingModuleFee: 500, + treasuryFee: 500, + stakeShareLimit: 10000, + status: 0, + name: "curated-onchain-v1", + lastDepositAt: 1732694279, + lastDepositBlock: 21277744, + exitedValidatorsCount: 88207, + priorityExitShareThreshold: 10000, + maxDepositsPerBlock: 150, + minDepositBlockDistance: 25 + }); + } + + if (_stakingModuleId == 2) { + return + StakingRouter.StakingModule({ + id: 2, + stakingModuleAddress: 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433, + stakingModuleFee: 800, + treasuryFee: 200, + stakeShareLimit: 400, + status: 0, + name: "SimpleDVT", + lastDepositAt: 1735217831, + lastDepositBlock: 21486781, + exitedValidatorsCount: 5, + priorityExitShareThreshold: 444, + maxDepositsPerBlock: 150, + minDepositBlockDistance: 25 + }); + } + + if (_stakingModuleId == 3) { + return + StakingRouter.StakingModule({ + id: 3, + stakingModuleAddress: 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F, + stakingModuleFee: 600, + treasuryFee: 400, + stakeShareLimit: 100, + status: 0, + name: "Community Staking", + lastDepositAt: 1735217387, + lastDepositBlock: 21486745, + exitedValidatorsCount: 104, + priorityExitShareThreshold: 125, + maxDepositsPerBlock: 30, + minDepositBlockDistance: 25 + }); + } + } } diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 2d2476d8f..64cfcb179 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -22,6 +22,8 @@ interface IAccounting { interface ILido { function getTotalShares() external view returns (uint256); + function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); + function resume() external; function getBeaconStat() @@ -36,6 +38,11 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { uint256 public ghost_clValidators; uint256 public ghost_depositedValidators; + uint256 public ghost_sharesMintAsFees; + uint256 public ghost_transferShares; + uint256 public ghost_totalRewards; + uint256 public ghost_principalClBalance; + uint256 public ghost_unifiedClBalance; address private accountingOracle; address private lidoExecutionLayerRewardVault; @@ -51,7 +58,6 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { accounting = IAccounting(_accounting); lido = ILido(_lido); accountingOracle = _accountingOracle; - ghost_clValidators = 0; limitList = _limitList; lidoExecutionLayerRewardVault = _lidoExecutionLayerRewardVault; } @@ -93,7 +99,6 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { _preClBalance = bound(_preClBalance, _preClValidators * stableBalance, _preClValidators * stableBalance); ghost_clValidators = _preClValidators; - // _clValidators = bound(_clValidators, _preClValidators, _preClValidators + 900); _clValidators = bound( _clValidators, _preClValidators, @@ -114,9 +119,8 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(_preClValidators)); vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(_preClBalance * 1 ether)); - vm.deal(lidoExecutionLayerRewardVault, 1000 * 1 ether); - // IncorrectELRewardsVaultBalance(0) - // sharesToMintAsFees + // research correlation with elRewardsVaultBalance + vm.deal(lidoExecutionLayerRewardVault, 300 ether); ReportValues memory currentReport = ReportValues({ timestamp: _timestamp, @@ -124,22 +128,47 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { clValidators: _clValidators, clBalance: _clBalance * 1 ether, withdrawalVaultBalance: 0, - elRewardsVaultBalance: 1_000 * 1 ether, + elRewardsVaultBalance: 200 ether, sharesRequestedToBurn: 0, withdrawalFinalizationBatches: new uint256[](0), vaultValues: new uint256[](0), netCashFlows: new int256[](0) }); + ghost_principalClBalance = + _preClBalance * + 1 ether + + (currentReport.clValidators - _preClValidators) * + stableBalance * + 1 ether; + ghost_unifiedClBalance = currentReport.clBalance + currentReport.withdrawalVaultBalance; // ? + + ghost_totalRewards = ghost_unifiedClBalance - ghost_principalClBalance + currentReport.elRewardsVaultBalance; + vm.prank(accountingOracle); + + vm.recordLogs(); accounting.handleOracleReport(currentReport); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + bytes32 totalSharesSignature = keccak256("Mock__MintedTotalShares(uint256)"); + bytes32 transferSharesSignature = keccak256("TransferShares(address,address,uint256)"); + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == totalSharesSignature) { + ghost_sharesMintAsFees = abi.decode(abi.encodePacked(entries[i].topics[1]), (uint256)); + } + + if (entries[i].topics[0] == transferSharesSignature) { + ghost_transferShares = abi.decode(entries[i].data, (uint256)); + } + } } } contract AccountingTest is BaseProtocolTest { AccountingHandler private accountingHandler; - uint256 private protocolStartBalance = 15_000 ether; + uint256 private protocolStartBalance = 1 ether; address private rootAccount = address(0x123); address private userAccount = address(0x321); @@ -172,26 +201,41 @@ contract AccountingTest is BaseProtocolTest { // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) // CLb + ELr <= 10% - // - user tokens must not be used except burner contract (from Zero / to Zero) + // - user tokens must not be used except burner as source (from Zero / to Zero). From burner to zerop + // - from zero to Treasure, burner + // // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal // - vault params do not affect protocol share rate //} /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 1 - * forge-config: default.invariant.depth = 1 + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true * * Should not be able to decrease validator number */ - function invariant_clValidators() public view { + function invariant_handleOracleReport() public view { ILido lido = ILido(lidoLocator.lido()); (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); assertGe(clValidators, accountingHandler.ghost_clValidators()); assertEq(depositedValidators, accountingHandler.ghost_depositedValidators()); - // console2.log(depositedValidators); + if (accountingHandler.ghost_unifiedClBalance() > accountingHandler.ghost_principalClBalance()) { + uint256 treasuryFeesETH = lido.getPooledEthByShares(accountingHandler.ghost_sharesMintAsFees()) / 1 ether; + uint256 reportRewardsMintedETH = lido.getPooledEthByShares(accountingHandler.ghost_transferShares()) / + 1 ether; + uint256 totalFees = treasuryFeesETH + reportRewardsMintedETH; + uint256 totalRewards = accountingHandler.ghost_totalRewards() / 1 ether; + + if (totalRewards != 0) { + uint256 percents = (totalFees * 100) / totalRewards; + + assertTrue(percents <= 10); + assertTrue(percents > 0); + } + } } } diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 97f76ee80..e7250f75a 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -3,16 +3,17 @@ pragma solidity ^0.8.0; +import "../0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol"; import "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; + import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; -import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; -import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; - interface IAccounting { function initialize(address _admin) external; } @@ -132,8 +133,33 @@ contract BaseProtocolTest is Test { acl.createPermission(userAccount, lidoProxyAddress, keccak256("RESUME_ROLE"), rootAccount); acl.createPermission(userAccount, lidoProxyAddress, keccak256("PAUSE_ROLE"), rootAccount); + StakingRouter__MockForLidoAccounting stakingRouter = new StakingRouter__MockForLidoAccounting(); + + uint256[] memory stakingModuleIds = new uint256[](3); + stakingModuleIds[0] = 1; + stakingModuleIds[1] = 2; + stakingModuleIds[2] = 3; + + uint96[] memory stakingModuleFees = new uint96[](3); + stakingModuleFees[0] = 4876942047684326532; + stakingModuleFees[1] = 145875332634464962; + stakingModuleFees[2] = 38263043302959438; + + address[] memory recipients = new address[](3); + recipients[0] = 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5; + recipients[1] = 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433; + recipients[2] = 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F; + + stakingRouter.mock__getStakingRewardsDistribution( + recipients, + stakingModuleIds, + stakingModuleFees, + 9999999999999999996, + 100000000000000000000 + ); + /// @dev deploy lido locator with dummy default values - lidoLocator = _deployLidoLocator(lidoProxyAddress); + lidoLocator = _deployLidoLocator(lidoProxyAddress, address(stakingRouter)); // Add accounting contract with handler to the protocol address accountingImpl = deployCode( @@ -168,13 +194,10 @@ contract BaseProtocolTest is Test { // Add burner contract to the protocol deployCodeTo( "LidoExecutionLayerRewardsVault.sol:LidoExecutionLayerRewardsVault", - abi.encode(rootAccount, lidoProxyAddress, lidoTreasury), + abi.encode(lidoProxyAddress, lidoTreasury), lidoLocator.elRewardsVault() ); - // Add staking router contract to the protocol - deployCodeTo("StakingRouter.sol:StakingRouter", abi.encode(depositContract), lidoLocator.stakingRouter()); - // Add oracle report sanity checker contract to the protocol deployCodeTo( "OracleReportSanityChecker.sol:OracleReportSanityChecker", @@ -207,8 +230,6 @@ contract BaseProtocolTest is Test { IAccounting(lidoLocator.accounting()).initialize(rootAccount); - // contracts/0.8.9/LidoExecutionLayerRewardsVault.sol - /// @dev deploy eip712steth address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress)); @@ -254,7 +275,7 @@ contract BaseProtocolTest is Test { } /// @dev deploy lido locator with dummy default values - function _deployLidoLocator(address lido) internal returns (ILidoLocator) { + function _deployLidoLocator(address lido, address stakingRouterAddress) internal returns (ILidoLocator) { LidoLocatorConfig memory config = LidoLocatorConfig({ accountingOracle: makeAddr("dummy-locator:accountingOracle"), depositSecurityModule: makeAddr("dummy-locator:depositSecurityModule"), @@ -264,7 +285,7 @@ contract BaseProtocolTest is Test { oracleReportSanityChecker: makeAddr("dummy-locator:oracleReportSanityChecker"), postTokenRebaseReceiver: address(0), burner: makeAddr("dummy-locator:burner"), - stakingRouter: makeAddr("dummy-locator:stakingRouter"), + stakingRouter: stakingRouterAddress, treasury: makeAddr("dummy-locator:treasury"), validatorsExitBusOracle: makeAddr("dummy-locator:validatorsExitBusOracle"), withdrawalQueue: makeAddr("dummy-locator:withdrawalQueue"), From 02ac654fa8be0de36ef79e8db2e0ebcc28bb2d27 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Thu, 30 Jan 2025 17:33:47 +0300 Subject: [PATCH 16/22] feat: invariant_handleOracleReport --- .../contracts/SecondOpinionOracle__Mock.sol | 30 +++ test/0.8.25/Accounting.t.sol | 203 ++++++++++++------ test/0.8.25/Protocol__Deployment.t.sol | 6 +- 3 files changed, 168 insertions(+), 71 deletions(-) create mode 100644 test/0.4.24/contracts/SecondOpinionOracle__Mock.sol diff --git a/test/0.4.24/contracts/SecondOpinionOracle__Mock.sol b/test/0.4.24/contracts/SecondOpinionOracle__Mock.sol new file mode 100644 index 000000000..6b9504d38 --- /dev/null +++ b/test/0.4.24/contracts/SecondOpinionOracle__Mock.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +contract SecondOpinionOracle__Mock { + bool private success; + uint256 private clBalanceGwei; + uint256 private withdrawalVaultBalanceWei; + uint256 private totalDepositedValidators; + uint256 private totalExitedValidators; + + function getReport(uint256 refSlot) external view returns (bool, uint256, uint256, uint256, uint256) { + return (success, clBalanceGwei, withdrawalVaultBalanceWei, totalDepositedValidators, totalExitedValidators); + } + + function mock__setReportValues( + bool _success, + uint256 _clBalanceGwei, + uint256 _withdrawalVaultBalanceWei, + uint256 _totalDepositedValidators, + uint256 _totalExitedValidators + ) external { + success = _success; + clBalanceGwei = _clBalanceGwei; + withdrawalVaultBalanceWei = _withdrawalVaultBalanceWei; + totalDepositedValidators = _totalDepositedValidators; + totalExitedValidators = _totalExitedValidators; + } +} diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index 64cfcb179..ff9732386 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -12,6 +12,7 @@ import {console2} from "forge-std/console2.sol"; import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; +import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; interface IAccounting { function handleOracleReport(ReportValues memory _report) external; @@ -32,17 +33,48 @@ interface ILido { returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance); } +interface ISecondOpinionOracleMock { + function mock__setReportValues( + bool _success, + uint256 _clBalanceGwei, + uint256 _withdrawalVaultBalanceWei, + uint256 _totalDepositedValidators, + uint256 _totalExitedValidators + ) external; +} + +// 0.002792 * 10^18 +// 0.0073 * 10^18 +uint256 constant maxYiedPerOperatorWei = 2_792_000_000_000_000; // which % of slashing could be? +uint256 constant maxLossPerOperatorWei = 7_300_000_000_000_000; + +struct FuzzValues { + uint256 _preClValidators; + uint256 _preClBalanceGwei; + uint256 _clValidators; + uint256 _clBalanceGwei; + uint256 _withdrawalVaultBalance; + uint256 _elRewardsVaultBalance; + uint256 _sharesRequestedToBurn; + uint256 _lidoExecutionLayerRewardVault; +} + contract AccountingHandler is CommonBase, StdCheats, StdUtils { + struct Ghost { + int256 clValidators; + int256 depositedValidators; + int256 sharesMintAsFees; + int256 transferShares; + int256 totalRewards; + int256 principalClBalance; + int256 unifiedClBalance; + } + IAccounting private accounting; ILido private lido; + ISecondOpinionOracleMock private secondOpinionOracle; - uint256 public ghost_clValidators; - uint256 public ghost_depositedValidators; - uint256 public ghost_sharesMintAsFees; - uint256 public ghost_transferShares; - uint256 public ghost_totalRewards; - uint256 public ghost_principalClBalance; - uint256 public ghost_unifiedClBalance; + Ghost public ghost; address private accountingOracle; address private lidoExecutionLayerRewardVault; @@ -53,24 +85,23 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { address _lido, address _accountingOracle, LimitsList memory _limitList, - address _lidoExecutionLayerRewardVault + address _lidoExecutionLayerRewardVault, + address _secondOpinionOracle ) { accounting = IAccounting(_accounting); lido = ILido(_lido); accountingOracle = _accountingOracle; limitList = _limitList; lidoExecutionLayerRewardVault = _lidoExecutionLayerRewardVault; + ghost = Ghost(0, 0, 0, 0, 0, 0, 0); + secondOpinionOracle = ISecondOpinionOracleMock(_secondOpinionOracle); + } + + function cutGwei(uint256 value) public returns (uint256) { + return (value / 1 gwei) * 1 gwei; } - function handleOracleReport( - uint256 _preClValidators, - uint256 _preClBalance, - uint256 _clValidators, - uint256 _clBalance, - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - uint256 _sharesRequestedToBurn - ) external { + function handleOracleReport(FuzzValues memory fuzz) external { uint256 _timeElapsed = 86_400; uint256 _timestamp = 1_737_366_566 + _timeElapsed; @@ -88,62 +119,80 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { // Min Balance = 16. If balVal < 16, then validator is deactivated // uint256 minBalance = 16; // uint256 maxBalance = 100; - uint256 stableBalance = 32; + uint256 stableBalanceWei = 32 * 1 ether; + + fuzz._lidoExecutionLayerRewardVault = bound(fuzz._lidoExecutionLayerRewardVault, 0, 1000); + fuzz._elRewardsVaultBalance = bound(fuzz._elRewardsVaultBalance, 0, fuzz._lidoExecutionLayerRewardVault); + + if (fuzz._elRewardsVaultBalance < fuzz._lidoExecutionLayerRewardVault) { + console2.log( + "reported values less then EL", + int256(fuzz._elRewardsVaultBalance) - int256(fuzz._lidoExecutionLayerRewardVault) + ); + } else if (fuzz._elRewardsVaultBalance == fuzz._lidoExecutionLayerRewardVault) { + console2.log("equal"); + } - // _withdrawalVaultBalance = bound(_withdrawalVaultBalance, 0, type(uint32).max); - // _elRewardsVaultBalance = bound(_elRewardsVaultBalance, 0, type(uint32).max); - // _sharesRequestedToBurn = bound(_sharesRequestedToBurn, 0, lido.getTotalShares()); - // _clValidators = Math.floor(_clValidators); + fuzz._preClValidators = bound(fuzz._preClValidators, 250_000, type(uint32).max); + fuzz._preClBalanceGwei = cutGwei(fuzz._preClValidators * stableBalanceWei); - _preClValidators = bound(_preClValidators, 250_000, type(uint32).max); - _preClBalance = bound(_preClBalance, _preClValidators * stableBalance, _preClValidators * stableBalance); - ghost_clValidators = _preClValidators; + ghost.clValidators = int256(fuzz._preClValidators); - _clValidators = bound( - _clValidators, - _preClValidators, - _preClValidators + limitList.appearedValidatorsPerDayLimit + fuzz._clValidators = bound( + fuzz._clValidators, + fuzz._preClValidators, + fuzz._preClValidators + limitList.appearedValidatorsPerDayLimit ); - _clBalance = bound(_clBalance, _clValidators * stableBalance, _clValidators * stableBalance + 1_000); + + uint256 minBalancePerValidator = fuzz._clValidators * (stableBalanceWei - maxLossPerOperatorWei); + uint256 maxBalancePerValidator = fuzz._clValidators * (stableBalanceWei + maxYiedPerOperatorWei); + fuzz._clBalanceGwei = cutGwei(bound(fuzz._clBalanceGwei, minBalancePerValidator, maxBalancePerValidator)); // depositedValidators is always greater or equal to beaconValidators // Todo: Upper extremum ? uint256 depositedValidators = bound( - _preClValidators, - _clValidators, - _clValidators + limitList.appearedValidatorsPerDayLimit + fuzz._preClValidators, + fuzz._clValidators, + fuzz._clValidators + limitList.appearedValidatorsPerDayLimit ); - ghost_depositedValidators = depositedValidators; + ghost.depositedValidators = int256(depositedValidators); vm.store(address(lido), keccak256("lido.Lido.depositedValidators"), bytes32(depositedValidators)); - vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(_preClValidators)); - vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(_preClBalance * 1 ether)); + vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(fuzz._preClValidators)); + vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(fuzz._preClBalanceGwei)); - // research correlation with elRewardsVaultBalance - vm.deal(lidoExecutionLayerRewardVault, 300 ether); + vm.deal(lidoExecutionLayerRewardVault, fuzz._lidoExecutionLayerRewardVault * 1 ether); ReportValues memory currentReport = ReportValues({ timestamp: _timestamp, timeElapsed: _timeElapsed, - clValidators: _clValidators, - clBalance: _clBalance * 1 ether, + clValidators: fuzz._clValidators, + clBalance: fuzz._clBalanceGwei, withdrawalVaultBalance: 0, - elRewardsVaultBalance: 200 ether, + elRewardsVaultBalance: fuzz._elRewardsVaultBalance * 1 ether, sharesRequestedToBurn: 0, withdrawalFinalizationBatches: new uint256[](0), vaultValues: new uint256[](0), netCashFlows: new int256[](0) }); - ghost_principalClBalance = - _preClBalance * - 1 ether + - (currentReport.clValidators - _preClValidators) * - stableBalance * - 1 ether; - ghost_unifiedClBalance = currentReport.clBalance + currentReport.withdrawalVaultBalance; // ? + ghost.unifiedClBalance = int256(currentReport.clBalance + currentReport.withdrawalVaultBalance); // ? + ghost.principalClBalance = int256( + fuzz._preClBalanceGwei + (currentReport.clValidators - fuzz._preClValidators) * stableBalanceWei * 1 ether + ); - ghost_totalRewards = ghost_unifiedClBalance - ghost_principalClBalance + currentReport.elRewardsVaultBalance; + ghost.totalRewards = + ghost.unifiedClBalance - + ghost.principalClBalance + + int256(currentReport.elRewardsVaultBalance); + + secondOpinionOracle.mock__setReportValues( + true, + currentReport.clBalance / 1e9, + currentReport.withdrawalVaultBalance, + uint256(ghost.depositedValidators), + 0 + ); vm.prank(accountingOracle); @@ -155,14 +204,18 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { bytes32 transferSharesSignature = keccak256("TransferShares(address,address,uint256)"); for (uint256 i = 0; i < entries.length; i++) { if (entries[i].topics[0] == totalSharesSignature) { - ghost_sharesMintAsFees = abi.decode(abi.encodePacked(entries[i].topics[1]), (uint256)); + ghost.sharesMintAsFees = int256(abi.decode(abi.encodePacked(entries[i].topics[1]), (uint256))); } if (entries[i].topics[0] == transferSharesSignature) { - ghost_transferShares = abi.decode(entries[i].data, (uint256)); + ghost.transferShares = int256(abi.decode(entries[i].data, (uint256))); } } } + + function getGhost() public view returns (Ghost memory) { + return ghost; + } } contract AccountingTest is BaseProtocolTest { @@ -181,7 +234,8 @@ contract AccountingTest is BaseProtocolTest { lidoLocator.lido(), lidoLocator.accountingOracle(), limitList, - lidoLocator.elRewardsVault() + lidoLocator.elRewardsVault(), + address(secondOpinionOracleMock) ); // Set target contract to the accounting handler @@ -197,10 +251,6 @@ contract AccountingTest is BaseProtocolTest { targetSelector(FuzzSelector({addr: address(accountingHandler), selectors: selectors})); } - //function invariant_fuzzTotalShares() public { - // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) - // CLb + ELr <= 10% - // - user tokens must not be used except burner as source (from Zero / to Zero). From burner to zerop // - from zero to Treasure, burner // @@ -213,29 +263,44 @@ contract AccountingTest is BaseProtocolTest { * forge-config: default.invariant.runs = 256 * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true - * - * Should not be able to decrease validator number */ function invariant_handleOracleReport() public view { ILido lido = ILido(lidoLocator.lido()); (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); - assertGe(clValidators, accountingHandler.ghost_clValidators()); - assertEq(depositedValidators, accountingHandler.ghost_depositedValidators()); + // Should not be able to decrease validator number + assertGe(clValidators, uint256(accountingHandler.getGhost().clValidators)); + assertEq(depositedValidators, uint256(accountingHandler.getGhost().depositedValidators)); + + // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) + // CLb + ELr <= 10% + if (accountingHandler.getGhost().unifiedClBalance > accountingHandler.getGhost().principalClBalance) { + if (accountingHandler.getGhost().sharesMintAsFees < 0) { + revert("sharesMintAsFees < 0"); + } + + if (accountingHandler.getGhost().transferShares < 0) { + revert("transferShares < 0"); + } - if (accountingHandler.ghost_unifiedClBalance() > accountingHandler.ghost_principalClBalance()) { - uint256 treasuryFeesETH = lido.getPooledEthByShares(accountingHandler.ghost_sharesMintAsFees()) / 1 ether; - uint256 reportRewardsMintedETH = lido.getPooledEthByShares(accountingHandler.ghost_transferShares()) / - 1 ether; - uint256 totalFees = treasuryFeesETH + reportRewardsMintedETH; - uint256 totalRewards = accountingHandler.ghost_totalRewards() / 1 ether; + int256 treasuryFeesETH = int256( + lido.getPooledEthByShares(uint256(accountingHandler.getGhost().sharesMintAsFees)) + ); + int256 reportRewardsMintedETH = int256( + lido.getPooledEthByShares(uint256(accountingHandler.getGhost().transferShares)) + ); + int256 totalFees = int256(treasuryFeesETH + reportRewardsMintedETH); + int256 totalRewards = accountingHandler.getGhost().totalRewards; if (totalRewards != 0) { - uint256 percents = (totalFees * 100) / totalRewards; + int256 percents = (totalFees * 100) / totalRewards; + console2.log("percents", percents); - assertTrue(percents <= 10); - assertTrue(percents > 0); + assertTrue(percents <= 10, "all distributed rewards > 10%"); + assertTrue(percents > 0, "all distributed rewards < 0%"); } + } else { + console2.log("Negative rebase. Skipping report", accountingHandler.getGhost().totalRewards / 1 ether); } } } diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index e7250f75a..909e3158c 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "../0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol"; +import "../0.4.24/contracts/SecondOpinionOracle__Mock.sol"; import "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; @@ -84,6 +85,7 @@ contract BaseProtocolTest is Test { ILido public lidoContract; ILidoLocator public lidoLocator; IACL public acl; + SecondOpinionOracle__Mock public secondOpinionOracleMock; IKernel private dao; address private rootAccount; @@ -221,11 +223,11 @@ contract BaseProtocolTest is Test { lidoLocator.oracleReportSanityChecker() ); - address secondOpinionOracle = makeAddr("dummy-OracleReportSanityChecker:secondOpinionOracle"); + secondOpinionOracleMock = new SecondOpinionOracle__Mock(); vm.store( lidoLocator.oracleReportSanityChecker(), bytes32(uint256(2)), - bytes32(uint256(uint160(secondOpinionOracle))) + bytes32(uint256(uint160(address(secondOpinionOracleMock)))) ); IAccounting(lidoLocator.accounting()).initialize(rootAccount); From 490652e6b4207af7403185024f8b0dbf7bd06977 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 31 Jan 2025 16:59:31 +0300 Subject: [PATCH 17/22] feat: check invariant lido.transfer --- .../StakingRouter__MockForLidoAccounting.sol | 4 + test/0.8.25/Accounting.t.sol | 184 +++++++++++------- 2 files changed, 123 insertions(+), 65 deletions(-) diff --git a/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol b/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol index 5339081f9..a168bbc68 100644 --- a/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol +++ b/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol @@ -62,6 +62,10 @@ contract StakingRouter__MockForLidoAccounting { return stakingModuleIds__mocked; } + function getRecipients() public view returns (address[] memory) { + return recipients__mocked; + } + function getStakingModule(uint256 _stakingModuleId) public view returns (StakingRouter.StakingModule memory) { if (_stakingModuleId >= 4) { revert("Staking module does not exist"); diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index ff9732386..f67fd7d88 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -7,12 +7,15 @@ import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; -import {console2} from "forge-std/console2.sol"; +import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; import {ReportValues} from "contracts/common/interfaces/ReportValues.sol"; -import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; + +interface IStakingRouter { + function getRecipients() external view returns (address[] memory); +} interface IAccounting { function handleOracleReport(ReportValues memory _report) external; @@ -47,16 +50,22 @@ interface ISecondOpinionOracleMock { // 0.0073 * 10^18 uint256 constant maxYiedPerOperatorWei = 2_792_000_000_000_000; // which % of slashing could be? uint256 constant maxLossPerOperatorWei = 7_300_000_000_000_000; +uint256 constant stableBalanceWei = 32 * 1 ether; struct FuzzValues { uint256 _preClValidators; - uint256 _preClBalanceGwei; + uint256 _preClBalanceWei; uint256 _clValidators; - uint256 _clBalanceGwei; + uint256 _clBalanceWei; uint256 _withdrawalVaultBalance; - uint256 _elRewardsVaultBalance; + uint256 _elRewardsVaultBalanceWei; uint256 _sharesRequestedToBurn; - uint256 _lidoExecutionLayerRewardVault; + uint256 _lidoExecutionLayerRewardVaultWei; +} + +struct LidoTransfer { + address from; + address to; } contract AccountingHandler is CommonBase, StdCheats, StdUtils { @@ -65,19 +74,22 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { int256 depositedValidators; int256 sharesMintAsFees; int256 transferShares; - int256 totalRewards; - int256 principalClBalance; - int256 unifiedClBalance; + int256 totalRewardsWei; + int256 principalClBalanceWei; + int256 unifiedClBalanceWei; } IAccounting private accounting; ILido private lido; ISecondOpinionOracleMock private secondOpinionOracle; + IStakingRouter public stakingRouter; Ghost public ghost; + LidoTransfer[] public ghost_lidoTransfers; address private accountingOracle; address private lidoExecutionLayerRewardVault; + address private burner; LimitsList public limitList; constructor( @@ -86,15 +98,20 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { address _accountingOracle, LimitsList memory _limitList, address _lidoExecutionLayerRewardVault, - address _secondOpinionOracle + address _secondOpinionOracle, + address _burnerAddress, + address _stakingRouter ) { accounting = IAccounting(_accounting); lido = ILido(_lido); accountingOracle = _accountingOracle; limitList = _limitList; lidoExecutionLayerRewardVault = _lidoExecutionLayerRewardVault; + ghost = Ghost(0, 0, 0, 0, 0, 0, 0); secondOpinionOracle = ISecondOpinionOracleMock(_secondOpinionOracle); + burner = _burnerAddress; + stakingRouter = IStakingRouter(_stakingRouter); } function cutGwei(uint256 value) public returns (uint256) { @@ -109,32 +126,15 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { // if (_report.timestamp >= block.timestamp) revert IncorrectReportTimestamp(_report.timestamp, block.timestamp); vm.warp(_timestamp + 1); - // How to determinate max possible balance of validator - // - // APR ~ 4-6 % - // BalVal = 32 ETH - // after 10 years staking 32 x (1 + 0.06)^10 ~= 57.4 - // after 20 years staking 32 x (1 + 0.06)^20 ~= 114.8 - // - // Min Balance = 16. If balVal < 16, then validator is deactivated - // uint256 minBalance = 16; - // uint256 maxBalance = 100; - uint256 stableBalanceWei = 32 * 1 ether; - - fuzz._lidoExecutionLayerRewardVault = bound(fuzz._lidoExecutionLayerRewardVault, 0, 1000); - fuzz._elRewardsVaultBalance = bound(fuzz._elRewardsVaultBalance, 0, fuzz._lidoExecutionLayerRewardVault); - - if (fuzz._elRewardsVaultBalance < fuzz._lidoExecutionLayerRewardVault) { - console2.log( - "reported values less then EL", - int256(fuzz._elRewardsVaultBalance) - int256(fuzz._lidoExecutionLayerRewardVault) - ); - } else if (fuzz._elRewardsVaultBalance == fuzz._lidoExecutionLayerRewardVault) { - console2.log("equal"); - } + fuzz._lidoExecutionLayerRewardVaultWei = bound(fuzz._lidoExecutionLayerRewardVaultWei, 0, 1_000) * 1 ether; + fuzz._elRewardsVaultBalanceWei = bound( + fuzz._elRewardsVaultBalanceWei, + 0, + fuzz._lidoExecutionLayerRewardVaultWei + ); - fuzz._preClValidators = bound(fuzz._preClValidators, 250_000, type(uint32).max); - fuzz._preClBalanceGwei = cutGwei(fuzz._preClValidators * stableBalanceWei); + fuzz._preClValidators = bound(fuzz._preClValidators, 250_000, 100_000_000_000); + fuzz._preClBalanceWei = cutGwei(fuzz._preClValidators * stableBalanceWei); ghost.clValidators = int256(fuzz._preClValidators); @@ -144,9 +144,9 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { fuzz._preClValidators + limitList.appearedValidatorsPerDayLimit ); - uint256 minBalancePerValidator = fuzz._clValidators * (stableBalanceWei - maxLossPerOperatorWei); - uint256 maxBalancePerValidator = fuzz._clValidators * (stableBalanceWei + maxYiedPerOperatorWei); - fuzz._clBalanceGwei = cutGwei(bound(fuzz._clBalanceGwei, minBalancePerValidator, maxBalancePerValidator)); + uint256 minBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei - maxLossPerOperatorWei); + uint256 maxBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei + maxYiedPerOperatorWei); + fuzz._clBalanceWei = bound(fuzz._clBalanceWei, minBalancePerValidatorWei, maxBalancePerValidatorWei); // depositedValidators is always greater or equal to beaconValidators // Todo: Upper extremum ? @@ -159,36 +159,36 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { vm.store(address(lido), keccak256("lido.Lido.depositedValidators"), bytes32(depositedValidators)); vm.store(address(lido), keccak256("lido.Lido.beaconValidators"), bytes32(fuzz._preClValidators)); - vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(fuzz._preClBalanceGwei)); + vm.store(address(lido), keccak256("lido.Lido.beaconBalance"), bytes32(fuzz._preClBalanceWei)); - vm.deal(lidoExecutionLayerRewardVault, fuzz._lidoExecutionLayerRewardVault * 1 ether); + vm.deal(lidoExecutionLayerRewardVault, fuzz._lidoExecutionLayerRewardVaultWei); ReportValues memory currentReport = ReportValues({ timestamp: _timestamp, timeElapsed: _timeElapsed, clValidators: fuzz._clValidators, - clBalance: fuzz._clBalanceGwei, + clBalance: (fuzz._clBalanceWei / 1e9) * 1e9, + elRewardsVaultBalance: fuzz._elRewardsVaultBalanceWei, withdrawalVaultBalance: 0, - elRewardsVaultBalance: fuzz._elRewardsVaultBalance * 1 ether, sharesRequestedToBurn: 0, withdrawalFinalizationBatches: new uint256[](0), vaultValues: new uint256[](0), netCashFlows: new int256[](0) }); - ghost.unifiedClBalance = int256(currentReport.clBalance + currentReport.withdrawalVaultBalance); // ? - ghost.principalClBalance = int256( - fuzz._preClBalanceGwei + (currentReport.clValidators - fuzz._preClValidators) * stableBalanceWei * 1 ether + ghost.unifiedClBalanceWei = int256(fuzz._clBalanceWei + currentReport.withdrawalVaultBalance); // ? + ghost.principalClBalanceWei = int256( + fuzz._preClBalanceWei + (currentReport.clValidators - fuzz._preClValidators) * stableBalanceWei ); - ghost.totalRewards = - ghost.unifiedClBalance - - ghost.principalClBalance + - int256(currentReport.elRewardsVaultBalance); + ghost.totalRewardsWei = + ghost.unifiedClBalanceWei - + ghost.principalClBalanceWei + + int256(fuzz._elRewardsVaultBalanceWei); secondOpinionOracle.mock__setReportValues( true, - currentReport.clBalance / 1e9, + fuzz._clBalanceWei / 1e9, currentReport.withdrawalVaultBalance, uint256(ghost.depositedValidators), 0 @@ -196,12 +196,15 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { vm.prank(accountingOracle); + delete ghost_lidoTransfers; vm.recordLogs(); accounting.handleOracleReport(currentReport); Vm.Log[] memory entries = vm.getRecordedLogs(); bytes32 totalSharesSignature = keccak256("Mock__MintedTotalShares(uint256)"); bytes32 transferSharesSignature = keccak256("TransferShares(address,address,uint256)"); + bytes32 lidoTransferSignature = keccak256("Transfer(address,address,uint256)"); + for (uint256 i = 0; i < entries.length; i++) { if (entries[i].topics[0] == totalSharesSignature) { ghost.sharesMintAsFees = int256(abi.decode(abi.encodePacked(entries[i].topics[1]), (uint256))); @@ -210,12 +213,25 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { if (entries[i].topics[0] == transferSharesSignature) { ghost.transferShares = int256(abi.decode(entries[i].data, (uint256))); } + + if (entries[i].topics[0] == lidoTransferSignature) { + if (entries[i].emitter == address(lido)) { + address from = abi.decode(abi.encodePacked(entries[i].topics[1]), (address)); + address to = abi.decode(abi.encodePacked(entries[i].topics[2]), (address)); + + ghost_lidoTransfers.push(LidoTransfer({from: from, to: to})); + } + } } } function getGhost() public view returns (Ghost memory) { return ghost; } + + function getLidoTransfers() public view returns (LidoTransfer[] memory) { + return ghost_lidoTransfers; + } } contract AccountingTest is BaseProtocolTest { @@ -226,6 +242,8 @@ contract AccountingTest is BaseProtocolTest { address private rootAccount = address(0x123); address private userAccount = address(0x321); + mapping(address => bool) public possibleLidoRecipients; + function setUp() public { BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); @@ -235,7 +253,9 @@ contract AccountingTest is BaseProtocolTest { lidoLocator.accountingOracle(), limitList, lidoLocator.elRewardsVault(), - address(secondOpinionOracleMock) + address(secondOpinionOracleMock), + lidoLocator.burner(), + lidoLocator.stakingRouter() ); // Set target contract to the accounting handler @@ -244,6 +264,13 @@ contract AccountingTest is BaseProtocolTest { vm.prank(userAccount); lidoContract.resume(); + possibleLidoRecipients[lidoLocator.burner()] = true; + possibleLidoRecipients[lidoLocator.treasury()] = true; + + for (uint256 i = 0; i < accountingHandler.stakingRouter().getRecipients().length; i++) { + possibleLidoRecipients[accountingHandler.stakingRouter().getRecipients()[i]] = true; + } + // Set target selectors to the accounting handler bytes4[] memory selectors = new bytes4[](1); selectors[0] = accountingHandler.handleOracleReport.selector; @@ -251,30 +278,38 @@ contract AccountingTest is BaseProtocolTest { targetSelector(FuzzSelector({addr: address(accountingHandler), selectors: selectors})); } - // - user tokens must not be used except burner as source (from Zero / to Zero). From burner to zerop - // - from zero to Treasure, burner - // // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal // - vault params do not affect protocol share rate - //} + // /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 256 - * forge-config: default.invariant.depth = 256 + * forge-config: default.invariant.runs = 128 + * forge-config: default.invariant.depth = 128 * forge-config: default.invariant.fail-on-revert = true */ - function invariant_handleOracleReport() public view { + function invariant_clValidatorNotDecreased() public view { ILido lido = ILido(lidoLocator.lido()); (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); // Should not be able to decrease validator number assertGe(clValidators, uint256(accountingHandler.getGhost().clValidators)); assertEq(depositedValidators, uint256(accountingHandler.getGhost().depositedValidators)); + } + + /** + * 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) + * CLb + ELr <= 10% + * + * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs + * forge-config: default.invariant.runs = 128 + * forge-config: default.invariant.depth = 128 + * forge-config: default.invariant.fail-on-revert = true + */ + function invariant_NonNegativeRebase() public view { + ILido lido = ILido(lidoLocator.lido()); - // - 0 OR 10% OF PROTOCOL FEES SHOULD BE REPORTED (Collect total fees from reports in handler) - // CLb + ELr <= 10% - if (accountingHandler.getGhost().unifiedClBalance > accountingHandler.getGhost().principalClBalance) { + if (accountingHandler.getGhost().unifiedClBalanceWei > accountingHandler.getGhost().principalClBalanceWei) { if (accountingHandler.getGhost().sharesMintAsFees < 0) { revert("sharesMintAsFees < 0"); } @@ -290,17 +325,36 @@ contract AccountingTest is BaseProtocolTest { lido.getPooledEthByShares(uint256(accountingHandler.getGhost().transferShares)) ); int256 totalFees = int256(treasuryFeesETH + reportRewardsMintedETH); - int256 totalRewards = accountingHandler.getGhost().totalRewards; + int256 totalRewards = accountingHandler.getGhost().totalRewardsWei; if (totalRewards != 0) { int256 percents = (totalFees * 100) / totalRewards; - console2.log("percents", percents); assertTrue(percents <= 10, "all distributed rewards > 10%"); - assertTrue(percents > 0, "all distributed rewards < 0%"); + assertTrue(percents >= 0, "all distributed rewards < 0%"); } } else { - console2.log("Negative rebase. Skipping report", accountingHandler.getGhost().totalRewards / 1 ether); + console2.log("Negative rebase. Skipping report", accountingHandler.getGhost().totalRewardsWei / 1 ether); + } + } + + /** + * Lido.Transfer from (0x00, to treasure or burner. Other -> collect and check what is it) + * + * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs + * forge-config: default.invariant.runs = 128 + * forge-config: default.invariant.depth = 128 + * forge-config: default.invariant.fail-on-revert = true + */ + function invariant_LidoTransfers() public view { + LidoTransfer[] memory lidoTransfers = accountingHandler.getLidoTransfers(); + + for (uint256 i = 0; i < lidoTransfers.length; i++) { + assertEq(lidoTransfers[i].from, address(0), "Lido.Transfer sender is not zero"); + assertTrue( + possibleLidoRecipients[lidoTransfers[i].to], + "Lido.Transfer recipient is not possibleLidoRecipients" + ); } } } From 8b88a72cbfc9086001ec656da9d447172eeaafce Mon Sep 17 00:00:00 2001 From: Sergey White Date: Tue, 4 Feb 2025 17:28:37 +0300 Subject: [PATCH 18/22] feat: check invariant_vaultsDonAffectSharesRate --- .../StakingRouter__MockForLidoAccounting.sol | 92 +----------- ...ngRouter__MockForLidoAccountingFuzzing.sol | 131 ++++++++++++++++++ test/0.8.25/Accounting.t.sol | 60 ++++++-- test/0.8.25/Protocol__Deployment.t.sol | 15 +- test/0.8.25/ShareRate.t.sol | 3 + 5 files changed, 194 insertions(+), 107 deletions(-) create mode 100644 test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol diff --git a/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol b/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol index a168bbc68..fc1890f8b 100644 --- a/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol +++ b/test/0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol @@ -1,13 +1,9 @@ // SPDX-License-Identifier: UNLICENSED // for testing purposes only - -pragma solidity 0.8.9; - -import {StakingRouter} from "contracts/0.8.9/StakingRouter.sol"; +pragma solidity 0.4.24; contract StakingRouter__MockForLidoAccounting { event Mock__MintedRewardsReported(); - event Mock__MintedTotalShares(uint256 indexed _totalShares); address[] private recipients__mocked; uint256[] private stakingModuleIds__mocked; @@ -33,21 +29,14 @@ contract StakingRouter__MockForLidoAccounting { precisionPoints = precisionPoint__mocked; } - function reportRewardsMinted(uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares) external { + function reportRewardsMinted(uint256[], uint256[]) external { emit Mock__MintedRewardsReported(); - - uint256 totalShares = 0; - for (uint256 i = 0; i < _totalShares.length; i++) { - totalShares += _totalShares[i]; - } - - emit Mock__MintedTotalShares(totalShares); } function mock__getStakingRewardsDistribution( - address[] calldata _recipients, - uint256[] calldata _stakingModuleIds, - uint96[] calldata _stakingModuleFees, + address[] _recipients, + uint256[] _stakingModuleIds, + uint96[] _stakingModuleFees, uint96 _totalFee, uint256 _precisionPoints ) external { @@ -57,75 +46,4 @@ contract StakingRouter__MockForLidoAccounting { totalFee__mocked = _totalFee; precisionPoint__mocked = _precisionPoints; } - - function getStakingModuleIds() public view returns (uint256[] memory) { - return stakingModuleIds__mocked; - } - - function getRecipients() public view returns (address[] memory) { - return recipients__mocked; - } - - function getStakingModule(uint256 _stakingModuleId) public view returns (StakingRouter.StakingModule memory) { - if (_stakingModuleId >= 4) { - revert("Staking module does not exist"); - } - - if (_stakingModuleId == 1) { - return - StakingRouter.StakingModule({ - id: 1, - stakingModuleAddress: 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5, - stakingModuleFee: 500, - treasuryFee: 500, - stakeShareLimit: 10000, - status: 0, - name: "curated-onchain-v1", - lastDepositAt: 1732694279, - lastDepositBlock: 21277744, - exitedValidatorsCount: 88207, - priorityExitShareThreshold: 10000, - maxDepositsPerBlock: 150, - minDepositBlockDistance: 25 - }); - } - - if (_stakingModuleId == 2) { - return - StakingRouter.StakingModule({ - id: 2, - stakingModuleAddress: 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433, - stakingModuleFee: 800, - treasuryFee: 200, - stakeShareLimit: 400, - status: 0, - name: "SimpleDVT", - lastDepositAt: 1735217831, - lastDepositBlock: 21486781, - exitedValidatorsCount: 5, - priorityExitShareThreshold: 444, - maxDepositsPerBlock: 150, - minDepositBlockDistance: 25 - }); - } - - if (_stakingModuleId == 3) { - return - StakingRouter.StakingModule({ - id: 3, - stakingModuleAddress: 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F, - stakingModuleFee: 600, - treasuryFee: 400, - stakeShareLimit: 100, - status: 0, - name: "Community Staking", - lastDepositAt: 1735217387, - lastDepositBlock: 21486745, - exitedValidatorsCount: 104, - priorityExitShareThreshold: 125, - maxDepositsPerBlock: 30, - minDepositBlockDistance: 25 - }); - } - } } diff --git a/test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol b/test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol new file mode 100644 index 000000000..6708c5371 --- /dev/null +++ b/test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +import {StakingRouter} from "contracts/0.8.9/StakingRouter.sol"; + +contract StakingRouter__MockForLidoAccountingFuzzing { + event Mock__MintedRewardsReported(); + event Mock__MintedTotalShares(uint256 indexed _totalShares); + + address[] private recipients__mocked; + uint256[] private stakingModuleIds__mocked; + uint96[] private stakingModuleFees__mocked; + uint96 private totalFee__mocked; + uint256 private precisionPoint__mocked; + + function getStakingRewardsDistribution() + public + view + returns ( + address[] memory recipients, + uint256[] memory stakingModuleIds, + uint96[] memory stakingModuleFees, + uint96 totalFee, + uint256 precisionPoints + ) + { + recipients = recipients__mocked; + stakingModuleIds = stakingModuleIds__mocked; + stakingModuleFees = stakingModuleFees__mocked; + totalFee = totalFee__mocked; + precisionPoints = precisionPoint__mocked; + } + + function reportRewardsMinted(uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares) external { + emit Mock__MintedRewardsReported(); + + uint256 totalShares = 0; + for (uint256 i = 0; i < _totalShares.length; i++) { + totalShares += _totalShares[i]; + } + + emit Mock__MintedTotalShares(totalShares); + } + + function mock__getStakingRewardsDistribution( + address[] calldata _recipients, + uint256[] calldata _stakingModuleIds, + uint96[] calldata _stakingModuleFees, + uint96 _totalFee, + uint256 _precisionPoints + ) external { + recipients__mocked = _recipients; + stakingModuleIds__mocked = _stakingModuleIds; + stakingModuleFees__mocked = _stakingModuleFees; + totalFee__mocked = _totalFee; + precisionPoint__mocked = _precisionPoints; + } + + function getStakingModuleIds() public view returns (uint256[] memory) { + return stakingModuleIds__mocked; + } + + function getRecipients() public view returns (address[] memory) { + return recipients__mocked; + } + + function getStakingModule(uint256 _stakingModuleId) public view returns (StakingRouter.StakingModule memory) { + if (_stakingModuleId >= 4) { + revert("Staking module does not exist"); + } + + if (_stakingModuleId == 1) { + return + StakingRouter.StakingModule({ + id: 1, + stakingModuleAddress: 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5, + stakingModuleFee: 500, + treasuryFee: 500, + stakeShareLimit: 10000, + status: 0, + name: "curated-onchain-v1", + lastDepositAt: 1732694279, + lastDepositBlock: 21277744, + exitedValidatorsCount: 88207, + priorityExitShareThreshold: 10000, + maxDepositsPerBlock: 150, + minDepositBlockDistance: 25 + }); + } + + if (_stakingModuleId == 2) { + return + StakingRouter.StakingModule({ + id: 2, + stakingModuleAddress: 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433, + stakingModuleFee: 800, + treasuryFee: 200, + stakeShareLimit: 400, + status: 0, + name: "SimpleDVT", + lastDepositAt: 1735217831, + lastDepositBlock: 21486781, + exitedValidatorsCount: 5, + priorityExitShareThreshold: 444, + maxDepositsPerBlock: 150, + minDepositBlockDistance: 25 + }); + } + + if (_stakingModuleId == 3) { + return + StakingRouter.StakingModule({ + id: 3, + stakingModuleAddress: 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F, + stakingModuleFee: 600, + treasuryFee: 400, + stakeShareLimit: 100, + status: 0, + name: "Community Staking", + lastDepositAt: 1735217387, + lastDepositBlock: 21486745, + exitedValidatorsCount: 104, + priorityExitShareThreshold: 125, + maxDepositsPerBlock: 30, + minDepositBlockDistance: 25 + }); + } + } +} diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index f67fd7d88..e0f7e9fda 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -2,12 +2,11 @@ // for testing purposes only pragma solidity ^0.8.0; -import "foundry/lib/forge-std/src/Vm.sol"; import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; -import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; +import {console2} from "forge-std/console2.sol"; import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; @@ -26,6 +25,10 @@ interface IAccounting { interface ILido { function getTotalShares() external view returns (uint256); + function getBufferedEther() external view returns (uint256); + + function getExternalShares() external view returns (uint256); + function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); function resume() external; @@ -48,7 +51,7 @@ interface ISecondOpinionOracleMock { // 0.002792 * 10^18 // 0.0073 * 10^18 -uint256 constant maxYiedPerOperatorWei = 2_792_000_000_000_000; // which % of slashing could be? +uint256 constant maxYieldPerOperatorWei = 2_792_000_000_000_000; // which % of slashing could be? uint256 constant maxLossPerOperatorWei = 7_300_000_000_000_000; uint256 constant stableBalanceWei = 32 * 1 ether; @@ -120,7 +123,7 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { function handleOracleReport(FuzzValues memory fuzz) external { uint256 _timeElapsed = 86_400; - uint256 _timestamp = 1_737_366_566 + _timeElapsed; + uint256 _timestamp = block.timestamp + _timeElapsed; // cheatCode for // if (_report.timestamp >= block.timestamp) revert IncorrectReportTimestamp(_report.timestamp, block.timestamp); @@ -145,14 +148,14 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { ); uint256 minBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei - maxLossPerOperatorWei); - uint256 maxBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei + maxYiedPerOperatorWei); + uint256 maxBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei + maxYieldPerOperatorWei); fuzz._clBalanceWei = bound(fuzz._clBalanceWei, minBalancePerValidatorWei, maxBalancePerValidatorWei); // depositedValidators is always greater or equal to beaconValidators // Todo: Upper extremum ? uint256 depositedValidators = bound( fuzz._preClValidators, - fuzz._clValidators, + fuzz._clValidators + 1, fuzz._clValidators + limitList.appearedValidatorsPerDayLimit ); ghost.depositedValidators = int256(depositedValidators); @@ -278,10 +281,6 @@ contract AccountingTest is BaseProtocolTest { targetSelector(FuzzSelector({addr: address(accountingHandler), selectors: selectors})); } - // - solvency - stETH <> ETH = 1:1 - internal and total share rates are equal - // - vault params do not affect protocol share rate - // - /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs * forge-config: default.invariant.runs = 128 @@ -339,8 +338,7 @@ contract AccountingTest is BaseProtocolTest { } /** - * Lido.Transfer from (0x00, to treasure or burner. Other -> collect and check what is it) - * + * Lido.Transfer from (0x00, to treasure or burner. Other -> collect and check what is it) * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs * forge-config: default.invariant.runs = 128 * forge-config: default.invariant.depth = 128 @@ -357,4 +355,42 @@ contract AccountingTest is BaseProtocolTest { ); } } + + /** + * solvency - stETH <> ETH = 1:1 - internal and total share rates are equal + * vault params do not affect protocol share rate + * + * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs + * forge-config: default.invariant.runs = 128 + * forge-config: default.invariant.depth = 128 + * forge-config: default.invariant.fail-on-revert = true + */ + function invariant_vaultsDonAffectSharesRate() public view { + ILido lido = ILido(lidoLocator.lido()); + + uint256 totalShares = lido.getTotalShares(); + uint256 totalEth = lido.getBufferedEther(); + uint256 totalShareRate = totalEth / totalShares; + + console2.log("totalShares", totalShares); + console2.log("totalEth", totalEth); + console2.log("totalShareRate", totalShareRate); + + (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); + // clValidators can never be less than deposited ones. + uint256 transientEther = (depositedValidators - clValidators) * 32 ether; + console2.log("transientEther", transientEther); + + uint256 internalEther = totalEth + clBalance + transientEther; + console2.log("internalEther", internalEther); + uint256 internalShares = totalShares - lido.getExternalShares(); + console2.log("internalShares", internalShares); + console2.log("getExternalShares", lido.getExternalShares()); + + uint256 internalShareRate = internalEther / internalShares; + + console2.log("internalShareRate", internalShareRate); + + assertEq(totalShareRate, internalShareRate); + } } diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 909e3158c..2c32f11d7 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -1,19 +1,18 @@ // SPDX-License-Identifier: UNLICENSED // for testing purposes only - pragma solidity ^0.8.0; -import "../0.4.24/contracts/StakingRouter__MockForLidoAccounting.sol"; -import "../0.4.24/contracts/SecondOpinionOracle__Mock.sol"; import "forge-std/Test.sol"; import {CommonBase} from "forge-std/Base.sol"; -import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; -import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; -import {StdCheats} from "forge-std/StdCheats.sol"; - import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; + +import "../0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol"; +import "../0.4.24/contracts/SecondOpinionOracle__Mock.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; interface IAccounting { function initialize(address _admin) external; @@ -135,7 +134,7 @@ contract BaseProtocolTest is Test { acl.createPermission(userAccount, lidoProxyAddress, keccak256("RESUME_ROLE"), rootAccount); acl.createPermission(userAccount, lidoProxyAddress, keccak256("PAUSE_ROLE"), rootAccount); - StakingRouter__MockForLidoAccounting stakingRouter = new StakingRouter__MockForLidoAccounting(); + StakingRouter__MockForLidoAccountingFuzzing stakingRouter = new StakingRouter__MockForLidoAccountingFuzzing(); uint256[] memory stakingModuleIds = new uint256[](3); stakingModuleIds[0] = 1; diff --git a/test/0.8.25/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol index 15c2b823d..45af94b67 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -111,6 +111,9 @@ contract ShareRateTest is BaseProtocolTest { * forge-config: default.invariant.runs = 256 * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true + * + * TODO: Maybe add an invariant that lido.getExternalShares = startExternalBalance + mintedExternal - burnedExternal? + * So we'll know it something is odd inside a math for external shares? */ function invariant_totalShares() public view { assertEq(lidoContract.getTotalShares(), shareRateHandler.getTotalShares()); From e9a302d4175e9a3217d30594bdd58d834c7452c8 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 10 Feb 2025 17:49:42 +0000 Subject: [PATCH 19/22] feat: stabilize tests --- ...ngRouter__MockForLidoAccountingFuzzing.sol | 131 -------------- test/0.8.25/Accounting.t.sol | 163 +++++++++++++++--- test/0.8.25/Protocol__Deployment.t.sol | 10 +- ...inionOracle__MockForAccountingFuzzing.sol} | 4 +- ...ngRouter__MockForLidoAccountingFuzzing.sol | 146 ++++++++++++++++ 5 files changed, 289 insertions(+), 165 deletions(-) delete mode 100644 test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol rename test/{0.4.24/contracts/SecondOpinionOracle__Mock.sol => 0.8.25/contracts/SecondOpinionOracle__MockForAccountingFuzzing.sol} (85%) create mode 100644 test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol diff --git a/test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol b/test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol deleted file mode 100644 index 6708c5371..000000000 --- a/test/0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.8.9; - -import {StakingRouter} from "contracts/0.8.9/StakingRouter.sol"; - -contract StakingRouter__MockForLidoAccountingFuzzing { - event Mock__MintedRewardsReported(); - event Mock__MintedTotalShares(uint256 indexed _totalShares); - - address[] private recipients__mocked; - uint256[] private stakingModuleIds__mocked; - uint96[] private stakingModuleFees__mocked; - uint96 private totalFee__mocked; - uint256 private precisionPoint__mocked; - - function getStakingRewardsDistribution() - public - view - returns ( - address[] memory recipients, - uint256[] memory stakingModuleIds, - uint96[] memory stakingModuleFees, - uint96 totalFee, - uint256 precisionPoints - ) - { - recipients = recipients__mocked; - stakingModuleIds = stakingModuleIds__mocked; - stakingModuleFees = stakingModuleFees__mocked; - totalFee = totalFee__mocked; - precisionPoints = precisionPoint__mocked; - } - - function reportRewardsMinted(uint256[] calldata _stakingModuleIds, uint256[] calldata _totalShares) external { - emit Mock__MintedRewardsReported(); - - uint256 totalShares = 0; - for (uint256 i = 0; i < _totalShares.length; i++) { - totalShares += _totalShares[i]; - } - - emit Mock__MintedTotalShares(totalShares); - } - - function mock__getStakingRewardsDistribution( - address[] calldata _recipients, - uint256[] calldata _stakingModuleIds, - uint96[] calldata _stakingModuleFees, - uint96 _totalFee, - uint256 _precisionPoints - ) external { - recipients__mocked = _recipients; - stakingModuleIds__mocked = _stakingModuleIds; - stakingModuleFees__mocked = _stakingModuleFees; - totalFee__mocked = _totalFee; - precisionPoint__mocked = _precisionPoints; - } - - function getStakingModuleIds() public view returns (uint256[] memory) { - return stakingModuleIds__mocked; - } - - function getRecipients() public view returns (address[] memory) { - return recipients__mocked; - } - - function getStakingModule(uint256 _stakingModuleId) public view returns (StakingRouter.StakingModule memory) { - if (_stakingModuleId >= 4) { - revert("Staking module does not exist"); - } - - if (_stakingModuleId == 1) { - return - StakingRouter.StakingModule({ - id: 1, - stakingModuleAddress: 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5, - stakingModuleFee: 500, - treasuryFee: 500, - stakeShareLimit: 10000, - status: 0, - name: "curated-onchain-v1", - lastDepositAt: 1732694279, - lastDepositBlock: 21277744, - exitedValidatorsCount: 88207, - priorityExitShareThreshold: 10000, - maxDepositsPerBlock: 150, - minDepositBlockDistance: 25 - }); - } - - if (_stakingModuleId == 2) { - return - StakingRouter.StakingModule({ - id: 2, - stakingModuleAddress: 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433, - stakingModuleFee: 800, - treasuryFee: 200, - stakeShareLimit: 400, - status: 0, - name: "SimpleDVT", - lastDepositAt: 1735217831, - lastDepositBlock: 21486781, - exitedValidatorsCount: 5, - priorityExitShareThreshold: 444, - maxDepositsPerBlock: 150, - minDepositBlockDistance: 25 - }); - } - - if (_stakingModuleId == 3) { - return - StakingRouter.StakingModule({ - id: 3, - stakingModuleAddress: 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F, - stakingModuleFee: 600, - treasuryFee: 400, - stakeShareLimit: 100, - status: 0, - name: "Community Staking", - lastDepositAt: 1735217387, - lastDepositBlock: 21486745, - exitedValidatorsCount: 104, - priorityExitShareThreshold: 125, - maxDepositsPerBlock: 30, - minDepositBlockDistance: 25 - }); - } - } -} diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index d26d5dc98..c54bb9734 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: UNLICENSED // for testing purposes only + pragma solidity ^0.8.0; +import {Vm} from "forge-std/Vm.sol"; import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; -import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; import {BaseProtocolTest} from "./Protocol__Deployment.t.sol"; @@ -25,6 +26,8 @@ interface IAccounting { interface ILido { function getTotalShares() external view returns (uint256); + function getTotalPooledEther() external view returns (uint256); + function getBufferedEther() external view returns (uint256); function getExternalShares() external view returns (uint256); @@ -82,6 +85,19 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { int256 unifiedClBalanceWei; } + struct BoundaryValues { + uint256 minPreClValidators; + uint256 maxPreClValidators; + uint256 minClValidators; + uint256 maxClValidators; + uint256 minClBalanceWei; + uint256 maxClBalanceWei; + uint256 minDepositedValidators; + uint256 maxDepositedValidators; + uint256 minElRewardsVaultBalanceWei; + uint256 maxElRewardsVaultBalanceWei; + } + IAccounting private accounting; ILido private lido; ISecondOpinionOracleMock private secondOpinionOracle; @@ -89,6 +105,7 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { Ghost public ghost; LidoTransfer[] public ghost_lidoTransfers; + BoundaryValues public boundaryValues; address private accountingOracle; address private lidoExecutionLayerRewardVault; @@ -115,9 +132,23 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { secondOpinionOracle = ISecondOpinionOracleMock(_secondOpinionOracle); burner = _burnerAddress; stakingRouter = IStakingRouter(_stakingRouter); + + // Initialize boundary values with extreme values + boundaryValues = BoundaryValues({ + minPreClValidators: type(uint256).max, + maxPreClValidators: 0, + minClValidators: type(uint256).max, + maxClValidators: 0, + minClBalanceWei: type(uint256).max, + maxClBalanceWei: 0, + minDepositedValidators: type(uint256).max, + maxDepositedValidators: 0, + minElRewardsVaultBalanceWei: type(uint256).max, + maxElRewardsVaultBalanceWei: 0 + }); } - function cutGwei(uint256 value) public returns (uint256) { + function cutGwei(uint256 value) public pure returns (uint256) { return (value / 1 gwei) * 1 gwei; } @@ -136,9 +167,25 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { fuzz._lidoExecutionLayerRewardVaultWei ); + // Update boundary values for elRewardsVaultBalanceWei + if (fuzz._elRewardsVaultBalanceWei < boundaryValues.minElRewardsVaultBalanceWei) { + boundaryValues.minElRewardsVaultBalanceWei = fuzz._elRewardsVaultBalanceWei; + } + if (fuzz._elRewardsVaultBalanceWei > boundaryValues.maxElRewardsVaultBalanceWei) { + boundaryValues.maxElRewardsVaultBalanceWei = fuzz._elRewardsVaultBalanceWei; + } + fuzz._preClValidators = bound(fuzz._preClValidators, 250_000, 100_000_000_000); fuzz._preClBalanceWei = cutGwei(fuzz._preClValidators * stableBalanceWei); + // Update boundary values for preClValidators + if (fuzz._preClValidators < boundaryValues.minPreClValidators) { + boundaryValues.minPreClValidators = fuzz._preClValidators; + } + if (fuzz._preClValidators > boundaryValues.maxPreClValidators) { + boundaryValues.maxPreClValidators = fuzz._preClValidators; + } + ghost.clValidators = int256(fuzz._preClValidators); fuzz._clValidators = bound( @@ -147,10 +194,26 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { fuzz._preClValidators + limitList.appearedValidatorsPerDayLimit ); + // Update boundary values for clValidators + if (fuzz._clValidators < boundaryValues.minClValidators) { + boundaryValues.minClValidators = fuzz._clValidators; + } + if (fuzz._clValidators > boundaryValues.maxClValidators) { + boundaryValues.maxClValidators = fuzz._clValidators; + } + uint256 minBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei - maxLossPerOperatorWei); uint256 maxBalancePerValidatorWei = fuzz._clValidators * (stableBalanceWei + maxYieldPerOperatorWei); fuzz._clBalanceWei = bound(fuzz._clBalanceWei, minBalancePerValidatorWei, maxBalancePerValidatorWei); + // Update boundary values for clBalanceWei + if (fuzz._clBalanceWei < boundaryValues.minClBalanceWei) { + boundaryValues.minClBalanceWei = fuzz._clBalanceWei; + } + if (fuzz._clBalanceWei > boundaryValues.maxClBalanceWei) { + boundaryValues.maxClBalanceWei = fuzz._clBalanceWei; + } + // depositedValidators is always greater or equal to beaconValidators // Todo: Upper extremum ? uint256 depositedValidators = bound( @@ -158,6 +221,15 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { fuzz._clValidators + 1, fuzz._clValidators + limitList.appearedValidatorsPerDayLimit ); + + // Update boundary values for depositedValidators + if (depositedValidators < boundaryValues.minDepositedValidators) { + boundaryValues.minDepositedValidators = depositedValidators; + } + if (depositedValidators > boundaryValues.maxDepositedValidators) { + boundaryValues.maxDepositedValidators = depositedValidators; + } + ghost.depositedValidators = int256(depositedValidators); vm.store(address(lido), keccak256("lido.Lido.depositedValidators"), bytes32(depositedValidators)); @@ -235,6 +307,10 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { function getLidoTransfers() public view returns (LidoTransfer[] memory) { return ghost_lidoTransfers; } + + function getBoundaryValues() public view returns (BoundaryValues memory) { + return boundaryValues; + } } contract AccountingTest is BaseProtocolTest { @@ -281,19 +357,37 @@ contract AccountingTest is BaseProtocolTest { targetSelector(FuzzSelector({addr: address(accountingHandler), selectors: selectors})); } + function logBoundaryValues() internal view { + AccountingHandler.BoundaryValues memory bounds = accountingHandler.getBoundaryValues(); + console2.log("Boundary Values:"); + console2.log("PreClValidators min:", bounds.minPreClValidators); + console2.log("PreClValidators max:", bounds.maxPreClValidators); + console2.log("ClValidators min:", bounds.minClValidators); + console2.log("ClValidators max:", bounds.maxClValidators); + console2.log("ClBalanceWei min:", bounds.minClBalanceWei); + console2.log("ClBalanceWei max:", bounds.maxClBalanceWei); + console2.log("DepositedValidators min:", bounds.minDepositedValidators); + console2.log("DepositedValidators max:", bounds.maxDepositedValidators); + console2.log("ElRewardsVaultBalanceWei min:", bounds.minElRewardsVaultBalanceWei); + console2.log("ElRewardsVaultBalanceWei max:", bounds.maxElRewardsVaultBalanceWei); + } + /** * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 128 - * forge-config: default.invariant.depth = 128 + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true */ function invariant_clValidatorNotDecreased() public view { ILido lido = ILido(lidoLocator.lido()); - (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); + + (uint256 depositedValidators, uint256 clValidators, ) = lido.getBeaconStat(); // Should not be able to decrease validator number assertGe(clValidators, uint256(accountingHandler.getGhost().clValidators)); assertEq(depositedValidators, uint256(accountingHandler.getGhost().depositedValidators)); + + logBoundaryValues(); } /** @@ -301,30 +395,29 @@ contract AccountingTest is BaseProtocolTest { * CLb + ELr <= 10% * * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 128 - * forge-config: default.invariant.depth = 128 + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true */ function invariant_NonNegativeRebase() public view { ILido lido = ILido(lidoLocator.lido()); - if (accountingHandler.getGhost().unifiedClBalanceWei > accountingHandler.getGhost().principalClBalanceWei) { - if (accountingHandler.getGhost().sharesMintAsFees < 0) { + AccountingHandler.Ghost memory ghost = accountingHandler.getGhost(); + + bool isRebasePositive = ghost.unifiedClBalanceWei > ghost.principalClBalanceWei; + if (isRebasePositive) { + if (ghost.sharesMintAsFees < 0) { revert("sharesMintAsFees < 0"); } - if (accountingHandler.getGhost().transferShares < 0) { + if (ghost.transferShares < 0) { revert("transferShares < 0"); } - int256 treasuryFeesETH = int256( - lido.getPooledEthByShares(uint256(accountingHandler.getGhost().sharesMintAsFees)) - ); - int256 reportRewardsMintedETH = int256( - lido.getPooledEthByShares(uint256(accountingHandler.getGhost().transferShares)) - ); + int256 treasuryFeesETH = int256(lido.getPooledEthByShares(uint256(ghost.sharesMintAsFees))); + int256 reportRewardsMintedETH = int256(lido.getPooledEthByShares(uint256(ghost.transferShares))); int256 totalFees = int256(treasuryFeesETH + reportRewardsMintedETH); - int256 totalRewards = accountingHandler.getGhost().totalRewardsWei; + int256 totalRewards = ghost.totalRewardsWei; if (totalRewards != 0) { int256 percents = (totalFees * 100) / totalRewards; @@ -333,15 +426,17 @@ contract AccountingTest is BaseProtocolTest { assertTrue(percents >= 0, "all distributed rewards < 0%"); } } else { - console2.log("Negative rebase. Skipping report", accountingHandler.getGhost().totalRewardsWei / 1 ether); + console2.log("Negative rebase. Skipping report", ghost.totalRewardsWei / 1 ether); } + + logBoundaryValues(); } /** * Lido.Transfer from (0x00, to treasure or burner. Other -> collect and check what is it) * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 128 - * forge-config: default.invariant.depth = 128 + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true */ function invariant_LidoTransfers() public view { @@ -354,6 +449,8 @@ contract AccountingTest is BaseProtocolTest { "Lido.Transfer recipient is not possibleLidoRecipients" ); } + + logBoundaryValues(); } /** @@ -361,36 +458,46 @@ contract AccountingTest is BaseProtocolTest { * vault params do not affect protocol share rate * * https://book.getfoundry.sh/reference/config/inline-test-config#in-line-invariant-configs - * forge-config: default.invariant.runs = 128 - * forge-config: default.invariant.depth = 128 + * forge-config: default.invariant.runs = 256 + * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true */ function invariant_vaultsDonAffectSharesRate() public view { ILido lido = ILido(lidoLocator.lido()); + uint256 totalPooledEther = lido.getTotalPooledEther(); + uint256 bufferedEther = lido.getBufferedEther(); uint256 totalShares = lido.getTotalShares(); - uint256 totalEth = lido.getBufferedEther(); - uint256 totalShareRate = totalEth / totalShares; + uint256 externalShares = lido.getExternalShares(); + + uint256 totalShareRate = totalPooledEther / totalShares; + console2.log("bufferedEther", bufferedEther); + console2.log("totalPooledEther", totalPooledEther); console2.log("totalShares", totalShares); - console2.log("totalEth", totalEth); console2.log("totalShareRate", totalShareRate); + // Get transient ether (uint256 depositedValidators, uint256 clValidators, uint256 clBalance) = lido.getBeaconStat(); // clValidators can never be less than deposited ones. uint256 transientEther = (depositedValidators - clValidators) * 32 ether; console2.log("transientEther", transientEther); - uint256 internalEther = totalEth + clBalance + transientEther; + // Calculate internal ether + uint256 internalEther = bufferedEther + clBalance + transientEther; console2.log("internalEther", internalEther); - uint256 internalShares = totalShares - lido.getExternalShares(); + + // Calculate internal shares + uint256 internalShares = totalShares - externalShares; console2.log("internalShares", internalShares); - console2.log("getExternalShares", lido.getExternalShares()); + console2.log("getExternalShares", externalShares); uint256 internalShareRate = internalEther / internalShares; console2.log("internalShareRate", internalShareRate); assertEq(totalShareRate, internalShareRate); + + logBoundaryValues(); } } diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 2c32f11d7..6a56f98c2 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: UNLICENSED // for testing purposes only + pragma solidity ^0.8.0; import "forge-std/Test.sol"; @@ -9,11 +10,12 @@ import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; -import "../0.4.24/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol"; -import "../0.4.24/contracts/SecondOpinionOracle__Mock.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; +import {StakingRouter__MockForLidoAccountingFuzzing} from "./contracts/StakingRouter__MockForLidoAccountingFuzzing.sol"; +import {SecondOpinionOracle__MockForAccountingFuzzing} from "./contracts/SecondOpinionOracle__MockForAccountingFuzzing.sol"; + interface IAccounting { function initialize(address _admin) external; } @@ -84,7 +86,7 @@ contract BaseProtocolTest is Test { ILido public lidoContract; ILidoLocator public lidoLocator; IACL public acl; - SecondOpinionOracle__Mock public secondOpinionOracleMock; + SecondOpinionOracle__MockForAccountingFuzzing public secondOpinionOracleMock; IKernel private dao; address private rootAccount; @@ -222,7 +224,7 @@ contract BaseProtocolTest is Test { lidoLocator.oracleReportSanityChecker() ); - secondOpinionOracleMock = new SecondOpinionOracle__Mock(); + secondOpinionOracleMock = new SecondOpinionOracle__MockForAccountingFuzzing(); vm.store( lidoLocator.oracleReportSanityChecker(), bytes32(uint256(2)), diff --git a/test/0.4.24/contracts/SecondOpinionOracle__Mock.sol b/test/0.8.25/contracts/SecondOpinionOracle__MockForAccountingFuzzing.sol similarity index 85% rename from test/0.4.24/contracts/SecondOpinionOracle__Mock.sol rename to test/0.8.25/contracts/SecondOpinionOracle__MockForAccountingFuzzing.sol index 6b9504d38..519d67e19 100644 --- a/test/0.4.24/contracts/SecondOpinionOracle__Mock.sol +++ b/test/0.8.25/contracts/SecondOpinionOracle__MockForAccountingFuzzing.sol @@ -3,14 +3,14 @@ pragma solidity 0.8.9; -contract SecondOpinionOracle__Mock { +contract SecondOpinionOracle__MockForAccountingFuzzing { bool private success; uint256 private clBalanceGwei; uint256 private withdrawalVaultBalanceWei; uint256 private totalDepositedValidators; uint256 private totalExitedValidators; - function getReport(uint256 refSlot) external view returns (bool, uint256, uint256, uint256, uint256) { + function getReport(uint256) external view returns (bool, uint256, uint256, uint256, uint256) { return (success, clBalanceGwei, withdrawalVaultBalanceWei, totalDepositedValidators, totalExitedValidators); } diff --git a/test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol b/test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol new file mode 100644 index 000000000..e861b7f6e --- /dev/null +++ b/test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +interface IStakingRouter { + struct StakingModule { + uint256 id; + address stakingModuleAddress; + uint96 stakingModuleFee; + uint96 treasuryFee; + uint256 stakeShareLimit; + uint256 status; + string name; + uint256 lastDepositAt; + uint256 lastDepositBlock; + uint256 exitedValidatorsCount; + uint256 priorityExitShareThreshold; + uint256 maxDepositsPerBlock; + uint256 minDepositBlockDistance; + } +} + +contract StakingRouter__MockForLidoAccountingFuzzing { + event Mock__MintedRewardsReported(); + event Mock__MintedTotalShares(uint256 indexed _totalShares); + + address[] private recipients__mocked; + uint256[] private stakingModuleIds__mocked; + uint96[] private stakingModuleFees__mocked; + uint96 private totalFee__mocked; + uint256 private precisionPoint__mocked; + + function getStakingRewardsDistribution() + public + view + returns ( + address[] memory recipients, + uint256[] memory stakingModuleIds, + uint96[] memory stakingModuleFees, + uint96 totalFee, + uint256 precisionPoints + ) + { + recipients = recipients__mocked; + stakingModuleIds = stakingModuleIds__mocked; + stakingModuleFees = stakingModuleFees__mocked; + totalFee = totalFee__mocked; + precisionPoints = precisionPoint__mocked; + } + + function reportRewardsMinted(uint256[] calldata, uint256[] calldata _totalShares) external { + emit Mock__MintedRewardsReported(); + + uint256 totalShares = 0; + for (uint256 i = 0; i < _totalShares.length; i++) { + totalShares += _totalShares[i]; + } + + emit Mock__MintedTotalShares(totalShares); + } + + function mock__getStakingRewardsDistribution( + address[] calldata _recipients, + uint256[] calldata _stakingModuleIds, + uint96[] calldata _stakingModuleFees, + uint96 _totalFee, + uint256 _precisionPoints + ) external { + recipients__mocked = _recipients; + stakingModuleIds__mocked = _stakingModuleIds; + stakingModuleFees__mocked = _stakingModuleFees; + totalFee__mocked = _totalFee; + precisionPoint__mocked = _precisionPoints; + } + + function getStakingModuleIds() public view returns (uint256[] memory) { + return stakingModuleIds__mocked; + } + + function getRecipients() public view returns (address[] memory) { + return recipients__mocked; + } + + function getStakingModule( + uint256 _stakingModuleId + ) public pure returns (IStakingRouter.StakingModule memory stakingModule) { + if (_stakingModuleId >= 4) { + revert("Staking module does not exist"); + } + + if (_stakingModuleId == 1) { + stakingModule = IStakingRouter.StakingModule({ + id: 1, + stakingModuleAddress: 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5, + stakingModuleFee: 500, + treasuryFee: 500, + stakeShareLimit: 10000, + status: 0, + name: "curated-onchain-v1", + lastDepositAt: 1732694279, + lastDepositBlock: 21277744, + exitedValidatorsCount: 88207, + priorityExitShareThreshold: 10000, + maxDepositsPerBlock: 150, + minDepositBlockDistance: 25 + }); + } + + if (_stakingModuleId == 2) { + stakingModule = IStakingRouter.StakingModule({ + id: 2, + stakingModuleAddress: 0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433, + stakingModuleFee: 800, + treasuryFee: 200, + stakeShareLimit: 400, + status: 0, + name: "SimpleDVT", + lastDepositAt: 1735217831, + lastDepositBlock: 21486781, + exitedValidatorsCount: 5, + priorityExitShareThreshold: 444, + maxDepositsPerBlock: 150, + minDepositBlockDistance: 25 + }); + } + + if (_stakingModuleId == 3) { + stakingModule = IStakingRouter.StakingModule({ + id: 3, + stakingModuleAddress: 0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F, + stakingModuleFee: 600, + treasuryFee: 400, + stakeShareLimit: 100, + status: 0, + name: "Community Staking", + lastDepositAt: 1735217387, + lastDepositBlock: 21486745, + exitedValidatorsCount: 104, + priorityExitShareThreshold: 125, + maxDepositsPerBlock: 30, + minDepositBlockDistance: 25 + }); + } + } +} From 5fa608a1554212e7c208e329d1d8b83f5a45a5d7 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Wed, 26 Feb 2025 18:08:33 +0300 Subject: [PATCH 20/22] feat: wip fuzzing shareRate --- test/0.8.25/Protocol__Deployment.t.sol | 49 ++++++- test/0.8.25/ShareRate.t.sol | 177 +++++++++++++++++++++++-- 2 files changed, 208 insertions(+), 18 deletions(-) diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 6a56f98c2..ac819895f 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -15,11 +15,20 @@ import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecke import {StakingRouter__MockForLidoAccountingFuzzing} from "./contracts/StakingRouter__MockForLidoAccountingFuzzing.sol"; import {SecondOpinionOracle__MockForAccountingFuzzing} from "./contracts/SecondOpinionOracle__MockForAccountingFuzzing.sol"; +import {WithdrawalQueue, IWstETH} from "../../contracts/0.8.9/WithdrawalQueue.sol"; +import {WithdrawalQueueERC721} from "../../contracts/0.8.9/WithdrawalQueueERC721.sol"; interface IAccounting { function initialize(address _admin) external; } +struct StakeLimitStateData { + uint32 prevStakeBlockNumber; // block number of the previous stake submit + uint96 prevStakeLimit; // limit value (<= `maxStakeLimit`) obtained on the previous stake submit + uint32 maxStakeLimitGrowthBlocks; // limit regeneration speed expressed in blocks + uint96 maxStakeLimit; // maximum limit value +} + interface ILido { function getTotalShares() external view returns (uint256); @@ -38,6 +47,23 @@ interface ILido { function resume() external; function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock) external; + + function transfer(address _recipient, uint256 _amount) external returns (bool); + + function submit(address _referral) external payable returns (uint256); + + function getStakeLimitFullInfo() + external + view + returns ( + bool isStakingPaused_, + bool isStakingLimitSet, + uint256 currentStakeLimit, + uint256 maxStakeLimit, + uint256 maxStakeLimitGrowthBlocks, + uint256 prevStakeLimit, + uint256 prevStakeBlockNumber + ); } interface IKernel { @@ -85,6 +111,7 @@ struct LidoLocatorConfig { contract BaseProtocolTest is Test { ILido public lidoContract; ILidoLocator public lidoLocator; + WithdrawalQueue public wq; IACL public acl; SecondOpinionOracle__MockForAccountingFuzzing public secondOpinionOracleMock; IKernel private dao; @@ -98,8 +125,10 @@ contract BaseProtocolTest is Test { address public daoFactoryAdr; uint256 public genesisTimestamp = 1_695_902_400; - address private depositContract = address(0x4242424242424242424242424242424242424242); - address public lidoTreasury = makeAddr("dummy-lido:treasury"); + address private depositContractAdr = address(0x4242424242424242424242424242424242424242); + address private withdrawalQueueAdr = makeAddr("dummy-locator:withdrawalQueue"); + address public lidoTreasuryAdr = makeAddr("dummy-lido:treasury"); + address public wstETHAdr = makeAddr("dummy-locator:wstETH"); LimitsList public limitList = LimitsList({ @@ -124,6 +153,7 @@ contract BaseProtocolTest is Test { vm.startPrank(rootAccount); (dao, acl) = createAragonDao(); + address lidoProxyAddress = addAragonApp(dao, impl); lidoContract = ILido(lidoProxyAddress); @@ -197,7 +227,7 @@ contract BaseProtocolTest is Test { // Add burner contract to the protocol deployCodeTo( "LidoExecutionLayerRewardsVault.sol:LidoExecutionLayerRewardsVault", - abi.encode(lidoProxyAddress, lidoTreasury), + abi.encode(lidoProxyAddress, lidoTreasuryAdr), lidoLocator.elRewardsVault() ); @@ -238,6 +268,15 @@ contract BaseProtocolTest is Test { lidoContract.initialize(address(lidoLocator), address(eip712steth)); + deployCodeTo("WstETH.sol:WstETH", abi.encode(lidoProxyAddress), wstETHAdr); + + wq = new WithdrawalQueueERC721(wstETHAdr, "withdrawalQueueERC721", "wstETH"); + vm.store(address(wq), keccak256("lido.Versioned.contractVersion"), bytes32(0)); + wq.initialize(rootAccount); + wq.grantRole(keccak256("RESUME_ROLE"), rootAccount); + + wq.resume(); + vm.stopPrank(); } @@ -291,11 +330,11 @@ contract BaseProtocolTest is Test { stakingRouter: stakingRouterAddress, treasury: makeAddr("dummy-locator:treasury"), validatorsExitBusOracle: makeAddr("dummy-locator:validatorsExitBusOracle"), - withdrawalQueue: makeAddr("dummy-locator:withdrawalQueue"), + withdrawalQueue: withdrawalQueueAdr, withdrawalVault: makeAddr("dummy-locator:withdrawalVault"), oracleDaemonConfig: makeAddr("dummy-locator:oracleDaemonConfig"), accounting: makeAddr("dummy-locator:accounting"), - wstETH: makeAddr("dummy-locator:wstETH") + wstETH: wstETHAdr }); return ILidoLocator(deployCode("LidoLocator.sol:LidoLocator", abi.encode(config))); diff --git a/test/0.8.25/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol index 45af94b67..af3643350 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -2,29 +2,64 @@ // for testing purposes only pragma solidity ^0.8.0; +import "./Protocol__Deployment.t.sol"; +import "@openzeppelin/contracts-v4.4/utils/StorageSlot.sol"; import "contracts/0.8.9/EIP712StETH.sol"; - +import {BaseProtocolTest, ILido} from "./Protocol__Deployment.t.sol"; import {CommonBase} from "forge-std/Base.sol"; + import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; +import {StorageSlot} from "@openzeppelin/contracts-v4.4/utils/StorageSlot.sol"; import {Vm} from "forge-std/Vm.sol"; +import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; import {console2} from "forge-std/console2.sol"; -import {BaseProtocolTest, ILido} from "./Protocol__Deployment.t.sol"; +uint256 constant ONE_DAY_IN_BLOCKS = 7_200; contract ShareRateHandler is CommonBase, StdCheats, StdUtils { + struct BoundaryValues { + address externalSharesRecipient; + uint256 mintedExternalShares; + uint256 burnExternalShares; + address transferRecipient; + uint256 transferAmount; + } + ILido public lidoContract; + WithdrawalQueue public wqContract; address public accounting; address public userAccount; + BoundaryValues public boundaryValues; + uint256 public maxAmountOfShares; - constructor(ILido _lido, address _accounting, address _userAccount, uint256 _maxAmountOfShares) { + mapping(address => uint256) public balances; + uint256[] public amountsQW; + + constructor( + ILido _lido, + WithdrawalQueue _wqContract, + address _accounting, + address _userAccount, + uint256 _maxAmountOfShares + ) { lidoContract = _lido; accounting = _accounting; userAccount = _userAccount; maxAmountOfShares = _maxAmountOfShares; + wqContract = _wqContract; + + // Initialize boundary values with extreme values + boundaryValues = BoundaryValues({ + externalSharesRecipient: makeAddr("randomRecipient"), + mintedExternalShares: 0, + burnExternalShares: 0, + transferRecipient: makeAddr("randomTransferRecipient"), + transferAmount: 0 + }); } function mintExternalShares(address _recipient, uint256 _amountOfShares) external { @@ -39,6 +74,10 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { lidoContract.resumeStaking(); vm.prank(accounting); + + boundaryValues.externalSharesRecipient = _recipient; + boundaryValues.mintedExternalShares = _amountOfShares; + lidoContract.mintExternalShares(_recipient, _amountOfShares); } @@ -54,19 +93,102 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { lidoContract.resumeStaking(); vm.prank(accounting); + + boundaryValues.burnExternalShares = _amountOfShares; + lidoContract.burnExternalShares(_amountOfShares); } function getTotalShares() external view returns (uint256) { return lidoContract.getTotalShares(); } + + function submit(address _sender, uint256 _amountETH) external payable returns (bool) { + if (_sender == address(0) || _amountETH == 0) { + return false; + } + + ( + bool isStakingPaused_, + bool isStakingLimitSet, + uint256 currentStakeLimit, + uint256 maxStakeLimit, + uint256 maxStakeLimitGrowthBlocks, + uint256 prevStakeLimit, + uint256 prevStakeBlockNumber + ) = lidoContract.getStakeLimitFullInfo(); + + if (_amountETH > 1000 ether || _amountETH == 0) { + _amountETH = bound(_amountETH, 1, 1000 ether); + } + + balances[_sender] += _amountETH; + vm.deal(_sender, _amountETH); + + vm.prank(_sender); + lidoContract.submit{value: _amountETH}(address(0)); + vm.roll(block.number + ONE_DAY_IN_BLOCKS); + return true; + } + + function transfer(address _sender, address _recipient, uint256 _amountTokens) external payable returns (bool) { + if ( + _recipient == address(0) || + _sender == address(0) || + _amountTokens == 0 || + _sender == _recipient || + _recipient == address(lidoContract) + ) { + return false; + } + + _amountTokens = bound(_amountTokens, 1, 1000 ether); + if (balances[_sender] == 0) { + console2.log("checking_sender_balance"); + vm.prank(_sender); + this.submit(_sender, _amountTokens); + } else { + console2.log("else:", balances[_sender]); + console2.log("else:", _sender.balance); + } + + console2.log("sender_balance", _sender.balance); + + _amountTokens = bound(_amountTokens, 1, balances[_sender]); + vm.prank(_sender); + lidoContract.transfer(_recipient, _amountTokens); + balances[_sender] -= _amountTokens; + + vm.roll(block.number + ONE_DAY_IN_BLOCKS); + + return true; + } + + function withdrawStEth(address _owner, uint256 _amountTokens) external payable returns (bool) { + if (_owner == address(0) || _amountTokens == 0 || balances[_owner] == 0) { + return false; + } + + _amountTokens = bound(_amountTokens, 1, balances[_owner]); + vm.prank(_owner); + + amountsQW.push(_amountTokens); + wqContract.requestWithdrawals(amountsQW, _owner); + amountsQW.pop(); + + return true; + } + + function getBoundaryValues() public view returns (BoundaryValues memory) { + return boundaryValues; + } } contract ShareRateTest is BaseProtocolTest { ShareRateHandler public shareRateHandler; uint256 private _maxExternalRatioBP = 10_000; - uint256 private _maxStakeLimit = 15_000 ether; + uint256 private _maxStakeLimit = 15_000_000 ether; uint256 private _stakeLimitIncreasePerBlock = 20 ether; uint256 private _maxAmountOfShares = 100; @@ -77,6 +199,7 @@ contract ShareRateTest is BaseProtocolTest { address private userAccount = address(0x321); function setUp() public { + keccak256("lido.StETH.totalShares"); BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); address accountingContract = lidoLocator.accounting(); @@ -87,23 +210,48 @@ contract ShareRateTest is BaseProtocolTest { lidoContract.resume(); vm.stopPrank(); - shareRateHandler = new ShareRateHandler(lidoContract, accountingContract, userAccount, _maxAmountOfShares); + shareRateHandler = new ShareRateHandler(lidoContract, wq, accountingContract, userAccount, _maxAmountOfShares); + + bytes4[] memory externalSharesSelectors = new bytes4[](3); + // externalSharesSelectors[0] = shareRateHandler.mintExternalShares.selector; + // externalSharesSelectors[1] = shareRateHandler.burnExternalShares.selector; + externalSharesSelectors[0] = shareRateHandler.submit.selector; + externalSharesSelectors[1] = shareRateHandler.transfer.selector; + externalSharesSelectors[2] = shareRateHandler.withdrawStEth.selector; + + // TODO: submit - lido + // TODO: transfers - steth + + // TODO: withdrawals request - requestWithdrawals - withdrawal queue + // TODO: claim - requestWithdrawals - withdrawal queue + targetContract(address(shareRateHandler)); + targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: externalSharesSelectors})); - bytes4[] memory selectors = new bytes4[](2); - selectors[0] = shareRateHandler.mintExternalShares.selector; - selectors[1] = shareRateHandler.burnExternalShares.selector; - // TODO: transfers - // TODO: submit - // TODO: withdrawals request - // TODO: claim + // bytes4[] memory actionsSelectors = new bytes4[](1); + // externalSharesSelectors[0] = shareRateHandler.transfer.selector; + // externalSharesSelectors[0] = shareRateHandler.submit.selector; - targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: selectors})); + // targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: actionsSelectors})); // @dev mint 10000 external shares to simulate some shares already minted, so // burnExternalShares will be able to actually burn some shares vm.prank(accountingContract); lidoContract.mintExternalShares(accountingContract, protocolStartExternalShares); + shareRateHandler.submit(makeAddr("randomAdr"), 10 ether); + + vm.roll(block.number + ONE_DAY_IN_BLOCKS); + } + + function logBoundaryValues() internal view { + ShareRateHandler.BoundaryValues memory bounds = shareRateHandler.getBoundaryValues(); + + console2.log("Boundary Values:"); + console2.log("External shares recipient:", bounds.externalSharesRecipient); + console2.log("Minted external shares:", bounds.mintedExternalShares); + console2.log("Burned external shares:", bounds.burnExternalShares); + console2.log("transfer recipient:", bounds.transferRecipient); + console2.log("transfer amount:", bounds.transferAmount); } /** @@ -117,5 +265,8 @@ contract ShareRateTest is BaseProtocolTest { */ function invariant_totalShares() public view { assertEq(lidoContract.getTotalShares(), shareRateHandler.getTotalShares()); + // assertEq(true, true); + + // logBoundaryValues(); } } From 9dd9bea9ace8cfd6008813a7deddb04160ae3949 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Thu, 27 Feb 2025 10:36:49 +0000 Subject: [PATCH 21/22] fix: make test work again --- test/0.8.25/Protocol__Deployment.t.sol | 5 ++++- test/0.8.25/ShareRate.t.sol | 25 +++++++------------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index ac819895f..7e1c8c868 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -130,6 +130,9 @@ contract BaseProtocolTest is Test { address public lidoTreasuryAdr = makeAddr("dummy-lido:treasury"); address public wstETHAdr = makeAddr("dummy-locator:wstETH"); + uint256 public constant VAULTS_LIMIT = 500; + uint256 public constant VAULTS_RELATIVE_SHARE_LIMIT = 10_00; + LimitsList public limitList = LimitsList({ exitedValidatorsPerDayLimit: 9000, @@ -197,7 +200,7 @@ contract BaseProtocolTest is Test { // Add accounting contract with handler to the protocol address accountingImpl = deployCode( "Accounting.sol:Accounting", - abi.encode([address(lidoLocator), lidoProxyAddress]) + abi.encode(address(lidoLocator), lidoProxyAddress, VAULTS_LIMIT, VAULTS_RELATIVE_SHARE_LIMIT) ); deployCodeTo( diff --git a/test/0.8.25/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol index af3643350..f4827c2ee 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -2,20 +2,20 @@ // for testing purposes only pragma solidity ^0.8.0; -import "./Protocol__Deployment.t.sol"; -import "@openzeppelin/contracts-v4.4/utils/StorageSlot.sol"; import "contracts/0.8.9/EIP712StETH.sol"; -import {BaseProtocolTest, ILido} from "./Protocol__Deployment.t.sol"; -import {CommonBase} from "forge-std/Base.sol"; -import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; +import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; -import {StorageSlot} from "@openzeppelin/contracts-v4.4/utils/StorageSlot.sol"; import {Vm} from "forge-std/Vm.sol"; -import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; import {console2} from "forge-std/console2.sol"; +import {StorageSlot} from "@openzeppelin/contracts-v4.4/utils/StorageSlot.sol"; + +import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; + +import {BaseProtocolTest, WithdrawalQueue, ILido} from "./Protocol__Deployment.t.sol"; + uint256 constant ONE_DAY_IN_BLOCKS = 7_200; contract ShareRateHandler is CommonBase, StdCheats, StdUtils { @@ -108,16 +108,6 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { return false; } - ( - bool isStakingPaused_, - bool isStakingLimitSet, - uint256 currentStakeLimit, - uint256 maxStakeLimit, - uint256 maxStakeLimitGrowthBlocks, - uint256 prevStakeLimit, - uint256 prevStakeBlockNumber - ) = lidoContract.getStakeLimitFullInfo(); - if (_amountETH > 1000 ether || _amountETH == 0) { _amountETH = bound(_amountETH, 1, 1000 ether); } @@ -199,7 +189,6 @@ contract ShareRateTest is BaseProtocolTest { address private userAccount = address(0x321); function setUp() public { - keccak256("lido.StETH.totalShares"); BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); address accountingContract = lidoLocator.accounting(); From 2dfe89e8a99ab6f0e0c76535e1d558bec7b5cf46 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 28 Feb 2025 14:58:03 +0300 Subject: [PATCH 22/22] feat: fuzzing shareRate --- test/0.8.25/Protocol__Deployment.t.sol | 6 +- test/0.8.25/ShareRate.t.sol | 244 ++++++++++++++++--------- 2 files changed, 158 insertions(+), 92 deletions(-) diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index ac819895f..801a3669a 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -64,6 +64,9 @@ interface ILido { uint256 prevStakeLimit, uint256 prevStakeBlockNumber ); + + function approve(address _spender, uint256 _amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); } interface IKernel { @@ -111,7 +114,7 @@ struct LidoLocatorConfig { contract BaseProtocolTest is Test { ILido public lidoContract; ILidoLocator public lidoLocator; - WithdrawalQueue public wq; + WithdrawalQueueERC721 public wq; IACL public acl; SecondOpinionOracle__MockForAccountingFuzzing public secondOpinionOracleMock; IKernel private dao; @@ -274,6 +277,7 @@ contract BaseProtocolTest is Test { vm.store(address(wq), keccak256("lido.Versioned.contractVersion"), bytes32(0)); wq.initialize(rootAccount); wq.grantRole(keccak256("RESUME_ROLE"), rootAccount); + wq.grantRole(keccak256("FINALIZE_ROLE"), rootAccount); wq.resume(); diff --git a/test/0.8.25/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol index af3643350..14707410a 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -2,18 +2,16 @@ // for testing purposes only pragma solidity ^0.8.0; -import "./Protocol__Deployment.t.sol"; -import "@openzeppelin/contracts-v4.4/utils/StorageSlot.sol"; +import "../../contracts/0.8.9/WithdrawalQueueBase.sol"; +import "../../contracts/0.8.9/WithdrawalQueueERC721.sol"; import "contracts/0.8.9/EIP712StETH.sol"; +import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; import {BaseProtocolTest, ILido} from "./Protocol__Deployment.t.sol"; -import {CommonBase} from "forge-std/Base.sol"; -import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; +import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; -import {StorageSlot} from "@openzeppelin/contracts-v4.4/utils/StorageSlot.sol"; import {Vm} from "forge-std/Vm.sol"; -import {console2} from "../../foundry/lib/forge-std/src/console2.sol"; import {console2} from "forge-std/console2.sol"; uint256 constant ONE_DAY_IN_BLOCKS = 7_200; @@ -23,32 +21,34 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { address externalSharesRecipient; uint256 mintedExternalShares; uint256 burnExternalShares; - address transferRecipient; - uint256 transferAmount; } ILido public lidoContract; - WithdrawalQueue public wqContract; + WithdrawalQueueERC721 public wqContract; address public accounting; address public userAccount; + address public rootAccount; BoundaryValues public boundaryValues; uint256 public maxAmountOfShares; - mapping(address => uint256) public balances; uint256[] public amountsQW; + address[] public users; + uint256 public constant userCount = 1_000; constructor( ILido _lido, - WithdrawalQueue _wqContract, + WithdrawalQueueERC721 _wqContract, address _accounting, address _userAccount, + address _rootAccount, uint256 _maxAmountOfShares ) { lidoContract = _lido; accounting = _accounting; userAccount = _userAccount; + rootAccount = _rootAccount; maxAmountOfShares = _maxAmountOfShares; wqContract = _wqContract; @@ -56,10 +56,15 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { boundaryValues = BoundaryValues({ externalSharesRecipient: makeAddr("randomRecipient"), mintedExternalShares: 0, - burnExternalShares: 0, - transferRecipient: makeAddr("randomTransferRecipient"), - transferAmount: 0 + burnExternalShares: 0 }); + + for (uint256 i = 0; i <= userCount; i++) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(i))); + address randomAddr = vm.addr(privateKey); + + users.push(randomAddr); + } } function mintExternalShares(address _recipient, uint256 _amountOfShares) external { @@ -67,8 +72,6 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { vm.assume(_recipient != address(0)); _amountOfShares = bound(_amountOfShares, 1, maxAmountOfShares); - // TODO: We need to make this condition work - // _amountOfShares = bound(_amountOfShares, 1, _amountOfShares); vm.prank(userAccount); lidoContract.resumeStaking(); @@ -103,78 +106,117 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { return lidoContract.getTotalShares(); } - function submit(address _sender, uint256 _amountETH) external payable returns (bool) { - if (_sender == address(0) || _amountETH == 0) { - return false; + function submit(uint256 _senderId, uint256 _amountETH) external payable returns (bool) { + if (_senderId > this.userCount()) { + _senderId = bound(_senderId, 0, this.userCount()); } - ( - bool isStakingPaused_, - bool isStakingLimitSet, - uint256 currentStakeLimit, - uint256 maxStakeLimit, - uint256 maxStakeLimitGrowthBlocks, - uint256 prevStakeLimit, - uint256 prevStakeBlockNumber - ) = lidoContract.getStakeLimitFullInfo(); - - if (_amountETH > 1000 ether || _amountETH == 0) { - _amountETH = bound(_amountETH, 1, 1000 ether); - } + address sender = users[_senderId]; - balances[_sender] += _amountETH; - vm.deal(_sender, _amountETH); + _amountETH = bound(_amountETH, 0.0005 ether, 1000 ether); + vm.deal(sender, _amountETH); - vm.prank(_sender); + vm.prank(sender); lidoContract.submit{value: _amountETH}(address(0)); + vm.roll(block.number + ONE_DAY_IN_BLOCKS); return true; } - function transfer(address _sender, address _recipient, uint256 _amountTokens) external payable returns (bool) { - if ( - _recipient == address(0) || - _sender == address(0) || - _amountTokens == 0 || - _sender == _recipient || - _recipient == address(lidoContract) - ) { + function getBalanceByUser(address _owner) public returns (uint256) { + return lidoContract.balanceOf(_owner); + } + + function transfer(uint256 _senderId, uint256 _recipientId, uint256 _amountTokens) external payable returns (bool) { + if (_senderId > this.userCount()) { + _senderId = bound(_senderId, 0, this.userCount()); + } + + if (_recipientId > this.userCount()) { + _recipientId = bound(_recipientId, 0, this.userCount()); + } + + if (_recipientId == _senderId) { return false; } - _amountTokens = bound(_amountTokens, 1, 1000 ether); - if (balances[_sender] == 0) { - console2.log("checking_sender_balance"); + address _sender = users[_senderId]; + address _recipient = users[_recipientId]; + + if (getBalanceByUser(_sender) == 0) { vm.prank(_sender); - this.submit(_sender, _amountTokens); - } else { - console2.log("else:", balances[_sender]); - console2.log("else:", _sender.balance); + this.submit(_senderId, _amountTokens); } - console2.log("sender_balance", _sender.balance); + _amountTokens = bound(_amountTokens, 1, getBalanceByUser(_sender)); - _amountTokens = bound(_amountTokens, 1, balances[_sender]); vm.prank(_sender); lidoContract.transfer(_recipient, _amountTokens); - balances[_sender] -= _amountTokens; - vm.roll(block.number + ONE_DAY_IN_BLOCKS); return true; } - function withdrawStEth(address _owner, uint256 _amountTokens) external payable returns (bool) { - if (_owner == address(0) || _amountTokens == 0 || balances[_owner] == 0) { - return false; + function withdrawStEth( + uint256 _ownerId, + uint256 _amountTokens, + uint256 maxShareRate + ) external payable returns (bool) { + if (_ownerId > this.userCount()) { + _ownerId = bound(_ownerId, 0, this.userCount()); + } + + address _owner = users[_ownerId]; + if (getBalanceByUser(_owner) == 0) { + vm.prank(_owner); + this.submit(_ownerId, _amountTokens); } - _amountTokens = bound(_amountTokens, 1, balances[_owner]); + if (getBalanceByUser(_owner) < wqContract.MIN_STETH_WITHDRAWAL_AMOUNT()) { + vm.prank(_owner); + this.submit(_ownerId, _amountTokens); + } + + uint256 userBalance = getBalanceByUser(_owner); + vm.prank(_owner); + lidoContract.approve(address(wqContract), userBalance); + vm.roll(block.number + 1); + + _amountTokens = bound(_amountTokens, wqContract.MIN_STETH_WITHDRAWAL_AMOUNT(), userBalance); + if (_amountTokens >= wqContract.MAX_STETH_WITHDRAWAL_AMOUNT()) { + while (_amountTokens >= wqContract.MAX_STETH_WITHDRAWAL_AMOUNT()) { + amountsQW.push(wqContract.MAX_STETH_WITHDRAWAL_AMOUNT()); + _amountTokens -= wqContract.MAX_STETH_WITHDRAWAL_AMOUNT(); + } + + if (_amountTokens > 0 && _amountTokens >= wqContract.MIN_STETH_WITHDRAWAL_AMOUNT()) { + amountsQW.push(_amountTokens); + } + } else { + amountsQW.push(_amountTokens); + } - amountsQW.push(_amountTokens); - wqContract.requestWithdrawals(amountsQW, _owner); - amountsQW.pop(); + vm.prank(_owner); + uint256[] memory requestIds = wqContract.requestWithdrawals(amountsQW, _owner); + delete amountsQW; + + vm.roll(block.number + 1_500); + vm.warp(block.timestamp + 1 days); + this.finalize(maxShareRate, _amountTokens + 10_000 * 1 ether); + + if (wqContract.getLastFinalizedRequestId() > 0) { + WithdrawalQueueBase.WithdrawalRequestStatus[] memory requestStatues = wqContract.getWithdrawalStatus( + requestIds + ); + for (uint256 i = 0; i < requestIds.length; i++) { + if (!requestStatues[i].isClaimed) { + vm.deal(_owner, 1 ether); + vm.prank(_owner); + wqContract.claimWithdrawal(requestIds[i]); + } + } + } return true; } @@ -182,6 +224,38 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { function getBoundaryValues() public view returns (BoundaryValues memory) { return boundaryValues; } + + function finalize(uint256 maxShareRate, uint256 ethBudget) public payable { + maxShareRate = bound(maxShareRate, 0.0001 * 10 ** 27, 100 * 10 ** 27); + + uint256[] memory batches = calculateBatches(ethBudget, maxShareRate); + + if (batches.length > 0) { + (uint256 eth, ) = wqContract.prefinalize(batches, maxShareRate); + + vm.deal(address(rootAccount), eth); + vm.prank(rootAccount); + wqContract.finalize{value: eth}(batches[batches.length - 1], maxShareRate); + } + } + + function calculateBatches(uint256 ethBudget, uint256 maxShareRate) public view returns (uint256[] memory batches) { + uint256[36] memory emptyBatches; + WithdrawalQueueBase.BatchesCalculationState memory state = WithdrawalQueueBase.BatchesCalculationState( + ethBudget * 1 ether, + false, + emptyBatches, + 0 + ); + while (!state.finished) { + state = wqContract.calculateFinalizationBatches(maxShareRate, block.timestamp + 1_000, 3, state); + } + + batches = new uint256[](state.batchesLength); + for (uint256 i; i < state.batchesLength; ++i) { + batches[i] = state.batches[i]; + } + } } contract ShareRateTest is BaseProtocolTest { @@ -199,7 +273,6 @@ contract ShareRateTest is BaseProtocolTest { address private userAccount = address(0x321); function setUp() public { - keccak256("lido.StETH.totalShares"); BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); address accountingContract = lidoLocator.accounting(); @@ -210,48 +283,41 @@ contract ShareRateTest is BaseProtocolTest { lidoContract.resume(); vm.stopPrank(); - shareRateHandler = new ShareRateHandler(lidoContract, wq, accountingContract, userAccount, _maxAmountOfShares); - - bytes4[] memory externalSharesSelectors = new bytes4[](3); - // externalSharesSelectors[0] = shareRateHandler.mintExternalShares.selector; - // externalSharesSelectors[1] = shareRateHandler.burnExternalShares.selector; - externalSharesSelectors[0] = shareRateHandler.submit.selector; - externalSharesSelectors[1] = shareRateHandler.transfer.selector; - externalSharesSelectors[2] = shareRateHandler.withdrawStEth.selector; - - // TODO: submit - lido - // TODO: transfers - steth - - // TODO: withdrawals request - requestWithdrawals - withdrawal queue - // TODO: claim - requestWithdrawals - withdrawal queue + shareRateHandler = new ShareRateHandler( + lidoContract, + wq, + accountingContract, + userAccount, + rootAccount, + _maxAmountOfShares + ); + + bytes4[] memory externalSharesSelectors = new bytes4[](5); + externalSharesSelectors[0] = shareRateHandler.mintExternalShares.selector; + externalSharesSelectors[1] = shareRateHandler.burnExternalShares.selector; + externalSharesSelectors[2] = shareRateHandler.submit.selector; + externalSharesSelectors[3] = shareRateHandler.transfer.selector; + externalSharesSelectors[4] = shareRateHandler.withdrawStEth.selector; targetContract(address(shareRateHandler)); targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: externalSharesSelectors})); - // bytes4[] memory actionsSelectors = new bytes4[](1); - // externalSharesSelectors[0] = shareRateHandler.transfer.selector; - // externalSharesSelectors[0] = shareRateHandler.submit.selector; - - // targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: actionsSelectors})); - // @dev mint 10000 external shares to simulate some shares already minted, so // burnExternalShares will be able to actually burn some shares vm.prank(accountingContract); lidoContract.mintExternalShares(accountingContract, protocolStartExternalShares); - shareRateHandler.submit(makeAddr("randomAdr"), 10 ether); + shareRateHandler.submit(0, 10 ether); vm.roll(block.number + ONE_DAY_IN_BLOCKS); } - function logBoundaryValues() internal view { + function logBoundaryValues() public view { ShareRateHandler.BoundaryValues memory bounds = shareRateHandler.getBoundaryValues(); console2.log("Boundary Values:"); console2.log("External shares recipient:", bounds.externalSharesRecipient); console2.log("Minted external shares:", bounds.mintedExternalShares); console2.log("Burned external shares:", bounds.burnExternalShares); - console2.log("transfer recipient:", bounds.transferRecipient); - console2.log("transfer amount:", bounds.transferAmount); } /** @@ -259,14 +325,10 @@ contract ShareRateTest is BaseProtocolTest { * forge-config: default.invariant.runs = 256 * forge-config: default.invariant.depth = 256 * forge-config: default.invariant.fail-on-revert = true - * - * TODO: Maybe add an invariant that lido.getExternalShares = startExternalBalance + mintedExternal - burnedExternal? - * So we'll know it something is odd inside a math for external shares? */ function invariant_totalShares() public view { assertEq(lidoContract.getTotalShares(), shareRateHandler.getTotalShares()); - // assertEq(true, true); - // logBoundaryValues(); + logBoundaryValues(); } }