From afc5fa1f826c9abcae2c1a9ac52c47bfecfc61ab Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 10 Jan 2025 17:40:41 +0300 Subject: [PATCH 01/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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/36] 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(); } } From 6e47b45a50e2eb2c5d91ffa6b318c30457832a50 Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 14 Mar 2025 12:08:54 +0300 Subject: [PATCH 23/36] feat: refactoring fuzzing StakingRouter mock --- test/0.8.25/Protocol__Deployment.t.sol | 6 - ...ngRouter__MockForLidoAccountingFuzzing.sol | 121 +++++++++--------- 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index dbe2259b5..1e7c25bea 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -174,11 +174,6 @@ contract BaseProtocolTest is Test { StakingRouter__MockForLidoAccountingFuzzing stakingRouter = new StakingRouter__MockForLidoAccountingFuzzing(); - 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; @@ -191,7 +186,6 @@ contract BaseProtocolTest is Test { stakingRouter.mock__getStakingRewardsDistribution( recipients, - stakingModuleIds, stakingModuleFees, 9999999999999999996, 100000000000000000000 diff --git a/test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol b/test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol index e861b7f6e..41db0a96e 100644 --- a/test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol +++ b/test/0.8.25/contracts/StakingRouter__MockForLidoAccountingFuzzing.sol @@ -26,11 +26,68 @@ contract StakingRouter__MockForLidoAccountingFuzzing { 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; + mapping(uint256 => IStakingRouter.StakingModule) private stakingModules; + uint256[] private stakingModulesIds; + + constructor() { + stakingModules[1] = 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 + }); + + stakingModulesIds.push(1); + + stakingModules[2] = 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 + }); + stakingModulesIds.push(2); + + stakingModules[3] = 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 + }); + + stakingModulesIds.push(3); + } + function getStakingRewardsDistribution() public view @@ -43,8 +100,8 @@ contract StakingRouter__MockForLidoAccountingFuzzing { ) { recipients = recipients__mocked; - stakingModuleIds = stakingModuleIds__mocked; stakingModuleFees = stakingModuleFees__mocked; + stakingModuleIds = stakingModulesIds; totalFee = totalFee__mocked; precisionPoints = precisionPoint__mocked; } @@ -62,20 +119,18 @@ contract StakingRouter__MockForLidoAccountingFuzzing { 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; + return stakingModulesIds; } function getRecipients() public view returns (address[] memory) { @@ -84,63 +139,11 @@ contract StakingRouter__MockForLidoAccountingFuzzing { function getStakingModule( uint256 _stakingModuleId - ) public pure returns (IStakingRouter.StakingModule memory stakingModule) { + ) public view 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 - }); - } + return stakingModules[_stakingModuleId]; } } From 7369dfea50d8ef3213d666ecd40ddafb13cf0be4 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Thu, 20 Mar 2025 12:40:58 +0200 Subject: [PATCH 24/36] feat: add solvency test for staking vault --- .../0.8.25/vaults/staking-vault/RandomLib.sol | 54 ++++ .../staking-vault/StakingVaultTest.t.sol | 271 ++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 test/0.8.25/vaults/staking-vault/RandomLib.sol create mode 100644 test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol diff --git a/test/0.8.25/vaults/staking-vault/RandomLib.sol b/test/0.8.25/vaults/staking-vault/RandomLib.sol new file mode 100644 index 000000000..471cc0844 --- /dev/null +++ b/test/0.8.25/vaults/staking-vault/RandomLib.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-v5.2/utils/math/Math.sol"; + +library RandomLib { + using Math for uint256; + + uint256 private constant Q96 = 2 ** 96; + uint256 private constant D18 = 1e18; + + struct Storage { + uint256 seed; + } + + function rand(Storage storage s) internal returns (uint256) { + s.seed = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, s.seed))); + return s.seed; + } + + function randInt(Storage storage s, uint256 maxValue) internal returns (uint256) { + return rand(s) % (maxValue + 1); + } + + function randInt(Storage storage s, uint256 minValue, uint256 maxValue) internal returns (uint256) { + if (maxValue < minValue) { + revert("RandomLib: maxValue < minValue"); + } + return (rand(s) % (maxValue - minValue + 1)) + minValue; + } + + function randFloatX96(Storage storage s, uint256 minValue, uint256 maxValue) internal returns (uint256) { + return randInt(s, minValue * Q96, maxValue * Q96); + } + + function randBool(Storage storage s) internal returns (bool) { + return rand(s) & 1 == 1; + } + + function randAddress(Storage storage s) internal returns (address) { + return address(uint160(rand(s))); + } + + function randAmountD18(Storage storage s) internal returns (uint256 result) { + uint256 result_x96 = randFloatX96(s, D18, 10 * D18); + if (randBool(s)) { + uint256 b_x96 = randFloatX96(s, 1, 1e6); + result = result_x96.mulDiv(b_x96, Q96) / Q96; + } else { + uint256 b_x96 = randFloatX96(s, 1e1, 1e10); + result = result_x96.mulDiv(Q96, b_x96) / Q96; + } + } +} diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol new file mode 100644 index 000000000..14a198aec --- /dev/null +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import {ERC1967Proxy} from "@openzeppelin/contracts-v5.2/proxy/ERC1967/ERC1967Proxy.sol"; +import {StakingVault} from "contracts/0.8.25/vaults/StakingVault.sol"; +import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol"; +import {DepositContract__MockForStakingVault} from "./contracts/DepositContract__MockForStakingVault.sol"; +import {VaultHub__MockForStakingVault} from "./contracts/VaultHub__MockForStakingVault.sol"; +import {RandomLib} from "./RandomLib.sol"; +import {console2} from "forge-std/console2.sol"; + +contract StakingVaultTest is Test { + using RandomLib for RandomLib.Storage; + + enum VaultState { + NoDepositsToBeaconChain, + DepositToBeaconChain + } + + uint256 constant ITERATIONS = 32; + uint256 constant MIN_REWARD = 0.001 ether; + uint256 constant MIN_FUND = 0.001 ether; + uint256 constant APR_MIN = 300; // 3.00% minimum APR + uint256 constant APR_MAX = 500; // 5.00% maximum APR + uint256 constant SECONDS_PER_DAY = 86400; + uint256 constant DAYS_PER_YEAR = 365; + uint256 constant APR_DENOMINATOR = 10000; + + StakingVault private stakingVault; + StakingVault private stakingVaultProxy; + VaultHub__MockForStakingVault private vaultHub; + + address[] private depositors; + uint256 private deposits; + uint256 private withdrawals; + uint256 private validatorBalance; + uint256 private rewards; + + RandomLib.Storage private rnd = RandomLib.Storage(42); + + address private depositor = address(0x001); + address private owner = address(0x002); + address private nodeOperator = address(0x003); + address private user = address(0x004); + + VaultState private vaultState; + mapping(VaultState => function() internal[]) internal stateTransitions; + + uint256 private lastRewardTimestamp; + uint256 private currentAPR; + + function setUp() public { + rnd.seed = vm.unixTime(); + vaultState = VaultState.NoDepositsToBeaconChain; + initializeTransitions(); + initializeRewards(); + } + + function testSolvencyAllTransitions() external { + deploy(); + + uint256 initialBalance = address(stakingVaultProxy).balance; + int256 initialInOutDelta = stakingVaultProxy.inOutDelta(); + + for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { + randomTransition(VaultState.NoDepositsToBeaconChain); + } + + transitionDepositToBeaconChain(); + + for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { + randomTransition(VaultState.DepositToBeaconChain); + } + + transitionValidatorWithdraw(); + + for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { + randomTransition(VaultState.NoDepositsToBeaconChain); + } + + uint256 finalBalance = address(stakingVaultProxy).balance; + int256 finalInOutDelta = stakingVaultProxy.inOutDelta(); + assertEq(deposits - withdrawals, finalBalance - initialBalance - rewards); + + console2.log("deltaBalance: %s", finalBalance - initialBalance); + console2.log("deltaDeposits: %s", deposits - withdrawals); + console2.log("deltaInOutDelta: %s", finalInOutDelta - initialInOutDelta); + } + + function initializeTransitions() internal { + function() internal[] memory noValidatorDepositsTransitions = new function() internal[](2); + noValidatorDepositsTransitions[0] = transitionRandomFund; + noValidatorDepositsTransitions[1] = transitionRandomWithdraw; + stateTransitions[VaultState.NoDepositsToBeaconChain] = noValidatorDepositsTransitions; + + function() internal[] memory withValidatorDepositsTransitions = new function() internal[](3); + withValidatorDepositsTransitions[0] = transitionRandomFund; + withValidatorDepositsTransitions[1] = transitionRandomWithdraw; + withValidatorDepositsTransitions[2] = transitionRandomReceiveReward; + stateTransitions[VaultState.DepositToBeaconChain] = withValidatorDepositsTransitions; + } + + function initializeRewards() internal { + lastRewardTimestamp = block.timestamp; + currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); + } + + function deploy() public { + DepositContract__MockForStakingVault depositContract = new DepositContract__MockForStakingVault(); + vaultHub = new VaultHub__MockForStakingVault(); + stakingVault = new StakingVault(address(vaultHub), depositor, address(depositContract)); + ERC1967Proxy proxy = new ERC1967Proxy( + address(stakingVault), + abi.encodeWithSelector(StakingVault.initialize.selector, owner, nodeOperator, "0x") + ); + stakingVaultProxy = StakingVault(payable(address(proxy))); + } + + function randomTransition(VaultState state) internal { + vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); + function() internal[] storage availableTransitions = stateTransitions[state]; + uint256 transitionIndex = rnd.randInt(availableTransitions.length - 1); + availableTransitions[transitionIndex](); + } + + function transitionRandomReceive() internal { + uint256 amount = rnd.randAmountD18(); + if (amount == 0) { + amount = MIN_FUND; + } + + uint256 userIndex; + if (rnd.randBool() || depositors.length == 0) { + address randomUser = rnd.randAddress(); + depositors.push(randomUser); + userIndex = depositors.length - 1; + } else { + userIndex = rnd.randInt(0, depositors.length - 1); + } + + address user = depositors[userIndex]; + deal(user, amount); + + vm.prank(user); + payable(address(stakingVaultProxy)).transfer(amount); + vm.stopPrank(); + + console2.log("transitionRandomReceive: %d", amount); + } + + function transitionRandomFund() internal { + uint256 amount = rnd.randAmountD18(); + if (amount == 0) { + amount = MIN_FUND; + } + + deal(owner, amount); + deposits += amount; + + vm.startPrank(owner); + stakingVaultProxy.fund{value: amount}(); + vm.stopPrank(); + + console2.log("transitionRandomFund: %s", amount); + } + + function transitionRandomWithdraw() internal { + uint256 availableAmount = stakingVaultProxy.unlocked(); + if (availableAmount == 0) { + return; + } + uint256 withdrawableAmount = rnd.randInt(availableAmount); + if (withdrawableAmount == 0) { + withdrawableAmount = availableAmount; + } + + deal(owner, withdrawableAmount); + withdrawals += withdrawableAmount; + + vm.prank(owner); + stakingVaultProxy.withdraw(owner, withdrawableAmount); + vm.stopPrank(); + + console2.log("transitionRandomWithdraw: %d", withdrawableAmount); + } + + function transitionRandomReceiveReward() internal { + uint256 timePassed = block.timestamp - lastRewardTimestamp; + if (timePassed < SECONDS_PER_DAY) { + return; + } + + uint256 dailyReward = dailyReward(timePassed); + + if (rnd.randBool()) { + currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); + } + + rewards += dailyReward; + vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + dailyReward); + } + + function dailyReward(uint256 timePassed) internal returns (uint256) { + uint256 daysPassed = timePassed / SECONDS_PER_DAY; + lastRewardTimestamp += daysPassed * SECONDS_PER_DAY; + + uint256 validatorBalance = 32 ether; + uint256 yearlyReward = (validatorBalance * currentAPR) / APR_DENOMINATOR; + uint256 dailyReward = (yearlyReward * daysPassed) / DAYS_PER_YEAR; + + int256 randomVariation = int256(rnd.randInt(200)) - 100; + dailyReward = uint256((int256(dailyReward) * (1000 + randomVariation)) / 1000); + + console2.log("transitionRandomReceiveReward: days=%d, apr=%d, reward=%d", daysPassed, currentAPR, dailyReward); + + return dailyReward; + } + + function transitionDepositToBeaconChain() internal { + console2.log("-------------------------------- transitionDepositToBeaconChain--------------------------------"); + + vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); + + vm.prank(owner); + deal(owner, 32 ether); + stakingVaultProxy.fund{value: 32 ether}(); + deposits += 32 ether; + vm.stopPrank(); + + vm.prank(depositor); + IStakingVault.Deposit[] memory deposits = new IStakingVault.Deposit[](1); + deposits[0] = IStakingVault.Deposit({ + pubkey: bytes.concat(bytes32(uint256(1))), + signature: bytes.concat(bytes32(uint256(2))), + amount: 32 ether, + depositDataRoot: bytes32(uint256(3)) + }); + stakingVaultProxy.depositToBeaconChain(deposits); + withdrawals += 32 ether; + } + + function transitionValidatorWithdraw() internal { + console2.log("-------------------------------- transitionValidatorWithdraw--------------------------------"); + + vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); + + vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + 32 ether); + deposits -= 32 ether; + + vm.prank(owner); + stakingVaultProxy.withdraw(owner, 32 ether); + vm.stopPrank(); + withdrawals -= 32 ether; + } + + function transitionRandomDepositToBeaconChain() internal { + vm.prank(depositor); + IStakingVault.Deposit[] memory deposits = new IStakingVault.Deposit[](1); + deposits[0] = IStakingVault.Deposit({ + pubkey: bytes.concat(bytes32(uint256(1))), + signature: bytes.concat(bytes32(uint256(2))), + amount: 0.1 ether, + depositDataRoot: bytes32(uint256(3)) + }); + stakingVaultProxy.depositToBeaconChain(deposits); + } +} From ad5a1dd19755961f1f615fcb06f684d182c3ff42 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 26 Mar 2025 00:07:55 +0200 Subject: [PATCH 25/36] fix: add vault connection tests --- .../staking-vault/StakingVaultTest.t.sol | 451 +++++++++++++----- 1 file changed, 336 insertions(+), 115 deletions(-) diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol index 14a198aec..a58b77e90 100644 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -3,41 +3,135 @@ pragma solidity ^0.8.0; -import "forge-std/Test.sol"; - +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import {Math} from "@openzeppelin/contracts-v5.2/utils/math/Math.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts-v5.2/proxy/ERC1967/ERC1967Proxy.sol"; import {StakingVault} from "contracts/0.8.25/vaults/StakingVault.sol"; import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol"; import {DepositContract__MockForStakingVault} from "./contracts/DepositContract__MockForStakingVault.sol"; import {VaultHub__MockForStakingVault} from "./contracts/VaultHub__MockForStakingVault.sol"; import {RandomLib} from "./RandomLib.sol"; -import {console2} from "forge-std/console2.sol"; -contract StakingVaultTest is Test { +contract VaultHubMock is Test { using RandomLib for RandomLib.Storage; - enum VaultState { - NoDepositsToBeaconChain, - DepositToBeaconChain + uint256 constant MAX_MINTABLE_RATIO_BP = 9000; // 90% can be used for minting (100% - reserve ratio) + uint256 constant TOTAL_BASIS_POINTS = 10000; + uint256 constant RESERVE_RATIO_BP = 1000; // 10% reserve ratio + uint256 constant REWARD_RATE_MIN_BP = 100; // 1% min reward rate + uint256 constant REWARD_RATE_MAX_BP = 500; // 5% max reward rate + uint256 constant TREASURY_FEE_BP = 500; // 5% treasury fee + uint256 constant REBALANCE_THRESHOLD_BP = 9500; // 95% - vault needs rebalance if valuation drops below this + + RandomLib.Storage private rnd; + + constructor(uint256 _seed) { + rnd.seed = _seed; } - uint256 constant ITERATIONS = 32; - uint256 constant MIN_REWARD = 0.001 ether; - uint256 constant MIN_FUND = 0.001 ether; + function totalEtherToLock(uint256 vaultValuation) public returns (uint256) { + uint256 maxMintableEther = (vaultValuation * MAX_MINTABLE_RATIO_BP) / TOTAL_BASIS_POINTS; + uint256 amountToMint = rnd.randInt(maxMintableEther); + if (amountToMint == 0) { + amountToMint = maxMintableEther; + } + return (amountToMint * TOTAL_BASIS_POINTS) / MAX_MINTABLE_RATIO_BP; + } + + function newValuation(uint256 currentValuation) public returns (uint256) { + uint256 rewardRateBP = REWARD_RATE_MIN_BP + rnd.randInt(REWARD_RATE_MAX_BP - REWARD_RATE_MIN_BP); + uint256 newValuation = currentValuation + (currentValuation * rewardRateBP) / TOTAL_BASIS_POINTS; + uint256 treasuryFee = ((newValuation - currentValuation) * TREASURY_FEE_BP) / TOTAL_BASIS_POINTS; + newValuation -= treasuryFee; + return newValuation; + } + + function newLocked(uint256 currentLocked) public returns (uint256) { + return rnd.randInt(currentLocked / 2, currentLocked); + } + + function amountToUnlock(uint256 currentValuation, uint256 currentLocked) public returns (uint256) { + uint256 minRequiredValuation = (currentLocked * TOTAL_BASIS_POINTS) / RESERVE_RATIO_BP; + uint256 rebalanceThreshold = (minRequiredValuation * REBALANCE_THRESHOLD_BP) / TOTAL_BASIS_POINTS; + + if (currentValuation >= rebalanceThreshold) { + return 0; + } + + uint256 targetLocked = (currentValuation * RESERVE_RATIO_BP) / TOTAL_BASIS_POINTS; + return currentLocked - targetLocked; + } +} + +contract ValidatorMock is Test { + using RandomLib for RandomLib.Storage; + + uint256 constant SECONDS_PER_DAY = 86400; + uint256 constant APR_DENOMINATOR = 10000; + uint256 constant DAYS_PER_YEAR = 365; uint256 constant APR_MIN = 300; // 3.00% minimum APR uint256 constant APR_MAX = 500; // 5.00% maximum APR + uint256 private validatorBalance; + + uint256 private currentAPR; + uint256 private lastRewardTimestamp; + RandomLib.Storage private rnd; + + constructor(uint256 _seed) { + rnd.seed = _seed; + lastRewardTimestamp = block.timestamp; + currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); + } + + function dailyReward() public returns (uint256) { + uint256 timePassed = block.timestamp - lastRewardTimestamp; + if (timePassed < SECONDS_PER_DAY) { + return 0; + } + + uint256 daysPassed = timePassed / SECONDS_PER_DAY; + lastRewardTimestamp += daysPassed * SECONDS_PER_DAY; + + uint256 validatorBalance = 32 ether; + uint256 yearlyReward = (validatorBalance * currentAPR) / APR_DENOMINATOR; + uint256 dailyReward = (yearlyReward * daysPassed) / DAYS_PER_YEAR; + + int256 randomVariation = int256(rnd.randInt(200)) - 100; + dailyReward = uint256((int256(dailyReward) * (1000 + randomVariation)) / 1000); + + if (rnd.randBool()) { + currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); + } + + return dailyReward; + } +} + +contract StakingVaultTest is Test { + using RandomLib for RandomLib.Storage; + + bool private isConnectedToHub; + bool private hasValidator; + + error LockedCannotDecreaseOutsideOfReport(uint256 currentlyLocked, uint256 attemptedLocked); + error NotAuthorized(string operation, address sender); + + uint256 constant ITERATIONS = 32; + uint256 constant MIN_REWARD = 0.001 ether; + uint256 constant MIN_FUND = 0.00001 ether; + uint256 constant VALIDATOR_DEPOSIT = 32 ether; + uint256 constant CONNECT_DEPOSIT = 1 ether; uint256 constant SECONDS_PER_DAY = 86400; - uint256 constant DAYS_PER_YEAR = 365; - uint256 constant APR_DENOMINATOR = 10000; StakingVault private stakingVault; StakingVault private stakingVaultProxy; - VaultHub__MockForStakingVault private vaultHub; + VaultHubMock private vaultHub; + ValidatorMock private validator; address[] private depositors; uint256 private deposits; uint256 private withdrawals; - uint256 private validatorBalance; uint256 private rewards; RandomLib.Storage private rnd = RandomLib.Storage(42); @@ -47,39 +141,26 @@ contract StakingVaultTest is Test { address private nodeOperator = address(0x003); address private user = address(0x004); - VaultState private vaultState; - mapping(VaultState => function() internal[]) internal stateTransitions; - - uint256 private lastRewardTimestamp; - uint256 private currentAPR; + function testSolvencyAllTransitions() external { + runTests(vm.unixTime()); + } - function setUp() public { - rnd.seed = vm.unixTime(); - vaultState = VaultState.NoDepositsToBeaconChain; - initializeTransitions(); - initializeRewards(); + function testFuzz_SolvencyAllTransitions(uint256 _seed) external { + runTests(_seed); } - function testSolvencyAllTransitions() external { - deploy(); + function runTests(uint256 _seed) internal { + deploy(_seed); uint256 initialBalance = address(stakingVaultProxy).balance; int256 initialInOutDelta = stakingVaultProxy.inOutDelta(); - for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { - randomTransition(VaultState.NoDepositsToBeaconChain); - } - - transitionDepositToBeaconChain(); - - for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { - randomTransition(VaultState.DepositToBeaconChain); - } - - transitionValidatorWithdraw(); - - for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { - randomTransition(VaultState.NoDepositsToBeaconChain); + uint256 STATE_TRANSITIONS = 32; + for (uint256 i = 0; i < STATE_TRANSITIONS; i++) { + doTransitionToNewVaultState(hasValidator, isConnectedToHub); + for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { + randomTransition(hasValidator, isConnectedToHub); + } } uint256 finalBalance = address(stakingVaultProxy).balance; @@ -91,28 +172,20 @@ contract StakingVaultTest is Test { console2.log("deltaInOutDelta: %s", finalInOutDelta - initialInOutDelta); } - function initializeTransitions() internal { - function() internal[] memory noValidatorDepositsTransitions = new function() internal[](2); - noValidatorDepositsTransitions[0] = transitionRandomFund; - noValidatorDepositsTransitions[1] = transitionRandomWithdraw; - stateTransitions[VaultState.NoDepositsToBeaconChain] = noValidatorDepositsTransitions; - - function() internal[] memory withValidatorDepositsTransitions = new function() internal[](3); - withValidatorDepositsTransitions[0] = transitionRandomFund; - withValidatorDepositsTransitions[1] = transitionRandomWithdraw; - withValidatorDepositsTransitions[2] = transitionRandomReceiveReward; - stateTransitions[VaultState.DepositToBeaconChain] = withValidatorDepositsTransitions; - } - - function initializeRewards() internal { - lastRewardTimestamp = block.timestamp; - currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); - } + function deploy(uint256 _seed) public { + rnd.seed = _seed; + isConnectedToHub = false; + hasValidator = false; + deposits = 0; + withdrawals = 0; + rewards = 0; + delete depositors; - function deploy() public { DepositContract__MockForStakingVault depositContract = new DepositContract__MockForStakingVault(); - vaultHub = new VaultHub__MockForStakingVault(); + vaultHub = new VaultHubMock(_seed); + validator = new ValidatorMock(_seed); stakingVault = new StakingVault(address(vaultHub), depositor, address(depositContract)); + ERC1967Proxy proxy = new ERC1967Proxy( address(stakingVault), abi.encodeWithSelector(StakingVault.initialize.selector, owner, nodeOperator, "0x") @@ -120,19 +193,104 @@ contract StakingVaultTest is Test { stakingVaultProxy = StakingVault(payable(address(proxy))); } - function randomTransition(VaultState state) internal { + function doTransitionToNewVaultState(bool _hasValidator, bool _isConnectedToHub) internal { + bool oldIsConnectedToHub = _isConnectedToHub; + bool oldHasValidator = _hasValidator; + + hasValidator = rnd.randBool(); + isConnectedToHub = rnd.randBool(); + + if (oldIsConnectedToHub != isConnectedToHub) { + if (isConnectedToHub) { + transitionConnectVaultToHub(); + } else if (!isConnectedToHub) { + transitionDisconnectVaultFromHub(); + } + } + + if (oldHasValidator != hasValidator) { + if (hasValidator) { + transitionDepositToBeaconChain(); + } else if (!hasValidator) { + transitionValidatorWithdraw(); + } + } + } + + function randomTransition(bool _hasValidator, bool _isConnectedToHub) internal { vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); - function() internal[] storage availableTransitions = stateTransitions[state]; + function() internal[] memory availableTransitions = getAvailableTransitions(_hasValidator, _isConnectedToHub); uint256 transitionIndex = rnd.randInt(availableTransitions.length - 1); availableTransitions[transitionIndex](); } + function baseTransitions() internal pure returns (function() internal[] memory) { + function() internal[] memory transitions = new function() internal[](2); + transitions[0] = transitionRandomFund; + transitions[1] = transitionRandomWithdraw; + return transitions; + } + + function validatorTransitions() internal pure returns (function() internal[] memory) { + function() internal[] memory transitions = new function() internal[](1); + transitions[0] = transitionRandomReceiveReward; + return transitions; + } + + function vaultHubTransitions() internal pure returns (function() internal[] memory) { + function() internal[] memory transitions = new function() internal[](3); + transitions[0] = transitionRandomMintShares; + transitions[1] = transitionRandomReport; + transitions[2] = transitionRandomRebalance; + return transitions; + } + + function mergeTransitions( + function() internal[] memory _transitionsA, + function() internal[] memory _transitionsB + ) internal pure returns (function() internal[] memory) { + function() internal[] memory result = new function() internal[](_transitionsA.length + _transitionsB.length); + for (uint256 txIdx = 0; txIdx < _transitionsA.length; txIdx++) { + result[txIdx] = _transitionsA[txIdx]; + } + for (uint256 txIdx = 0; txIdx < _transitionsB.length; txIdx++) { + result[_transitionsA.length + txIdx] = _transitionsB[txIdx]; + } + return result; + } + + function getAvailableTransitions( + bool _hasValidator, + bool _isConnectedToHub + ) internal returns (function() internal[] memory) { + if (_hasValidator && _isConnectedToHub) { + return mergeTransitions(baseTransitions(), mergeTransitions(validatorTransitions(), vaultHubTransitions())); + } else if (_hasValidator && !_isConnectedToHub) { + return mergeTransitions(validatorTransitions(), baseTransitions()); + } else if (!_hasValidator && _isConnectedToHub) { + return mergeTransitions(baseTransitions(), vaultHubTransitions()); + } else { + return baseTransitions(); + } + } + function transitionRandomReceive() internal { uint256 amount = rnd.randAmountD18(); if (amount == 0) { amount = MIN_FUND; } + address user = randomDepositor(); + deal(user, amount); + + vm.prank(user); + payable(address(stakingVaultProxy)).transfer(amount); + vm.stopPrank(); + + console2.log("transitionRandomReceive: %d", amount); + } + + function randomDepositor() internal returns (address) { uint256 userIndex; if (rnd.randBool() || depositors.length == 0) { address randomUser = rnd.randAddress(); @@ -141,15 +299,7 @@ contract StakingVaultTest is Test { } else { userIndex = rnd.randInt(0, depositors.length - 1); } - - address user = depositors[userIndex]; - deal(user, amount); - - vm.prank(user); - payable(address(stakingVaultProxy)).transfer(amount); - vm.stopPrank(); - - console2.log("transitionRandomReceive: %d", amount); + return depositors[userIndex]; } function transitionRandomFund() internal { @@ -169,13 +319,12 @@ contract StakingVaultTest is Test { } function transitionRandomWithdraw() internal { - uint256 availableAmount = stakingVaultProxy.unlocked(); - if (availableAmount == 0) { - return; - } - uint256 withdrawableAmount = rnd.randInt(availableAmount); + uint256 unlocked = stakingVaultProxy.unlocked(); + uint256 vaultBalance = address(stakingVaultProxy).balance; + uint256 minWithdrawal = Math.min(unlocked, vaultBalance); + uint256 withdrawableAmount = rnd.randInt(minWithdrawal); if (withdrawableAmount == 0) { - withdrawableAmount = availableAmount; + return; } deal(owner, withdrawableAmount); @@ -189,35 +338,11 @@ contract StakingVaultTest is Test { } function transitionRandomReceiveReward() internal { - uint256 timePassed = block.timestamp - lastRewardTimestamp; - if (timePassed < SECONDS_PER_DAY) { - return; - } - - uint256 dailyReward = dailyReward(timePassed); - - if (rnd.randBool()) { - currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); - } - + uint256 dailyReward = validator.dailyReward(); rewards += dailyReward; vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + dailyReward); - } - function dailyReward(uint256 timePassed) internal returns (uint256) { - uint256 daysPassed = timePassed / SECONDS_PER_DAY; - lastRewardTimestamp += daysPassed * SECONDS_PER_DAY; - - uint256 validatorBalance = 32 ether; - uint256 yearlyReward = (validatorBalance * currentAPR) / APR_DENOMINATOR; - uint256 dailyReward = (yearlyReward * daysPassed) / DAYS_PER_YEAR; - - int256 randomVariation = int256(rnd.randInt(200)) - 100; - dailyReward = uint256((int256(dailyReward) * (1000 + randomVariation)) / 1000); - - console2.log("transitionRandomReceiveReward: days=%d, apr=%d, reward=%d", daysPassed, currentAPR, dailyReward); - - return dailyReward; + console2.log("transitionRandomReceiveReward: %d", dailyReward); } function transitionDepositToBeaconChain() internal { @@ -225,47 +350,143 @@ contract StakingVaultTest is Test { vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); + deal(owner, VALIDATOR_DEPOSIT); vm.prank(owner); - deal(owner, 32 ether); - stakingVaultProxy.fund{value: 32 ether}(); - deposits += 32 ether; + stakingVaultProxy.fund{value: VALIDATOR_DEPOSIT}(); vm.stopPrank(); + deposits += VALIDATOR_DEPOSIT; - vm.prank(depositor); IStakingVault.Deposit[] memory deposits = new IStakingVault.Deposit[](1); + + // Create 48-byte pubkey by concatenating two parts + bytes memory pubkey = new bytes(48); + bytes32 firstPart = bytes32(uint256(1)); + bytes16 secondPart = bytes16(bytes32(uint256(2))); + + assembly { + mstore(add(pubkey, 32), firstPart) + mstore(add(pubkey, 64), secondPart) + } + deposits[0] = IStakingVault.Deposit({ - pubkey: bytes.concat(bytes32(uint256(1))), + pubkey: pubkey, signature: bytes.concat(bytes32(uint256(2))), - amount: 32 ether, + amount: VALIDATOR_DEPOSIT, depositDataRoot: bytes32(uint256(3)) }); + vm.prank(depositor); stakingVaultProxy.depositToBeaconChain(deposits); - withdrawals += 32 ether; + vm.stopPrank(); + + withdrawals += VALIDATOR_DEPOSIT; } function transitionValidatorWithdraw() internal { console2.log("-------------------------------- transitionValidatorWithdraw--------------------------------"); - vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); - vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + 32 ether); - deposits -= 32 ether; + deal(owner, VALIDATOR_DEPOSIT); + vm.startPrank(owner); + stakingVaultProxy.fund{value: VALIDATOR_DEPOSIT}(); + vm.stopPrank(); + deposits -= VALIDATOR_DEPOSIT; vm.prank(owner); - stakingVaultProxy.withdraw(owner, 32 ether); + stakingVaultProxy.withdraw(owner, VALIDATOR_DEPOSIT); vm.stopPrank(); - withdrawals -= 32 ether; + withdrawals -= VALIDATOR_DEPOSIT; } function transitionRandomDepositToBeaconChain() internal { vm.prank(depositor); + + bytes memory pubkey = new bytes(48); + bytes32 firstPart = bytes32(uint256(1)); + bytes16 secondPart = bytes16(bytes32(uint256(2))); + + assembly { + mstore(add(pubkey, 32), firstPart) + mstore(add(pubkey, 64), secondPart) + } + IStakingVault.Deposit[] memory deposits = new IStakingVault.Deposit[](1); deposits[0] = IStakingVault.Deposit({ - pubkey: bytes.concat(bytes32(uint256(1))), + pubkey: pubkey, signature: bytes.concat(bytes32(uint256(2))), amount: 0.1 ether, depositDataRoot: bytes32(uint256(3)) }); stakingVaultProxy.depositToBeaconChain(deposits); } + + function transitionConnectVaultToHub() internal { + console2.log("-------------------------------- transitionConnectVaultToHub--------------------------------"); + vm.prank(address(vaultHub)); + stakingVaultProxy.lock(CONNECT_DEPOSIT); + vm.stopPrank(); + } + + function transitionDisconnectVaultFromHub() internal { + console2.log( + "-------------------------------- transitionDisconnectVaultFromHub--------------------------------" + ); + uint256 valuation = stakingVaultProxy.valuation(); + int256 inOutDelta = stakingVaultProxy.inOutDelta(); + vm.prank(address(vaultHub)); + stakingVaultProxy.report(valuation, inOutDelta, 0); + vm.stopPrank(); + } + + function transitionRandomMintShares() internal { + uint256 vaultValuation = stakingVaultProxy.valuation(); + uint256 totalEtherToLock = vaultHub.totalEtherToLock(vaultValuation); + uint256 currentLocked = stakingVaultProxy.locked(); + vm.prank(address(vaultHub)); + if (totalEtherToLock < currentLocked) { + vm.expectRevert( + abi.encodeWithSelector(LockedCannotDecreaseOutsideOfReport.selector, currentLocked, totalEtherToLock) + ); + } + stakingVaultProxy.lock(totalEtherToLock); + vm.stopPrank(); + + console2.log( + "transitionRandomMintShares: valuation=%f ETH, amount=%f ETH, locked=%f ETH", + vaultValuation / 1e18, + totalEtherToLock / 1e18, + currentLocked / 1e18 + ); + } + + function transitionRandomReport() internal { + uint256 currentValuation = stakingVaultProxy.valuation(); + int256 currentInOutDelta = stakingVaultProxy.inOutDelta(); + uint256 currentLocked = stakingVaultProxy.locked(); + + uint256 newValuation = vaultHub.newValuation(currentValuation); + uint256 newLocked = vaultHub.newLocked(currentLocked); + + vm.prank(address(vaultHub)); + stakingVaultProxy.report(newValuation, currentInOutDelta, newLocked); + vm.stopPrank(); + } + + function transitionRandomRebalance() internal { + uint256 currentValuation = stakingVaultProxy.valuation(); + uint256 currentLocked = stakingVaultProxy.locked(); + uint256 currentBalance = address(stakingVaultProxy).balance; + uint256 etherToRebalance = vaultHub.amountToUnlock(currentValuation, currentLocked); + if (etherToRebalance == 0 || etherToRebalance > currentBalance) { + return; + } + + if (etherToRebalance < currentLocked) { + vm.expectRevert(abi.encodeWithSelector(NotAuthorized.selector, "rebalance", address(vaultHub))); + } + vm.prank(address(vaultHub)); + stakingVaultProxy.rebalance(etherToRebalance); + vm.stopPrank(); + + console2.log("address(vaultHub): %s", address(vaultHub)); + } } From 66e45a02c1a7452ec69415c21f06fcf919129de4 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Thu, 27 Mar 2025 15:07:17 +0100 Subject: [PATCH 26/36] fix: fix fuzz tests --- .../staking-vault/StakingVaultTest.t.sol | 198 +++++++++--------- 1 file changed, 103 insertions(+), 95 deletions(-) diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol index a58b77e90..4e299b9ea 100644 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -1,5 +1,23 @@ // SPDX-License-Identifier: UNLICENSED // for testing purposes only +// ┌────────────────────┐ +// │ │ +// │ Owner │ +// │ │ +// └──┬──────────────┬──┘ +// │ │ +// fund withdraw +// SV.balance += x SV.balance -= x +// inOutDelta += x inOutDelta -= x +// │ │ +// │ │ +// ┌──────────────────┐──report(old data)──▶┌─▼──────────────▼─┐ depositToBeaconChain ┌───────────────────────┐ +// │ │ │ │◀───SV.balance -= deposit──│ Depositor │ +// │ │ rebalance │ │ └───────────────────────┘ +// │ VaultHub │─ SV.balance -= x──▶│ StakingVault │ +// │ │ inOutDelta -= x │ │ rewards ┌───────────────────────┐ +// │ │ │ │◀───SV.balance += reward───│ Validator │ +// └──────────────────┘───────lock─────────▶└──────────────────┘ └───────────────────────┘ pragma solidity ^0.8.0; @@ -10,14 +28,13 @@ import {ERC1967Proxy} from "@openzeppelin/contracts-v5.2/proxy/ERC1967/ERC1967Pr import {StakingVault} from "contracts/0.8.25/vaults/StakingVault.sol"; import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol"; import {DepositContract__MockForStakingVault} from "./contracts/DepositContract__MockForStakingVault.sol"; -import {VaultHub__MockForStakingVault} from "./contracts/VaultHub__MockForStakingVault.sol"; import {RandomLib} from "./RandomLib.sol"; contract VaultHubMock is Test { using RandomLib for RandomLib.Storage; - uint256 constant MAX_MINTABLE_RATIO_BP = 9000; // 90% can be used for minting (100% - reserve ratio) uint256 constant TOTAL_BASIS_POINTS = 10000; + uint256 constant MAX_MINTABLE_RATIO_BP = 8000; // 80% can be used for minting (100% - reserve ratio) uint256 constant RESERVE_RATIO_BP = 1000; // 10% reserve ratio uint256 constant REWARD_RATE_MIN_BP = 100; // 1% min reward rate uint256 constant REWARD_RATE_MAX_BP = 500; // 5% max reward rate @@ -30,16 +47,13 @@ contract VaultHubMock is Test { rnd.seed = _seed; } - function totalEtherToLock(uint256 vaultValuation) public returns (uint256) { + function getTotalEtherToLock(uint256 vaultValuation) public returns (uint256) { uint256 maxMintableEther = (vaultValuation * MAX_MINTABLE_RATIO_BP) / TOTAL_BASIS_POINTS; uint256 amountToMint = rnd.randInt(maxMintableEther); - if (amountToMint == 0) { - amountToMint = maxMintableEther; - } return (amountToMint * TOTAL_BASIS_POINTS) / MAX_MINTABLE_RATIO_BP; } - function newValuation(uint256 currentValuation) public returns (uint256) { + function getNewValuation(uint256 currentValuation) public returns (uint256) { uint256 rewardRateBP = REWARD_RATE_MIN_BP + rnd.randInt(REWARD_RATE_MAX_BP - REWARD_RATE_MIN_BP); uint256 newValuation = currentValuation + (currentValuation * rewardRateBP) / TOTAL_BASIS_POINTS; uint256 treasuryFee = ((newValuation - currentValuation) * TREASURY_FEE_BP) / TOTAL_BASIS_POINTS; @@ -47,11 +61,11 @@ contract VaultHubMock is Test { return newValuation; } - function newLocked(uint256 currentLocked) public returns (uint256) { + function getNewLocked(uint256 currentLocked) public returns (uint256) { return rnd.randInt(currentLocked / 2, currentLocked); } - function amountToUnlock(uint256 currentValuation, uint256 currentLocked) public returns (uint256) { + function getAmountToUnlock(uint256 currentValuation, uint256 currentLocked) public pure returns (uint256) { uint256 minRequiredValuation = (currentLocked * TOTAL_BASIS_POINTS) / RESERVE_RATIO_BP; uint256 rebalanceThreshold = (minRequiredValuation * REBALANCE_THRESHOLD_BP) / TOTAL_BASIS_POINTS; @@ -84,7 +98,7 @@ contract ValidatorMock is Test { currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); } - function dailyReward() public returns (uint256) { + function getDailyReward() public returns (uint256) { uint256 timePassed = block.timestamp - lastRewardTimestamp; if (timePassed < SECONDS_PER_DAY) { return 0; @@ -111,15 +125,14 @@ contract ValidatorMock is Test { contract StakingVaultTest is Test { using RandomLib for RandomLib.Storage; - bool private isConnectedToHub; - bool private hasValidator; - error LockedCannotDecreaseOutsideOfReport(uint256 currentlyLocked, uint256 attemptedLocked); error NotAuthorized(string operation, address sender); + error ZeroArgument(string name); + error InsufficientBalance(uint256 balance); + error RebalanceAmountExceedsValuation(uint256 valuation, uint256 rebalanceAmount); uint256 constant ITERATIONS = 32; - uint256 constant MIN_REWARD = 0.001 ether; - uint256 constant MIN_FUND = 0.00001 ether; + uint256 constant STATE_TRANSITIONS = 32; uint256 constant VALIDATOR_DEPOSIT = 32 ether; uint256 constant CONNECT_DEPOSIT = 1 ether; uint256 constant SECONDS_PER_DAY = 86400; @@ -129,20 +142,24 @@ contract StakingVaultTest is Test { VaultHubMock private vaultHub; ValidatorMock private validator; - address[] private depositors; uint256 private deposits; uint256 private withdrawals; uint256 private rewards; - - RandomLib.Storage private rnd = RandomLib.Storage(42); + uint256 private randomUserDeposits; + uint256 private depositsToBeaconChain; address private depositor = address(0x001); address private owner = address(0x002); address private nodeOperator = address(0x003); address private user = address(0x004); + RandomLib.Storage private rnd; + + bool private isConnectedToHub; + bool private hasValidator; + function testSolvencyAllTransitions() external { - runTests(vm.unixTime()); + runTests(42); } function testFuzz_SolvencyAllTransitions(uint256 _seed) external { @@ -150,12 +167,15 @@ contract StakingVaultTest is Test { } function runTests(uint256 _seed) internal { + require( + STATE_TRANSITIONS * ITERATIONS * 10 ** 25 <= type(uint256).max / 2 - 1, + "STATE_TRANSITIONS * ITERATIONS overflow" + ); deploy(_seed); uint256 initialBalance = address(stakingVaultProxy).balance; int256 initialInOutDelta = stakingVaultProxy.inOutDelta(); - uint256 STATE_TRANSITIONS = 32; for (uint256 i = 0; i < STATE_TRANSITIONS; i++) { doTransitionToNewVaultState(hasValidator, isConnectedToHub); for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { @@ -165,11 +185,12 @@ contract StakingVaultTest is Test { uint256 finalBalance = address(stakingVaultProxy).balance; int256 finalInOutDelta = stakingVaultProxy.inOutDelta(); - assertEq(deposits - withdrawals, finalBalance - initialBalance - rewards); - console2.log("deltaBalance: %s", finalBalance - initialBalance); - console2.log("deltaDeposits: %s", deposits - withdrawals); - console2.log("deltaInOutDelta: %s", finalInOutDelta - initialInOutDelta); + assertEq( + deposits + initialBalance + rewards + randomUserDeposits, + finalBalance + withdrawals + depositsToBeaconChain + ); + assertEq(initialInOutDelta + int256(deposits), finalInOutDelta + int256(withdrawals)); } function deploy(uint256 _seed) public { @@ -179,7 +200,8 @@ contract StakingVaultTest is Test { deposits = 0; withdrawals = 0; rewards = 0; - delete depositors; + randomUserDeposits = 0; + depositsToBeaconChain = 0; DepositContract__MockForStakingVault depositContract = new DepositContract__MockForStakingVault(); vaultHub = new VaultHubMock(_seed); @@ -212,7 +234,7 @@ contract StakingVaultTest is Test { if (hasValidator) { transitionDepositToBeaconChain(); } else if (!hasValidator) { - transitionValidatorWithdraw(); + transitionValidatorExitAndReturnDeposit(); } } } @@ -225,9 +247,10 @@ contract StakingVaultTest is Test { } function baseTransitions() internal pure returns (function() internal[] memory) { - function() internal[] memory transitions = new function() internal[](2); + function() internal[] memory transitions = new function() internal[](3); transitions[0] = transitionRandomFund; transitions[1] = transitionRandomWithdraw; + transitions[2] = transitionRandomUserDeposit; return transitions; } @@ -262,7 +285,7 @@ contract StakingVaultTest is Test { function getAvailableTransitions( bool _hasValidator, bool _isConnectedToHub - ) internal returns (function() internal[] memory) { + ) internal pure returns (function() internal[] memory) { if (_hasValidator && _isConnectedToHub) { return mergeTransitions(baseTransitions(), mergeTransitions(validatorTransitions(), vaultHubTransitions())); } else if (_hasValidator && !_isConnectedToHub) { @@ -274,46 +297,31 @@ contract StakingVaultTest is Test { } } - function transitionRandomReceive() internal { + function transitionRandomUserDeposit() internal { uint256 amount = rnd.randAmountD18(); + address user = rnd.randAddress(); + deal(user, amount); if (amount == 0) { - amount = MIN_FUND; + vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "msg.value")); } - - address user = randomDepositor(); - deal(user, amount); - vm.prank(user); payable(address(stakingVaultProxy)).transfer(amount); vm.stopPrank(); + randomUserDeposits += amount; - console2.log("transitionRandomReceive: %d", amount); - } - - function randomDepositor() internal returns (address) { - uint256 userIndex; - if (rnd.randBool() || depositors.length == 0) { - address randomUser = rnd.randAddress(); - depositors.push(randomUser); - userIndex = depositors.length - 1; - } else { - userIndex = rnd.randInt(0, depositors.length - 1); - } - return depositors[userIndex]; + console2.log("transitionRandomUserDeposit: %d", amount); } function transitionRandomFund() internal { uint256 amount = rnd.randAmountD18(); + deal(owner, amount); if (amount == 0) { - amount = MIN_FUND; + vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "msg.value")); } - - deal(owner, amount); - deposits += amount; - vm.startPrank(owner); stakingVaultProxy.fund{value: amount}(); vm.stopPrank(); + deposits += amount; console2.log("transitionRandomFund: %s", amount); } @@ -323,24 +331,23 @@ contract StakingVaultTest is Test { uint256 vaultBalance = address(stakingVaultProxy).balance; uint256 minWithdrawal = Math.min(unlocked, vaultBalance); uint256 withdrawableAmount = rnd.randInt(minWithdrawal); - if (withdrawableAmount == 0) { - return; - } deal(owner, withdrawableAmount); - withdrawals += withdrawableAmount; - + if (withdrawableAmount == 0) { + vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); + } vm.prank(owner); stakingVaultProxy.withdraw(owner, withdrawableAmount); vm.stopPrank(); + withdrawals += withdrawableAmount; console2.log("transitionRandomWithdraw: %d", withdrawableAmount); } function transitionRandomReceiveReward() internal { - uint256 dailyReward = validator.dailyReward(); - rewards += dailyReward; + uint256 dailyReward = validator.getDailyReward(); vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + dailyReward); + rewards += dailyReward; console2.log("transitionRandomReceiveReward: %d", dailyReward); } @@ -358,7 +365,6 @@ contract StakingVaultTest is Test { IStakingVault.Deposit[] memory deposits = new IStakingVault.Deposit[](1); - // Create 48-byte pubkey by concatenating two parts bytes memory pubkey = new bytes(48); bytes32 firstPart = bytes32(uint256(1)); bytes16 secondPart = bytes16(bytes32(uint256(2))); @@ -378,27 +384,22 @@ contract StakingVaultTest is Test { stakingVaultProxy.depositToBeaconChain(deposits); vm.stopPrank(); - withdrawals += VALIDATOR_DEPOSIT; + depositsToBeaconChain += VALIDATOR_DEPOSIT; } - function transitionValidatorWithdraw() internal { - console2.log("-------------------------------- transitionValidatorWithdraw--------------------------------"); + function transitionValidatorExitAndReturnDeposit() internal { + console2.log( + "-------------------------------- transitionValidatorExitAndReturnDeposit--------------------------------" + ); vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); - deal(owner, VALIDATOR_DEPOSIT); - vm.startPrank(owner); - stakingVaultProxy.fund{value: VALIDATOR_DEPOSIT}(); - vm.stopPrank(); - deposits -= VALIDATOR_DEPOSIT; - - vm.prank(owner); - stakingVaultProxy.withdraw(owner, VALIDATOR_DEPOSIT); - vm.stopPrank(); - withdrawals -= VALIDATOR_DEPOSIT; + // receive validator deposit + deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + VALIDATOR_DEPOSIT); + depositsToBeaconChain -= VALIDATOR_DEPOSIT; } function transitionRandomDepositToBeaconChain() internal { - vm.prank(depositor); + uint256 amount = rnd.randAmountD18(); bytes memory pubkey = new bytes(48); bytes32 firstPart = bytes32(uint256(1)); @@ -413,10 +414,17 @@ contract StakingVaultTest is Test { deposits[0] = IStakingVault.Deposit({ pubkey: pubkey, signature: bytes.concat(bytes32(uint256(2))), - amount: 0.1 ether, + amount: amount, depositDataRoot: bytes32(uint256(3)) }); + + vm.prank(depositor); stakingVaultProxy.depositToBeaconChain(deposits); + vm.stopPrank(); + + depositsToBeaconChain += amount; + + console2.log("transitionRandomDepositToBeaconChain: %d", amount); } function transitionConnectVaultToHub() internal { @@ -427,9 +435,7 @@ contract StakingVaultTest is Test { } function transitionDisconnectVaultFromHub() internal { - console2.log( - "-------------------------------- transitionDisconnectVaultFromHub--------------------------------" - ); + console2.log("-------------------------------- transitionDisconnectVaultFromHub----------------------------"); uint256 valuation = stakingVaultProxy.valuation(); int256 inOutDelta = stakingVaultProxy.inOutDelta(); vm.prank(address(vaultHub)); @@ -439,23 +445,18 @@ contract StakingVaultTest is Test { function transitionRandomMintShares() internal { uint256 vaultValuation = stakingVaultProxy.valuation(); - uint256 totalEtherToLock = vaultHub.totalEtherToLock(vaultValuation); + uint256 totalEtherToLock = vaultHub.getTotalEtherToLock(vaultValuation); uint256 currentLocked = stakingVaultProxy.locked(); - vm.prank(address(vaultHub)); if (totalEtherToLock < currentLocked) { vm.expectRevert( abi.encodeWithSelector(LockedCannotDecreaseOutsideOfReport.selector, currentLocked, totalEtherToLock) ); } + vm.prank(address(vaultHub)); stakingVaultProxy.lock(totalEtherToLock); vm.stopPrank(); - console2.log( - "transitionRandomMintShares: valuation=%f ETH, amount=%f ETH, locked=%f ETH", - vaultValuation / 1e18, - totalEtherToLock / 1e18, - currentLocked / 1e18 - ); + console2.log("transitionRandomMintShares: %d", totalEtherToLock); } function transitionRandomReport() internal { @@ -463,30 +464,37 @@ contract StakingVaultTest is Test { int256 currentInOutDelta = stakingVaultProxy.inOutDelta(); uint256 currentLocked = stakingVaultProxy.locked(); - uint256 newValuation = vaultHub.newValuation(currentValuation); - uint256 newLocked = vaultHub.newLocked(currentLocked); + uint256 newValuation = vaultHub.getNewValuation(currentValuation); + uint256 newLocked = vaultHub.getNewLocked(currentLocked); vm.prank(address(vaultHub)); stakingVaultProxy.report(newValuation, currentInOutDelta, newLocked); vm.stopPrank(); + + console2.log("transitionRandomReport: %d", newValuation); } function transitionRandomRebalance() internal { uint256 currentValuation = stakingVaultProxy.valuation(); - uint256 currentLocked = stakingVaultProxy.locked(); uint256 currentBalance = address(stakingVaultProxy).balance; - uint256 etherToRebalance = vaultHub.amountToUnlock(currentValuation, currentLocked); - if (etherToRebalance == 0 || etherToRebalance > currentBalance) { - return; - } + uint256 currentLocked = stakingVaultProxy.locked(); + uint256 etherToRebalance = vaultHub.getAmountToUnlock(currentValuation, currentLocked); - if (etherToRebalance < currentLocked) { + if (etherToRebalance == 0) { + vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); + } else if (etherToRebalance > currentBalance) { + vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector, currentBalance)); + } else if (etherToRebalance > currentValuation) { + vm.expectRevert( + abi.encodeWithSelector(RebalanceAmountExceedsValuation.selector, currentValuation, etherToRebalance) + ); + } else if (currentValuation >= currentLocked) { vm.expectRevert(abi.encodeWithSelector(NotAuthorized.selector, "rebalance", address(vaultHub))); } vm.prank(address(vaultHub)); stakingVaultProxy.rebalance(etherToRebalance); vm.stopPrank(); - console2.log("address(vaultHub): %s", address(vaultHub)); + console2.log("transitionRandomRebalance: %d", etherToRebalance); } } From 73c02da4a7c5e2eb50b10ff201d07ea02dcb2cc0 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Thu, 27 Mar 2025 22:30:35 +0000 Subject: [PATCH 27/36] fix: fuzzing setup --- test/0.8.25/Protocol__Deployment.t.sol | 50 ++++++++++++-------------- test/0.8.25/ShareRate.t.sol | 18 +++++----- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 1e7c25bea..e56e4fb8c 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -10,7 +10,7 @@ import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; -import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; import {LimitsList} from "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol"; import {StakingRouter__MockForLidoAccountingFuzzing} from "./contracts/StakingRouter__MockForLidoAccountingFuzzing.sol"; @@ -18,7 +18,7 @@ import {SecondOpinionOracle__MockForAccountingFuzzing} from "./contracts/SecondO import {WithdrawalQueue, IWstETH} from "../../contracts/0.8.9/WithdrawalQueue.sol"; import {WithdrawalQueueERC721} from "../../contracts/0.8.9/WithdrawalQueueERC721.sol"; -interface IAccounting { +interface IVaultHub { function initialize(address _admin) external; } @@ -92,28 +92,9 @@ 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; + LidoLocator public lidoLocator; WithdrawalQueueERC721 public wq; IACL public acl; SecondOpinionOracle__MockForAccountingFuzzing public secondOpinionOracleMock; @@ -197,7 +178,7 @@ contract BaseProtocolTest is Test { // Add accounting contract with handler to the protocol address accountingImpl = deployCode( "Accounting.sol:Accounting", - abi.encode(address(lidoLocator), lidoProxyAddress, VAULTS_LIMIT, VAULTS_RELATIVE_SHARE_LIMIT) + abi.encode(address(lidoLocator), lidoProxyAddress) ); deployCodeTo( @@ -206,6 +187,17 @@ contract BaseProtocolTest is Test { lidoLocator.accounting() ); + address vaultHubImpl = deployCode( + "VaultHub.sol:VaultHub", + abi.encode(address(lidoLocator), lidoProxyAddress, VAULTS_LIMIT, VAULTS_RELATIVE_SHARE_LIMIT) + ); + + deployCodeTo( + "OssifiableProxy.sol:OssifiableProxy", + abi.encode(vaultHubImpl, rootAccount, new bytes(0)), + lidoLocator.vaultHub() + ); + deployCodeTo( "AccountingOracle.sol:AccountingOracle", abi.encode( @@ -261,7 +253,7 @@ contract BaseProtocolTest is Test { bytes32(uint256(uint160(address(secondOpinionOracleMock)))) ); - IAccounting(lidoLocator.accounting()).initialize(rootAccount); + IVaultHub(lidoLocator.vaultHub()).initialize(rootAccount); /// @dev deploy eip712steth address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress)); @@ -318,8 +310,8 @@ contract BaseProtocolTest is Test { } /// @dev deploy lido locator with dummy default values - function _deployLidoLocator(address lido, address stakingRouterAddress) internal returns (ILidoLocator) { - LidoLocatorConfig memory config = LidoLocatorConfig({ + function _deployLidoLocator(address lido, address stakingRouterAddress) internal returns (LidoLocator) { + LidoLocator.Config memory config = LidoLocator.Config({ accountingOracle: makeAddr("dummy-locator:accountingOracle"), depositSecurityModule: makeAddr("dummy-locator:depositSecurityModule"), elRewardsVault: makeAddr("dummy-locator:elRewardsVault"), @@ -335,9 +327,11 @@ contract BaseProtocolTest is Test { withdrawalVault: makeAddr("dummy-locator:withdrawalVault"), oracleDaemonConfig: makeAddr("dummy-locator:oracleDaemonConfig"), accounting: makeAddr("dummy-locator:accounting"), - wstETH: wstETHAdr + predepositGuarantee: makeAddr("dummy-locator:predeposit_guarantee"), + wstETH: wstETHAdr, + vaultHub: makeAddr("dummy-locator:vaultHub") }); - return ILidoLocator(deployCode("LidoLocator.sol:LidoLocator", abi.encode(config))); + return LidoLocator(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 14707410a..4c45f4695 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -25,7 +25,7 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { ILido public lidoContract; WithdrawalQueueERC721 public wqContract; - address public accounting; + address public vaultHub; address public userAccount; address public rootAccount; @@ -40,13 +40,13 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { constructor( ILido _lido, WithdrawalQueueERC721 _wqContract, - address _accounting, + address _vaultHub, address _userAccount, address _rootAccount, uint256 _maxAmountOfShares ) { lidoContract = _lido; - accounting = _accounting; + vaultHub = _vaultHub; userAccount = _userAccount; rootAccount = _rootAccount; maxAmountOfShares = _maxAmountOfShares; @@ -76,7 +76,7 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { vm.prank(userAccount); lidoContract.resumeStaking(); - vm.prank(accounting); + vm.prank(vaultHub); boundaryValues.externalSharesRecipient = _recipient; boundaryValues.mintedExternalShares = _amountOfShares; @@ -95,7 +95,7 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { vm.prank(userAccount); lidoContract.resumeStaking(); - vm.prank(accounting); + vm.prank(vaultHub); boundaryValues.burnExternalShares = _amountOfShares; @@ -275,7 +275,7 @@ contract ShareRateTest is BaseProtocolTest { function setUp() public { BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); - address accountingContract = lidoLocator.accounting(); + address vaultHubAddress = lidoLocator.vaultHub(); vm.startPrank(userAccount); lidoContract.setMaxExternalRatioBP(_maxExternalRatioBP); @@ -286,7 +286,7 @@ contract ShareRateTest is BaseProtocolTest { shareRateHandler = new ShareRateHandler( lidoContract, wq, - accountingContract, + vaultHubAddress, userAccount, rootAccount, _maxAmountOfShares @@ -304,8 +304,8 @@ contract ShareRateTest is BaseProtocolTest { // @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); + vm.prank(vaultHubAddress); + lidoContract.mintExternalShares(vaultHubAddress, protocolStartExternalShares); shareRateHandler.submit(0, 10 ether); vm.roll(block.number + ONE_DAY_IN_BLOCKS); From d18273c469233fc7aa39d60bdea695d65ae89bc0 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Fri, 28 Mar 2025 00:04:29 +0000 Subject: [PATCH 28/36] chore: small refactoring --- test/0.8.25/Protocol__Deployment.t.sol | 181 ++++++++++---- test/0.8.25/ShareRate.t.sol | 321 ++++++++++++++----------- 2 files changed, 307 insertions(+), 195 deletions(-) diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index e56e4fb8c..46849401a 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -18,10 +18,16 @@ import {SecondOpinionOracle__MockForAccountingFuzzing} from "./contracts/SecondO import {WithdrawalQueue, IWstETH} from "../../contracts/0.8.9/WithdrawalQueue.sol"; import {WithdrawalQueueERC721} from "../../contracts/0.8.9/WithdrawalQueueERC721.sol"; +/** + * @title Interface for VaultHub + */ interface IVaultHub { function initialize(address _admin) external; } +/** + * @title Stake Limit State Data Structure + */ struct StakeLimitStateData { uint32 prevStakeBlockNumber; // block number of the previous stake submit uint96 prevStakeLimit; // limit value (<= `maxStakeLimit`) obtained on the previous stake submit @@ -29,29 +35,21 @@ struct StakeLimitStateData { uint96 maxStakeLimit; // maximum limit value } +/** + * @title Interface for Lido contract + */ 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; - 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; - function transfer(address _recipient, uint256 _amount) external returns (bool); - function submit(address _referral) external payable returns (uint256); - function getStakeLimitFullInfo() external view @@ -64,14 +62,15 @@ interface ILido { uint256 prevStakeLimit, uint256 prevStakeBlockNumber ); - function approve(address _spender, uint256 _amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } +/** + * @title Interface for Aragon Kernel + */ interface IKernel { function acl() external view returns (IACL); - function newAppInstance( bytes32 _appId, address _appBase, @@ -80,19 +79,28 @@ interface IKernel { ) external; } +/** + * @title Interface for Aragon ACL + */ 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); } +/** + * @title Interface for Aragon DAO Factory + */ interface IDaoFactory { function newDAO(address _root) external returns (IKernel); } +/** + * @title Base Protocol Test Contract + * @notice Sets up the Lido protocol for testing + */ contract BaseProtocolTest is Test { + // Main protocol contracts ILido public lidoContract; LidoLocator public lidoLocator; WithdrawalQueueERC721 public wq; @@ -100,20 +108,24 @@ contract BaseProtocolTest is Test { SecondOpinionOracle__MockForAccountingFuzzing public secondOpinionOracleMock; IKernel private dao; + // Account addresses address private rootAccount; address private userAccount; + // Aragon DAO components address public kernelBase; address public aclBase; address public evmScriptRegistryFactory; address public daoFactoryAdr; + // Protocol configuration + address public depositContractAdr = address(0x4242424242424242424242424242424242424242); + address public withdrawalQueueAdr = makeAddr("dummy-locator:withdrawalQueue"); uint256 public genesisTimestamp = 1_695_902_400; - 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"); + // Constants uint256 public constant VAULTS_LIMIT = 500; uint256 public constant VAULTS_RELATIVE_SHARE_LIMIT = 10_00; @@ -132,27 +144,75 @@ contract BaseProtocolTest is Test { clBalanceOraclesErrorUpperBPLimit: 50 }); + /** + * @notice Sets up the protocol with initial configuration + * @param _startBalance Initial balance for the Lido contract + * @param _rootAccount Admin account address + * @param _userAccount User account address + */ function setUpProtocol(uint256 _startBalance, address _rootAccount, address _userAccount) public { rootAccount = _rootAccount; userAccount = _userAccount; + // Deploy Lido implementation address impl = deployCode("Lido.sol:Lido"); vm.startPrank(rootAccount); - (dao, acl) = createAragonDao(); - address lidoProxyAddress = addAragonApp(dao, impl); + // Create Aragon DAO + (dao, acl) = _createAragonDao(); + // Add Lido as an Aragon app + address lidoProxyAddress = _addAragonApp(dao, impl); lidoContract = ILido(lidoProxyAddress); - /// @dev deal lido contract with start balance + // Fund Lido contract vm.deal(lidoProxyAddress, _startBalance); + // Set up permissions + _setupLidoPermissions(lidoProxyAddress); + + // Set up staking router mock + StakingRouter__MockForLidoAccountingFuzzing stakingRouter = _setupStakingRouterMock(); + + // Deploy Lido locator + lidoLocator = _deployLidoLocator(lidoProxyAddress, address(stakingRouter)); + + // Deploy and set up protocol components + _deployProtocolComponents(lidoProxyAddress); + + // Initialize VaultHub + IVaultHub(lidoLocator.vaultHub()).initialize(rootAccount); + + // Deploy and initialize EIP712StETH + address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress)); + lidoContract.initialize(address(lidoLocator), address(eip712steth)); + + // Deploy WstETH + deployCodeTo("WstETH.sol:WstETH", abi.encode(lidoProxyAddress), wstETHAdr); + + // Set up withdrawal queue + _setupWithdrawalQueue(); + + vm.stopPrank(); + } + + /** + * @notice Sets up permissions for the Lido contract + * @param lidoProxyAddress Address of the Lido proxy + */ + function _setupLidoPermissions(address lidoProxyAddress) internal { 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); + } + /** + * @notice Sets up the staking router mock + * @return StakingRouter__MockForLidoAccountingFuzzing The configured staking router mock + */ + function _setupStakingRouterMock() internal returns (StakingRouter__MockForLidoAccountingFuzzing) { StakingRouter__MockForLidoAccountingFuzzing stakingRouter = new StakingRouter__MockForLidoAccountingFuzzing(); uint96[] memory stakingModuleFees = new uint96[](3); @@ -172,58 +232,66 @@ contract BaseProtocolTest is Test { 100000000000000000000 ); - /// @dev deploy lido locator with dummy default values - lidoLocator = _deployLidoLocator(lidoProxyAddress, address(stakingRouter)); + return stakingRouter; + } - // Add accounting contract with handler to the protocol + /** + * @notice Deploys all protocol components + * @param lidoProxyAddress Address of the Lido proxy + */ + function _deployProtocolComponents(address lidoProxyAddress) internal { + // Deploy Accounting address accountingImpl = deployCode( "Accounting.sol:Accounting", abi.encode(address(lidoLocator), lidoProxyAddress) ); - deployCodeTo( "OssifiableProxy.sol:OssifiableProxy", abi.encode(accountingImpl, rootAccount, new bytes(0)), lidoLocator.accounting() ); + // Deploy VaultHub address vaultHubImpl = deployCode( "VaultHub.sol:VaultHub", abi.encode(address(lidoLocator), lidoProxyAddress, VAULTS_LIMIT, VAULTS_RELATIVE_SHARE_LIMIT) ); - deployCodeTo( "OssifiableProxy.sol:OssifiableProxy", abi.encode(vaultHubImpl, rootAccount, new bytes(0)), lidoLocator.vaultHub() ); + // Deploy AccountingOracle deployCodeTo( "AccountingOracle.sol:AccountingOracle", - abi.encode( - address(lidoLocator), - lidoLocator.legacyOracle(), - 12, // secondsPerSlot - genesisTimestamp - ), + abi.encode(address(lidoLocator), lidoLocator.legacyOracle(), 12, genesisTimestamp), lidoLocator.accountingOracle() ); - // Add burner contract to the protocol + // Deploy Burner deployCodeTo( "Burner.sol:Burner", abi.encode(rootAccount, address(lidoLocator), lidoProxyAddress, 0, 0), lidoLocator.burner() ); - // Add burner contract to the protocol + // Deploy EL Rewards Vault deployCodeTo( "LidoExecutionLayerRewardsVault.sol:LidoExecutionLayerRewardsVault", abi.encode(lidoProxyAddress, lidoTreasuryAdr), lidoLocator.elRewardsVault() ); - // Add oracle report sanity checker contract to the protocol + // Deploy Oracle Report Sanity Checker + _deployOracleReportSanityChecker(); + } + + /** + * @notice Deploys the Oracle Report Sanity Checker + */ + function _deployOracleReportSanityChecker() internal { + // Deploy the sanity checker deployCodeTo( "OracleReportSanityChecker.sol:OracleReportSanityChecker", abi.encode( @@ -246,35 +314,34 @@ contract BaseProtocolTest is Test { lidoLocator.oracleReportSanityChecker() ); + // Set up second opinion oracle mock secondOpinionOracleMock = new SecondOpinionOracle__MockForAccountingFuzzing(); vm.store( lidoLocator.oracleReportSanityChecker(), bytes32(uint256(2)), bytes32(uint256(uint160(address(secondOpinionOracleMock)))) ); + } - IVaultHub(lidoLocator.vaultHub()).initialize(rootAccount); - - /// @dev deploy eip712steth - address eip712steth = deployCode("EIP712StETH.sol:EIP712StETH", abi.encode(lidoProxyAddress)); - - lidoContract.initialize(address(lidoLocator), address(eip712steth)); - - deployCodeTo("WstETH.sol:WstETH", abi.encode(lidoProxyAddress), wstETHAdr); - + /** + * @notice Sets up the withdrawal queue + */ + function _setupWithdrawalQueue() internal { 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.grantRole(keccak256("FINALIZE_ROLE"), rootAccount); - wq.resume(); - - vm.stopPrank(); } - /// @dev create aragon dao and return kernel and acl - function createAragonDao() private returns (IKernel, IACL) { + /** + * @notice Creates an Aragon DAO and returns the kernel and ACL + * @return IKernel The DAO kernel + * @return IACL The DAO ACL + */ + function _createAragonDao() internal returns (IKernel, IACL) { + // Deploy Aragon components kernelBase = deployCode("Kernel.sol:Kernel", abi.encode(true)); aclBase = deployCode("ACL.sol:ACL"); evmScriptRegistryFactory = deployCode("EVMScriptRegistryFactory.sol:EVMScriptRegistryFactory"); @@ -285,6 +352,7 @@ contract BaseProtocolTest is Test { IDaoFactory daoFactory = IDaoFactory(daoFactoryAdr); + // Create new DAO vm.recordLogs(); daoFactory.newDAO(rootAccount); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -293,13 +361,19 @@ contract BaseProtocolTest is Test { IKernel _dao = IKernel(address(daoAddress)); IACL _acl = IACL(address(_dao.acl())); + // Set up permissions _acl.createPermission(rootAccount, daoAddress, keccak256("APP_MANAGER_ROLE"), rootAccount); return (_dao, _acl); } - /// @dev add aragon app to dao and return proxy address - function addAragonApp(IKernel _dao, address _impl) private returns (address) { + /** + * @notice Adds an Aragon app to the DAO and returns the proxy address + * @param _dao The DAO kernel + * @param _impl The implementation address + * @return address The proxy address + */ + function _addAragonApp(IKernel _dao, address _impl) internal returns (address) { vm.recordLogs(); _dao.newAppInstance(keccak256(bytes("lido.aragonpm.test")), _impl, "", false); Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -309,7 +383,12 @@ contract BaseProtocolTest is Test { return proxyAddress; } - /// @dev deploy lido locator with dummy default values + /** + * @notice Deploys Lido locator with default values + * @param lido The Lido contract address + * @param stakingRouterAddress The staking router address + * @return LidoLocator The deployed Lido locator + */ function _deployLidoLocator(address lido, address stakingRouterAddress) internal returns (LidoLocator) { LidoLocator.Config memory config = LidoLocator.Config({ accountingOracle: makeAddr("dummy-locator:accountingOracle"), diff --git a/test/0.8.25/ShareRate.t.sol b/test/0.8.25/ShareRate.t.sol index 4c45f4695..0e7170252 100644 --- a/test/0.8.25/ShareRate.t.sol +++ b/test/0.8.25/ShareRate.t.sol @@ -2,9 +2,9 @@ // for testing purposes only pragma solidity ^0.8.0; -import "../../contracts/0.8.9/WithdrawalQueueBase.sol"; -import "../../contracts/0.8.9/WithdrawalQueueERC721.sol"; -import "contracts/0.8.9/EIP712StETH.sol"; +import {WithdrawalQueueBase} from "contracts/0.8.9/WithdrawalQueueBase.sol"; +import {WithdrawalQueueERC721} from "contracts/0.8.9/WithdrawalQueueERC721.sol"; +import {EIP712StETH} from "contracts/0.8.9/EIP712StETH.sol"; import {LidoLocator} from "contracts/0.8.9/LidoLocator.sol"; import {BaseProtocolTest, ILido} from "./Protocol__Deployment.t.sol"; @@ -14,25 +14,50 @@ import {StdUtils} from "forge-std/StdUtils.sol"; import {Vm} from "forge-std/Vm.sol"; import {console2} from "forge-std/console2.sol"; +// Number of blocks in one day (assuming 12 second block time) uint256 constant ONE_DAY_IN_BLOCKS = 7_200; -contract ShareRateHandler is CommonBase, StdCheats, StdUtils { - struct BoundaryValues { - address externalSharesRecipient; - uint256 mintedExternalShares; - uint256 burnExternalShares; - } +// Protocol configuration constants +uint256 constant MAX_EXTERNAL_RATIO_BP = 10_000; // 100% +uint256 constant MAX_STAKE_LIMIT = 15_000_000 ether; +uint256 constant STAKE_LIMIT_INCREASE_PER_BLOCK = 20 ether; +uint256 constant MAX_AMOUNT_OF_SHARES = 100; +uint256 constant PROTOCOL_START_BALANCE = 15_000 ether; +uint256 constant PROTOCOL_START_EXTERNAL_SHARES = 10_000; + +// Test account addresses +address constant ROOT_ACCOUNT = address(0x123); +address constant USER_ACCOUNT = address(0x321); + +// Withdrawal queue configuration +uint256 constant MIN_WITHDRAWAL_AMOUNT = 0.0005 ether; +uint256 constant MAX_WITHDRAWAL_AMOUNT = 1000 ether; +uint256 constant FINALIZATION_DELAY_BLOCKS = 1_500; +uint256 constant FINALIZATION_DELAY_DAYS = 1; +uint256 constant FINALIZATION_ETH_BUFFER = 10_000 ether; +uint256 constant MAX_BATCHES_SIZE = 36; +uint256 constant BATCH_CALCULATION_TIMEOUT = 1_000; +uint256 constant BATCH_CALCULATION_MAX_ITERATIONS = 3; + +// Share rate bounds for finalization +uint256 constant MIN_SHARE_RATE = 0.0001 * 10 ** 27; +uint256 constant MAX_SHARE_RATE = 100 * 10 ** 27; + +// Test user configuration +uint256 constant USER_COUNT = 1_000; +contract ShareRateHandler is CommonBase, StdCheats, StdUtils { + // Protocol contracts ILido public lidoContract; WithdrawalQueueERC721 public wqContract; address public vaultHub; + + // Account addresses address public userAccount; address public rootAccount; - BoundaryValues public boundaryValues; - + // Test state tracking uint256 public maxAmountOfShares; - uint256[] public amountsQW; address[] public users; uint256 public constant userCount = 1_000; @@ -46,29 +71,18 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { uint256 _maxAmountOfShares ) { lidoContract = _lido; + wqContract = _wqContract; vaultHub = _vaultHub; userAccount = _userAccount; rootAccount = _rootAccount; maxAmountOfShares = _maxAmountOfShares; - wqContract = _wqContract; - - // Initialize boundary values with extreme values - boundaryValues = BoundaryValues({ - externalSharesRecipient: makeAddr("randomRecipient"), - mintedExternalShares: 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); - } + _initializeUsers(); } - function mintExternalShares(address _recipient, uint256 _amountOfShares) external { - // we don't want to test the zero address case, as it would revert + /// Actions for fuzzing + + function mintExternalShares(address _recipient, uint256 _amountOfShares) external returns (bool) { vm.assume(_recipient != address(0)); _amountOfShares = bound(_amountOfShares, 1, maxAmountOfShares); @@ -77,14 +91,12 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { lidoContract.resumeStaking(); vm.prank(vaultHub); - - boundaryValues.externalSharesRecipient = _recipient; - boundaryValues.mintedExternalShares = _amountOfShares; - lidoContract.mintExternalShares(_recipient, _amountOfShares); + + return true; } - function burnExternalShares(uint256 _amountOfShares) external { + function burnExternalShares(uint256 _amountOfShares) external returns (bool) { uint256 totalShares = lidoContract.getExternalShares(); if (totalShares != 0) { _amountOfShares = bound(_amountOfShares, 2, maxAmountOfShares); @@ -96,45 +108,28 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { lidoContract.resumeStaking(); vm.prank(vaultHub); - - boundaryValues.burnExternalShares = _amountOfShares; - lidoContract.burnExternalShares(_amountOfShares); - } - function getTotalShares() external view returns (uint256) { - return lidoContract.getTotalShares(); + return true; } function submit(uint256 _senderId, uint256 _amountETH) external payable returns (bool) { - if (_senderId > this.userCount()) { - _senderId = bound(_senderId, 0, this.userCount()); - } - + _senderId = _boundSenderId(_senderId); address sender = users[_senderId]; - _amountETH = bound(_amountETH, 0.0005 ether, 1000 ether); + _amountETH = bound(_amountETH, MIN_WITHDRAWAL_AMOUNT, MAX_WITHDRAWAL_AMOUNT); vm.deal(sender, _amountETH); vm.prank(sender); lidoContract.submit{value: _amountETH}(address(0)); - vm.roll(block.number + ONE_DAY_IN_BLOCKS); - return true; - } - function getBalanceByUser(address _owner) public returns (uint256) { - return lidoContract.balanceOf(_owner); + return true; } 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()); - } + _senderId = _boundSenderId(_senderId); + _recipientId = _boundSenderId(_recipientId); if (_recipientId == _senderId) { return false; @@ -143,12 +138,12 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { address _sender = users[_senderId]; address _recipient = users[_recipientId]; - if (getBalanceByUser(_sender) == 0) { + if (_getLidoUserBalance(_sender) == 0) { vm.prank(_sender); this.submit(_senderId, _amountTokens); } - _amountTokens = bound(_amountTokens, 1, getBalanceByUser(_sender)); + _amountTokens = bound(_amountTokens, 1, _getLidoUserBalance(_sender)); vm.prank(_sender); lidoContract.transfer(_recipient, _amountTokens); @@ -160,30 +155,77 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { function withdrawStEth( uint256 _ownerId, uint256 _amountTokens, - uint256 maxShareRate + uint256 _maxShareRate ) external payable returns (bool) { - if (_ownerId > this.userCount()) { - _ownerId = bound(_ownerId, 0, this.userCount()); + _ownerId = _boundSenderId(_ownerId); + address _owner = users[_ownerId]; + + _ensureUserHasTokens(_owner, _ownerId, _amountTokens); + + uint256 userBalance = _getLidoUserBalance(_owner); + + vm.prank(_owner); + lidoContract.approve(address(wqContract), userBalance); + vm.roll(block.number + 1); + + _amountTokens = _prepareWithdrawalAmounts(_amountTokens, userBalance); + + vm.prank(_owner); + uint256[] memory requestIds = wqContract.requestWithdrawals(amountsQW, _owner); + delete amountsQW; + + vm.roll(block.number + FINALIZATION_DELAY_BLOCKS); + vm.warp(block.timestamp + FINALIZATION_DELAY_DAYS); + + _finalize(_maxShareRate, _amountTokens + FINALIZATION_ETH_BUFFER); + + _claimWithdrawals(_owner, requestIds); + + return true; + } + + // Getters + + function getTotalShares() external view returns (uint256) { + return lidoContract.getTotalShares(); + } + + /// Helpers + + function _getLidoUserBalance(address _owner) public view returns (uint256) { + return lidoContract.balanceOf(_owner); + } + + function _initializeUsers() private { + for (uint256 i = 0; i <= USER_COUNT; i++) { + uint256 privateKey = uint256(keccak256(abi.encodePacked(i))); + address randomAddr = vm.addr(privateKey); + users.push(randomAddr); } + } - address _owner = users[_ownerId]; - if (getBalanceByUser(_owner) == 0) { + function _boundSenderId(uint256 _senderId) private view returns (uint256) { + if (_senderId > this.userCount()) { + return bound(_senderId, 0, this.userCount()); + } + return _senderId; + } + + function _ensureUserHasTokens(address _owner, uint256 _ownerId, uint256 _amountTokens) private { + if (_getLidoUserBalance(_owner) == 0) { vm.prank(_owner); this.submit(_ownerId, _amountTokens); } - if (getBalanceByUser(_owner) < wqContract.MIN_STETH_WITHDRAWAL_AMOUNT()) { + if (_getLidoUserBalance(_owner) < wqContract.MIN_STETH_WITHDRAWAL_AMOUNT()) { vm.prank(_owner); this.submit(_ownerId, _amountTokens); } + } - uint256 userBalance = getBalanceByUser(_owner); + function _prepareWithdrawalAmounts(uint256 _amountTokens, uint256 _userBalance) private returns (uint256) { + _amountTokens = bound(_amountTokens, wqContract.MIN_STETH_WITHDRAWAL_AMOUNT(), _userBalance); - 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()); @@ -197,101 +239,90 @@ contract ShareRateHandler is CommonBase, StdCheats, StdUtils { amountsQW.push(_amountTokens); } - vm.prank(_owner); - uint256[] memory requestIds = wqContract.requestWithdrawals(amountsQW, _owner); - delete amountsQW; + return _amountTokens; + } - vm.roll(block.number + 1_500); - vm.warp(block.timestamp + 1 days); - this.finalize(maxShareRate, _amountTokens + 10_000 * 1 ether); + function _calculateBatches( + uint256 _ethBudget, + uint256 _maxShareRate + ) public view returns (uint256[] memory batches) { + uint256[MAX_BATCHES_SIZE] memory emptyBatches; + WithdrawalQueueBase.BatchesCalculationState memory state = WithdrawalQueueBase.BatchesCalculationState( + _ethBudget * 1 ether, + false, + emptyBatches, + 0 + ); - if (wqContract.getLastFinalizedRequestId() > 0) { - WithdrawalQueueBase.WithdrawalRequestStatus[] memory requestStatues = wqContract.getWithdrawalStatus( - requestIds + while (!state.finished) { + state = wqContract.calculateFinalizationBatches( + _maxShareRate, + block.timestamp + BATCH_CALCULATION_TIMEOUT, + BATCH_CALCULATION_MAX_ITERATIONS, + state ); - 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; - } - - function getBoundaryValues() public view returns (BoundaryValues memory) { - return boundaryValues; + batches = new uint256[](state.batchesLength); + for (uint256 i; i < state.batchesLength; ++i) { + batches[i] = state.batches[i]; + } } - function finalize(uint256 maxShareRate, uint256 ethBudget) public payable { - maxShareRate = bound(maxShareRate, 0.0001 * 10 ** 27, 100 * 10 ** 27); + function _finalize(uint256 _maxShareRate, uint256 _ethBudget) public payable { + _maxShareRate = bound(_maxShareRate, MIN_SHARE_RATE, MAX_SHARE_RATE); - uint256[] memory batches = calculateBatches(ethBudget, maxShareRate); + uint256[] memory batches = _calculateBatches(_ethBudget, _maxShareRate); if (batches.length > 0) { - (uint256 eth, ) = wqContract.prefinalize(batches, maxShareRate); + (uint256 eth, ) = wqContract.prefinalize(batches, _maxShareRate); vm.deal(address(rootAccount), eth); vm.prank(rootAccount); - wqContract.finalize{value: eth}(batches[batches.length - 1], maxShareRate); + 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); - } + function _claimWithdrawals(address _owner, uint256[] memory _requestIds) private { + if (wqContract.getLastFinalizedRequestId() > 0) { + WithdrawalQueueBase.WithdrawalRequestStatus[] memory requestStatues = wqContract.getWithdrawalStatus( + _requestIds + ); - batches = new uint256[](state.batchesLength); - for (uint256 i; i < state.batchesLength; ++i) { - batches[i] = state.batches[i]; + for (uint256 i = 0; i < _requestIds.length; i++) { + if (!requestStatues[i].isClaimed) { + vm.deal(_owner, 1 ether); + vm.prank(_owner); + wqContract.claimWithdrawal(_requestIds[i]); + } + } } } } contract ShareRateTest is BaseProtocolTest { + // Contract under test ShareRateHandler public shareRateHandler; - uint256 private _maxExternalRatioBP = 10_000; - uint256 private _maxStakeLimit = 15_000_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 { - BaseProtocolTest.setUpProtocol(protocolStartBalance, rootAccount, userAccount); - + // Initialize protocol with starting balance and accounts + BaseProtocolTest.setUpProtocol(PROTOCOL_START_BALANCE, ROOT_ACCOUNT, USER_ACCOUNT); address vaultHubAddress = lidoLocator.vaultHub(); - vm.startPrank(userAccount); - lidoContract.setMaxExternalRatioBP(_maxExternalRatioBP); - lidoContract.setStakingLimit(_maxStakeLimit, _stakeLimitIncreasePerBlock); - lidoContract.resume(); - vm.stopPrank(); + // Configure protocol parameters + _configureProtocolSettings(); + // Initialize the handler shareRateHandler = new ShareRateHandler( lidoContract, wq, vaultHubAddress, - userAccount, - rootAccount, - _maxAmountOfShares + USER_ACCOUNT, + ROOT_ACCOUNT, + MAX_AMOUNT_OF_SHARES ); + // Configure fuzzing targets bytes4[] memory externalSharesSelectors = new bytes4[](5); externalSharesSelectors[0] = shareRateHandler.mintExternalShares.selector; externalSharesSelectors[1] = shareRateHandler.burnExternalShares.selector; @@ -302,22 +333,26 @@ contract ShareRateTest is BaseProtocolTest { targetContract(address(shareRateHandler)); targetSelector(FuzzSelector({addr: address(shareRateHandler), selectors: externalSharesSelectors})); - // @dev mint 10000 external shares to simulate some shares already minted, so - // burnExternalShares will be able to actually burn some shares - vm.prank(vaultHubAddress); - lidoContract.mintExternalShares(vaultHubAddress, protocolStartExternalShares); - shareRateHandler.submit(0, 10 ether); + // Initialize with starting shares + _provisionProtocol(vaultHubAddress); + // Advance blockchain state vm.roll(block.number + ONE_DAY_IN_BLOCKS); } - function logBoundaryValues() public view { - ShareRateHandler.BoundaryValues memory bounds = shareRateHandler.getBoundaryValues(); + function _configureProtocolSettings() private { + vm.startPrank(USER_ACCOUNT); + lidoContract.setMaxExternalRatioBP(MAX_EXTERNAL_RATIO_BP); + lidoContract.setStakingLimit(MAX_STAKE_LIMIT, STAKE_LIMIT_INCREASE_PER_BLOCK); + lidoContract.resume(); + vm.stopPrank(); + } - 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); + function _provisionProtocol(address vaultHubAddress) private { + // Mint external shares to simulate existing shares for burn operations + vm.prank(vaultHubAddress); + lidoContract.mintExternalShares(vaultHubAddress, PROTOCOL_START_EXTERNAL_SHARES); + shareRateHandler.submit(0, 10 ether); } /** @@ -328,7 +363,5 @@ contract ShareRateTest is BaseProtocolTest { */ function invariant_totalShares() public view { assertEq(lidoContract.getTotalShares(), shareRateHandler.getTotalShares()); - - logBoundaryValues(); } } From bcb6f2d352acd3170a0fe433800b656a3f4cb564 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Fri, 28 Mar 2025 11:28:44 +0100 Subject: [PATCH 29/36] fix: small test refactoring --- .../staking-vault/StakingVaultTest.t.sol | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol index 4e299b9ea..d60418e9c 100644 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -90,6 +90,7 @@ contract ValidatorMock is Test { uint256 private currentAPR; uint256 private lastRewardTimestamp; + RandomLib.Storage private rnd; constructor(uint256 _seed) { @@ -132,7 +133,7 @@ contract StakingVaultTest is Test { error RebalanceAmountExceedsValuation(uint256 valuation, uint256 rebalanceAmount); uint256 constant ITERATIONS = 32; - uint256 constant STATE_TRANSITIONS = 32; + uint256 constant MAJOR_STATE_TRANSITIONS = 32; uint256 constant VALIDATOR_DEPOSIT = 32 ether; uint256 constant CONNECT_DEPOSIT = 1 ether; uint256 constant SECONDS_PER_DAY = 86400; @@ -168,16 +169,16 @@ contract StakingVaultTest is Test { function runTests(uint256 _seed) internal { require( - STATE_TRANSITIONS * ITERATIONS * 10 ** 25 <= type(uint256).max / 2 - 1, - "STATE_TRANSITIONS * ITERATIONS overflow" + MAJOR_STATE_TRANSITIONS * ITERATIONS * 10 ** 25 * 2 <= type(uint256).max, + "MAJOR_STATE_TRANSITIONS * ITERATIONS overflow" ); deploy(_seed); uint256 initialBalance = address(stakingVaultProxy).balance; int256 initialInOutDelta = stakingVaultProxy.inOutDelta(); - for (uint256 i = 0; i < STATE_TRANSITIONS; i++) { - doTransitionToNewVaultState(hasValidator, isConnectedToHub); + for (uint256 i = 0; i < MAJOR_STATE_TRANSITIONS; i++) { + performMajorStateTransition(hasValidator, isConnectedToHub); for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { randomTransition(hasValidator, isConnectedToHub); } @@ -215,7 +216,7 @@ contract StakingVaultTest is Test { stakingVaultProxy = StakingVault(payable(address(proxy))); } - function doTransitionToNewVaultState(bool _hasValidator, bool _isConnectedToHub) internal { + function performMajorStateTransition(bool _hasValidator, bool _isConnectedToHub) internal { bool oldIsConnectedToHub = _isConnectedToHub; bool oldHasValidator = _hasValidator; @@ -299,6 +300,8 @@ contract StakingVaultTest is Test { function transitionRandomUserDeposit() internal { uint256 amount = rnd.randAmountD18(); + console2.log("Deposit by random user %d", amount); + address user = rnd.randAddress(); deal(user, amount); if (amount == 0) { @@ -308,12 +311,12 @@ contract StakingVaultTest is Test { payable(address(stakingVaultProxy)).transfer(amount); vm.stopPrank(); randomUserDeposits += amount; - - console2.log("transitionRandomUserDeposit: %d", amount); } function transitionRandomFund() internal { uint256 amount = rnd.randAmountD18(); + console2.log("Fund vault %d", amount); + deal(owner, amount); if (amount == 0) { vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "msg.value")); @@ -322,8 +325,6 @@ contract StakingVaultTest is Test { stakingVaultProxy.fund{value: amount}(); vm.stopPrank(); deposits += amount; - - console2.log("transitionRandomFund: %s", amount); } function transitionRandomWithdraw() internal { @@ -331,6 +332,7 @@ contract StakingVaultTest is Test { uint256 vaultBalance = address(stakingVaultProxy).balance; uint256 minWithdrawal = Math.min(unlocked, vaultBalance); uint256 withdrawableAmount = rnd.randInt(minWithdrawal); + console2.log("Withdraw funds %d", withdrawableAmount); deal(owner, withdrawableAmount); if (withdrawableAmount == 0) { @@ -340,22 +342,17 @@ contract StakingVaultTest is Test { stakingVaultProxy.withdraw(owner, withdrawableAmount); vm.stopPrank(); withdrawals += withdrawableAmount; - - console2.log("transitionRandomWithdraw: %d", withdrawableAmount); } function transitionRandomReceiveReward() internal { uint256 dailyReward = validator.getDailyReward(); + console2.log("Receive reward %d", dailyReward); vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + dailyReward); rewards += dailyReward; - - console2.log("transitionRandomReceiveReward: %d", dailyReward); } function transitionDepositToBeaconChain() internal { - console2.log("-------------------------------- transitionDepositToBeaconChain--------------------------------"); - - vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); + console2.log("------Deposit to Beacon Chain and start simulating validator------"); deal(owner, VALIDATOR_DEPOSIT); vm.prank(owner); @@ -388,18 +385,15 @@ contract StakingVaultTest is Test { } function transitionValidatorExitAndReturnDeposit() internal { - console2.log( - "-------------------------------- transitionValidatorExitAndReturnDeposit--------------------------------" - ); - vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); + console2.log("------Validator exit and return deposit------"); - // receive validator deposit deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + VALIDATOR_DEPOSIT); depositsToBeaconChain -= VALIDATOR_DEPOSIT; } function transitionRandomDepositToBeaconChain() internal { uint256 amount = rnd.randAmountD18(); + console2.log("Deposit to Beacon Chain %d", amount); bytes memory pubkey = new bytes(48); bytes32 firstPart = bytes32(uint256(1)); @@ -423,19 +417,19 @@ contract StakingVaultTest is Test { vm.stopPrank(); depositsToBeaconChain += amount; - - console2.log("transitionRandomDepositToBeaconChain: %d", amount); } function transitionConnectVaultToHub() internal { - console2.log("-------------------------------- transitionConnectVaultToHub--------------------------------"); + console2.log("------Connect Vault to Hub------"); + vm.prank(address(vaultHub)); stakingVaultProxy.lock(CONNECT_DEPOSIT); vm.stopPrank(); } function transitionDisconnectVaultFromHub() internal { - console2.log("-------------------------------- transitionDisconnectVaultFromHub----------------------------"); + console2.log("------Disconnect Vault from Hub------"); + uint256 valuation = stakingVaultProxy.valuation(); int256 inOutDelta = stakingVaultProxy.inOutDelta(); vm.prank(address(vaultHub)); @@ -446,6 +440,8 @@ contract StakingVaultTest is Test { function transitionRandomMintShares() internal { uint256 vaultValuation = stakingVaultProxy.valuation(); uint256 totalEtherToLock = vaultHub.getTotalEtherToLock(vaultValuation); + console2.log("Mint shares %d", totalEtherToLock); + uint256 currentLocked = stakingVaultProxy.locked(); if (totalEtherToLock < currentLocked) { vm.expectRevert( @@ -455,11 +451,11 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.lock(totalEtherToLock); vm.stopPrank(); - - console2.log("transitionRandomMintShares: %d", totalEtherToLock); } function transitionRandomReport() internal { + console2.log("Receive report"); + uint256 currentValuation = stakingVaultProxy.valuation(); int256 currentInOutDelta = stakingVaultProxy.inOutDelta(); uint256 currentLocked = stakingVaultProxy.locked(); @@ -470,8 +466,6 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.report(newValuation, currentInOutDelta, newLocked); vm.stopPrank(); - - console2.log("transitionRandomReport: %d", newValuation); } function transitionRandomRebalance() internal { @@ -479,6 +473,7 @@ contract StakingVaultTest is Test { uint256 currentBalance = address(stakingVaultProxy).balance; uint256 currentLocked = stakingVaultProxy.locked(); uint256 etherToRebalance = vaultHub.getAmountToUnlock(currentValuation, currentLocked); + console2.log("Rebalance %d", etherToRebalance); if (etherToRebalance == 0) { vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); @@ -494,7 +489,5 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.rebalance(etherToRebalance); vm.stopPrank(); - - console2.log("transitionRandomRebalance: %d", etherToRebalance); } } From db8c0b34d242f48f34a085e512322765aeaf07bb Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 31 Mar 2025 12:16:34 +0100 Subject: [PATCH 30/36] feat: reduce logs --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 471024d4e..f132b4bd0 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -37,4 +37,4 @@ jobs: run: forge --version - name: Run fuzzing and invariant tests - run: forge test -vvv + run: forge test From 9bd545dba366ef44486d0e8c7448b187dc2e1ee3 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 31 Mar 2025 13:16:10 +0100 Subject: [PATCH 31/36] feat: add detailed logs --- .../staking-vault/StakingVaultTest.t.sol | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol index d60418e9c..b76e285c5 100644 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -86,7 +86,7 @@ contract ValidatorMock is Test { uint256 constant DAYS_PER_YEAR = 365; uint256 constant APR_MIN = 300; // 3.00% minimum APR uint256 constant APR_MAX = 500; // 5.00% maximum APR - uint256 private validatorBalance; + uint256 constant MIN_VALIDATOR_BALANCE = 32 ether; uint256 private currentAPR; uint256 private lastRewardTimestamp; @@ -108,8 +108,7 @@ contract ValidatorMock is Test { uint256 daysPassed = timePassed / SECONDS_PER_DAY; lastRewardTimestamp += daysPassed * SECONDS_PER_DAY; - uint256 validatorBalance = 32 ether; - uint256 yearlyReward = (validatorBalance * currentAPR) / APR_DENOMINATOR; + uint256 yearlyReward = (MIN_VALIDATOR_BALANCE * currentAPR) / APR_DENOMINATOR; uint256 dailyReward = (yearlyReward * daysPassed) / DAYS_PER_YEAR; int256 randomVariation = int256(rnd.randInt(200)) - 100; @@ -299,21 +298,25 @@ contract StakingVaultTest is Test { } function transitionRandomUserDeposit() internal { + console2.log("------Deposit by random user------"); + uint256 amount = rnd.randAmountD18(); console2.log("Deposit by random user %d", amount); - address user = rnd.randAddress(); - deal(user, amount); + address randomUser = rnd.randAddress(); + deal(randomUser, amount); if (amount == 0) { vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "msg.value")); } - vm.prank(user); + vm.prank(randomUser); payable(address(stakingVaultProxy)).transfer(amount); vm.stopPrank(); randomUserDeposits += amount; } function transitionRandomFund() internal { + console2.log("------Fund vault------"); + uint256 amount = rnd.randAmountD18(); console2.log("Fund vault %d", amount); @@ -328,6 +331,8 @@ contract StakingVaultTest is Test { } function transitionRandomWithdraw() internal { + console2.log("------Withdraw funds------"); + uint256 unlocked = stakingVaultProxy.unlocked(); uint256 vaultBalance = address(stakingVaultProxy).balance; uint256 minWithdrawal = Math.min(unlocked, vaultBalance); @@ -345,6 +350,8 @@ contract StakingVaultTest is Test { } function transitionRandomReceiveReward() internal { + console2.log("------Receive reward------"); + uint256 dailyReward = validator.getDailyReward(); console2.log("Receive reward %d", dailyReward); vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + dailyReward); @@ -360,7 +367,7 @@ contract StakingVaultTest is Test { vm.stopPrank(); deposits += VALIDATOR_DEPOSIT; - IStakingVault.Deposit[] memory deposits = new IStakingVault.Deposit[](1); + IStakingVault.Deposit[] memory newDeposits = new IStakingVault.Deposit[](1); bytes memory pubkey = new bytes(48); bytes32 firstPart = bytes32(uint256(1)); @@ -371,14 +378,14 @@ contract StakingVaultTest is Test { mstore(add(pubkey, 64), secondPart) } - deposits[0] = IStakingVault.Deposit({ + newDeposits[0] = IStakingVault.Deposit({ pubkey: pubkey, signature: bytes.concat(bytes32(uint256(2))), amount: VALIDATOR_DEPOSIT, depositDataRoot: bytes32(uint256(3)) }); vm.prank(depositor); - stakingVaultProxy.depositToBeaconChain(deposits); + stakingVaultProxy.depositToBeaconChain(newDeposits); vm.stopPrank(); depositsToBeaconChain += VALIDATOR_DEPOSIT; @@ -392,6 +399,8 @@ contract StakingVaultTest is Test { } function transitionRandomDepositToBeaconChain() internal { + console2.log("------Deposit to Beacon Chain------"); + uint256 amount = rnd.randAmountD18(); console2.log("Deposit to Beacon Chain %d", amount); @@ -404,8 +413,8 @@ contract StakingVaultTest is Test { mstore(add(pubkey, 64), secondPart) } - IStakingVault.Deposit[] memory deposits = new IStakingVault.Deposit[](1); - deposits[0] = IStakingVault.Deposit({ + IStakingVault.Deposit[] memory newDeposits = new IStakingVault.Deposit[](1); + newDeposits[0] = IStakingVault.Deposit({ pubkey: pubkey, signature: bytes.concat(bytes32(uint256(2))), amount: amount, @@ -413,7 +422,7 @@ contract StakingVaultTest is Test { }); vm.prank(depositor); - stakingVaultProxy.depositToBeaconChain(deposits); + stakingVaultProxy.depositToBeaconChain(newDeposits); vm.stopPrank(); depositsToBeaconChain += amount; @@ -424,7 +433,8 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.lock(CONNECT_DEPOSIT); - vm.stopPrank(); + + assertEq(stakingVaultProxy.locked(), CONNECT_DEPOSIT); } function transitionDisconnectVaultFromHub() internal { @@ -432,12 +442,16 @@ contract StakingVaultTest is Test { uint256 valuation = stakingVaultProxy.valuation(); int256 inOutDelta = stakingVaultProxy.inOutDelta(); + vm.prank(address(vaultHub)); stakingVaultProxy.report(valuation, inOutDelta, 0); - vm.stopPrank(); + + assertEq(stakingVaultProxy.locked(), 0); } function transitionRandomMintShares() internal { + console2.log("------Mint shares------"); + uint256 vaultValuation = stakingVaultProxy.valuation(); uint256 totalEtherToLock = vaultHub.getTotalEtherToLock(vaultValuation); console2.log("Mint shares %d", totalEtherToLock); @@ -448,13 +462,15 @@ contract StakingVaultTest is Test { abi.encodeWithSelector(LockedCannotDecreaseOutsideOfReport.selector, currentLocked, totalEtherToLock) ); } + vm.prank(address(vaultHub)); stakingVaultProxy.lock(totalEtherToLock); - vm.stopPrank(); + + assertEq(stakingVaultProxy.locked(), totalEtherToLock); } function transitionRandomReport() internal { - console2.log("Receive report"); + console2.log("------Receive report------"); uint256 currentValuation = stakingVaultProxy.valuation(); int256 currentInOutDelta = stakingVaultProxy.inOutDelta(); @@ -465,10 +481,14 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.report(newValuation, currentInOutDelta, newLocked); - vm.stopPrank(); + + assertEq(stakingVaultProxy.valuation(), newValuation); + assertEq(stakingVaultProxy.locked(), newLocked); } function transitionRandomRebalance() internal { + console2.log("------Rebalance------"); + uint256 currentValuation = stakingVaultProxy.valuation(); uint256 currentBalance = address(stakingVaultProxy).balance; uint256 currentLocked = stakingVaultProxy.locked(); From e4311b4c640b811a767cac2ccba426dc267e807e Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 31 Mar 2025 14:14:48 +0100 Subject: [PATCH 32/36] feat: cleanup --- test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol index b76e285c5..1ce28ca1c 100644 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -465,8 +465,6 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.lock(totalEtherToLock); - - assertEq(stakingVaultProxy.locked(), totalEtherToLock); } function transitionRandomReport() internal { @@ -481,9 +479,6 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.report(newValuation, currentInOutDelta, newLocked); - - assertEq(stakingVaultProxy.valuation(), newValuation); - assertEq(stakingVaultProxy.locked(), newLocked); } function transitionRandomRebalance() internal { From 1e475ca276b7cfd641127b00dbfe11739c72871b Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 31 Mar 2025 15:32:46 +0100 Subject: [PATCH 33/36] feat: disable logs --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 471024d4e..f132b4bd0 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -37,4 +37,4 @@ jobs: run: forge --version - name: Run fuzzing and invariant tests - run: forge test -vvv + run: forge test From 571ab8776495f507cf5315a054311434215e34a0 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 1 Apr 2025 13:47:38 +0200 Subject: [PATCH 34/36] fix: add asserts, fix fund error --- .../staking-vault/StakingVaultTest.t.sol | 90 ++++++++++++++----- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol index 1ce28ca1c..568bbb997 100644 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -76,6 +76,12 @@ contract VaultHubMock is Test { uint256 targetLocked = (currentValuation * RESERVE_RATIO_BP) / TOTAL_BASIS_POINTS; return currentLocked - targetLocked; } + + event Mock__Rebalanced(address indexed vault, uint256 amount); + + function rebalance() external payable { + emit Mock__Rebalanced(msg.sender, msg.value); + } } contract ValidatorMock is Test { @@ -147,6 +153,7 @@ contract StakingVaultTest is Test { uint256 private rewards; uint256 private randomUserDeposits; uint256 private depositsToBeaconChain; + uint256 private vaultHubBalance; address private depositor = address(0x001); address private owner = address(0x002); @@ -159,7 +166,7 @@ contract StakingVaultTest is Test { bool private hasValidator; function testSolvencyAllTransitions() external { - runTests(42); + runTests(5686631772487049791906286); } function testFuzz_SolvencyAllTransitions(uint256 _seed) external { @@ -186,11 +193,12 @@ contract StakingVaultTest is Test { uint256 finalBalance = address(stakingVaultProxy).balance; int256 finalInOutDelta = stakingVaultProxy.inOutDelta(); + console2.log("VaultHub balance: %d", vaultHubBalance); assertEq( deposits + initialBalance + rewards + randomUserDeposits, - finalBalance + withdrawals + depositsToBeaconChain + finalBalance + withdrawals + depositsToBeaconChain + vaultHubBalance ); - assertEq(initialInOutDelta + int256(deposits), finalInOutDelta + int256(withdrawals)); + assertEq(initialInOutDelta + int256(deposits), finalInOutDelta + int256(withdrawals) + int256(vaultHubBalance)); } function deploy(uint256 _seed) public { @@ -202,6 +210,7 @@ contract StakingVaultTest is Test { rewards = 0; randomUserDeposits = 0; depositsToBeaconChain = 0; + vaultHubBalance = 0; DepositContract__MockForStakingVault depositContract = new DepositContract__MockForStakingVault(); vaultHub = new VaultHubMock(_seed); @@ -301,8 +310,6 @@ contract StakingVaultTest is Test { console2.log("------Deposit by random user------"); uint256 amount = rnd.randAmountD18(); - console2.log("Deposit by random user %d", amount); - address randomUser = rnd.randAddress(); deal(randomUser, amount); if (amount == 0) { @@ -310,7 +317,6 @@ contract StakingVaultTest is Test { } vm.prank(randomUser); payable(address(stakingVaultProxy)).transfer(amount); - vm.stopPrank(); randomUserDeposits += amount; } @@ -318,16 +324,22 @@ contract StakingVaultTest is Test { console2.log("------Fund vault------"); uint256 amount = rnd.randAmountD18(); - console2.log("Fund vault %d", amount); + int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); + uint256 valuationBefore = stakingVaultProxy.valuation(); + uint256 balanceBefore = address(stakingVaultProxy).balance; deal(owner, amount); if (amount == 0) { vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "msg.value")); } - vm.startPrank(owner); + + vm.prank(owner); stakingVaultProxy.fund{value: amount}(); - vm.stopPrank(); deposits += amount; + + assertEq(stakingVaultProxy.inOutDelta(), inOutDeltaBefore + int256(amount)); + assertEq(stakingVaultProxy.valuation(), valuationBefore + amount); + assertEq(address(stakingVaultProxy).balance, balanceBefore + amount); } function transitionRandomWithdraw() internal { @@ -337,25 +349,36 @@ contract StakingVaultTest is Test { uint256 vaultBalance = address(stakingVaultProxy).balance; uint256 minWithdrawal = Math.min(unlocked, vaultBalance); uint256 withdrawableAmount = rnd.randInt(minWithdrawal); - console2.log("Withdraw funds %d", withdrawableAmount); + int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); + uint256 valuationBefore = stakingVaultProxy.valuation(); + uint256 balanceBefore = address(stakingVaultProxy).balance; deal(owner, withdrawableAmount); if (withdrawableAmount == 0) { vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); } + vm.prank(owner); stakingVaultProxy.withdraw(owner, withdrawableAmount); - vm.stopPrank(); withdrawals += withdrawableAmount; + + assertEq(inOutDeltaBefore, stakingVaultProxy.inOutDelta() + int256(withdrawableAmount)); + assertEq(valuationBefore, stakingVaultProxy.valuation() + withdrawableAmount); + assertEq(balanceBefore, address(stakingVaultProxy).balance + withdrawableAmount); } function transitionRandomReceiveReward() internal { console2.log("------Receive reward------"); uint256 dailyReward = validator.getDailyReward(); - console2.log("Receive reward %d", dailyReward); + uint256 valuationBefore = stakingVaultProxy.valuation(); + uint256 balanceBefore = address(stakingVaultProxy).balance; + vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + dailyReward); rewards += dailyReward; + + assertEq(valuationBefore, stakingVaultProxy.valuation()); + assertEq(address(stakingVaultProxy).balance, balanceBefore + dailyReward); } function transitionDepositToBeaconChain() internal { @@ -364,45 +387,53 @@ contract StakingVaultTest is Test { deal(owner, VALIDATOR_DEPOSIT); vm.prank(owner); stakingVaultProxy.fund{value: VALIDATOR_DEPOSIT}(); - vm.stopPrank(); deposits += VALIDATOR_DEPOSIT; - IStakingVault.Deposit[] memory newDeposits = new IStakingVault.Deposit[](1); + int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); + uint256 valuationBefore = stakingVaultProxy.valuation(); + uint256 balanceBefore = address(stakingVaultProxy).balance; + IStakingVault.Deposit[] memory newDeposits = new IStakingVault.Deposit[](1); bytes memory pubkey = new bytes(48); bytes32 firstPart = bytes32(uint256(1)); bytes16 secondPart = bytes16(bytes32(uint256(2))); - assembly { mstore(add(pubkey, 32), firstPart) mstore(add(pubkey, 64), secondPart) } - newDeposits[0] = IStakingVault.Deposit({ pubkey: pubkey, signature: bytes.concat(bytes32(uint256(2))), amount: VALIDATOR_DEPOSIT, depositDataRoot: bytes32(uint256(3)) }); + vm.prank(depositor); stakingVaultProxy.depositToBeaconChain(newDeposits); - vm.stopPrank(); - depositsToBeaconChain += VALIDATOR_DEPOSIT; + + assertEq(inOutDeltaBefore, stakingVaultProxy.inOutDelta()); + assertEq(valuationBefore, stakingVaultProxy.valuation()); + assertEq(balanceBefore, address(stakingVaultProxy).balance + VALIDATOR_DEPOSIT); } function transitionValidatorExitAndReturnDeposit() internal { console2.log("------Validator exit and return deposit------"); + uint256 balanceBefore = address(stakingVaultProxy).balance; + uint256 valuationBefore = stakingVaultProxy.valuation(); + deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + VALIDATOR_DEPOSIT); depositsToBeaconChain -= VALIDATOR_DEPOSIT; + + assertEq(address(stakingVaultProxy).balance, balanceBefore + VALIDATOR_DEPOSIT); + assertEq(valuationBefore, stakingVaultProxy.valuation()); } function transitionRandomDepositToBeaconChain() internal { console2.log("------Deposit to Beacon Chain------"); uint256 amount = rnd.randAmountD18(); - console2.log("Deposit to Beacon Chain %d", amount); bytes memory pubkey = new bytes(48); bytes32 firstPart = bytes32(uint256(1)); @@ -423,8 +454,6 @@ contract StakingVaultTest is Test { vm.prank(depositor); stakingVaultProxy.depositToBeaconChain(newDeposits); - vm.stopPrank(); - depositsToBeaconChain += amount; } @@ -447,6 +476,11 @@ contract StakingVaultTest is Test { stakingVaultProxy.report(valuation, inOutDelta, 0); assertEq(stakingVaultProxy.locked(), 0); + assertEq(inOutDelta, stakingVaultProxy.inOutDelta()); + assertEq( + int256(stakingVaultProxy.valuation()) + inOutDelta, + int256(valuation) + stakingVaultProxy.inOutDelta() + ); } function transitionRandomMintShares() internal { @@ -454,7 +488,6 @@ contract StakingVaultTest is Test { uint256 vaultValuation = stakingVaultProxy.valuation(); uint256 totalEtherToLock = vaultHub.getTotalEtherToLock(vaultValuation); - console2.log("Mint shares %d", totalEtherToLock); uint256 currentLocked = stakingVaultProxy.locked(); if (totalEtherToLock < currentLocked) { @@ -465,6 +498,8 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.lock(totalEtherToLock); + + assertEq(stakingVaultProxy.locked(), totalEtherToLock < currentLocked ? currentLocked : totalEtherToLock); } function transitionRandomReport() internal { @@ -479,6 +514,13 @@ contract StakingVaultTest is Test { vm.prank(address(vaultHub)); stakingVaultProxy.report(newValuation, currentInOutDelta, newLocked); + + assertEq(stakingVaultProxy.locked(), newLocked); + assertEq(currentInOutDelta, stakingVaultProxy.inOutDelta()); + assertEq( + int256(stakingVaultProxy.valuation()) + currentInOutDelta, + int256(newValuation) + stakingVaultProxy.inOutDelta() + ); } function transitionRandomRebalance() internal { @@ -488,7 +530,6 @@ contract StakingVaultTest is Test { uint256 currentBalance = address(stakingVaultProxy).balance; uint256 currentLocked = stakingVaultProxy.locked(); uint256 etherToRebalance = vaultHub.getAmountToUnlock(currentValuation, currentLocked); - console2.log("Rebalance %d", etherToRebalance); if (etherToRebalance == 0) { vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); @@ -500,9 +541,10 @@ contract StakingVaultTest is Test { ); } else if (currentValuation >= currentLocked) { vm.expectRevert(abi.encodeWithSelector(NotAuthorized.selector, "rebalance", address(vaultHub))); + } else { + vaultHubBalance += etherToRebalance; } vm.prank(address(vaultHub)); stakingVaultProxy.rebalance(etherToRebalance); - vm.stopPrank(); } } From a915fef5d41cdf1d71378b26681f2083e59f6876 Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Tue, 1 Apr 2025 14:30:59 +0200 Subject: [PATCH 35/36] fix: add more asserts --- .../staking-vault/StakingVaultTest.t.sol | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol index 568bbb997..699fe8b71 100644 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol @@ -307,7 +307,7 @@ contract StakingVaultTest is Test { } function transitionRandomUserDeposit() internal { - console2.log("------Deposit by random user------"); + console2.log("Deposit by random user with random amount"); uint256 amount = rnd.randAmountD18(); address randomUser = rnd.randAddress(); @@ -321,7 +321,7 @@ contract StakingVaultTest is Test { } function transitionRandomFund() internal { - console2.log("------Fund vault------"); + console2.log("Fund vault with random amount of funds"); uint256 amount = rnd.randAmountD18(); int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); @@ -343,7 +343,7 @@ contract StakingVaultTest is Test { } function transitionRandomWithdraw() internal { - console2.log("------Withdraw funds------"); + console2.log("Withdraw random amount of funds"); uint256 unlocked = stakingVaultProxy.unlocked(); uint256 vaultBalance = address(stakingVaultProxy).balance; @@ -368,7 +368,7 @@ contract StakingVaultTest is Test { } function transitionRandomReceiveReward() internal { - console2.log("------Receive reward------"); + console2.log("Receive random reward"); uint256 dailyReward = validator.getDailyReward(); uint256 valuationBefore = stakingVaultProxy.valuation(); @@ -431,9 +431,12 @@ contract StakingVaultTest is Test { } function transitionRandomDepositToBeaconChain() internal { - console2.log("------Deposit to Beacon Chain------"); + console2.log("Deposit to Beacon Chain with random amount"); uint256 amount = rnd.randAmountD18(); + int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); + uint256 valuationBefore = stakingVaultProxy.valuation(); + uint256 balanceBefore = address(stakingVaultProxy).balance; bytes memory pubkey = new bytes(48); bytes32 firstPart = bytes32(uint256(1)); @@ -455,6 +458,10 @@ contract StakingVaultTest is Test { vm.prank(depositor); stakingVaultProxy.depositToBeaconChain(newDeposits); depositsToBeaconChain += amount; + + assertEq(inOutDeltaBefore, stakingVaultProxy.inOutDelta()); + assertEq(valuationBefore, stakingVaultProxy.valuation()); + assertEq(balanceBefore, address(stakingVaultProxy).balance + amount); } function transitionConnectVaultToHub() internal { @@ -484,7 +491,7 @@ contract StakingVaultTest is Test { } function transitionRandomMintShares() internal { - console2.log("------Mint shares------"); + console2.log("Mint shares with random amount"); uint256 vaultValuation = stakingVaultProxy.valuation(); uint256 totalEtherToLock = vaultHub.getTotalEtherToLock(vaultValuation); @@ -503,7 +510,7 @@ contract StakingVaultTest is Test { } function transitionRandomReport() internal { - console2.log("------Receive report------"); + console2.log("Receive report"); uint256 currentValuation = stakingVaultProxy.valuation(); int256 currentInOutDelta = stakingVaultProxy.inOutDelta(); @@ -524,27 +531,40 @@ contract StakingVaultTest is Test { } function transitionRandomRebalance() internal { - console2.log("------Rebalance------"); + console2.log("Rebalance with random amount"); - uint256 currentValuation = stakingVaultProxy.valuation(); - uint256 currentBalance = address(stakingVaultProxy).balance; - uint256 currentLocked = stakingVaultProxy.locked(); - uint256 etherToRebalance = vaultHub.getAmountToUnlock(currentValuation, currentLocked); + int256 inOutDelta = stakingVaultProxy.inOutDelta(); + uint256 valuation = stakingVaultProxy.valuation(); + uint256 balance = address(stakingVaultProxy).balance; + uint256 locked = stakingVaultProxy.locked(); + uint256 etherToRebalance = vaultHub.getAmountToUnlock(valuation, locked); + bool isRebalanceAllowed = false; if (etherToRebalance == 0) { vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); - } else if (etherToRebalance > currentBalance) { - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector, currentBalance)); - } else if (etherToRebalance > currentValuation) { + } else if (etherToRebalance > balance) { + vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector, balance)); + } else if (etherToRebalance > valuation) { vm.expectRevert( - abi.encodeWithSelector(RebalanceAmountExceedsValuation.selector, currentValuation, etherToRebalance) + abi.encodeWithSelector(RebalanceAmountExceedsValuation.selector, valuation, etherToRebalance) ); - } else if (currentValuation >= currentLocked) { + } else if (valuation >= locked) { vm.expectRevert(abi.encodeWithSelector(NotAuthorized.selector, "rebalance", address(vaultHub))); } else { + isRebalanceAllowed = true; vaultHubBalance += etherToRebalance; } vm.prank(address(vaultHub)); stakingVaultProxy.rebalance(etherToRebalance); + + if (isRebalanceAllowed) { + assertEq(inOutDelta, stakingVaultProxy.inOutDelta() + int256(etherToRebalance)); + assertEq(valuation, stakingVaultProxy.valuation() + etherToRebalance); + assertEq(balance, address(stakingVaultProxy).balance + etherToRebalance); + } else { + assertEq(inOutDelta, stakingVaultProxy.inOutDelta()); + assertEq(valuation, stakingVaultProxy.valuation()); + assertEq(balance, address(stakingVaultProxy).balance); + } } } From 7e52fb5f0189e77fbf1b9c730b6de28c36f7d73d Mon Sep 17 00:00:00 2001 From: Anton Kovalchuk Date: Wed, 25 Jun 2025 10:56:19 +0200 Subject: [PATCH 36/36] fix: fix accounting tests --- test/0.8.25/Accounting.t.sol | 3 +- test/0.8.25/Protocol__Deployment.t.sol | 16 +- .../staking-vault/StakingVaultTest.t.sol | 570 ------------------ 3 files changed, 13 insertions(+), 576 deletions(-) delete mode 100644 test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol diff --git a/test/0.8.25/Accounting.t.sol b/test/0.8.25/Accounting.t.sol index c54bb9734..7ceec98c9 100644 --- a/test/0.8.25/Accounting.t.sol +++ b/test/0.8.25/Accounting.t.sol @@ -247,8 +247,7 @@ contract AccountingHandler is CommonBase, StdCheats, StdUtils { withdrawalVaultBalance: 0, sharesRequestedToBurn: 0, withdrawalFinalizationBatches: new uint256[](0), - vaultValues: new uint256[](0), - inOutDeltas: new int256[](0) + vaultsTotalDeficit: 0 }); ghost.unifiedClBalanceWei = int256(fuzz._clBalanceWei + currentReport.withdrawalVaultBalance); // ? diff --git a/test/0.8.25/Protocol__Deployment.t.sol b/test/0.8.25/Protocol__Deployment.t.sol index 46849401a..3bf2f79ec 100644 --- a/test/0.8.25/Protocol__Deployment.t.sol +++ b/test/0.8.25/Protocol__Deployment.t.sol @@ -14,7 +14,9 @@ import {LidoLocator} from "contracts/0.8.9/LidoLocator.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"; +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"; @@ -265,7 +267,7 @@ contract BaseProtocolTest is Test { // Deploy AccountingOracle deployCodeTo( "AccountingOracle.sol:AccountingOracle", - abi.encode(address(lidoLocator), lidoLocator.legacyOracle(), 12, genesisTimestamp), + abi.encode(address(lidoLocator), 12, genesisTimestamp), lidoLocator.accountingOracle() ); @@ -296,6 +298,8 @@ contract BaseProtocolTest is Test { "OracleReportSanityChecker.sol:OracleReportSanityChecker", abi.encode( address(lidoLocator), + lidoLocator.accountingOracle(), + lidoLocator.accounting(), rootAccount, [ limitList.exitedValidatorsPerDayLimit, @@ -394,7 +398,6 @@ contract BaseProtocolTest is Test { 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), @@ -408,7 +411,12 @@ contract BaseProtocolTest is Test { accounting: makeAddr("dummy-locator:accounting"), predepositGuarantee: makeAddr("dummy-locator:predeposit_guarantee"), wstETH: wstETHAdr, - vaultHub: makeAddr("dummy-locator:vaultHub") + vaultHub: makeAddr("dummy-locator:vaultHub"), + lazyOracle: makeAddr("dummy-locator:lazyOracle"), + operatorGrid: makeAddr("dummy-locator:operatorGrid"), + validatorExitDelayVerifier: makeAddr("dummy-locator:validatorExitDelayVerifier"), + triggerableWithdrawalsGateway: makeAddr("dummy-locator:triggerableWithdrawalsGateway"), + vaultFactory: makeAddr("dummy-locator:vaultFactory") }); return LidoLocator(deployCode("LidoLocator.sol:LidoLocator", abi.encode(config))); diff --git a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol b/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol deleted file mode 100644 index 699fe8b71..000000000 --- a/test/0.8.25/vaults/staking-vault/StakingVaultTest.t.sol +++ /dev/null @@ -1,570 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only -// ┌────────────────────┐ -// │ │ -// │ Owner │ -// │ │ -// └──┬──────────────┬──┘ -// │ │ -// fund withdraw -// SV.balance += x SV.balance -= x -// inOutDelta += x inOutDelta -= x -// │ │ -// │ │ -// ┌──────────────────┐──report(old data)──▶┌─▼──────────────▼─┐ depositToBeaconChain ┌───────────────────────┐ -// │ │ │ │◀───SV.balance -= deposit──│ Depositor │ -// │ │ rebalance │ │ └───────────────────────┘ -// │ VaultHub │─ SV.balance -= x──▶│ StakingVault │ -// │ │ inOutDelta -= x │ │ rewards ┌───────────────────────┐ -// │ │ │ │◀───SV.balance += reward───│ Validator │ -// └──────────────────┘───────lock─────────▶└──────────────────┘ └───────────────────────┘ - -pragma solidity ^0.8.0; - -import {Test} from "forge-std/Test.sol"; -import {console2} from "forge-std/console2.sol"; -import {Math} from "@openzeppelin/contracts-v5.2/utils/math/Math.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts-v5.2/proxy/ERC1967/ERC1967Proxy.sol"; -import {StakingVault} from "contracts/0.8.25/vaults/StakingVault.sol"; -import {IStakingVault} from "contracts/0.8.25/vaults/interfaces/IStakingVault.sol"; -import {DepositContract__MockForStakingVault} from "./contracts/DepositContract__MockForStakingVault.sol"; -import {RandomLib} from "./RandomLib.sol"; - -contract VaultHubMock is Test { - using RandomLib for RandomLib.Storage; - - uint256 constant TOTAL_BASIS_POINTS = 10000; - uint256 constant MAX_MINTABLE_RATIO_BP = 8000; // 80% can be used for minting (100% - reserve ratio) - uint256 constant RESERVE_RATIO_BP = 1000; // 10% reserve ratio - uint256 constant REWARD_RATE_MIN_BP = 100; // 1% min reward rate - uint256 constant REWARD_RATE_MAX_BP = 500; // 5% max reward rate - uint256 constant TREASURY_FEE_BP = 500; // 5% treasury fee - uint256 constant REBALANCE_THRESHOLD_BP = 9500; // 95% - vault needs rebalance if valuation drops below this - - RandomLib.Storage private rnd; - - constructor(uint256 _seed) { - rnd.seed = _seed; - } - - function getTotalEtherToLock(uint256 vaultValuation) public returns (uint256) { - uint256 maxMintableEther = (vaultValuation * MAX_MINTABLE_RATIO_BP) / TOTAL_BASIS_POINTS; - uint256 amountToMint = rnd.randInt(maxMintableEther); - return (amountToMint * TOTAL_BASIS_POINTS) / MAX_MINTABLE_RATIO_BP; - } - - function getNewValuation(uint256 currentValuation) public returns (uint256) { - uint256 rewardRateBP = REWARD_RATE_MIN_BP + rnd.randInt(REWARD_RATE_MAX_BP - REWARD_RATE_MIN_BP); - uint256 newValuation = currentValuation + (currentValuation * rewardRateBP) / TOTAL_BASIS_POINTS; - uint256 treasuryFee = ((newValuation - currentValuation) * TREASURY_FEE_BP) / TOTAL_BASIS_POINTS; - newValuation -= treasuryFee; - return newValuation; - } - - function getNewLocked(uint256 currentLocked) public returns (uint256) { - return rnd.randInt(currentLocked / 2, currentLocked); - } - - function getAmountToUnlock(uint256 currentValuation, uint256 currentLocked) public pure returns (uint256) { - uint256 minRequiredValuation = (currentLocked * TOTAL_BASIS_POINTS) / RESERVE_RATIO_BP; - uint256 rebalanceThreshold = (minRequiredValuation * REBALANCE_THRESHOLD_BP) / TOTAL_BASIS_POINTS; - - if (currentValuation >= rebalanceThreshold) { - return 0; - } - - uint256 targetLocked = (currentValuation * RESERVE_RATIO_BP) / TOTAL_BASIS_POINTS; - return currentLocked - targetLocked; - } - - event Mock__Rebalanced(address indexed vault, uint256 amount); - - function rebalance() external payable { - emit Mock__Rebalanced(msg.sender, msg.value); - } -} - -contract ValidatorMock is Test { - using RandomLib for RandomLib.Storage; - - uint256 constant SECONDS_PER_DAY = 86400; - uint256 constant APR_DENOMINATOR = 10000; - uint256 constant DAYS_PER_YEAR = 365; - uint256 constant APR_MIN = 300; // 3.00% minimum APR - uint256 constant APR_MAX = 500; // 5.00% maximum APR - uint256 constant MIN_VALIDATOR_BALANCE = 32 ether; - - uint256 private currentAPR; - uint256 private lastRewardTimestamp; - - RandomLib.Storage private rnd; - - constructor(uint256 _seed) { - rnd.seed = _seed; - lastRewardTimestamp = block.timestamp; - currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); - } - - function getDailyReward() public returns (uint256) { - uint256 timePassed = block.timestamp - lastRewardTimestamp; - if (timePassed < SECONDS_PER_DAY) { - return 0; - } - - uint256 daysPassed = timePassed / SECONDS_PER_DAY; - lastRewardTimestamp += daysPassed * SECONDS_PER_DAY; - - uint256 yearlyReward = (MIN_VALIDATOR_BALANCE * currentAPR) / APR_DENOMINATOR; - uint256 dailyReward = (yearlyReward * daysPassed) / DAYS_PER_YEAR; - - int256 randomVariation = int256(rnd.randInt(200)) - 100; - dailyReward = uint256((int256(dailyReward) * (1000 + randomVariation)) / 1000); - - if (rnd.randBool()) { - currentAPR = APR_MIN + rnd.randInt(APR_MAX - APR_MIN); - } - - return dailyReward; - } -} - -contract StakingVaultTest is Test { - using RandomLib for RandomLib.Storage; - - error LockedCannotDecreaseOutsideOfReport(uint256 currentlyLocked, uint256 attemptedLocked); - error NotAuthorized(string operation, address sender); - error ZeroArgument(string name); - error InsufficientBalance(uint256 balance); - error RebalanceAmountExceedsValuation(uint256 valuation, uint256 rebalanceAmount); - - uint256 constant ITERATIONS = 32; - uint256 constant MAJOR_STATE_TRANSITIONS = 32; - uint256 constant VALIDATOR_DEPOSIT = 32 ether; - uint256 constant CONNECT_DEPOSIT = 1 ether; - uint256 constant SECONDS_PER_DAY = 86400; - - StakingVault private stakingVault; - StakingVault private stakingVaultProxy; - VaultHubMock private vaultHub; - ValidatorMock private validator; - - uint256 private deposits; - uint256 private withdrawals; - uint256 private rewards; - uint256 private randomUserDeposits; - uint256 private depositsToBeaconChain; - uint256 private vaultHubBalance; - - address private depositor = address(0x001); - address private owner = address(0x002); - address private nodeOperator = address(0x003); - address private user = address(0x004); - - RandomLib.Storage private rnd; - - bool private isConnectedToHub; - bool private hasValidator; - - function testSolvencyAllTransitions() external { - runTests(5686631772487049791906286); - } - - function testFuzz_SolvencyAllTransitions(uint256 _seed) external { - runTests(_seed); - } - - function runTests(uint256 _seed) internal { - require( - MAJOR_STATE_TRANSITIONS * ITERATIONS * 10 ** 25 * 2 <= type(uint256).max, - "MAJOR_STATE_TRANSITIONS * ITERATIONS overflow" - ); - deploy(_seed); - - uint256 initialBalance = address(stakingVaultProxy).balance; - int256 initialInOutDelta = stakingVaultProxy.inOutDelta(); - - for (uint256 i = 0; i < MAJOR_STATE_TRANSITIONS; i++) { - performMajorStateTransition(hasValidator, isConnectedToHub); - for (uint256 iterationIdx = 0; iterationIdx < ITERATIONS; iterationIdx++) { - randomTransition(hasValidator, isConnectedToHub); - } - } - - uint256 finalBalance = address(stakingVaultProxy).balance; - int256 finalInOutDelta = stakingVaultProxy.inOutDelta(); - - console2.log("VaultHub balance: %d", vaultHubBalance); - assertEq( - deposits + initialBalance + rewards + randomUserDeposits, - finalBalance + withdrawals + depositsToBeaconChain + vaultHubBalance - ); - assertEq(initialInOutDelta + int256(deposits), finalInOutDelta + int256(withdrawals) + int256(vaultHubBalance)); - } - - function deploy(uint256 _seed) public { - rnd.seed = _seed; - isConnectedToHub = false; - hasValidator = false; - deposits = 0; - withdrawals = 0; - rewards = 0; - randomUserDeposits = 0; - depositsToBeaconChain = 0; - vaultHubBalance = 0; - - DepositContract__MockForStakingVault depositContract = new DepositContract__MockForStakingVault(); - vaultHub = new VaultHubMock(_seed); - validator = new ValidatorMock(_seed); - stakingVault = new StakingVault(address(vaultHub), depositor, address(depositContract)); - - ERC1967Proxy proxy = new ERC1967Proxy( - address(stakingVault), - abi.encodeWithSelector(StakingVault.initialize.selector, owner, nodeOperator, "0x") - ); - stakingVaultProxy = StakingVault(payable(address(proxy))); - } - - function performMajorStateTransition(bool _hasValidator, bool _isConnectedToHub) internal { - bool oldIsConnectedToHub = _isConnectedToHub; - bool oldHasValidator = _hasValidator; - - hasValidator = rnd.randBool(); - isConnectedToHub = rnd.randBool(); - - if (oldIsConnectedToHub != isConnectedToHub) { - if (isConnectedToHub) { - transitionConnectVaultToHub(); - } else if (!isConnectedToHub) { - transitionDisconnectVaultFromHub(); - } - } - - if (oldHasValidator != hasValidator) { - if (hasValidator) { - transitionDepositToBeaconChain(); - } else if (!hasValidator) { - transitionValidatorExitAndReturnDeposit(); - } - } - } - - function randomTransition(bool _hasValidator, bool _isConnectedToHub) internal { - vm.warp(block.timestamp + rnd.randInt(2 * SECONDS_PER_DAY)); - function() internal[] memory availableTransitions = getAvailableTransitions(_hasValidator, _isConnectedToHub); - uint256 transitionIndex = rnd.randInt(availableTransitions.length - 1); - availableTransitions[transitionIndex](); - } - - function baseTransitions() internal pure returns (function() internal[] memory) { - function() internal[] memory transitions = new function() internal[](3); - transitions[0] = transitionRandomFund; - transitions[1] = transitionRandomWithdraw; - transitions[2] = transitionRandomUserDeposit; - return transitions; - } - - function validatorTransitions() internal pure returns (function() internal[] memory) { - function() internal[] memory transitions = new function() internal[](1); - transitions[0] = transitionRandomReceiveReward; - return transitions; - } - - function vaultHubTransitions() internal pure returns (function() internal[] memory) { - function() internal[] memory transitions = new function() internal[](3); - transitions[0] = transitionRandomMintShares; - transitions[1] = transitionRandomReport; - transitions[2] = transitionRandomRebalance; - return transitions; - } - - function mergeTransitions( - function() internal[] memory _transitionsA, - function() internal[] memory _transitionsB - ) internal pure returns (function() internal[] memory) { - function() internal[] memory result = new function() internal[](_transitionsA.length + _transitionsB.length); - for (uint256 txIdx = 0; txIdx < _transitionsA.length; txIdx++) { - result[txIdx] = _transitionsA[txIdx]; - } - for (uint256 txIdx = 0; txIdx < _transitionsB.length; txIdx++) { - result[_transitionsA.length + txIdx] = _transitionsB[txIdx]; - } - return result; - } - - function getAvailableTransitions( - bool _hasValidator, - bool _isConnectedToHub - ) internal pure returns (function() internal[] memory) { - if (_hasValidator && _isConnectedToHub) { - return mergeTransitions(baseTransitions(), mergeTransitions(validatorTransitions(), vaultHubTransitions())); - } else if (_hasValidator && !_isConnectedToHub) { - return mergeTransitions(validatorTransitions(), baseTransitions()); - } else if (!_hasValidator && _isConnectedToHub) { - return mergeTransitions(baseTransitions(), vaultHubTransitions()); - } else { - return baseTransitions(); - } - } - - function transitionRandomUserDeposit() internal { - console2.log("Deposit by random user with random amount"); - - uint256 amount = rnd.randAmountD18(); - address randomUser = rnd.randAddress(); - deal(randomUser, amount); - if (amount == 0) { - vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "msg.value")); - } - vm.prank(randomUser); - payable(address(stakingVaultProxy)).transfer(amount); - randomUserDeposits += amount; - } - - function transitionRandomFund() internal { - console2.log("Fund vault with random amount of funds"); - - uint256 amount = rnd.randAmountD18(); - int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); - uint256 valuationBefore = stakingVaultProxy.valuation(); - uint256 balanceBefore = address(stakingVaultProxy).balance; - - deal(owner, amount); - if (amount == 0) { - vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "msg.value")); - } - - vm.prank(owner); - stakingVaultProxy.fund{value: amount}(); - deposits += amount; - - assertEq(stakingVaultProxy.inOutDelta(), inOutDeltaBefore + int256(amount)); - assertEq(stakingVaultProxy.valuation(), valuationBefore + amount); - assertEq(address(stakingVaultProxy).balance, balanceBefore + amount); - } - - function transitionRandomWithdraw() internal { - console2.log("Withdraw random amount of funds"); - - uint256 unlocked = stakingVaultProxy.unlocked(); - uint256 vaultBalance = address(stakingVaultProxy).balance; - uint256 minWithdrawal = Math.min(unlocked, vaultBalance); - uint256 withdrawableAmount = rnd.randInt(minWithdrawal); - int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); - uint256 valuationBefore = stakingVaultProxy.valuation(); - uint256 balanceBefore = address(stakingVaultProxy).balance; - - deal(owner, withdrawableAmount); - if (withdrawableAmount == 0) { - vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); - } - - vm.prank(owner); - stakingVaultProxy.withdraw(owner, withdrawableAmount); - withdrawals += withdrawableAmount; - - assertEq(inOutDeltaBefore, stakingVaultProxy.inOutDelta() + int256(withdrawableAmount)); - assertEq(valuationBefore, stakingVaultProxy.valuation() + withdrawableAmount); - assertEq(balanceBefore, address(stakingVaultProxy).balance + withdrawableAmount); - } - - function transitionRandomReceiveReward() internal { - console2.log("Receive random reward"); - - uint256 dailyReward = validator.getDailyReward(); - uint256 valuationBefore = stakingVaultProxy.valuation(); - uint256 balanceBefore = address(stakingVaultProxy).balance; - - vm.deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + dailyReward); - rewards += dailyReward; - - assertEq(valuationBefore, stakingVaultProxy.valuation()); - assertEq(address(stakingVaultProxy).balance, balanceBefore + dailyReward); - } - - function transitionDepositToBeaconChain() internal { - console2.log("------Deposit to Beacon Chain and start simulating validator------"); - - deal(owner, VALIDATOR_DEPOSIT); - vm.prank(owner); - stakingVaultProxy.fund{value: VALIDATOR_DEPOSIT}(); - deposits += VALIDATOR_DEPOSIT; - - int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); - uint256 valuationBefore = stakingVaultProxy.valuation(); - uint256 balanceBefore = address(stakingVaultProxy).balance; - - IStakingVault.Deposit[] memory newDeposits = new IStakingVault.Deposit[](1); - bytes memory pubkey = new bytes(48); - bytes32 firstPart = bytes32(uint256(1)); - bytes16 secondPart = bytes16(bytes32(uint256(2))); - assembly { - mstore(add(pubkey, 32), firstPart) - mstore(add(pubkey, 64), secondPart) - } - newDeposits[0] = IStakingVault.Deposit({ - pubkey: pubkey, - signature: bytes.concat(bytes32(uint256(2))), - amount: VALIDATOR_DEPOSIT, - depositDataRoot: bytes32(uint256(3)) - }); - - vm.prank(depositor); - stakingVaultProxy.depositToBeaconChain(newDeposits); - depositsToBeaconChain += VALIDATOR_DEPOSIT; - - assertEq(inOutDeltaBefore, stakingVaultProxy.inOutDelta()); - assertEq(valuationBefore, stakingVaultProxy.valuation()); - assertEq(balanceBefore, address(stakingVaultProxy).balance + VALIDATOR_DEPOSIT); - } - - function transitionValidatorExitAndReturnDeposit() internal { - console2.log("------Validator exit and return deposit------"); - - uint256 balanceBefore = address(stakingVaultProxy).balance; - uint256 valuationBefore = stakingVaultProxy.valuation(); - - deal(address(stakingVaultProxy), address(stakingVaultProxy).balance + VALIDATOR_DEPOSIT); - depositsToBeaconChain -= VALIDATOR_DEPOSIT; - - assertEq(address(stakingVaultProxy).balance, balanceBefore + VALIDATOR_DEPOSIT); - assertEq(valuationBefore, stakingVaultProxy.valuation()); - } - - function transitionRandomDepositToBeaconChain() internal { - console2.log("Deposit to Beacon Chain with random amount"); - - uint256 amount = rnd.randAmountD18(); - int256 inOutDeltaBefore = stakingVaultProxy.inOutDelta(); - uint256 valuationBefore = stakingVaultProxy.valuation(); - uint256 balanceBefore = address(stakingVaultProxy).balance; - - bytes memory pubkey = new bytes(48); - bytes32 firstPart = bytes32(uint256(1)); - bytes16 secondPart = bytes16(bytes32(uint256(2))); - - assembly { - mstore(add(pubkey, 32), firstPart) - mstore(add(pubkey, 64), secondPart) - } - - IStakingVault.Deposit[] memory newDeposits = new IStakingVault.Deposit[](1); - newDeposits[0] = IStakingVault.Deposit({ - pubkey: pubkey, - signature: bytes.concat(bytes32(uint256(2))), - amount: amount, - depositDataRoot: bytes32(uint256(3)) - }); - - vm.prank(depositor); - stakingVaultProxy.depositToBeaconChain(newDeposits); - depositsToBeaconChain += amount; - - assertEq(inOutDeltaBefore, stakingVaultProxy.inOutDelta()); - assertEq(valuationBefore, stakingVaultProxy.valuation()); - assertEq(balanceBefore, address(stakingVaultProxy).balance + amount); - } - - function transitionConnectVaultToHub() internal { - console2.log("------Connect Vault to Hub------"); - - vm.prank(address(vaultHub)); - stakingVaultProxy.lock(CONNECT_DEPOSIT); - - assertEq(stakingVaultProxy.locked(), CONNECT_DEPOSIT); - } - - function transitionDisconnectVaultFromHub() internal { - console2.log("------Disconnect Vault from Hub------"); - - uint256 valuation = stakingVaultProxy.valuation(); - int256 inOutDelta = stakingVaultProxy.inOutDelta(); - - vm.prank(address(vaultHub)); - stakingVaultProxy.report(valuation, inOutDelta, 0); - - assertEq(stakingVaultProxy.locked(), 0); - assertEq(inOutDelta, stakingVaultProxy.inOutDelta()); - assertEq( - int256(stakingVaultProxy.valuation()) + inOutDelta, - int256(valuation) + stakingVaultProxy.inOutDelta() - ); - } - - function transitionRandomMintShares() internal { - console2.log("Mint shares with random amount"); - - uint256 vaultValuation = stakingVaultProxy.valuation(); - uint256 totalEtherToLock = vaultHub.getTotalEtherToLock(vaultValuation); - - uint256 currentLocked = stakingVaultProxy.locked(); - if (totalEtherToLock < currentLocked) { - vm.expectRevert( - abi.encodeWithSelector(LockedCannotDecreaseOutsideOfReport.selector, currentLocked, totalEtherToLock) - ); - } - - vm.prank(address(vaultHub)); - stakingVaultProxy.lock(totalEtherToLock); - - assertEq(stakingVaultProxy.locked(), totalEtherToLock < currentLocked ? currentLocked : totalEtherToLock); - } - - function transitionRandomReport() internal { - console2.log("Receive report"); - - uint256 currentValuation = stakingVaultProxy.valuation(); - int256 currentInOutDelta = stakingVaultProxy.inOutDelta(); - uint256 currentLocked = stakingVaultProxy.locked(); - - uint256 newValuation = vaultHub.getNewValuation(currentValuation); - uint256 newLocked = vaultHub.getNewLocked(currentLocked); - - vm.prank(address(vaultHub)); - stakingVaultProxy.report(newValuation, currentInOutDelta, newLocked); - - assertEq(stakingVaultProxy.locked(), newLocked); - assertEq(currentInOutDelta, stakingVaultProxy.inOutDelta()); - assertEq( - int256(stakingVaultProxy.valuation()) + currentInOutDelta, - int256(newValuation) + stakingVaultProxy.inOutDelta() - ); - } - - function transitionRandomRebalance() internal { - console2.log("Rebalance with random amount"); - - int256 inOutDelta = stakingVaultProxy.inOutDelta(); - uint256 valuation = stakingVaultProxy.valuation(); - uint256 balance = address(stakingVaultProxy).balance; - uint256 locked = stakingVaultProxy.locked(); - uint256 etherToRebalance = vaultHub.getAmountToUnlock(valuation, locked); - bool isRebalanceAllowed = false; - - if (etherToRebalance == 0) { - vm.expectRevert(abi.encodeWithSelector(ZeroArgument.selector, "_ether")); - } else if (etherToRebalance > balance) { - vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector, balance)); - } else if (etherToRebalance > valuation) { - vm.expectRevert( - abi.encodeWithSelector(RebalanceAmountExceedsValuation.selector, valuation, etherToRebalance) - ); - } else if (valuation >= locked) { - vm.expectRevert(abi.encodeWithSelector(NotAuthorized.selector, "rebalance", address(vaultHub))); - } else { - isRebalanceAllowed = true; - vaultHubBalance += etherToRebalance; - } - vm.prank(address(vaultHub)); - stakingVaultProxy.rebalance(etherToRebalance); - - if (isRebalanceAllowed) { - assertEq(inOutDelta, stakingVaultProxy.inOutDelta() + int256(etherToRebalance)); - assertEq(valuation, stakingVaultProxy.valuation() + etherToRebalance); - assertEq(balance, address(stakingVaultProxy).balance + etherToRebalance); - } else { - assertEq(inOutDelta, stakingVaultProxy.inOutDelta()); - assertEq(valuation, stakingVaultProxy.valuation()); - assertEq(balance, address(stakingVaultProxy).balance); - } - } -}