From afc5fa1f826c9abcae2c1a9ac52c47bfecfc61ab Mon Sep 17 00:00:00 2001 From: Sergey White Date: Fri, 10 Jan 2025 17:40:41 +0300 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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" + ); } } }