From b8b4e4a3bd3a76ce08d9f46ef2d1bb44b1597def Mon Sep 17 00:00:00 2001 From: dominik-velan Date: Tue, 6 Aug 2024 14:40:12 +0200 Subject: [PATCH 01/67] initial setup --- certora/confs/DualGovernance_sanity.conf | 44 +++++++ .../EmergencyActivationCommittee_sanity.conf | 21 ++++ .../EmergencyExecutionCommittee_sanity.conf | 21 ++++ .../EmergencyProtectedTimelock_sanity.conf | 29 +++++ certora/confs/Escrow_sanity.conf | 43 +++++++ certora/confs/Executor_sanity.conf | 20 +++ certora/confs/ResealManager_sanity.conf | 25 ++++ certora/confs/TiebreakerCore_sanity.conf | 21 ++++ .../confs/TiebreakerSubCommittee_sanity.conf | 25 ++++ .../DualGovernance_builtin_assertions.conf | 22 ++++ ...nce_sanity_with_all_default_summaries.conf | 25 ++++ .../DualGovernance_sanity_with_erc20cvl.conf | 25 ++++ ...overnance_sanity_with_erc20dispatched.conf | 26 ++++ ...ctivationCommittee_builtin_assertions.conf | 21 ++++ ...tee_sanity_with_all_default_summaries.conf | 24 ++++ ...ivationCommittee_sanity_with_erc20cvl.conf | 24 ++++ ...Committee_sanity_with_erc20dispatched.conf | 25 ++++ ...ExecutionCommittee_builtin_assertions.conf | 21 ++++ ...tee_sanity_with_all_default_summaries.conf | 24 ++++ ...ecutionCommittee_sanity_with_erc20cvl.conf | 24 ++++ ...Committee_sanity_with_erc20dispatched.conf | 25 ++++ ...yProtectedTimelock_builtin_assertions.conf | 21 ++++ ...ock_sanity_with_all_default_summaries.conf | 24 ++++ ...rotectedTimelock_sanity_with_erc20cvl.conf | 24 ++++ ...dTimelock_sanity_with_erc20dispatched.conf | 25 ++++ .../extra/Escrow_builtin_assertions.conf | 21 ++++ ...row_sanity_with_all_default_summaries.conf | 24 ++++ .../extra/Escrow_sanity_with_erc20cvl.conf | 24 ++++ .../Escrow_sanity_with_erc20dispatched.conf | 25 ++++ .../extra/Executor_builtin_assertions.conf | 20 +++ ...tor_sanity_with_all_default_summaries.conf | 23 ++++ .../extra/Executor_sanity_with_erc20cvl.conf | 23 ++++ .../Executor_sanity_with_erc20dispatched.conf | 24 ++++ .../ResealManager_builtin_assertions.conf | 21 ++++ ...ger_sanity_with_all_default_summaries.conf | 24 ++++ .../ResealManager_sanity_with_erc20cvl.conf | 24 ++++ ...alManager_sanity_with_erc20dispatched.conf | 25 ++++ .../TiebreakerCore_builtin_assertions.conf | 21 ++++ ...ore_sanity_with_all_default_summaries.conf | 24 ++++ .../TiebreakerCore_sanity_with_erc20cvl.conf | 24 ++++ ...eakerCore_sanity_with_erc20dispatched.conf | 25 ++++ ...reakerSubCommittee_builtin_assertions.conf | 21 ++++ ...tee_sanity_with_all_default_summaries.conf | 24 ++++ ...akerSubCommittee_sanity_with_erc20cvl.conf | 24 ++++ ...Committee_sanity_with_erc20dispatched.conf | 25 ++++ certora/harnesses/ERC20Like/DummyERC20A.sol | 51 ++++++++ certora/harnesses/ERC20Like/DummyERC20B.sol | 51 ++++++++ certora/harnesses/ERC20Like/DummyStETH.sol | 115 ++++++++++++++++++ certora/harnesses/ERC20Like/DummyWeth.sol | 60 +++++++++ certora/harnesses/Utilities.sol | 14 +++ certora/helpers/DummyWithdrawalQueue.sol | 81 ++++++++++++ certora/specs/DeX/curve.spec | 6 + certora/specs/DeX/pancakeswap.spec | 7 ++ certora/specs/ERC1155/erc1155.spec | 16 +++ certora/specs/ERC1967/erc1967.spec | 7 ++ certora/specs/ERC20/WETHcvl.spec | 35 ++++++ certora/specs/ERC20/erc20cvl.spec | 68 +++++++++++ certora/specs/ERC20/erc20dispatched.spec | 16 +++ certora/specs/ERC721/erc721.spec | 9 ++ certora/specs/PriceAggregators/chainlink.spec | 4 + certora/specs/PriceAggregators/tellor.spec | 3 + certora/specs/Staking/eigenlayer.spec | 15 +++ certora/specs/Staking/lido.spec | 16 +++ certora/specs/Staking/wrappedETH.spec | 13 ++ certora/specs/generic.spec | 105 ++++++++++++++++ certora/specs/optimizations.spec | 5 + certora/specs/problems.spec | 1 + certora/specs/setup/builtin_assertions.spec | 12 ++ certora/specs/setup/sanity.spec | 5 + .../specs/setup/sanity_DualGovernance.spec | 15 +++ certora/specs/setup/sanity_Escrow.spec | 11 ++ certora/specs/setup/sanity_Timelock.spec | 10 ++ .../sanity_with_all_default_summaries.spec | 14 +++ certora/specs/setup/sanity_with_erc20cvl.spec | 8 ++ .../setup/sanity_with_erc20dispatched.spec | 7 ++ certora/specs/shared.spec | 12 ++ certora/specs/unresolved.spec | 4 + 77 files changed, 1891 insertions(+) create mode 100644 certora/confs/DualGovernance_sanity.conf create mode 100644 certora/confs/EmergencyActivationCommittee_sanity.conf create mode 100644 certora/confs/EmergencyExecutionCommittee_sanity.conf create mode 100644 certora/confs/EmergencyProtectedTimelock_sanity.conf create mode 100644 certora/confs/Escrow_sanity.conf create mode 100644 certora/confs/Executor_sanity.conf create mode 100644 certora/confs/ResealManager_sanity.conf create mode 100644 certora/confs/TiebreakerCore_sanity.conf create mode 100644 certora/confs/TiebreakerSubCommittee_sanity.conf create mode 100644 certora/confs/extra/DualGovernance_builtin_assertions.conf create mode 100644 certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/Escrow_builtin_assertions.conf create mode 100644 certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/Escrow_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/Executor_builtin_assertions.conf create mode 100644 certora/confs/extra/Executor_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/Executor_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/Executor_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/ResealManager_builtin_assertions.conf create mode 100644 certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/TiebreakerCore_builtin_assertions.conf create mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf create mode 100644 certora/harnesses/ERC20Like/DummyERC20A.sol create mode 100644 certora/harnesses/ERC20Like/DummyERC20B.sol create mode 100644 certora/harnesses/ERC20Like/DummyStETH.sol create mode 100644 certora/harnesses/ERC20Like/DummyWeth.sol create mode 100644 certora/harnesses/Utilities.sol create mode 100644 certora/helpers/DummyWithdrawalQueue.sol create mode 100644 certora/specs/DeX/curve.spec create mode 100644 certora/specs/DeX/pancakeswap.spec create mode 100644 certora/specs/ERC1155/erc1155.spec create mode 100644 certora/specs/ERC1967/erc1967.spec create mode 100644 certora/specs/ERC20/WETHcvl.spec create mode 100644 certora/specs/ERC20/erc20cvl.spec create mode 100644 certora/specs/ERC20/erc20dispatched.spec create mode 100644 certora/specs/ERC721/erc721.spec create mode 100644 certora/specs/PriceAggregators/chainlink.spec create mode 100644 certora/specs/PriceAggregators/tellor.spec create mode 100644 certora/specs/Staking/eigenlayer.spec create mode 100644 certora/specs/Staking/lido.spec create mode 100644 certora/specs/Staking/wrappedETH.spec create mode 100644 certora/specs/generic.spec create mode 100644 certora/specs/optimizations.spec create mode 100644 certora/specs/problems.spec create mode 100644 certora/specs/setup/builtin_assertions.spec create mode 100644 certora/specs/setup/sanity.spec create mode 100644 certora/specs/setup/sanity_DualGovernance.spec create mode 100644 certora/specs/setup/sanity_Escrow.spec create mode 100644 certora/specs/setup/sanity_Timelock.spec create mode 100644 certora/specs/setup/sanity_with_all_default_summaries.spec create mode 100644 certora/specs/setup/sanity_with_erc20cvl.spec create mode 100644 certora/specs/setup/sanity_with_erc20dispatched.spec create mode 100644 certora/specs/shared.spec create mode 100644 certora/specs/unresolved.spec diff --git a/certora/confs/DualGovernance_sanity.conf b/certora/confs/DualGovernance_sanity.conf new file mode 100644 index 00000000..7c9ea4ef --- /dev/null +++ b/certora/confs/DualGovernance_sanity.conf @@ -0,0 +1,44 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/DualGovernance.sol", + "contracts/libraries/DualGovernanceState.sol", + "contracts/Escrow.sol", + "contracts/Configuration.sol", + "contracts/ConfigurationProvider.sol", + "contracts/EmergencyProtectedTimelock.sol", + "contracts/ResealManager.sol", + "contracts/Types/Duration.sol:Durations", + "certora/harnesses/ERC20Like/DummyStETH.sol", + ], + "link": [ + "ConfigurationProvider:CONFIG=Configuration", + "DualGovernance:CONFIG=Configuration", + "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", + "EmergencyProtectedTimelock:CONFIG=Configuration", + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", // this makes the prover fail + "DualGovernance:_resealManager=ResealManager", + "Escrow:ST_ETH=DummyStETH", + ], + "struct_link": [ + "DualGovernance:rageQuitEscrow=Escrow", + "DualGovernance:signallingEscrow=Escrow", + "DualGovernance:resealManager=ResealManager", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_DualGovernance.spec" +} \ No newline at end of file diff --git a/certora/confs/EmergencyActivationCommittee_sanity.conf b/certora/confs/EmergencyActivationCommittee_sanity.conf new file mode 100644 index 00000000..d7a7434f --- /dev/null +++ b/certora/confs/EmergencyActivationCommittee_sanity.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/EmergencyExecutionCommittee_sanity.conf b/certora/confs/EmergencyExecutionCommittee_sanity.conf new file mode 100644 index 00000000..75739961 --- /dev/null +++ b/certora/confs/EmergencyExecutionCommittee_sanity.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/EmergencyProtectedTimelock_sanity.conf b/certora/confs/EmergencyProtectedTimelock_sanity.conf new file mode 100644 index 00000000..363b6152 --- /dev/null +++ b/certora/confs/EmergencyProtectedTimelock_sanity.conf @@ -0,0 +1,29 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/EmergencyProtectedTimelock.sol", +// "contracts/ConfigurationProvider.sol", + "contracts/Configuration.sol", + "contracts/Executor.sol", +// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", + ], + "link": [ + "EmergencyProtectedTimelock:CONFIG=Configuration", +// "ConfigurationProvider:CONFIG=Configuration", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_Timelock.spec" +} \ No newline at end of file diff --git a/certora/confs/Escrow_sanity.conf b/certora/confs/Escrow_sanity.conf new file mode 100644 index 00000000..8c120103 --- /dev/null +++ b/certora/confs/Escrow_sanity.conf @@ -0,0 +1,43 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Escrow.sol", + "contracts/DualGovernance.sol", + "contracts/EmergencyProtectedTimelock.sol", + "contracts/libraries/DualGovernanceState.sol", + "contracts/Configuration.sol", + "contracts/ConfigurationProvider.sol", + "contracts/EmergencyProtectedTimelock.sol", + "certora/helpers/DummyWithdrawalQueue.sol", + "certora/harnesses/ERC20Like/DummyStETH.sol", + ], + "link": [ + "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", + "Escrow:_dualGovernance=DualGovernance", + "ConfigurationProvider:CONFIG=Configuration", + "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", + "Escrow:ST_ETH=DummyStETH", + "Escrow:CONFIG=Configuration", + "DualGovernance:CONFIG=Configuration", + ], + "struct_link": [ + "DualGovernance:rageQuitEscrow=Escrow", + "DualGovernance:signallingEscrow=Escrow" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "10", + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_Escrow.spec" +} \ No newline at end of file diff --git a/certora/confs/Executor_sanity.conf b/certora/confs/Executor_sanity.conf new file mode 100644 index 00000000..568cfa4c --- /dev/null +++ b/certora/confs/Executor_sanity.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/ResealManager_sanity.conf b/certora/confs/ResealManager_sanity.conf new file mode 100644 index 00000000..5c2b41fc --- /dev/null +++ b/certora/confs/ResealManager_sanity.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/ResealManager.sol", + "contracts/EmergencyProtectedTimelock.sol", + ], + "link": [ + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/TiebreakerCore_sanity.conf b/certora/confs/TiebreakerCore_sanity.conf new file mode 100644 index 00000000..ea5089ed --- /dev/null +++ b/certora/confs/TiebreakerCore_sanity.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/TiebreakerSubCommittee_sanity.conf b/certora/confs/TiebreakerSubCommittee_sanity.conf new file mode 100644 index 00000000..4b0972e1 --- /dev/null +++ b/certora/confs/TiebreakerSubCommittee_sanity.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerSubCommittee.sol", + "contracts/committees/TiebreakerCore.sol", + ], + "link": [ + "TiebreakerSubCommittee:TIEBREAKER_CORE=TiebreakerCore", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_builtin_assertions.conf b/certora/confs/extra/DualGovernance_builtin_assertions.conf new file mode 100644 index 00000000..566adeb5 --- /dev/null +++ b/certora/confs/extra/DualGovernance_builtin_assertions.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/DualGovernance.sol", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, +// "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf b/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..24ec9e84 --- /dev/null +++ b/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/DualGovernance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf b/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..26911379 --- /dev/null +++ b/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/DualGovernance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf b/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..73b46140 --- /dev/null +++ b/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf @@ -0,0 +1,26 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/DualGovernance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf b/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf new file mode 100644 index 00000000..e38009fe --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..0a2b260e --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..bfbf3f71 --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..117f617f --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf b/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf new file mode 100644 index 00000000..0da6f51c --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..98a5ac3f --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..5aea5b35 --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..9714a454 --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf b/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf new file mode 100644 index 00000000..29901693 --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..98d79e66 --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..7ea22d9d --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..fc91c53d --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_builtin_assertions.conf b/certora/confs/extra/Escrow_builtin_assertions.conf new file mode 100644 index 00000000..0b11e69f --- /dev/null +++ b/certora/confs/extra/Escrow_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf b/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..1c086d0c --- /dev/null +++ b/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf b/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..6193c3f5 --- /dev/null +++ b/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf b/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..a7f404b0 --- /dev/null +++ b/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_builtin_assertions.conf b/certora/confs/extra/Executor_builtin_assertions.conf new file mode 100644 index 00000000..bc661b3e --- /dev/null +++ b/certora/confs/extra/Executor_builtin_assertions.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf b/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..c140f316 --- /dev/null +++ b/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_erc20cvl.conf b/certora/confs/extra/Executor_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..4d07e19b --- /dev/null +++ b/certora/confs/extra/Executor_sanity_with_erc20cvl.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf b/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..eda809cf --- /dev/null +++ b/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_builtin_assertions.conf b/certora/confs/extra/ResealManager_builtin_assertions.conf new file mode 100644 index 00000000..a6a71379 --- /dev/null +++ b/certora/confs/extra/ResealManager_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf b/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..a716708f --- /dev/null +++ b/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf b/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..78247dd5 --- /dev/null +++ b/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf b/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..c55c2b47 --- /dev/null +++ b/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_builtin_assertions.conf b/certora/confs/extra/TiebreakerCore_builtin_assertions.conf new file mode 100644 index 00000000..5a1f0fce --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf b/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..ea96237e --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf b/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..4ad26619 --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf b/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..30d41a83 --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf b/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf new file mode 100644 index 00000000..016c8101 --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..26571a3c --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..84be27a7 --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..79633981 --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/harnesses/ERC20Like/DummyERC20A.sol b/certora/harnesses/ERC20Like/DummyERC20A.sol new file mode 100644 index 00000000..679c2274 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20A.sol @@ -0,0 +1,51 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20A { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyERC20B.sol b/certora/harnesses/ERC20Like/DummyERC20B.sol new file mode 100644 index 00000000..7105667f --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20B.sol @@ -0,0 +1,51 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20B { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyStETH.sol b/certora/harnesses/ERC20Like/DummyStETH.sol new file mode 100644 index 00000000..523b6130 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyStETH.sol @@ -0,0 +1,115 @@ +pragma solidity >=0.8.0; + +import "../../../contracts/interfaces/IStETH.sol"; + +contract DummyStETH is IStETH { + uint256 internal totalShares; + mapping(address => uint256) private shares; + mapping(address => mapping(address => uint256)) private allowances; + + function getTotalShares() external view returns (uint256) { + return totalShares; + } + + function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { + return ethAmount * 5 / 3; + } + + function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256) { + return sharesAmount * 3 / 5; + } + + function transferShares(address to, uint256 amount) external { + _transferShares(msg.sender, to, amount); + // uint256 tokensAmount = getPooledEthByShares(amount); + // uint256 tokensAmount = amount*3/5; + // return tokensAmount; + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256) { + // uint256 tokensAmount = getPooledEthByShares(_sharesAmount); + uint256 tokensAmount = _sharesAmount * 3 / 5; + _spendAllowance(_sender, msg.sender, tokensAmount); + _transferShares(_sender, _recipient, _sharesAmount); + return tokensAmount; + } + + function transfer(address _recipient, uint256 _amount) external returns (bool) { + _transfer(msg.sender, _recipient, _amount); + return true; + } + + function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool) { + _spendAllowance(_sender, msg.sender, _amount); + _transfer(_sender, _recipient, _amount); + return true; + } + + function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool) { + _approve(msg.sender, _spender, allowances[msg.sender][_spender] + (_addedValue)); + return true; + } + + function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool) { + uint256 currentAllowance = allowances[msg.sender][_spender]; + require(currentAllowance >= _subtractedValue, "ALLOWANCE_BELOW_ZERO"); + _approve(msg.sender, _spender, currentAllowance - (_subtractedValue)); + return true; + } + + function totalSupply() external view returns (uint256) { + return totalShares * 3 / 5; + } + + function approve(address _spender, uint256 _amount) external returns (bool) { + _approve(msg.sender, _spender, _amount); + return true; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return allowances[_owner][_spender]; + } + + function balanceOf(address _account) external view returns (uint256) { + // return getPooledEthByShares(_sharesOf(_account)); + return _sharesOf(_account) * 3 / 5; + } + + function _sharesOf(address account) internal view returns (uint256) { + return shares[account]; + } + + function _transfer(address sender, address recipient, uint256 amount) internal { + uint256 sharesToTransfer = amount * 5 / 3; + _transferShares(sender, recipient, sharesToTransfer); + } + + function _transferShares(address sender, address recipient, uint256 sharesAmount) internal { + require(sender != address(0), "TRANSFER_FROM_ZERO_ADDR"); + require(recipient != address(0), "TRANSFER_TO_ZERO_ADDR"); + require(recipient != address(this), "TRANSFER_TO_STETH_CONTRACT"); + + uint256 currentSenderShares = shares[sender]; + require(sharesAmount <= currentSenderShares, "BALANCE_EXCEEDED"); + + shares[sender] = currentSenderShares - (sharesAmount); + shares[recipient] = shares[recipient] + (sharesAmount); + } + + function _spendAllowance(address owner, address spender, uint256 amount) internal { + uint256 currentAllowance = allowances[owner][spender]; + require(currentAllowance >= amount, "ALLOWANCE_EXCEEDED"); + _approve(owner, spender, currentAllowance - amount); + } + + function _approve(address owner, address spender, uint256 amount) internal { + require(owner != address(0), "APPROVE_FROM_ZERO_ADDR"); + require(spender != address(0), "APPROVE_TO_ZERO_ADDR"); + + allowances[owner][spender] = amount; + } +} diff --git a/certora/harnesses/ERC20Like/DummyWeth.sol b/certora/harnesses/ERC20Like/DummyWeth.sol new file mode 100644 index 00000000..9c691504 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyWeth.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity >=0.8.0; + +/** + * Dummy Weth token. + */ +contract DummyWeth { + uint256 t; + + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + return true; + } + + // WETH + function deposit() external payable { + b[msg.sender] += msg.value; + } + + function withdraw(uint256 amt) external { + b[msg.sender] -= amt; + payable(msg.sender).transfer(amt); // use optimistic_fallback here + } +} diff --git a/certora/harnesses/Utilities.sol b/certora/harnesses/Utilities.sol new file mode 100644 index 00000000..bae8c012 --- /dev/null +++ b/certora/harnesses/Utilities.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.8.0; + +contract Utilities { + function havocAll() external { + (bool success,) = address(0xdeadbeef).call(abi.encodeWithSelector(0x12345678)); + require(success); + } + + function justRevert() external { + revert(); + } + + function nop() external {} +} diff --git a/certora/helpers/DummyWithdrawalQueue.sol b/certora/helpers/DummyWithdrawalQueue.sol new file mode 100644 index 00000000..e4a8d70e --- /dev/null +++ b/certora/helpers/DummyWithdrawalQueue.sol @@ -0,0 +1,81 @@ +pragma solidity ^0.8.26; + +import {IWithdrawalQueue, WithdrawalRequestStatus} from "../../contracts/interfaces/IWithdrawalQueue.sol"; + +// This implementation is only mock which will is later summarised by NONDET and HAVOC summary +contract DummyWithdrawalQueue is IWithdrawalQueue { + function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + uint256 res; + return res; + } + + function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + uint256 res; + return res; + } + + function getLastRequestId() external view returns (uint256) { + uint256 res; + return res; + } + + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hints + ) external view returns (uint256[] memory claimableEthValues) { + uint256[] memory res; + return res; + } + + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + uint256[] memory res; + return res; + } + + function getLastCheckpointIndex() external view returns (uint256) { + uint256 res; + return res; + } + + function grantRole(bytes32 role, address account) external {} + function pauseFor(uint256 duration) external {} + + function isPaused() external returns (bool) { + bool res; + return res; + } + + function requestWithdrawalsWstETH(uint256[] calldata amounts, address owner) external returns (uint256[] memory) { + uint256[] memory res; + return res; + } + + function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external {} + + function getLastFinalizedRequestId() external view returns (uint256) { + uint256 res; + return res; + } + + function transferFrom(address from, address to, uint256 requestId) external {} + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + {} + + function balanceOf(address owner) external view returns (uint256) { + uint256 res; + return res; + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) external returns (uint256[] memory requestIds) {} +} diff --git a/certora/specs/DeX/curve.spec b/certora/specs/DeX/curve.spec new file mode 100644 index 00000000..d2e062f4 --- /dev/null +++ b/certora/specs/DeX/curve.spec @@ -0,0 +1,6 @@ +methods { + function _.exchange_underlying(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); + function _.exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); + function _.get_virtual_price() external => NONDET; // expect (uint256); + function _.get_dy(uint256 i, iunt256 j, uint256 dx) external => NONDET; // expect (uint256); +} \ No newline at end of file diff --git a/certora/specs/DeX/pancakeswap.spec b/certora/specs/DeX/pancakeswap.spec new file mode 100644 index 00000000..1767b85c --- /dev/null +++ b/certora/specs/DeX/pancakeswap.spec @@ -0,0 +1,7 @@ +methods { + // interface IPancackeV3SwapRouter + function _.WETH9() external => HAVOC_ECF; // expect (address); // xxx not marked as view but suspect it is... + function _.unwrapWETH9(uint256 amountMinimum, address recipient) external => HAVOC_ECF; // payable, expect void; + // xxx to use this, must import IPancackeV3SwapRouter + // function _.exactInputSingle(IPancackeV3SwapRouter.ExactInputSingleParams /* calldata */ params) external => HAVOC_ECF; // payable, expect (uint256 amountOut); +} \ No newline at end of file diff --git a/certora/specs/ERC1155/erc1155.spec b/certora/specs/ERC1155/erc1155.spec new file mode 100644 index 00000000..07948f01 --- /dev/null +++ b/certora/specs/ERC1155/erc1155.spec @@ -0,0 +1,16 @@ +methods { + function _.onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes /* calldata */ data + ) external => HAVOC_ECF; // expect (bytes4); + function _.onERC1155BatchReceived( + address operator, + address from, + uint256[] /* calldata */ ids, + uint256[] /* calldata */ values, + bytes /* calldata */ data + ) external => HAVOC_ECF; // expect (bytes4); +} \ No newline at end of file diff --git a/certora/specs/ERC1967/erc1967.spec b/certora/specs/ERC1967/erc1967.spec new file mode 100644 index 00000000..0bab6c78 --- /dev/null +++ b/certora/specs/ERC1967/erc1967.spec @@ -0,0 +1,7 @@ +methods { + // avoids linking messages upon upgradeToAndCall + function _._upgradeToAndCall(address,bytes,bool) external => HAVOC_ECF; + function _._upgradeToAndCallUUPS(address,bytes,bool) external => HAVOC_ECF; + // view function + function _.proxiableUUID() external => NONDET; // expect bytes32 +} diff --git a/certora/specs/ERC20/WETHcvl.spec b/certora/specs/ERC20/WETHcvl.spec new file mode 100644 index 00000000..8b1d8b70 --- /dev/null +++ b/certora/specs/ERC20/WETHcvl.spec @@ -0,0 +1,35 @@ +using DummyWeth as weth; // we are limited by the fact that we cannot do transfers from CVL +using Utilities as utils; + +methods { + // Utilities + function Utilities.justRevert() external envfree; + + // WETH + function _.deposit() external with (env e) => wethDeposit(calledContract, e.msg.sender, e.msg.value) expect void; + function _.withdraw(uint256 amount) external with (env e) => wethWithdraw(calledContract, e.msg.sender, amount) expect void; +} + +function wethDeposit(address target, address caller, uint256 value) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + // money will be transferred because of the payability of deposit + env e2; + require e2.msg.sender == caller; + require e2.msg.value == value; + weth.deposit(e2); + } +} + +function wethWithdraw(address target, address caller, uint256 amount) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + env e2; + require e2.msg.sender == caller; + weth.withdraw(e2, amount); + } +} \ No newline at end of file diff --git a/certora/specs/ERC20/erc20cvl.spec b/certora/specs/ERC20/erc20cvl.spec new file mode 100644 index 00000000..03f5a422 --- /dev/null +++ b/certora/specs/ERC20/erc20cvl.spec @@ -0,0 +1,68 @@ +methods { + // ERC20 standard + function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.decimals() external => PER_CALLEE_CONSTANT; + function _.totalSupply() external => totalSupplyCVL(calledContract) expect uint256; + function _.balanceOf(address a) external => balanceOfCVL(calledContract, a) expect uint256; + function _.allowance(address a, address b) external => allowanceCVL(calledContract, a, b) expect uint256; + function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; + +} + + +/// CVL simple implementations of IERC20: +/// token => totalSupply +ghost mapping(address => uint256) totalSupplyByToken; +/// token => account => balance +ghost mapping(address => mapping(address => uint256)) balanceByToken; +/// token => owner => spender => allowance +ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken; + +// function tokenBalanceOf(address token, address account) returns uint256 { +// return balanceByToken[token][account]; +// } + +function totalSupplyCVL(address token) returns uint256 { + return totalSupplyByToken[token]; +} + +function balanceOfCVL(address token, address a) returns uint256 { + return balanceByToken[token][a]; +} + +function allowanceCVL(address token, address a, address b) returns uint256 { + return allowanceByToken[token][a][b]; +} + +function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + allowanceByToken[token][approver][spender] = amount; + return true; +} + +function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + return transferCVL(token, from, to, amount); +} + +function transferCVL(address token, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if(balanceByToken[token][from] < amount) return false; + balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. + return true; +} \ No newline at end of file diff --git a/certora/specs/ERC20/erc20dispatched.spec b/certora/specs/ERC20/erc20dispatched.spec new file mode 100644 index 00000000..c40394bb --- /dev/null +++ b/certora/specs/ERC20/erc20dispatched.spec @@ -0,0 +1,16 @@ +methods { + // ERC20 standard + function _.name() external => DISPATCHER(true); + function _.symbol() external => DISPATCHER(true); + function _.decimals() external => DISPATCHER(true); + function _.totalSupply() external => DISPATCHER(true); + function _.balanceOf(address) external => DISPATCHER(true); + function _.allowance(address,address) external => DISPATCHER(true); + function _.approve(address,uint256) external => DISPATCHER(true); + function _.transfer(address,uint256) external => DISPATCHER(true); + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); + + // WETH + function _.deposit() external => DISPATCHER(true); + function _.withdraw(uint256) external => DISPATCHER(true); +} diff --git a/certora/specs/ERC721/erc721.spec b/certora/specs/ERC721/erc721.spec new file mode 100644 index 00000000..474b69f3 --- /dev/null +++ b/certora/specs/ERC721/erc721.spec @@ -0,0 +1,9 @@ +methods { + // likely unsound, but assumes no callback + function _.onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes data + ) external => NONDET; /* expects bytes4 */ +} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/chainlink.spec b/certora/specs/PriceAggregators/chainlink.spec new file mode 100644 index 00000000..889e39e6 --- /dev/null +++ b/certora/specs/PriceAggregators/chainlink.spec @@ -0,0 +1,4 @@ +methods { + function _.getRoundData(uint80) external => NONDET; + function _.latestRoundData() external => NONDET; +} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/tellor.spec b/certora/specs/PriceAggregators/tellor.spec new file mode 100644 index 00000000..03f90c77 --- /dev/null +++ b/certora/specs/PriceAggregators/tellor.spec @@ -0,0 +1,3 @@ +methods { + function _.getTellorCurrentValue(uint256) external => NONDET; +} \ No newline at end of file diff --git a/certora/specs/Staking/eigenlayer.spec b/certora/specs/Staking/eigenlayer.spec new file mode 100644 index 00000000..1aa91c7f --- /dev/null +++ b/certora/specs/Staking/eigenlayer.spec @@ -0,0 +1,15 @@ +methods { + // Strategy manager + function _.withdrawalRootPending(bytes32 _withdrawalRoot) external => NONDET; // expect (bool); + function _.numWithdrawalsQueued(address _user) external => NONDET; // expect (uint96); + function _.pauserRegistry() external => HAVOC_ECF; // expect (IPauserRegistry); + function _.paused(uint8 index) external => NONDET; // expect (bool); + function _.unpause(uint256 newPausedStatus) external => HAVOC_ECF; // expect void + + // interface IEigenPod + function _.withdrawBeforeRestaking() external => HAVOC_ECF; // expect void + + // interface IEigenPodManager + function _.getPod(address podOwner) external => NONDET; // expect address; // (IEigenPod) + function _.createPod() external => HAVOC_ECF; // expect (address); +} \ No newline at end of file diff --git a/certora/specs/Staking/lido.spec b/certora/specs/Staking/lido.spec new file mode 100644 index 00000000..69f4a4e3 --- /dev/null +++ b/certora/specs/Staking/lido.spec @@ -0,0 +1,16 @@ +methods { + // Lido + function _.getTotalPooledEther() external => NONDET; // expect (uint256); + function _.getTotalShares() external => NONDET; // expect (uint256); + function _.submit(address _referral) external => HAVOC_ECF; // payable, expect (uint256); + + // may be shared with other contracts XXX + function _.nonces(address _user) external => NONDET; // expect (uint256); + function _.DOMAIN_SEPARATOR() external => NONDET; // expect (bytes32); + + // Lido Withdrawal Queue + function _.MAX_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); + function _.MIN_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); + function _.requestWithdrawals(uint256[] /* calldata */ _amount, address _depositor) external => HAVOC_ECF; // expect (uint256[] memory); + function _.claimWithdrawals(uint256[] /* calldata */ _requestIds, uint256[] /* calldata */ _hints) external => HAVOC_ECF; // expect void; +} \ No newline at end of file diff --git a/certora/specs/Staking/wrappedETH.spec b/certora/specs/Staking/wrappedETH.spec new file mode 100644 index 00000000..235c82d3 --- /dev/null +++ b/certora/specs/Staking/wrappedETH.spec @@ -0,0 +1,13 @@ +import "../shared.spec"; + +using Utilities as utils; + +methods { + // e.g. cbETH (Coinbase Wrapped Staked ETH), WBETH (Wrapped Beacon ETH) + function _.mint(address _to, uint256 _amount) external => HAVOC_ECF; // expect void; + function _.exchangeRate() external => NONDET; // expect (uint256 _exchangeRate); + + // WBETH + function _.deposit(address referral) external with (env e) => pay_and_havoc(calledContract, e); // payable, expect void +} + diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec new file mode 100644 index 00000000..22c0ad8e --- /dev/null +++ b/certora/specs/generic.spec @@ -0,0 +1,105 @@ +/* +This rule find which functions are privileged. +A function is privileged if there is only one address that can call it. + +The rules finds this by finding which functions can be called by two different users. +*/ +rule privilegedOperation(method f, address privileged) { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged + bool secondSucceeded = !lastReverted; + + assert !(firstSucceeded && secondSucceeded); +} + +rule timeoutChecker(method f) { + storage before = lastStorage; + env e; calldataarg arg; + f(e,arg); + assert before == lastStorage; +} + +/* +This rule find which functions that can be called, may fail due to someone else calling a function right before. + +This is n expensive rule - might fail on the demo site on big contracts +*/ +rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + storage initialStorage = lastStorage; + f@withrevert(e1, arg); + bool firstSucceeded = !lastReverted; + env e2; + calldataarg arg2; + require e2.msg.sender != e1.msg.sender; + f(e2, arg2) at initialStorage; + f@withrevert(e1, arg); + bool succeeded = !lastReverted; + assert succeeded; +} + +rule noRevert(method f) { + env e; + calldataarg arg; + require e.msg.value == 0; + f@withrevert(e, arg); + assert !lastReverted; +} + + +rule alwaysRevert(method f) { + env e; + calldataarg arg; + f@withrevert(e, arg); + assert lastReverted; +} + +/* failing CALL should lead to a revert */ +ghost bool saw_failing_call; + +hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + saw_failing_call = saw_failing_call || rc == 0; +} + +rule failing_CALL_leads_to_revert(method f) { + saw_failing_call = false; + env e; + calldataarg arg; + f@withrevert(e, arg); + bool reverted = lastReverted; + assert saw_failing_call => reverted; +} + +// All usages +use builtin rule sanity; +use builtin rule hasDelegateCalls; +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; + +/** + +// Integrate rules from generic.spec in importing specs like this: + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } + **/ diff --git a/certora/specs/optimizations.spec b/certora/specs/optimizations.spec new file mode 100644 index 00000000..e92485dc --- /dev/null +++ b/certora/specs/optimizations.spec @@ -0,0 +1,5 @@ +// optimizing summaries +methods { + +} + diff --git a/certora/specs/problems.spec b/certora/specs/problems.spec new file mode 100644 index 00000000..3ebe9e37 --- /dev/null +++ b/certora/specs/problems.spec @@ -0,0 +1 @@ +// workarounds for crashes \ No newline at end of file diff --git a/certora/specs/setup/builtin_assertions.spec b/certora/specs/setup/builtin_assertions.spec new file mode 100644 index 00000000..477d9680 --- /dev/null +++ b/certora/specs/setup/builtin_assertions.spec @@ -0,0 +1,12 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +rule check_builtin_assertions(method f) + filtered { f -> f.contract == currentContract } +{ + env e; + calldataarg arg; + f(e, arg); + assert true; +} diff --git a/certora/specs/setup/sanity.spec b/certora/specs/setup/sanity.spec new file mode 100644 index 00000000..d74f4afe --- /dev/null +++ b/certora/specs/setup/sanity.spec @@ -0,0 +1,5 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_DualGovernance.spec b/certora/specs/setup/sanity_DualGovernance.spec new file mode 100644 index 00000000..941f8b91 --- /dev/null +++ b/certora/specs/setup/sanity_DualGovernance.spec @@ -0,0 +1,15 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + + + +methods { + function _.getRageQuitSupport() external => DISPATCHER(true); + function _.isRageQuitFinalized() external => DISPATCHER(true); + function _.MASTER_COPY() external => DISPATCHER(true); + function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); + function _.initialize(address) external => DISPATCHER(true); +} + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_Escrow.spec b/certora/specs/setup/sanity_Escrow.spec new file mode 100644 index 00000000..52216b50 --- /dev/null +++ b/certora/specs/setup/sanity_Escrow.spec @@ -0,0 +1,11 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +methods { + function _.getRageQuitSupport() external => DISPATCHER(true); + function _.isRageQuitFinalized() external => DISPATCHER(true); + function _.MASTER_COPY() external => DISPATCHER(true); +} + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_Timelock.spec b/certora/specs/setup/sanity_Timelock.spec new file mode 100644 index 00000000..66f44e89 --- /dev/null +++ b/certora/specs/setup/sanity_Timelock.spec @@ -0,0 +1,10 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +methods { + function _.execute(address,uint256,bytes) external => DISPATCHER(true); + function _.transferOwnership(address) external => DISPATCHER(true); +} + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_all_default_summaries.spec b/certora/specs/setup/sanity_with_all_default_summaries.spec new file mode 100644 index 00000000..9c6f8055 --- /dev/null +++ b/certora/specs/setup/sanity_with_all_default_summaries.spec @@ -0,0 +1,14 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +import "../generic.spec"; // pick additional rules from here + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20cvl.spec b/certora/specs/setup/sanity_with_erc20cvl.spec new file mode 100644 index 00000000..181e6ad7 --- /dev/null +++ b/certora/specs/setup/sanity_with_erc20cvl.spec @@ -0,0 +1,8 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20dispatched.spec b/certora/specs/setup/sanity_with_erc20dispatched.spec new file mode 100644 index 00000000..8a7a0324 --- /dev/null +++ b/certora/specs/setup/sanity_with_erc20dispatched.spec @@ -0,0 +1,7 @@ +import "../ERC20/erc20dispatched.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec new file mode 100644 index 00000000..325fff72 --- /dev/null +++ b/certora/specs/shared.spec @@ -0,0 +1,12 @@ +function pay_and_havoc(address receiver, env e) { + if (e.msg.sender == receiver) { + utils.havocAll(); + return; + } + + uint oldBalanceSender = nativeBalances[e.msg.sender]; + uint oldBalanceRecipient = nativeBalances[receiver]; + utils.havocAll(); + require nativeBalances[e.msg.sender] == oldBalanceSender - e.msg.value; + require nativeBalances[receiver] == oldBalanceRecipient + e.msg.value; +} \ No newline at end of file diff --git a/certora/specs/unresolved.spec b/certora/specs/unresolved.spec new file mode 100644 index 00000000..6bae1d48 --- /dev/null +++ b/certora/specs/unresolved.spec @@ -0,0 +1,4 @@ +// summaries for unresolved calls +methods { + +} \ No newline at end of file From 297f80d5398bdcf603540f00504a00226d50aa43 Mon Sep 17 00:00:00 2001 From: dominik-velan Date: Wed, 7 Aug 2024 14:34:38 +0200 Subject: [PATCH 02/67] update linking --- .../confs/EmergencyProtectedTimelock_sanity.conf | 7 ++++--- certora/confs/Escrow_sanity.conf | 5 +++-- certora/specs/setup/sanity.spec | 11 +++++++++++ certora/specs/setup/sanity_DualGovernance.spec | 12 +++++++++++- certora/specs/setup/sanity_Escrow.spec | 14 ++++++++++++++ certora/specs/setup/sanity_Timelock.spec | 14 ++++++++++++-- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/certora/confs/EmergencyProtectedTimelock_sanity.conf b/certora/confs/EmergencyProtectedTimelock_sanity.conf index 363b6152..f114a126 100644 --- a/certora/confs/EmergencyProtectedTimelock_sanity.conf +++ b/certora/confs/EmergencyProtectedTimelock_sanity.conf @@ -2,14 +2,15 @@ "assert_autofinder_success": true, "files": [ "contracts/EmergencyProtectedTimelock.sol", -// "contracts/ConfigurationProvider.sol", "contracts/Configuration.sol", "contracts/Executor.sol", -// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", +// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", // the contract is abstract so it cannot be added here ], "link": [ "EmergencyProtectedTimelock:CONFIG=Configuration", -// "ConfigurationProvider:CONFIG=Configuration", + ], + "struct_link": [ + "EmergencyProtectedTimelock:executor=Executor", ], "java_args": [ " -ea -Dlevel.setup.helpers=info" diff --git a/certora/confs/Escrow_sanity.conf b/certora/confs/Escrow_sanity.conf index 8c120103..bc07c303 100644 --- a/certora/confs/Escrow_sanity.conf +++ b/certora/confs/Escrow_sanity.conf @@ -8,14 +8,15 @@ "contracts/Configuration.sol", "contracts/ConfigurationProvider.sol", "contracts/EmergencyProtectedTimelock.sol", - "certora/helpers/DummyWithdrawalQueue.sol", +// "certora/helpers/DummyWithdrawalQueue.sol", // it causes sanity issue "certora/harnesses/ERC20Like/DummyStETH.sol", + "contracts/Types/Duration.sol:Durations", ], "link": [ "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", "Escrow:_dualGovernance=DualGovernance", "ConfigurationProvider:CONFIG=Configuration", - "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", +// "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", "Escrow:ST_ETH=DummyStETH", "Escrow:CONFIG=Configuration", "DualGovernance:CONFIG=Configuration", diff --git a/certora/specs/setup/sanity.spec b/certora/specs/setup/sanity.spec index d74f4afe..5269a3c8 100644 --- a/certora/specs/setup/sanity.spec +++ b/certora/specs/setup/sanity.spec @@ -1,5 +1,16 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; +import "../generic.spec"; use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_DualGovernance.spec b/certora/specs/setup/sanity_DualGovernance.spec index 941f8b91..f2914b16 100644 --- a/certora/specs/setup/sanity_DualGovernance.spec +++ b/certora/specs/setup/sanity_DualGovernance.spec @@ -2,7 +2,7 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; - +import "../generic.spec"; methods { function _.getRageQuitSupport() external => DISPATCHER(true); @@ -13,3 +13,13 @@ methods { } use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_Escrow.spec b/certora/specs/setup/sanity_Escrow.spec index 52216b50..f2914b16 100644 --- a/certora/specs/setup/sanity_Escrow.spec +++ b/certora/specs/setup/sanity_Escrow.spec @@ -2,10 +2,24 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; +import "../generic.spec"; + methods { function _.getRageQuitSupport() external => DISPATCHER(true); function _.isRageQuitFinalized() external => DISPATCHER(true); function _.MASTER_COPY() external => DISPATCHER(true); + function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); + function _.initialize(address) external => DISPATCHER(true); } use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_Timelock.spec b/certora/specs/setup/sanity_Timelock.spec index 66f44e89..cbaaa1cb 100644 --- a/certora/specs/setup/sanity_Timelock.spec +++ b/certora/specs/setup/sanity_Timelock.spec @@ -1,10 +1,20 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; +import "../generic.spec"; methods { - function _.execute(address,uint256,bytes) external => DISPATCHER(true); - function _.transferOwnership(address) external => DISPATCHER(true); + function _.transferOwnership(address) external => DISPATCHER(true); } use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file From 0a19b581c57b2c0ff8dfd9da42db448344b97290 Mon Sep 17 00:00:00 2001 From: dominik-velan Date: Wed, 7 Aug 2024 17:09:26 +0200 Subject: [PATCH 03/67] fix path in configs, set higher timeout for big contracts --- certora/confs/DualGovernance_sanity.conf | 13 +++---------- .../EmergencyActivationCommittee_sanity.conf | 8 -------- .../confs/EmergencyExecutionCommittee_sanity.conf | 8 -------- .../confs/EmergencyProtectedTimelock_sanity.conf | 8 -------- certora/confs/Escrow_sanity.conf | 15 ++++----------- certora/confs/Executor_sanity.conf | 8 -------- certora/confs/ResealManager_sanity.conf | 8 -------- certora/confs/TiebreakerCore_sanity.conf | 8 -------- certora/confs/TiebreakerSubCommittee_sanity.conf | 8 -------- 9 files changed, 7 insertions(+), 77 deletions(-) diff --git a/certora/confs/DualGovernance_sanity.conf b/certora/confs/DualGovernance_sanity.conf index 7c9ea4ef..fd9dcd54 100644 --- a/certora/confs/DualGovernance_sanity.conf +++ b/certora/confs/DualGovernance_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/DualGovernance.sol", "contracts/libraries/DualGovernanceState.sol", @@ -8,7 +7,7 @@ "contracts/ConfigurationProvider.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", - "contracts/Types/Duration.sol:Durations", + "contracts/types/Duration.sol:Durations", "certora/harnesses/ERC20Like/DummyStETH.sol", ], "link": [ @@ -16,7 +15,7 @@ "DualGovernance:CONFIG=Configuration", "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", "EmergencyProtectedTimelock:CONFIG=Configuration", - "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", // this makes the prover fail + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", "DualGovernance:_resealManager=ResealManager", "Escrow:ST_ETH=DummyStETH", ], @@ -25,20 +24,14 @@ "DualGovernance:signallingEscrow=Escrow", "DualGovernance:resealManager=ResealManager", ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, "loop_iter": "3", - "solc_via_ir": false, + "smt_timeout": "3600", "verify": "DualGovernance:certora/specs/setup/sanity_DualGovernance.spec" } \ No newline at end of file diff --git a/certora/confs/EmergencyActivationCommittee_sanity.conf b/certora/confs/EmergencyActivationCommittee_sanity.conf index d7a7434f..2b1e71f0 100644 --- a/certora/confs/EmergencyActivationCommittee_sanity.conf +++ b/certora/confs/EmergencyActivationCommittee_sanity.conf @@ -1,21 +1,13 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/EmergencyActivationCommittee.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/EmergencyExecutionCommittee_sanity.conf b/certora/confs/EmergencyExecutionCommittee_sanity.conf index 75739961..08d85aba 100644 --- a/certora/confs/EmergencyExecutionCommittee_sanity.conf +++ b/certora/confs/EmergencyExecutionCommittee_sanity.conf @@ -1,21 +1,13 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/EmergencyExecutionCommittee.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/EmergencyProtectedTimelock_sanity.conf b/certora/confs/EmergencyProtectedTimelock_sanity.conf index f114a126..3c3af124 100644 --- a/certora/confs/EmergencyProtectedTimelock_sanity.conf +++ b/certora/confs/EmergencyProtectedTimelock_sanity.conf @@ -1,10 +1,8 @@ { - "assert_autofinder_success": true, "files": [ "contracts/EmergencyProtectedTimelock.sol", "contracts/Configuration.sol", "contracts/Executor.sol", -// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", // the contract is abstract so it cannot be added here ], "link": [ "EmergencyProtectedTimelock:CONFIG=Configuration", @@ -12,17 +10,11 @@ "struct_link": [ "EmergencyProtectedTimelock:executor=Executor", ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, "solc_via_ir": false, diff --git a/certora/confs/Escrow_sanity.conf b/certora/confs/Escrow_sanity.conf index bc07c303..ff021046 100644 --- a/certora/confs/Escrow_sanity.conf +++ b/certora/confs/Escrow_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/Escrow.sol", "contracts/DualGovernance.sol", @@ -8,9 +7,9 @@ "contracts/Configuration.sol", "contracts/ConfigurationProvider.sol", "contracts/EmergencyProtectedTimelock.sol", -// "certora/helpers/DummyWithdrawalQueue.sol", // it causes sanity issue +// "certora/helpers/DummyWithdrawalQueue.sol", // it causes sanity issue, the dummy needs to be fixed before it is used "certora/harnesses/ERC20Like/DummyStETH.sol", - "contracts/Types/Duration.sol:Durations", + "contracts/types/Duration.sol:Durations", ], "link": [ "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", @@ -25,20 +24,14 @@ "DualGovernance:rageQuitEscrow=Escrow", "DualGovernance:signallingEscrow=Escrow" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "loop_iter": "10", - "solc_via_ir": false, + "optimistic_fallback": true, + "loop_iter": "3", "verify": "Escrow:certora/specs/setup/sanity_Escrow.spec" } \ No newline at end of file diff --git a/certora/confs/Executor_sanity.conf b/certora/confs/Executor_sanity.conf index 568cfa4c..c3ed904c 100644 --- a/certora/confs/Executor_sanity.conf +++ b/certora/confs/Executor_sanity.conf @@ -1,20 +1,12 @@ { - "assert_autofinder_success": true, "files": [ "contracts/Executor.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", - "solc_via_ir": false, "verify": "Executor:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/ResealManager_sanity.conf b/certora/confs/ResealManager_sanity.conf index 5c2b41fc..0c4d62e9 100644 --- a/certora/confs/ResealManager_sanity.conf +++ b/certora/confs/ResealManager_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/ResealManager.sol", "contracts/EmergencyProtectedTimelock.sol", @@ -7,19 +6,12 @@ "link": [ "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "ResealManager:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/TiebreakerCore_sanity.conf b/certora/confs/TiebreakerCore_sanity.conf index ea5089ed..24d2fb66 100644 --- a/certora/confs/TiebreakerCore_sanity.conf +++ b/certora/confs/TiebreakerCore_sanity.conf @@ -1,21 +1,13 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/TiebreakerCore.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "TiebreakerCore:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/TiebreakerSubCommittee_sanity.conf b/certora/confs/TiebreakerSubCommittee_sanity.conf index 4b0972e1..0d75b70d 100644 --- a/certora/confs/TiebreakerSubCommittee_sanity.conf +++ b/certora/confs/TiebreakerSubCommittee_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/TiebreakerSubCommittee.sol", "contracts/committees/TiebreakerCore.sol", @@ -7,19 +6,12 @@ "link": [ "TiebreakerSubCommittee:TIEBREAKER_CORE=TiebreakerCore", ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity.spec" } \ No newline at end of file From d1ee6c4bb4b4661162b880d46152415abe567fca Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 19 Aug 2024 09:51:54 +0100 Subject: [PATCH 04/67] Delete a few empty unused spec files I kept in the ERCs, DeX, PriceAggregators, Staking ones for now --- certora/specs/generic.spec | 105 ------------------------------- certora/specs/optimizations.spec | 5 -- certora/specs/problems.spec | 1 - certora/specs/shared.spec | 12 ---- certora/specs/unresolved.spec | 4 -- 5 files changed, 127 deletions(-) delete mode 100644 certora/specs/generic.spec delete mode 100644 certora/specs/optimizations.spec delete mode 100644 certora/specs/problems.spec delete mode 100644 certora/specs/shared.spec delete mode 100644 certora/specs/unresolved.spec diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec deleted file mode 100644 index 22c0ad8e..00000000 --- a/certora/specs/generic.spec +++ /dev/null @@ -1,105 +0,0 @@ -/* -This rule find which functions are privileged. -A function is privileged if there is only one address that can call it. - -The rules finds this by finding which functions can be called by two different users. -*/ -rule privilegedOperation(method f, address privileged) { - env e1; - calldataarg arg; - require e1.msg.sender == privileged; - - storage initialStorage = lastStorage; - f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. - bool firstSucceeded = !lastReverted; - - env e2; - calldataarg arg2; - require e2.msg.sender != privileged; - f@withrevert(e2, arg2) at initialStorage; // unprivileged - bool secondSucceeded = !lastReverted; - - assert !(firstSucceeded && secondSucceeded); -} - -rule timeoutChecker(method f) { - storage before = lastStorage; - env e; calldataarg arg; - f(e,arg); - assert before == lastStorage; -} - -/* -This rule find which functions that can be called, may fail due to someone else calling a function right before. - -This is n expensive rule - might fail on the demo site on big contracts -*/ -rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { - env e1; - calldataarg arg; - require e1.msg.sender == privileged; - storage initialStorage = lastStorage; - f@withrevert(e1, arg); - bool firstSucceeded = !lastReverted; - env e2; - calldataarg arg2; - require e2.msg.sender != e1.msg.sender; - f(e2, arg2) at initialStorage; - f@withrevert(e1, arg); - bool succeeded = !lastReverted; - assert succeeded; -} - -rule noRevert(method f) { - env e; - calldataarg arg; - require e.msg.value == 0; - f@withrevert(e, arg); - assert !lastReverted; -} - - -rule alwaysRevert(method f) { - env e; - calldataarg arg; - f@withrevert(e, arg); - assert lastReverted; -} - -/* failing CALL should lead to a revert */ -ghost bool saw_failing_call; - -hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { - saw_failing_call = saw_failing_call || rc == 0; -} - -rule failing_CALL_leads_to_revert(method f) { - saw_failing_call = false; - env e; - calldataarg arg; - f@withrevert(e, arg); - bool reverted = lastReverted; - assert saw_failing_call => reverted; -} - -// All usages -use builtin rule sanity; -use builtin rule hasDelegateCalls; -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; - -/** - -// Integrate rules from generic.spec in importing specs like this: - -use builtin rule sanity filtered { f -> f.contract == currentContract } -use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; -use rule privilegedOperation filtered { f -> f.contract == currentContract } -use rule timeoutChecker filtered { f -> f.contract == currentContract } -use rule simpleFrontRunning filtered { f -> f.contract == currentContract } -use rule noRevert filtered { f -> f.contract == currentContract } -use rule alwaysRevert filtered { f -> f.contract == currentContract } -use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } - **/ diff --git a/certora/specs/optimizations.spec b/certora/specs/optimizations.spec deleted file mode 100644 index e92485dc..00000000 --- a/certora/specs/optimizations.spec +++ /dev/null @@ -1,5 +0,0 @@ -// optimizing summaries -methods { - -} - diff --git a/certora/specs/problems.spec b/certora/specs/problems.spec deleted file mode 100644 index 3ebe9e37..00000000 --- a/certora/specs/problems.spec +++ /dev/null @@ -1 +0,0 @@ -// workarounds for crashes \ No newline at end of file diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec deleted file mode 100644 index 325fff72..00000000 --- a/certora/specs/shared.spec +++ /dev/null @@ -1,12 +0,0 @@ -function pay_and_havoc(address receiver, env e) { - if (e.msg.sender == receiver) { - utils.havocAll(); - return; - } - - uint oldBalanceSender = nativeBalances[e.msg.sender]; - uint oldBalanceRecipient = nativeBalances[receiver]; - utils.havocAll(); - require nativeBalances[e.msg.sender] == oldBalanceSender - e.msg.value; - require nativeBalances[receiver] == oldBalanceRecipient + e.msg.value; -} \ No newline at end of file diff --git a/certora/specs/unresolved.spec b/certora/specs/unresolved.spec deleted file mode 100644 index 6bae1d48..00000000 --- a/certora/specs/unresolved.spec +++ /dev/null @@ -1,4 +0,0 @@ -// summaries for unresolved calls -methods { - -} \ No newline at end of file From 9946904354a70b3ae1fa7fdfd11fc4a57f702d6d Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 19 Aug 2024 10:39:32 +0100 Subject: [PATCH 05/67] run script. adding back generic spec, delete unused imports --- certora/scripts/runAllSetupConfs.py | 27 +++++ certora/specs/generic.spec | 105 ++++++++++++++++++ certora/specs/setup/builtin_assertions.spec | 4 - certora/specs/setup/sanity.spec | 3 - .../specs/setup/sanity_DualGovernance.spec | 4 - certora/specs/setup/sanity_Escrow.spec | 4 - certora/specs/setup/sanity_Timelock.spec | 3 - certora/specs/setup/sanity_with_erc20cvl.spec | 4 - .../setup/sanity_with_erc20dispatched.spec | 4 - 9 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 certora/scripts/runAllSetupConfs.py create mode 100644 certora/specs/generic.spec diff --git a/certora/scripts/runAllSetupConfs.py b/certora/scripts/runAllSetupConfs.py new file mode 100644 index 00000000..9187681d --- /dev/null +++ b/certora/scripts/runAllSetupConfs.py @@ -0,0 +1,27 @@ +import argparse +import subprocess + +parser = argparse.ArgumentParser() +parser.add_argument('-M', '--batchMsg', metavar='M', type=str, nargs='?', + default='', + help='a message for all the jobs') + +setup_confs = { + "DualGovernance", + "EmergencyActivationCommittee", + "EmergencyExecutionCommittee", + "EmergencyProtectedTimelock", + "Escrow", + "Executor", + "ResealManager", + "TieBreakerCore", + "TiebreakerSubCommittee" +} + +for name in setup_confs: + args = parser.parse_args() + script = f"certora/confs/{name}_sanity.conf" + command = f"certoraRun {script} --msg \"{name} : {args.batchMsg}\"" + print(f"runing {command}") + subprocess.run(command, shell=True) + diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec new file mode 100644 index 00000000..22c0ad8e --- /dev/null +++ b/certora/specs/generic.spec @@ -0,0 +1,105 @@ +/* +This rule find which functions are privileged. +A function is privileged if there is only one address that can call it. + +The rules finds this by finding which functions can be called by two different users. +*/ +rule privilegedOperation(method f, address privileged) { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged + bool secondSucceeded = !lastReverted; + + assert !(firstSucceeded && secondSucceeded); +} + +rule timeoutChecker(method f) { + storage before = lastStorage; + env e; calldataarg arg; + f(e,arg); + assert before == lastStorage; +} + +/* +This rule find which functions that can be called, may fail due to someone else calling a function right before. + +This is n expensive rule - might fail on the demo site on big contracts +*/ +rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + storage initialStorage = lastStorage; + f@withrevert(e1, arg); + bool firstSucceeded = !lastReverted; + env e2; + calldataarg arg2; + require e2.msg.sender != e1.msg.sender; + f(e2, arg2) at initialStorage; + f@withrevert(e1, arg); + bool succeeded = !lastReverted; + assert succeeded; +} + +rule noRevert(method f) { + env e; + calldataarg arg; + require e.msg.value == 0; + f@withrevert(e, arg); + assert !lastReverted; +} + + +rule alwaysRevert(method f) { + env e; + calldataarg arg; + f@withrevert(e, arg); + assert lastReverted; +} + +/* failing CALL should lead to a revert */ +ghost bool saw_failing_call; + +hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + saw_failing_call = saw_failing_call || rc == 0; +} + +rule failing_CALL_leads_to_revert(method f) { + saw_failing_call = false; + env e; + calldataarg arg; + f@withrevert(e, arg); + bool reverted = lastReverted; + assert saw_failing_call => reverted; +} + +// All usages +use builtin rule sanity; +use builtin rule hasDelegateCalls; +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; + +/** + +// Integrate rules from generic.spec in importing specs like this: + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } + **/ diff --git a/certora/specs/setup/builtin_assertions.spec b/certora/specs/setup/builtin_assertions.spec index 477d9680..e6937f83 100644 --- a/certora/specs/setup/builtin_assertions.spec +++ b/certora/specs/setup/builtin_assertions.spec @@ -1,7 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - rule check_builtin_assertions(method f) filtered { f -> f.contract == currentContract } { diff --git a/certora/specs/setup/sanity.spec b/certora/specs/setup/sanity.spec index 5269a3c8..8ee60b15 100644 --- a/certora/specs/setup/sanity.spec +++ b/certora/specs/setup/sanity.spec @@ -1,6 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; import "../generic.spec"; use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_DualGovernance.spec b/certora/specs/setup/sanity_DualGovernance.spec index f2914b16..7fbf87e7 100644 --- a/certora/specs/setup/sanity_DualGovernance.spec +++ b/certora/specs/setup/sanity_DualGovernance.spec @@ -1,7 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - import "../generic.spec"; methods { diff --git a/certora/specs/setup/sanity_Escrow.spec b/certora/specs/setup/sanity_Escrow.spec index f2914b16..7fbf87e7 100644 --- a/certora/specs/setup/sanity_Escrow.spec +++ b/certora/specs/setup/sanity_Escrow.spec @@ -1,7 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - import "../generic.spec"; methods { diff --git a/certora/specs/setup/sanity_Timelock.spec b/certora/specs/setup/sanity_Timelock.spec index cbaaa1cb..a05b3a43 100644 --- a/certora/specs/setup/sanity_Timelock.spec +++ b/certora/specs/setup/sanity_Timelock.spec @@ -1,6 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; import "../generic.spec"; methods { diff --git a/certora/specs/setup/sanity_with_erc20cvl.spec b/certora/specs/setup/sanity_with_erc20cvl.spec index 181e6ad7..628255b1 100644 --- a/certora/specs/setup/sanity_with_erc20cvl.spec +++ b/certora/specs/setup/sanity_with_erc20cvl.spec @@ -1,8 +1,4 @@ import "../ERC20/erc20cvl.spec"; import "../ERC20/WETHcvl.spec"; -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20dispatched.spec b/certora/specs/setup/sanity_with_erc20dispatched.spec index 8a7a0324..4648a1e1 100644 --- a/certora/specs/setup/sanity_with_erc20dispatched.spec +++ b/certora/specs/setup/sanity_with_erc20dispatched.spec @@ -1,7 +1,3 @@ import "../ERC20/erc20dispatched.spec"; -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - use builtin rule sanity filtered { f -> f.contract == currentContract } From a180ae004e6ef1ac9ffa9d36c97c3e1adb40050f Mon Sep 17 00:00:00 2001 From: dominik-velan Date: Tue, 6 Aug 2024 14:40:12 +0200 Subject: [PATCH 06/67] initial setup --- certora/confs/DualGovernance_sanity.conf | 44 +++++++ .../EmergencyActivationCommittee_sanity.conf | 21 ++++ .../EmergencyExecutionCommittee_sanity.conf | 21 ++++ .../EmergencyProtectedTimelock_sanity.conf | 29 +++++ certora/confs/Escrow_sanity.conf | 43 +++++++ certora/confs/Executor_sanity.conf | 20 +++ certora/confs/ResealManager_sanity.conf | 25 ++++ certora/confs/TiebreakerCore_sanity.conf | 21 ++++ .../confs/TiebreakerSubCommittee_sanity.conf | 25 ++++ .../DualGovernance_builtin_assertions.conf | 22 ++++ ...nce_sanity_with_all_default_summaries.conf | 25 ++++ .../DualGovernance_sanity_with_erc20cvl.conf | 25 ++++ ...overnance_sanity_with_erc20dispatched.conf | 26 ++++ ...ctivationCommittee_builtin_assertions.conf | 21 ++++ ...tee_sanity_with_all_default_summaries.conf | 24 ++++ ...ivationCommittee_sanity_with_erc20cvl.conf | 24 ++++ ...Committee_sanity_with_erc20dispatched.conf | 25 ++++ ...ExecutionCommittee_builtin_assertions.conf | 21 ++++ ...tee_sanity_with_all_default_summaries.conf | 24 ++++ ...ecutionCommittee_sanity_with_erc20cvl.conf | 24 ++++ ...Committee_sanity_with_erc20dispatched.conf | 25 ++++ ...yProtectedTimelock_builtin_assertions.conf | 21 ++++ ...ock_sanity_with_all_default_summaries.conf | 24 ++++ ...rotectedTimelock_sanity_with_erc20cvl.conf | 24 ++++ ...dTimelock_sanity_with_erc20dispatched.conf | 25 ++++ .../extra/Escrow_builtin_assertions.conf | 21 ++++ ...row_sanity_with_all_default_summaries.conf | 24 ++++ .../extra/Escrow_sanity_with_erc20cvl.conf | 24 ++++ .../Escrow_sanity_with_erc20dispatched.conf | 25 ++++ .../extra/Executor_builtin_assertions.conf | 20 +++ ...tor_sanity_with_all_default_summaries.conf | 23 ++++ .../extra/Executor_sanity_with_erc20cvl.conf | 23 ++++ .../Executor_sanity_with_erc20dispatched.conf | 24 ++++ .../ResealManager_builtin_assertions.conf | 21 ++++ ...ger_sanity_with_all_default_summaries.conf | 24 ++++ .../ResealManager_sanity_with_erc20cvl.conf | 24 ++++ ...alManager_sanity_with_erc20dispatched.conf | 25 ++++ .../TiebreakerCore_builtin_assertions.conf | 21 ++++ ...ore_sanity_with_all_default_summaries.conf | 24 ++++ .../TiebreakerCore_sanity_with_erc20cvl.conf | 24 ++++ ...eakerCore_sanity_with_erc20dispatched.conf | 25 ++++ ...reakerSubCommittee_builtin_assertions.conf | 21 ++++ ...tee_sanity_with_all_default_summaries.conf | 24 ++++ ...akerSubCommittee_sanity_with_erc20cvl.conf | 24 ++++ ...Committee_sanity_with_erc20dispatched.conf | 25 ++++ certora/harnesses/ERC20Like/DummyERC20A.sol | 51 ++++++++ certora/harnesses/ERC20Like/DummyERC20B.sol | 51 ++++++++ certora/harnesses/ERC20Like/DummyStETH.sol | 115 ++++++++++++++++++ certora/harnesses/ERC20Like/DummyWeth.sol | 60 +++++++++ certora/harnesses/Utilities.sol | 14 +++ certora/helpers/DummyWithdrawalQueue.sol | 81 ++++++++++++ certora/specs/DeX/curve.spec | 6 + certora/specs/DeX/pancakeswap.spec | 7 ++ certora/specs/ERC1155/erc1155.spec | 16 +++ certora/specs/ERC1967/erc1967.spec | 7 ++ certora/specs/ERC20/WETHcvl.spec | 35 ++++++ certora/specs/ERC20/erc20cvl.spec | 68 +++++++++++ certora/specs/ERC20/erc20dispatched.spec | 16 +++ certora/specs/ERC721/erc721.spec | 9 ++ certora/specs/PriceAggregators/chainlink.spec | 4 + certora/specs/PriceAggregators/tellor.spec | 3 + certora/specs/Staking/eigenlayer.spec | 15 +++ certora/specs/Staking/lido.spec | 16 +++ certora/specs/Staking/wrappedETH.spec | 13 ++ certora/specs/generic.spec | 105 ++++++++++++++++ certora/specs/optimizations.spec | 5 + certora/specs/problems.spec | 1 + certora/specs/setup/builtin_assertions.spec | 12 ++ certora/specs/setup/sanity.spec | 5 + .../specs/setup/sanity_DualGovernance.spec | 15 +++ certora/specs/setup/sanity_Escrow.spec | 11 ++ certora/specs/setup/sanity_Timelock.spec | 10 ++ .../sanity_with_all_default_summaries.spec | 14 +++ certora/specs/setup/sanity_with_erc20cvl.spec | 8 ++ .../setup/sanity_with_erc20dispatched.spec | 7 ++ certora/specs/shared.spec | 12 ++ certora/specs/unresolved.spec | 4 + 77 files changed, 1891 insertions(+) create mode 100644 certora/confs/DualGovernance_sanity.conf create mode 100644 certora/confs/EmergencyActivationCommittee_sanity.conf create mode 100644 certora/confs/EmergencyExecutionCommittee_sanity.conf create mode 100644 certora/confs/EmergencyProtectedTimelock_sanity.conf create mode 100644 certora/confs/Escrow_sanity.conf create mode 100644 certora/confs/Executor_sanity.conf create mode 100644 certora/confs/ResealManager_sanity.conf create mode 100644 certora/confs/TiebreakerCore_sanity.conf create mode 100644 certora/confs/TiebreakerSubCommittee_sanity.conf create mode 100644 certora/confs/extra/DualGovernance_builtin_assertions.conf create mode 100644 certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/Escrow_builtin_assertions.conf create mode 100644 certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/Escrow_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/Executor_builtin_assertions.conf create mode 100644 certora/confs/extra/Executor_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/Executor_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/Executor_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/ResealManager_builtin_assertions.conf create mode 100644 certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/TiebreakerCore_builtin_assertions.conf create mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf create mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf create mode 100644 certora/harnesses/ERC20Like/DummyERC20A.sol create mode 100644 certora/harnesses/ERC20Like/DummyERC20B.sol create mode 100644 certora/harnesses/ERC20Like/DummyStETH.sol create mode 100644 certora/harnesses/ERC20Like/DummyWeth.sol create mode 100644 certora/harnesses/Utilities.sol create mode 100644 certora/helpers/DummyWithdrawalQueue.sol create mode 100644 certora/specs/DeX/curve.spec create mode 100644 certora/specs/DeX/pancakeswap.spec create mode 100644 certora/specs/ERC1155/erc1155.spec create mode 100644 certora/specs/ERC1967/erc1967.spec create mode 100644 certora/specs/ERC20/WETHcvl.spec create mode 100644 certora/specs/ERC20/erc20cvl.spec create mode 100644 certora/specs/ERC20/erc20dispatched.spec create mode 100644 certora/specs/ERC721/erc721.spec create mode 100644 certora/specs/PriceAggregators/chainlink.spec create mode 100644 certora/specs/PriceAggregators/tellor.spec create mode 100644 certora/specs/Staking/eigenlayer.spec create mode 100644 certora/specs/Staking/lido.spec create mode 100644 certora/specs/Staking/wrappedETH.spec create mode 100644 certora/specs/generic.spec create mode 100644 certora/specs/optimizations.spec create mode 100644 certora/specs/problems.spec create mode 100644 certora/specs/setup/builtin_assertions.spec create mode 100644 certora/specs/setup/sanity.spec create mode 100644 certora/specs/setup/sanity_DualGovernance.spec create mode 100644 certora/specs/setup/sanity_Escrow.spec create mode 100644 certora/specs/setup/sanity_Timelock.spec create mode 100644 certora/specs/setup/sanity_with_all_default_summaries.spec create mode 100644 certora/specs/setup/sanity_with_erc20cvl.spec create mode 100644 certora/specs/setup/sanity_with_erc20dispatched.spec create mode 100644 certora/specs/shared.spec create mode 100644 certora/specs/unresolved.spec diff --git a/certora/confs/DualGovernance_sanity.conf b/certora/confs/DualGovernance_sanity.conf new file mode 100644 index 00000000..7c9ea4ef --- /dev/null +++ b/certora/confs/DualGovernance_sanity.conf @@ -0,0 +1,44 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/DualGovernance.sol", + "contracts/libraries/DualGovernanceState.sol", + "contracts/Escrow.sol", + "contracts/Configuration.sol", + "contracts/ConfigurationProvider.sol", + "contracts/EmergencyProtectedTimelock.sol", + "contracts/ResealManager.sol", + "contracts/Types/Duration.sol:Durations", + "certora/harnesses/ERC20Like/DummyStETH.sol", + ], + "link": [ + "ConfigurationProvider:CONFIG=Configuration", + "DualGovernance:CONFIG=Configuration", + "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", + "EmergencyProtectedTimelock:CONFIG=Configuration", + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", // this makes the prover fail + "DualGovernance:_resealManager=ResealManager", + "Escrow:ST_ETH=DummyStETH", + ], + "struct_link": [ + "DualGovernance:rageQuitEscrow=Escrow", + "DualGovernance:signallingEscrow=Escrow", + "DualGovernance:resealManager=ResealManager", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_DualGovernance.spec" +} \ No newline at end of file diff --git a/certora/confs/EmergencyActivationCommittee_sanity.conf b/certora/confs/EmergencyActivationCommittee_sanity.conf new file mode 100644 index 00000000..d7a7434f --- /dev/null +++ b/certora/confs/EmergencyActivationCommittee_sanity.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/EmergencyExecutionCommittee_sanity.conf b/certora/confs/EmergencyExecutionCommittee_sanity.conf new file mode 100644 index 00000000..75739961 --- /dev/null +++ b/certora/confs/EmergencyExecutionCommittee_sanity.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/EmergencyProtectedTimelock_sanity.conf b/certora/confs/EmergencyProtectedTimelock_sanity.conf new file mode 100644 index 00000000..363b6152 --- /dev/null +++ b/certora/confs/EmergencyProtectedTimelock_sanity.conf @@ -0,0 +1,29 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/EmergencyProtectedTimelock.sol", +// "contracts/ConfigurationProvider.sol", + "contracts/Configuration.sol", + "contracts/Executor.sol", +// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", + ], + "link": [ + "EmergencyProtectedTimelock:CONFIG=Configuration", +// "ConfigurationProvider:CONFIG=Configuration", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_Timelock.spec" +} \ No newline at end of file diff --git a/certora/confs/Escrow_sanity.conf b/certora/confs/Escrow_sanity.conf new file mode 100644 index 00000000..8c120103 --- /dev/null +++ b/certora/confs/Escrow_sanity.conf @@ -0,0 +1,43 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Escrow.sol", + "contracts/DualGovernance.sol", + "contracts/EmergencyProtectedTimelock.sol", + "contracts/libraries/DualGovernanceState.sol", + "contracts/Configuration.sol", + "contracts/ConfigurationProvider.sol", + "contracts/EmergencyProtectedTimelock.sol", + "certora/helpers/DummyWithdrawalQueue.sol", + "certora/harnesses/ERC20Like/DummyStETH.sol", + ], + "link": [ + "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", + "Escrow:_dualGovernance=DualGovernance", + "ConfigurationProvider:CONFIG=Configuration", + "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", + "Escrow:ST_ETH=DummyStETH", + "Escrow:CONFIG=Configuration", + "DualGovernance:CONFIG=Configuration", + ], + "struct_link": [ + "DualGovernance:rageQuitEscrow=Escrow", + "DualGovernance:signallingEscrow=Escrow" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "10", + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_Escrow.spec" +} \ No newline at end of file diff --git a/certora/confs/Executor_sanity.conf b/certora/confs/Executor_sanity.conf new file mode 100644 index 00000000..568cfa4c --- /dev/null +++ b/certora/confs/Executor_sanity.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/ResealManager_sanity.conf b/certora/confs/ResealManager_sanity.conf new file mode 100644 index 00000000..5c2b41fc --- /dev/null +++ b/certora/confs/ResealManager_sanity.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/ResealManager.sol", + "contracts/EmergencyProtectedTimelock.sol", + ], + "link": [ + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/TiebreakerCore_sanity.conf b/certora/confs/TiebreakerCore_sanity.conf new file mode 100644 index 00000000..ea5089ed --- /dev/null +++ b/certora/confs/TiebreakerCore_sanity.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/TiebreakerSubCommittee_sanity.conf b/certora/confs/TiebreakerSubCommittee_sanity.conf new file mode 100644 index 00000000..4b0972e1 --- /dev/null +++ b/certora/confs/TiebreakerSubCommittee_sanity.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerSubCommittee.sol", + "contracts/committees/TiebreakerCore.sol", + ], + "link": [ + "TiebreakerSubCommittee:TIEBREAKER_CORE=TiebreakerCore", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_builtin_assertions.conf b/certora/confs/extra/DualGovernance_builtin_assertions.conf new file mode 100644 index 00000000..566adeb5 --- /dev/null +++ b/certora/confs/extra/DualGovernance_builtin_assertions.conf @@ -0,0 +1,22 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/DualGovernance.sol", + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, +// "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf b/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..24ec9e84 --- /dev/null +++ b/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/DualGovernance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf b/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..26911379 --- /dev/null +++ b/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/DualGovernance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf b/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..73b46140 --- /dev/null +++ b/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf @@ -0,0 +1,26 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/DualGovernance.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "3", + "solc_via_ir": false, + "verify": "DualGovernance:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf b/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf new file mode 100644 index 00000000..e38009fe --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..0a2b260e --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..bfbf3f71 --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..117f617f --- /dev/null +++ b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/EmergencyActivationCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf b/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf new file mode 100644 index 00000000..0da6f51c --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..98a5ac3f --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..5aea5b35 --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..9714a454 --- /dev/null +++ b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/EmergencyExecutionCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf b/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf new file mode 100644 index 00000000..29901693 --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..98d79e66 --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..7ea22d9d --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..fc91c53d --- /dev/null +++ b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/EmergencyProtectedTimelock.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_builtin_assertions.conf b/certora/confs/extra/Escrow_builtin_assertions.conf new file mode 100644 index 00000000..0b11e69f --- /dev/null +++ b/certora/confs/extra/Escrow_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf b/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..1c086d0c --- /dev/null +++ b/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf b/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..6193c3f5 --- /dev/null +++ b/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf b/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..a7f404b0 --- /dev/null +++ b/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/Escrow.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "Escrow:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_builtin_assertions.conf b/certora/confs/extra/Executor_builtin_assertions.conf new file mode 100644 index 00000000..bc661b3e --- /dev/null +++ b/certora/confs/extra/Executor_builtin_assertions.conf @@ -0,0 +1,20 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf b/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..c140f316 --- /dev/null +++ b/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_erc20cvl.conf b/certora/confs/extra/Executor_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..4d07e19b --- /dev/null +++ b/certora/confs/extra/Executor_sanity_with_erc20cvl.conf @@ -0,0 +1,23 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf b/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..eda809cf --- /dev/null +++ b/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/Executor.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "solc_via_ir": false, + "verify": "Executor:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_builtin_assertions.conf b/certora/confs/extra/ResealManager_builtin_assertions.conf new file mode 100644 index 00000000..a6a71379 --- /dev/null +++ b/certora/confs/extra/ResealManager_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf b/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..a716708f --- /dev/null +++ b/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf b/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..78247dd5 --- /dev/null +++ b/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf b/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..c55c2b47 --- /dev/null +++ b/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/ResealManager.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "ResealManager:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_builtin_assertions.conf b/certora/confs/extra/TiebreakerCore_builtin_assertions.conf new file mode 100644 index 00000000..5a1f0fce --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf b/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..ea96237e --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf b/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..4ad26619 --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf b/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..30d41a83 --- /dev/null +++ b/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/TiebreakerCore.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerCore:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf b/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf new file mode 100644 index 00000000..016c8101 --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf @@ -0,0 +1,21 @@ +{ + "assert_autofinder_success": true, + "files": [ + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "builtin_assertions", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/builtin_assertions.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf new file mode 100644 index 00000000..26571a3c --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_all_default_summaries", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf new file mode 100644 index 00000000..84be27a7 --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf @@ -0,0 +1,24 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyWeth.sol", + "certora/harnesses/Utilities.sol", + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20cvl", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" +} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf new file mode 100644 index 00000000..79633981 --- /dev/null +++ b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf @@ -0,0 +1,25 @@ +{ + "assert_autofinder_success": true, + "files": [ + "certora/harnesses/ERC20Like/DummyERC20A.sol", + "certora/harnesses/ERC20Like/DummyERC20B.sol", + "certora/harnesses/ERC20Like/DummyWeth.sol", + "contracts/committees/TiebreakerSubCommittee.sol" + ], + "java_args": [ + " -ea -Dlevel.setup.helpers=info" + ], + "msg": "sanity_with_erc20dispatched", + "optimistic_fallback": true, + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "prover_args": [ + " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" +} \ No newline at end of file diff --git a/certora/harnesses/ERC20Like/DummyERC20A.sol b/certora/harnesses/ERC20Like/DummyERC20A.sol new file mode 100644 index 00000000..679c2274 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20A.sol @@ -0,0 +1,51 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20A { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyERC20B.sol b/certora/harnesses/ERC20Like/DummyERC20B.sol new file mode 100644 index 00000000..7105667f --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20B.sol @@ -0,0 +1,51 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20B { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyStETH.sol b/certora/harnesses/ERC20Like/DummyStETH.sol new file mode 100644 index 00000000..523b6130 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyStETH.sol @@ -0,0 +1,115 @@ +pragma solidity >=0.8.0; + +import "../../../contracts/interfaces/IStETH.sol"; + +contract DummyStETH is IStETH { + uint256 internal totalShares; + mapping(address => uint256) private shares; + mapping(address => mapping(address => uint256)) private allowances; + + function getTotalShares() external view returns (uint256) { + return totalShares; + } + + function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { + return ethAmount * 5 / 3; + } + + function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256) { + return sharesAmount * 3 / 5; + } + + function transferShares(address to, uint256 amount) external { + _transferShares(msg.sender, to, amount); + // uint256 tokensAmount = getPooledEthByShares(amount); + // uint256 tokensAmount = amount*3/5; + // return tokensAmount; + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256) { + // uint256 tokensAmount = getPooledEthByShares(_sharesAmount); + uint256 tokensAmount = _sharesAmount * 3 / 5; + _spendAllowance(_sender, msg.sender, tokensAmount); + _transferShares(_sender, _recipient, _sharesAmount); + return tokensAmount; + } + + function transfer(address _recipient, uint256 _amount) external returns (bool) { + _transfer(msg.sender, _recipient, _amount); + return true; + } + + function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool) { + _spendAllowance(_sender, msg.sender, _amount); + _transfer(_sender, _recipient, _amount); + return true; + } + + function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool) { + _approve(msg.sender, _spender, allowances[msg.sender][_spender] + (_addedValue)); + return true; + } + + function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool) { + uint256 currentAllowance = allowances[msg.sender][_spender]; + require(currentAllowance >= _subtractedValue, "ALLOWANCE_BELOW_ZERO"); + _approve(msg.sender, _spender, currentAllowance - (_subtractedValue)); + return true; + } + + function totalSupply() external view returns (uint256) { + return totalShares * 3 / 5; + } + + function approve(address _spender, uint256 _amount) external returns (bool) { + _approve(msg.sender, _spender, _amount); + return true; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return allowances[_owner][_spender]; + } + + function balanceOf(address _account) external view returns (uint256) { + // return getPooledEthByShares(_sharesOf(_account)); + return _sharesOf(_account) * 3 / 5; + } + + function _sharesOf(address account) internal view returns (uint256) { + return shares[account]; + } + + function _transfer(address sender, address recipient, uint256 amount) internal { + uint256 sharesToTransfer = amount * 5 / 3; + _transferShares(sender, recipient, sharesToTransfer); + } + + function _transferShares(address sender, address recipient, uint256 sharesAmount) internal { + require(sender != address(0), "TRANSFER_FROM_ZERO_ADDR"); + require(recipient != address(0), "TRANSFER_TO_ZERO_ADDR"); + require(recipient != address(this), "TRANSFER_TO_STETH_CONTRACT"); + + uint256 currentSenderShares = shares[sender]; + require(sharesAmount <= currentSenderShares, "BALANCE_EXCEEDED"); + + shares[sender] = currentSenderShares - (sharesAmount); + shares[recipient] = shares[recipient] + (sharesAmount); + } + + function _spendAllowance(address owner, address spender, uint256 amount) internal { + uint256 currentAllowance = allowances[owner][spender]; + require(currentAllowance >= amount, "ALLOWANCE_EXCEEDED"); + _approve(owner, spender, currentAllowance - amount); + } + + function _approve(address owner, address spender, uint256 amount) internal { + require(owner != address(0), "APPROVE_FROM_ZERO_ADDR"); + require(spender != address(0), "APPROVE_TO_ZERO_ADDR"); + + allowances[owner][spender] = amount; + } +} diff --git a/certora/harnesses/ERC20Like/DummyWeth.sol b/certora/harnesses/ERC20Like/DummyWeth.sol new file mode 100644 index 00000000..9c691504 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyWeth.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity >=0.8.0; + +/** + * Dummy Weth token. + */ +contract DummyWeth { + uint256 t; + + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + return true; + } + + // WETH + function deposit() external payable { + b[msg.sender] += msg.value; + } + + function withdraw(uint256 amt) external { + b[msg.sender] -= amt; + payable(msg.sender).transfer(amt); // use optimistic_fallback here + } +} diff --git a/certora/harnesses/Utilities.sol b/certora/harnesses/Utilities.sol new file mode 100644 index 00000000..bae8c012 --- /dev/null +++ b/certora/harnesses/Utilities.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.8.0; + +contract Utilities { + function havocAll() external { + (bool success,) = address(0xdeadbeef).call(abi.encodeWithSelector(0x12345678)); + require(success); + } + + function justRevert() external { + revert(); + } + + function nop() external {} +} diff --git a/certora/helpers/DummyWithdrawalQueue.sol b/certora/helpers/DummyWithdrawalQueue.sol new file mode 100644 index 00000000..e4a8d70e --- /dev/null +++ b/certora/helpers/DummyWithdrawalQueue.sol @@ -0,0 +1,81 @@ +pragma solidity ^0.8.26; + +import {IWithdrawalQueue, WithdrawalRequestStatus} from "../../contracts/interfaces/IWithdrawalQueue.sol"; + +// This implementation is only mock which will is later summarised by NONDET and HAVOC summary +contract DummyWithdrawalQueue is IWithdrawalQueue { + function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + uint256 res; + return res; + } + + function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + uint256 res; + return res; + } + + function getLastRequestId() external view returns (uint256) { + uint256 res; + return res; + } + + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hints + ) external view returns (uint256[] memory claimableEthValues) { + uint256[] memory res; + return res; + } + + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + uint256[] memory res; + return res; + } + + function getLastCheckpointIndex() external view returns (uint256) { + uint256 res; + return res; + } + + function grantRole(bytes32 role, address account) external {} + function pauseFor(uint256 duration) external {} + + function isPaused() external returns (bool) { + bool res; + return res; + } + + function requestWithdrawalsWstETH(uint256[] calldata amounts, address owner) external returns (uint256[] memory) { + uint256[] memory res; + return res; + } + + function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external {} + + function getLastFinalizedRequestId() external view returns (uint256) { + uint256 res; + return res; + } + + function transferFrom(address from, address to, uint256 requestId) external {} + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + {} + + function balanceOf(address owner) external view returns (uint256) { + uint256 res; + return res; + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) external returns (uint256[] memory requestIds) {} +} diff --git a/certora/specs/DeX/curve.spec b/certora/specs/DeX/curve.spec new file mode 100644 index 00000000..d2e062f4 --- /dev/null +++ b/certora/specs/DeX/curve.spec @@ -0,0 +1,6 @@ +methods { + function _.exchange_underlying(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); + function _.exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); + function _.get_virtual_price() external => NONDET; // expect (uint256); + function _.get_dy(uint256 i, iunt256 j, uint256 dx) external => NONDET; // expect (uint256); +} \ No newline at end of file diff --git a/certora/specs/DeX/pancakeswap.spec b/certora/specs/DeX/pancakeswap.spec new file mode 100644 index 00000000..1767b85c --- /dev/null +++ b/certora/specs/DeX/pancakeswap.spec @@ -0,0 +1,7 @@ +methods { + // interface IPancackeV3SwapRouter + function _.WETH9() external => HAVOC_ECF; // expect (address); // xxx not marked as view but suspect it is... + function _.unwrapWETH9(uint256 amountMinimum, address recipient) external => HAVOC_ECF; // payable, expect void; + // xxx to use this, must import IPancackeV3SwapRouter + // function _.exactInputSingle(IPancackeV3SwapRouter.ExactInputSingleParams /* calldata */ params) external => HAVOC_ECF; // payable, expect (uint256 amountOut); +} \ No newline at end of file diff --git a/certora/specs/ERC1155/erc1155.spec b/certora/specs/ERC1155/erc1155.spec new file mode 100644 index 00000000..07948f01 --- /dev/null +++ b/certora/specs/ERC1155/erc1155.spec @@ -0,0 +1,16 @@ +methods { + function _.onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes /* calldata */ data + ) external => HAVOC_ECF; // expect (bytes4); + function _.onERC1155BatchReceived( + address operator, + address from, + uint256[] /* calldata */ ids, + uint256[] /* calldata */ values, + bytes /* calldata */ data + ) external => HAVOC_ECF; // expect (bytes4); +} \ No newline at end of file diff --git a/certora/specs/ERC1967/erc1967.spec b/certora/specs/ERC1967/erc1967.spec new file mode 100644 index 00000000..0bab6c78 --- /dev/null +++ b/certora/specs/ERC1967/erc1967.spec @@ -0,0 +1,7 @@ +methods { + // avoids linking messages upon upgradeToAndCall + function _._upgradeToAndCall(address,bytes,bool) external => HAVOC_ECF; + function _._upgradeToAndCallUUPS(address,bytes,bool) external => HAVOC_ECF; + // view function + function _.proxiableUUID() external => NONDET; // expect bytes32 +} diff --git a/certora/specs/ERC20/WETHcvl.spec b/certora/specs/ERC20/WETHcvl.spec new file mode 100644 index 00000000..8b1d8b70 --- /dev/null +++ b/certora/specs/ERC20/WETHcvl.spec @@ -0,0 +1,35 @@ +using DummyWeth as weth; // we are limited by the fact that we cannot do transfers from CVL +using Utilities as utils; + +methods { + // Utilities + function Utilities.justRevert() external envfree; + + // WETH + function _.deposit() external with (env e) => wethDeposit(calledContract, e.msg.sender, e.msg.value) expect void; + function _.withdraw(uint256 amount) external with (env e) => wethWithdraw(calledContract, e.msg.sender, amount) expect void; +} + +function wethDeposit(address target, address caller, uint256 value) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + // money will be transferred because of the payability of deposit + env e2; + require e2.msg.sender == caller; + require e2.msg.value == value; + weth.deposit(e2); + } +} + +function wethWithdraw(address target, address caller, uint256 amount) { + // should be reverting if target != weth. Instead, we will use a contract to revert + if (target != weth) { + utils.justRevert(); // check this works xxx + } else { + env e2; + require e2.msg.sender == caller; + weth.withdraw(e2, amount); + } +} \ No newline at end of file diff --git a/certora/specs/ERC20/erc20cvl.spec b/certora/specs/ERC20/erc20cvl.spec new file mode 100644 index 00000000..03f5a422 --- /dev/null +++ b/certora/specs/ERC20/erc20cvl.spec @@ -0,0 +1,68 @@ +methods { + // ERC20 standard + function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? + function _.decimals() external => PER_CALLEE_CONSTANT; + function _.totalSupply() external => totalSupplyCVL(calledContract) expect uint256; + function _.balanceOf(address a) external => balanceOfCVL(calledContract, a) expect uint256; + function _.allowance(address a, address b) external => allowanceCVL(calledContract, a, b) expect uint256; + function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; + function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; + +} + + +/// CVL simple implementations of IERC20: +/// token => totalSupply +ghost mapping(address => uint256) totalSupplyByToken; +/// token => account => balance +ghost mapping(address => mapping(address => uint256)) balanceByToken; +/// token => owner => spender => allowance +ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken; + +// function tokenBalanceOf(address token, address account) returns uint256 { +// return balanceByToken[token][account]; +// } + +function totalSupplyCVL(address token) returns uint256 { + return totalSupplyByToken[token]; +} + +function balanceOfCVL(address token, address a) returns uint256 { + return balanceByToken[token][a]; +} + +function allowanceCVL(address token, address a, address b) returns uint256 { + return allowanceByToken[token][a][b]; +} + +function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + allowanceByToken[token][approver][spender] = amount; + return true; +} + +function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if (allowanceByToken[token][from][spender] < amount) return false; + allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); + return transferCVL(token, from, to, amount); +} + +function transferCVL(address token, address from, address to, uint256 amount) returns bool { + // should be randomly reverting xxx + bool nondetSuccess; + if (!nondetSuccess) return false; + + if(balanceByToken[token][from] < amount) return false; + balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); + balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. + return true; +} \ No newline at end of file diff --git a/certora/specs/ERC20/erc20dispatched.spec b/certora/specs/ERC20/erc20dispatched.spec new file mode 100644 index 00000000..c40394bb --- /dev/null +++ b/certora/specs/ERC20/erc20dispatched.spec @@ -0,0 +1,16 @@ +methods { + // ERC20 standard + function _.name() external => DISPATCHER(true); + function _.symbol() external => DISPATCHER(true); + function _.decimals() external => DISPATCHER(true); + function _.totalSupply() external => DISPATCHER(true); + function _.balanceOf(address) external => DISPATCHER(true); + function _.allowance(address,address) external => DISPATCHER(true); + function _.approve(address,uint256) external => DISPATCHER(true); + function _.transfer(address,uint256) external => DISPATCHER(true); + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); + + // WETH + function _.deposit() external => DISPATCHER(true); + function _.withdraw(uint256) external => DISPATCHER(true); +} diff --git a/certora/specs/ERC721/erc721.spec b/certora/specs/ERC721/erc721.spec new file mode 100644 index 00000000..474b69f3 --- /dev/null +++ b/certora/specs/ERC721/erc721.spec @@ -0,0 +1,9 @@ +methods { + // likely unsound, but assumes no callback + function _.onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes data + ) external => NONDET; /* expects bytes4 */ +} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/chainlink.spec b/certora/specs/PriceAggregators/chainlink.spec new file mode 100644 index 00000000..889e39e6 --- /dev/null +++ b/certora/specs/PriceAggregators/chainlink.spec @@ -0,0 +1,4 @@ +methods { + function _.getRoundData(uint80) external => NONDET; + function _.latestRoundData() external => NONDET; +} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/tellor.spec b/certora/specs/PriceAggregators/tellor.spec new file mode 100644 index 00000000..03f90c77 --- /dev/null +++ b/certora/specs/PriceAggregators/tellor.spec @@ -0,0 +1,3 @@ +methods { + function _.getTellorCurrentValue(uint256) external => NONDET; +} \ No newline at end of file diff --git a/certora/specs/Staking/eigenlayer.spec b/certora/specs/Staking/eigenlayer.spec new file mode 100644 index 00000000..1aa91c7f --- /dev/null +++ b/certora/specs/Staking/eigenlayer.spec @@ -0,0 +1,15 @@ +methods { + // Strategy manager + function _.withdrawalRootPending(bytes32 _withdrawalRoot) external => NONDET; // expect (bool); + function _.numWithdrawalsQueued(address _user) external => NONDET; // expect (uint96); + function _.pauserRegistry() external => HAVOC_ECF; // expect (IPauserRegistry); + function _.paused(uint8 index) external => NONDET; // expect (bool); + function _.unpause(uint256 newPausedStatus) external => HAVOC_ECF; // expect void + + // interface IEigenPod + function _.withdrawBeforeRestaking() external => HAVOC_ECF; // expect void + + // interface IEigenPodManager + function _.getPod(address podOwner) external => NONDET; // expect address; // (IEigenPod) + function _.createPod() external => HAVOC_ECF; // expect (address); +} \ No newline at end of file diff --git a/certora/specs/Staking/lido.spec b/certora/specs/Staking/lido.spec new file mode 100644 index 00000000..69f4a4e3 --- /dev/null +++ b/certora/specs/Staking/lido.spec @@ -0,0 +1,16 @@ +methods { + // Lido + function _.getTotalPooledEther() external => NONDET; // expect (uint256); + function _.getTotalShares() external => NONDET; // expect (uint256); + function _.submit(address _referral) external => HAVOC_ECF; // payable, expect (uint256); + + // may be shared with other contracts XXX + function _.nonces(address _user) external => NONDET; // expect (uint256); + function _.DOMAIN_SEPARATOR() external => NONDET; // expect (bytes32); + + // Lido Withdrawal Queue + function _.MAX_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); + function _.MIN_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); + function _.requestWithdrawals(uint256[] /* calldata */ _amount, address _depositor) external => HAVOC_ECF; // expect (uint256[] memory); + function _.claimWithdrawals(uint256[] /* calldata */ _requestIds, uint256[] /* calldata */ _hints) external => HAVOC_ECF; // expect void; +} \ No newline at end of file diff --git a/certora/specs/Staking/wrappedETH.spec b/certora/specs/Staking/wrappedETH.spec new file mode 100644 index 00000000..235c82d3 --- /dev/null +++ b/certora/specs/Staking/wrappedETH.spec @@ -0,0 +1,13 @@ +import "../shared.spec"; + +using Utilities as utils; + +methods { + // e.g. cbETH (Coinbase Wrapped Staked ETH), WBETH (Wrapped Beacon ETH) + function _.mint(address _to, uint256 _amount) external => HAVOC_ECF; // expect void; + function _.exchangeRate() external => NONDET; // expect (uint256 _exchangeRate); + + // WBETH + function _.deposit(address referral) external with (env e) => pay_and_havoc(calledContract, e); // payable, expect void +} + diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec new file mode 100644 index 00000000..22c0ad8e --- /dev/null +++ b/certora/specs/generic.spec @@ -0,0 +1,105 @@ +/* +This rule find which functions are privileged. +A function is privileged if there is only one address that can call it. + +The rules finds this by finding which functions can be called by two different users. +*/ +rule privilegedOperation(method f, address privileged) { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged + bool secondSucceeded = !lastReverted; + + assert !(firstSucceeded && secondSucceeded); +} + +rule timeoutChecker(method f) { + storage before = lastStorage; + env e; calldataarg arg; + f(e,arg); + assert before == lastStorage; +} + +/* +This rule find which functions that can be called, may fail due to someone else calling a function right before. + +This is n expensive rule - might fail on the demo site on big contracts +*/ +rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + storage initialStorage = lastStorage; + f@withrevert(e1, arg); + bool firstSucceeded = !lastReverted; + env e2; + calldataarg arg2; + require e2.msg.sender != e1.msg.sender; + f(e2, arg2) at initialStorage; + f@withrevert(e1, arg); + bool succeeded = !lastReverted; + assert succeeded; +} + +rule noRevert(method f) { + env e; + calldataarg arg; + require e.msg.value == 0; + f@withrevert(e, arg); + assert !lastReverted; +} + + +rule alwaysRevert(method f) { + env e; + calldataarg arg; + f@withrevert(e, arg); + assert lastReverted; +} + +/* failing CALL should lead to a revert */ +ghost bool saw_failing_call; + +hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + saw_failing_call = saw_failing_call || rc == 0; +} + +rule failing_CALL_leads_to_revert(method f) { + saw_failing_call = false; + env e; + calldataarg arg; + f@withrevert(e, arg); + bool reverted = lastReverted; + assert saw_failing_call => reverted; +} + +// All usages +use builtin rule sanity; +use builtin rule hasDelegateCalls; +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; + +/** + +// Integrate rules from generic.spec in importing specs like this: + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } + **/ diff --git a/certora/specs/optimizations.spec b/certora/specs/optimizations.spec new file mode 100644 index 00000000..e92485dc --- /dev/null +++ b/certora/specs/optimizations.spec @@ -0,0 +1,5 @@ +// optimizing summaries +methods { + +} + diff --git a/certora/specs/problems.spec b/certora/specs/problems.spec new file mode 100644 index 00000000..3ebe9e37 --- /dev/null +++ b/certora/specs/problems.spec @@ -0,0 +1 @@ +// workarounds for crashes \ No newline at end of file diff --git a/certora/specs/setup/builtin_assertions.spec b/certora/specs/setup/builtin_assertions.spec new file mode 100644 index 00000000..477d9680 --- /dev/null +++ b/certora/specs/setup/builtin_assertions.spec @@ -0,0 +1,12 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +rule check_builtin_assertions(method f) + filtered { f -> f.contract == currentContract } +{ + env e; + calldataarg arg; + f(e, arg); + assert true; +} diff --git a/certora/specs/setup/sanity.spec b/certora/specs/setup/sanity.spec new file mode 100644 index 00000000..d74f4afe --- /dev/null +++ b/certora/specs/setup/sanity.spec @@ -0,0 +1,5 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_DualGovernance.spec b/certora/specs/setup/sanity_DualGovernance.spec new file mode 100644 index 00000000..941f8b91 --- /dev/null +++ b/certora/specs/setup/sanity_DualGovernance.spec @@ -0,0 +1,15 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + + + +methods { + function _.getRageQuitSupport() external => DISPATCHER(true); + function _.isRageQuitFinalized() external => DISPATCHER(true); + function _.MASTER_COPY() external => DISPATCHER(true); + function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); + function _.initialize(address) external => DISPATCHER(true); +} + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_Escrow.spec b/certora/specs/setup/sanity_Escrow.spec new file mode 100644 index 00000000..52216b50 --- /dev/null +++ b/certora/specs/setup/sanity_Escrow.spec @@ -0,0 +1,11 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +methods { + function _.getRageQuitSupport() external => DISPATCHER(true); + function _.isRageQuitFinalized() external => DISPATCHER(true); + function _.MASTER_COPY() external => DISPATCHER(true); +} + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_Timelock.spec b/certora/specs/setup/sanity_Timelock.spec new file mode 100644 index 00000000..66f44e89 --- /dev/null +++ b/certora/specs/setup/sanity_Timelock.spec @@ -0,0 +1,10 @@ +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +methods { + function _.execute(address,uint256,bytes) external => DISPATCHER(true); + function _.transferOwnership(address) external => DISPATCHER(true); +} + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_all_default_summaries.spec b/certora/specs/setup/sanity_with_all_default_summaries.spec new file mode 100644 index 00000000..9c6f8055 --- /dev/null +++ b/certora/specs/setup/sanity_with_all_default_summaries.spec @@ -0,0 +1,14 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; +import "../ERC721/erc721.spec"; +import "../ERC1967/erc1967.spec"; +import "../PriceAggregators/chainlink.spec"; +import "../PriceAggregators/tellor.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +import "../generic.spec"; // pick additional rules from here + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20cvl.spec b/certora/specs/setup/sanity_with_erc20cvl.spec new file mode 100644 index 00000000..181e6ad7 --- /dev/null +++ b/certora/specs/setup/sanity_with_erc20cvl.spec @@ -0,0 +1,8 @@ +import "../ERC20/erc20cvl.spec"; +import "../ERC20/WETHcvl.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20dispatched.spec b/certora/specs/setup/sanity_with_erc20dispatched.spec new file mode 100644 index 00000000..8a7a0324 --- /dev/null +++ b/certora/specs/setup/sanity_with_erc20dispatched.spec @@ -0,0 +1,7 @@ +import "../ERC20/erc20dispatched.spec"; + +import "../problems.spec"; +import "../unresolved.spec"; +import "../optimizations.spec"; + +use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec new file mode 100644 index 00000000..325fff72 --- /dev/null +++ b/certora/specs/shared.spec @@ -0,0 +1,12 @@ +function pay_and_havoc(address receiver, env e) { + if (e.msg.sender == receiver) { + utils.havocAll(); + return; + } + + uint oldBalanceSender = nativeBalances[e.msg.sender]; + uint oldBalanceRecipient = nativeBalances[receiver]; + utils.havocAll(); + require nativeBalances[e.msg.sender] == oldBalanceSender - e.msg.value; + require nativeBalances[receiver] == oldBalanceRecipient + e.msg.value; +} \ No newline at end of file diff --git a/certora/specs/unresolved.spec b/certora/specs/unresolved.spec new file mode 100644 index 00000000..6bae1d48 --- /dev/null +++ b/certora/specs/unresolved.spec @@ -0,0 +1,4 @@ +// summaries for unresolved calls +methods { + +} \ No newline at end of file From 55da68d996bd8cb78fbb2882e73d8875b8cc3fdf Mon Sep 17 00:00:00 2001 From: dominik-velan Date: Wed, 7 Aug 2024 14:34:38 +0200 Subject: [PATCH 07/67] update linking --- .../confs/EmergencyProtectedTimelock_sanity.conf | 7 ++++--- certora/confs/Escrow_sanity.conf | 5 +++-- certora/specs/setup/sanity.spec | 11 +++++++++++ certora/specs/setup/sanity_DualGovernance.spec | 12 +++++++++++- certora/specs/setup/sanity_Escrow.spec | 14 ++++++++++++++ certora/specs/setup/sanity_Timelock.spec | 14 ++++++++++++-- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/certora/confs/EmergencyProtectedTimelock_sanity.conf b/certora/confs/EmergencyProtectedTimelock_sanity.conf index 363b6152..f114a126 100644 --- a/certora/confs/EmergencyProtectedTimelock_sanity.conf +++ b/certora/confs/EmergencyProtectedTimelock_sanity.conf @@ -2,14 +2,15 @@ "assert_autofinder_success": true, "files": [ "contracts/EmergencyProtectedTimelock.sol", -// "contracts/ConfigurationProvider.sol", "contracts/Configuration.sol", "contracts/Executor.sol", -// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", +// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", // the contract is abstract so it cannot be added here ], "link": [ "EmergencyProtectedTimelock:CONFIG=Configuration", -// "ConfigurationProvider:CONFIG=Configuration", + ], + "struct_link": [ + "EmergencyProtectedTimelock:executor=Executor", ], "java_args": [ " -ea -Dlevel.setup.helpers=info" diff --git a/certora/confs/Escrow_sanity.conf b/certora/confs/Escrow_sanity.conf index 8c120103..bc07c303 100644 --- a/certora/confs/Escrow_sanity.conf +++ b/certora/confs/Escrow_sanity.conf @@ -8,14 +8,15 @@ "contracts/Configuration.sol", "contracts/ConfigurationProvider.sol", "contracts/EmergencyProtectedTimelock.sol", - "certora/helpers/DummyWithdrawalQueue.sol", +// "certora/helpers/DummyWithdrawalQueue.sol", // it causes sanity issue "certora/harnesses/ERC20Like/DummyStETH.sol", + "contracts/Types/Duration.sol:Durations", ], "link": [ "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", "Escrow:_dualGovernance=DualGovernance", "ConfigurationProvider:CONFIG=Configuration", - "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", +// "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", "Escrow:ST_ETH=DummyStETH", "Escrow:CONFIG=Configuration", "DualGovernance:CONFIG=Configuration", diff --git a/certora/specs/setup/sanity.spec b/certora/specs/setup/sanity.spec index d74f4afe..5269a3c8 100644 --- a/certora/specs/setup/sanity.spec +++ b/certora/specs/setup/sanity.spec @@ -1,5 +1,16 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; +import "../generic.spec"; use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_DualGovernance.spec b/certora/specs/setup/sanity_DualGovernance.spec index 941f8b91..f2914b16 100644 --- a/certora/specs/setup/sanity_DualGovernance.spec +++ b/certora/specs/setup/sanity_DualGovernance.spec @@ -2,7 +2,7 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; - +import "../generic.spec"; methods { function _.getRageQuitSupport() external => DISPATCHER(true); @@ -13,3 +13,13 @@ methods { } use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_Escrow.spec b/certora/specs/setup/sanity_Escrow.spec index 52216b50..f2914b16 100644 --- a/certora/specs/setup/sanity_Escrow.spec +++ b/certora/specs/setup/sanity_Escrow.spec @@ -2,10 +2,24 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; +import "../generic.spec"; + methods { function _.getRageQuitSupport() external => DISPATCHER(true); function _.isRageQuitFinalized() external => DISPATCHER(true); function _.MASTER_COPY() external => DISPATCHER(true); + function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); + function _.initialize(address) external => DISPATCHER(true); } use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_Timelock.spec b/certora/specs/setup/sanity_Timelock.spec index 66f44e89..cbaaa1cb 100644 --- a/certora/specs/setup/sanity_Timelock.spec +++ b/certora/specs/setup/sanity_Timelock.spec @@ -1,10 +1,20 @@ import "../problems.spec"; import "../unresolved.spec"; import "../optimizations.spec"; +import "../generic.spec"; methods { - function _.execute(address,uint256,bytes) external => DISPATCHER(true); - function _.transferOwnership(address) external => DISPATCHER(true); + function _.transferOwnership(address) external => DISPATCHER(true); } use builtin rule sanity filtered { f -> f.contract == currentContract } + +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file From 33786fe5c68d5195038d3118de564d07f2165336 Mon Sep 17 00:00:00 2001 From: dominik-velan Date: Wed, 7 Aug 2024 17:09:26 +0200 Subject: [PATCH 08/67] fix path in configs, set higher timeout for big contracts --- certora/confs/DualGovernance_sanity.conf | 13 +++---------- .../EmergencyActivationCommittee_sanity.conf | 8 -------- .../confs/EmergencyExecutionCommittee_sanity.conf | 8 -------- .../confs/EmergencyProtectedTimelock_sanity.conf | 8 -------- certora/confs/Escrow_sanity.conf | 15 ++++----------- certora/confs/Executor_sanity.conf | 8 -------- certora/confs/ResealManager_sanity.conf | 8 -------- certora/confs/TiebreakerCore_sanity.conf | 8 -------- certora/confs/TiebreakerSubCommittee_sanity.conf | 8 -------- 9 files changed, 7 insertions(+), 77 deletions(-) diff --git a/certora/confs/DualGovernance_sanity.conf b/certora/confs/DualGovernance_sanity.conf index 7c9ea4ef..fd9dcd54 100644 --- a/certora/confs/DualGovernance_sanity.conf +++ b/certora/confs/DualGovernance_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/DualGovernance.sol", "contracts/libraries/DualGovernanceState.sol", @@ -8,7 +7,7 @@ "contracts/ConfigurationProvider.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", - "contracts/Types/Duration.sol:Durations", + "contracts/types/Duration.sol:Durations", "certora/harnesses/ERC20Like/DummyStETH.sol", ], "link": [ @@ -16,7 +15,7 @@ "DualGovernance:CONFIG=Configuration", "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", "EmergencyProtectedTimelock:CONFIG=Configuration", - "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", // this makes the prover fail + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", "DualGovernance:_resealManager=ResealManager", "Escrow:ST_ETH=DummyStETH", ], @@ -25,20 +24,14 @@ "DualGovernance:signallingEscrow=Escrow", "DualGovernance:resealManager=ResealManager", ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, "loop_iter": "3", - "solc_via_ir": false, + "smt_timeout": "3600", "verify": "DualGovernance:certora/specs/setup/sanity_DualGovernance.spec" } \ No newline at end of file diff --git a/certora/confs/EmergencyActivationCommittee_sanity.conf b/certora/confs/EmergencyActivationCommittee_sanity.conf index d7a7434f..2b1e71f0 100644 --- a/certora/confs/EmergencyActivationCommittee_sanity.conf +++ b/certora/confs/EmergencyActivationCommittee_sanity.conf @@ -1,21 +1,13 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/EmergencyActivationCommittee.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/EmergencyExecutionCommittee_sanity.conf b/certora/confs/EmergencyExecutionCommittee_sanity.conf index 75739961..08d85aba 100644 --- a/certora/confs/EmergencyExecutionCommittee_sanity.conf +++ b/certora/confs/EmergencyExecutionCommittee_sanity.conf @@ -1,21 +1,13 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/EmergencyExecutionCommittee.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/EmergencyProtectedTimelock_sanity.conf b/certora/confs/EmergencyProtectedTimelock_sanity.conf index f114a126..3c3af124 100644 --- a/certora/confs/EmergencyProtectedTimelock_sanity.conf +++ b/certora/confs/EmergencyProtectedTimelock_sanity.conf @@ -1,10 +1,8 @@ { - "assert_autofinder_success": true, "files": [ "contracts/EmergencyProtectedTimelock.sol", "contracts/Configuration.sol", "contracts/Executor.sol", -// "lib/openzeppelin-contracts/contracts/access/Ownable.sol", // the contract is abstract so it cannot be added here ], "link": [ "EmergencyProtectedTimelock:CONFIG=Configuration", @@ -12,17 +10,11 @@ "struct_link": [ "EmergencyProtectedTimelock:executor=Executor", ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, "solc_via_ir": false, diff --git a/certora/confs/Escrow_sanity.conf b/certora/confs/Escrow_sanity.conf index bc07c303..ff021046 100644 --- a/certora/confs/Escrow_sanity.conf +++ b/certora/confs/Escrow_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/Escrow.sol", "contracts/DualGovernance.sol", @@ -8,9 +7,9 @@ "contracts/Configuration.sol", "contracts/ConfigurationProvider.sol", "contracts/EmergencyProtectedTimelock.sol", -// "certora/helpers/DummyWithdrawalQueue.sol", // it causes sanity issue +// "certora/helpers/DummyWithdrawalQueue.sol", // it causes sanity issue, the dummy needs to be fixed before it is used "certora/harnesses/ERC20Like/DummyStETH.sol", - "contracts/Types/Duration.sol:Durations", + "contracts/types/Duration.sol:Durations", ], "link": [ "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", @@ -25,20 +24,14 @@ "DualGovernance:rageQuitEscrow=Escrow", "DualGovernance:signallingEscrow=Escrow" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "loop_iter": "10", - "solc_via_ir": false, + "optimistic_fallback": true, + "loop_iter": "3", "verify": "Escrow:certora/specs/setup/sanity_Escrow.spec" } \ No newline at end of file diff --git a/certora/confs/Executor_sanity.conf b/certora/confs/Executor_sanity.conf index 568cfa4c..c3ed904c 100644 --- a/certora/confs/Executor_sanity.conf +++ b/certora/confs/Executor_sanity.conf @@ -1,20 +1,12 @@ { - "assert_autofinder_success": true, "files": [ "contracts/Executor.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", - "solc_via_ir": false, "verify": "Executor:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/ResealManager_sanity.conf b/certora/confs/ResealManager_sanity.conf index 5c2b41fc..0c4d62e9 100644 --- a/certora/confs/ResealManager_sanity.conf +++ b/certora/confs/ResealManager_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/ResealManager.sol", "contracts/EmergencyProtectedTimelock.sol", @@ -7,19 +6,12 @@ "link": [ "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "ResealManager:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/TiebreakerCore_sanity.conf b/certora/confs/TiebreakerCore_sanity.conf index ea5089ed..24d2fb66 100644 --- a/certora/confs/TiebreakerCore_sanity.conf +++ b/certora/confs/TiebreakerCore_sanity.conf @@ -1,21 +1,13 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/TiebreakerCore.sol" ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "TiebreakerCore:certora/specs/setup/sanity.spec" } \ No newline at end of file diff --git a/certora/confs/TiebreakerSubCommittee_sanity.conf b/certora/confs/TiebreakerSubCommittee_sanity.conf index 4b0972e1..0d75b70d 100644 --- a/certora/confs/TiebreakerSubCommittee_sanity.conf +++ b/certora/confs/TiebreakerSubCommittee_sanity.conf @@ -1,5 +1,4 @@ { - "assert_autofinder_success": true, "files": [ "contracts/committees/TiebreakerSubCommittee.sol", "contracts/committees/TiebreakerCore.sol", @@ -7,19 +6,12 @@ "link": [ "TiebreakerSubCommittee:TIEBREAKER_CORE=TiebreakerCore", ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], "msg": "sanity", "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], "solc": "solc8.26", "optimistic_loop": true, - "solc_via_ir": false, "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity.spec" } \ No newline at end of file From 2760d8a447499b30731c0e153ce3df863116ff3e Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 19 Aug 2024 09:51:54 +0100 Subject: [PATCH 09/67] Delete a few empty unused spec files I kept in the ERCs, DeX, PriceAggregators, Staking ones for now --- certora/specs/generic.spec | 105 ------------------------------- certora/specs/optimizations.spec | 5 -- certora/specs/problems.spec | 1 - certora/specs/shared.spec | 12 ---- certora/specs/unresolved.spec | 4 -- 5 files changed, 127 deletions(-) delete mode 100644 certora/specs/generic.spec delete mode 100644 certora/specs/optimizations.spec delete mode 100644 certora/specs/problems.spec delete mode 100644 certora/specs/shared.spec delete mode 100644 certora/specs/unresolved.spec diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec deleted file mode 100644 index 22c0ad8e..00000000 --- a/certora/specs/generic.spec +++ /dev/null @@ -1,105 +0,0 @@ -/* -This rule find which functions are privileged. -A function is privileged if there is only one address that can call it. - -The rules finds this by finding which functions can be called by two different users. -*/ -rule privilegedOperation(method f, address privileged) { - env e1; - calldataarg arg; - require e1.msg.sender == privileged; - - storage initialStorage = lastStorage; - f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. - bool firstSucceeded = !lastReverted; - - env e2; - calldataarg arg2; - require e2.msg.sender != privileged; - f@withrevert(e2, arg2) at initialStorage; // unprivileged - bool secondSucceeded = !lastReverted; - - assert !(firstSucceeded && secondSucceeded); -} - -rule timeoutChecker(method f) { - storage before = lastStorage; - env e; calldataarg arg; - f(e,arg); - assert before == lastStorage; -} - -/* -This rule find which functions that can be called, may fail due to someone else calling a function right before. - -This is n expensive rule - might fail on the demo site on big contracts -*/ -rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { - env e1; - calldataarg arg; - require e1.msg.sender == privileged; - storage initialStorage = lastStorage; - f@withrevert(e1, arg); - bool firstSucceeded = !lastReverted; - env e2; - calldataarg arg2; - require e2.msg.sender != e1.msg.sender; - f(e2, arg2) at initialStorage; - f@withrevert(e1, arg); - bool succeeded = !lastReverted; - assert succeeded; -} - -rule noRevert(method f) { - env e; - calldataarg arg; - require e.msg.value == 0; - f@withrevert(e, arg); - assert !lastReverted; -} - - -rule alwaysRevert(method f) { - env e; - calldataarg arg; - f@withrevert(e, arg); - assert lastReverted; -} - -/* failing CALL should lead to a revert */ -ghost bool saw_failing_call; - -hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { - saw_failing_call = saw_failing_call || rc == 0; -} - -rule failing_CALL_leads_to_revert(method f) { - saw_failing_call = false; - env e; - calldataarg arg; - f@withrevert(e, arg); - bool reverted = lastReverted; - assert saw_failing_call => reverted; -} - -// All usages -use builtin rule sanity; -use builtin rule hasDelegateCalls; -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; - -/** - -// Integrate rules from generic.spec in importing specs like this: - -use builtin rule sanity filtered { f -> f.contract == currentContract } -use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; -use rule privilegedOperation filtered { f -> f.contract == currentContract } -use rule timeoutChecker filtered { f -> f.contract == currentContract } -use rule simpleFrontRunning filtered { f -> f.contract == currentContract } -use rule noRevert filtered { f -> f.contract == currentContract } -use rule alwaysRevert filtered { f -> f.contract == currentContract } -use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } - **/ diff --git a/certora/specs/optimizations.spec b/certora/specs/optimizations.spec deleted file mode 100644 index e92485dc..00000000 --- a/certora/specs/optimizations.spec +++ /dev/null @@ -1,5 +0,0 @@ -// optimizing summaries -methods { - -} - diff --git a/certora/specs/problems.spec b/certora/specs/problems.spec deleted file mode 100644 index 3ebe9e37..00000000 --- a/certora/specs/problems.spec +++ /dev/null @@ -1 +0,0 @@ -// workarounds for crashes \ No newline at end of file diff --git a/certora/specs/shared.spec b/certora/specs/shared.spec deleted file mode 100644 index 325fff72..00000000 --- a/certora/specs/shared.spec +++ /dev/null @@ -1,12 +0,0 @@ -function pay_and_havoc(address receiver, env e) { - if (e.msg.sender == receiver) { - utils.havocAll(); - return; - } - - uint oldBalanceSender = nativeBalances[e.msg.sender]; - uint oldBalanceRecipient = nativeBalances[receiver]; - utils.havocAll(); - require nativeBalances[e.msg.sender] == oldBalanceSender - e.msg.value; - require nativeBalances[receiver] == oldBalanceRecipient + e.msg.value; -} \ No newline at end of file diff --git a/certora/specs/unresolved.spec b/certora/specs/unresolved.spec deleted file mode 100644 index 6bae1d48..00000000 --- a/certora/specs/unresolved.spec +++ /dev/null @@ -1,4 +0,0 @@ -// summaries for unresolved calls -methods { - -} \ No newline at end of file From 6d062eefb9ad7d15631a7a53d0c33d472f35a979 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 19 Aug 2024 10:39:32 +0100 Subject: [PATCH 10/67] run script. adding back generic spec, delete unused imports --- certora/scripts/runAllSetupConfs.py | 27 +++++ certora/specs/generic.spec | 105 ++++++++++++++++++ certora/specs/setup/builtin_assertions.spec | 4 - certora/specs/setup/sanity.spec | 3 - .../specs/setup/sanity_DualGovernance.spec | 4 - certora/specs/setup/sanity_Escrow.spec | 4 - certora/specs/setup/sanity_Timelock.spec | 3 - certora/specs/setup/sanity_with_erc20cvl.spec | 4 - .../setup/sanity_with_erc20dispatched.spec | 4 - 9 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 certora/scripts/runAllSetupConfs.py create mode 100644 certora/specs/generic.spec diff --git a/certora/scripts/runAllSetupConfs.py b/certora/scripts/runAllSetupConfs.py new file mode 100644 index 00000000..9187681d --- /dev/null +++ b/certora/scripts/runAllSetupConfs.py @@ -0,0 +1,27 @@ +import argparse +import subprocess + +parser = argparse.ArgumentParser() +parser.add_argument('-M', '--batchMsg', metavar='M', type=str, nargs='?', + default='', + help='a message for all the jobs') + +setup_confs = { + "DualGovernance", + "EmergencyActivationCommittee", + "EmergencyExecutionCommittee", + "EmergencyProtectedTimelock", + "Escrow", + "Executor", + "ResealManager", + "TieBreakerCore", + "TiebreakerSubCommittee" +} + +for name in setup_confs: + args = parser.parse_args() + script = f"certora/confs/{name}_sanity.conf" + command = f"certoraRun {script} --msg \"{name} : {args.batchMsg}\"" + print(f"runing {command}") + subprocess.run(command, shell=True) + diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec new file mode 100644 index 00000000..22c0ad8e --- /dev/null +++ b/certora/specs/generic.spec @@ -0,0 +1,105 @@ +/* +This rule find which functions are privileged. +A function is privileged if there is only one address that can call it. + +The rules finds this by finding which functions can be called by two different users. +*/ +rule privilegedOperation(method f, address privileged) { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + + storage initialStorage = lastStorage; + f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. + bool firstSucceeded = !lastReverted; + + env e2; + calldataarg arg2; + require e2.msg.sender != privileged; + f@withrevert(e2, arg2) at initialStorage; // unprivileged + bool secondSucceeded = !lastReverted; + + assert !(firstSucceeded && secondSucceeded); +} + +rule timeoutChecker(method f) { + storage before = lastStorage; + env e; calldataarg arg; + f(e,arg); + assert before == lastStorage; +} + +/* +This rule find which functions that can be called, may fail due to someone else calling a function right before. + +This is n expensive rule - might fail on the demo site on big contracts +*/ +rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { + env e1; + calldataarg arg; + require e1.msg.sender == privileged; + storage initialStorage = lastStorage; + f@withrevert(e1, arg); + bool firstSucceeded = !lastReverted; + env e2; + calldataarg arg2; + require e2.msg.sender != e1.msg.sender; + f(e2, arg2) at initialStorage; + f@withrevert(e1, arg); + bool succeeded = !lastReverted; + assert succeeded; +} + +rule noRevert(method f) { + env e; + calldataarg arg; + require e.msg.value == 0; + f@withrevert(e, arg); + assert !lastReverted; +} + + +rule alwaysRevert(method f) { + env e; + calldataarg arg; + f@withrevert(e, arg); + assert lastReverted; +} + +/* failing CALL should lead to a revert */ +ghost bool saw_failing_call; + +hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + saw_failing_call = saw_failing_call || rc == 0; +} + +rule failing_CALL_leads_to_revert(method f) { + saw_failing_call = false; + env e; + calldataarg arg; + f@withrevert(e, arg); + bool reverted = lastReverted; + assert saw_failing_call => reverted; +} + +// All usages +use builtin rule sanity; +use builtin rule hasDelegateCalls; +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; + +/** + +// Integrate rules from generic.spec in importing specs like this: + +use builtin rule sanity filtered { f -> f.contract == currentContract } +use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } +use builtin rule msgValueInLoopRule; +use builtin rule viewReentrancy; +use rule privilegedOperation filtered { f -> f.contract == currentContract } +use rule timeoutChecker filtered { f -> f.contract == currentContract } +use rule simpleFrontRunning filtered { f -> f.contract == currentContract } +use rule noRevert filtered { f -> f.contract == currentContract } +use rule alwaysRevert filtered { f -> f.contract == currentContract } +use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } + **/ diff --git a/certora/specs/setup/builtin_assertions.spec b/certora/specs/setup/builtin_assertions.spec index 477d9680..e6937f83 100644 --- a/certora/specs/setup/builtin_assertions.spec +++ b/certora/specs/setup/builtin_assertions.spec @@ -1,7 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - rule check_builtin_assertions(method f) filtered { f -> f.contract == currentContract } { diff --git a/certora/specs/setup/sanity.spec b/certora/specs/setup/sanity.spec index 5269a3c8..8ee60b15 100644 --- a/certora/specs/setup/sanity.spec +++ b/certora/specs/setup/sanity.spec @@ -1,6 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; import "../generic.spec"; use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_DualGovernance.spec b/certora/specs/setup/sanity_DualGovernance.spec index f2914b16..7fbf87e7 100644 --- a/certora/specs/setup/sanity_DualGovernance.spec +++ b/certora/specs/setup/sanity_DualGovernance.spec @@ -1,7 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - import "../generic.spec"; methods { diff --git a/certora/specs/setup/sanity_Escrow.spec b/certora/specs/setup/sanity_Escrow.spec index f2914b16..7fbf87e7 100644 --- a/certora/specs/setup/sanity_Escrow.spec +++ b/certora/specs/setup/sanity_Escrow.spec @@ -1,7 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - import "../generic.spec"; methods { diff --git a/certora/specs/setup/sanity_Timelock.spec b/certora/specs/setup/sanity_Timelock.spec index cbaaa1cb..a05b3a43 100644 --- a/certora/specs/setup/sanity_Timelock.spec +++ b/certora/specs/setup/sanity_Timelock.spec @@ -1,6 +1,3 @@ -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; import "../generic.spec"; methods { diff --git a/certora/specs/setup/sanity_with_erc20cvl.spec b/certora/specs/setup/sanity_with_erc20cvl.spec index 181e6ad7..628255b1 100644 --- a/certora/specs/setup/sanity_with_erc20cvl.spec +++ b/certora/specs/setup/sanity_with_erc20cvl.spec @@ -1,8 +1,4 @@ import "../ERC20/erc20cvl.spec"; import "../ERC20/WETHcvl.spec"; -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20dispatched.spec b/certora/specs/setup/sanity_with_erc20dispatched.spec index 8a7a0324..4648a1e1 100644 --- a/certora/specs/setup/sanity_with_erc20dispatched.spec +++ b/certora/specs/setup/sanity_with_erc20dispatched.spec @@ -1,7 +1,3 @@ import "../ERC20/erc20dispatched.spec"; -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - use builtin rule sanity filtered { f -> f.contract == currentContract } From 61c70e9f0b7eaa838690735fb6c8a006584061f7 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Wed, 21 Aug 2024 16:25:17 +0100 Subject: [PATCH 11/67] Fix run all script --- certora/scripts/runAllSetupConfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/scripts/runAllSetupConfs.py b/certora/scripts/runAllSetupConfs.py index 9187681d..5ef359ca 100644 --- a/certora/scripts/runAllSetupConfs.py +++ b/certora/scripts/runAllSetupConfs.py @@ -14,7 +14,7 @@ "Escrow", "Executor", "ResealManager", - "TieBreakerCore", + "TiebreakerCore", "TiebreakerSubCommittee" } From 04dabc17d83e302a5e9272d9e03a0a4064c1afdc Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Wed, 21 Aug 2024 18:56:17 +0200 Subject: [PATCH 12/67] a little start, trying to figure out how to interact with _proposals --- certora/confs/EmergencyProtectedTimelock.conf | 28 ++++++++++++++++ certora/specs/Timelock.spec | 33 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 certora/confs/EmergencyProtectedTimelock.conf create mode 100644 certora/specs/Timelock.spec diff --git a/certora/confs/EmergencyProtectedTimelock.conf b/certora/confs/EmergencyProtectedTimelock.conf new file mode 100644 index 00000000..f1ea1642 --- /dev/null +++ b/certora/confs/EmergencyProtectedTimelock.conf @@ -0,0 +1,28 @@ +{ + "files": [ + "contracts/EmergencyProtectedTimelock.sol", + "contracts/Configuration.sol", + "contracts/Executor.sol", + "contracts/libraries/Proposals.sol", + "contracts/libraries/EmergencyProtection.sol", + "contracts/types/Timestamp.sol:Timestamps", + "contracts/types/Duration.sol:Durations" + ], + "link": [ + "EmergencyProtectedTimelock:CONFIG=Configuration", + "EmergencyProtectedTimelock:_proposals=Proposals", + "EmergencyProtectedTimelock:_emergencyProtection=EmergencyProtection", + ], + "struct_link": [ + "EmergencyProtectedTimelock:executor=Executor", + ], + "msg": "Emergency Protected Timelock", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/Timelock.spec" +} \ No newline at end of file diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec new file mode 100644 index 00000000..6a1b825f --- /dev/null +++ b/certora/specs/Timelock.spec @@ -0,0 +1,33 @@ +using Proposals as proposals; +using Configuration as CONFIG; + +methods { + //function proposals.getProposalSubmissionTime(uint) internal returns (uint40) envfree; + function CONFIG.AFTER_SUBMIT_DELAY() external returns (Durations.Duration) envfree; +} + +rule EPT_KP_1 { + env e; + uint proposalId; + + schedule(e, proposalId); + + assert currentContract._proposals.proposals[proposalId].submittedAt + CONFIG.AFTER_SUBMIT_DELAY() < e.block.timestamp; +} + +rule EPT_2a { + env e; + uint proposalId; + + schedule(e, proposalId); + + assert e.msg.sender == currentContract._governance; +} + +rule EPT_2b { + env e; + calldataarg args; + submit(e, args); + + assert e.msg.sender == currentContract._governance; +} \ No newline at end of file From 3af2f71bd5a451d64f04a9e00eea2f0b9ad71dcd Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Wed, 21 Aug 2024 19:12:46 +0200 Subject: [PATCH 13/67] EPT_KP_1 verifies using the accessor from EPT --- certora/confs/EmergencyProtectedTimelock.conf | 2 -- certora/specs/Timelock.spec | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/certora/confs/EmergencyProtectedTimelock.conf b/certora/confs/EmergencyProtectedTimelock.conf index f1ea1642..ae516e50 100644 --- a/certora/confs/EmergencyProtectedTimelock.conf +++ b/certora/confs/EmergencyProtectedTimelock.conf @@ -10,8 +10,6 @@ ], "link": [ "EmergencyProtectedTimelock:CONFIG=Configuration", - "EmergencyProtectedTimelock:_proposals=Proposals", - "EmergencyProtectedTimelock:_emergencyProtection=EmergencyProtection", ], "struct_link": [ "EmergencyProtectedTimelock:executor=Executor", diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index 6a1b825f..d5e0be44 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -1,9 +1,8 @@ -using Proposals as proposals; using Configuration as CONFIG; methods { - //function proposals.getProposalSubmissionTime(uint) internal returns (uint40) envfree; function CONFIG.AFTER_SUBMIT_DELAY() external returns (Durations.Duration) envfree; + function getProposalSubmissionTime(uint256) external returns (Timestamps.Timestamp) envfree; } rule EPT_KP_1 { @@ -12,7 +11,7 @@ rule EPT_KP_1 { schedule(e, proposalId); - assert currentContract._proposals.proposals[proposalId].submittedAt + CONFIG.AFTER_SUBMIT_DELAY() < e.block.timestamp; + assert getProposalSubmissionTime(proposalId) + CONFIG.AFTER_SUBMIT_DELAY() <= e.block.timestamp; } rule EPT_2a { From b765d9b9bd77beeb4ebd32eb55e1ac6c674f8b9d Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Thu, 22 Aug 2024 15:45:23 +0200 Subject: [PATCH 14/67] Added W1_4 and EPT_10, plus comments and better rule names --- certora/confs/EmergencyProtectedTimelock.conf | 3 +- certora/specs/Timelock.spec | 109 +++++++++++++++++- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/certora/confs/EmergencyProtectedTimelock.conf b/certora/confs/EmergencyProtectedTimelock.conf index ae516e50..dfed2903 100644 --- a/certora/confs/EmergencyProtectedTimelock.conf +++ b/certora/confs/EmergencyProtectedTimelock.conf @@ -22,5 +22,6 @@ "solc": "solc8.26", "optimistic_loop": true, "solc_via_ir": false, - "verify": "EmergencyProtectedTimelock:certora/specs/Timelock.spec" + "verify": "EmergencyProtectedTimelock:certora/specs/Timelock.spec", + "rule_sanity": "basic" } \ No newline at end of file diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index d5e0be44..f921b0ba 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -2,19 +2,57 @@ using Configuration as CONFIG; methods { function CONFIG.AFTER_SUBMIT_DELAY() external returns (Durations.Duration) envfree; - function getProposalSubmissionTime(uint256) external returns (Timestamps.Timestamp) envfree; + function CONFIG.AFTER_SCHEDULE_DELAY() external returns (Durations.Duration) envfree; + function getProposal(uint256) external returns (Proposals.Proposal) envfree; + + // TODO: Improve this to instead resolving the inner unresolved calls to anything in EPT + function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; } -rule EPT_KP_1 { +// TODO: maybe we can get rid of the filter if we resolve the unresolved calls inside execute, +// right now we're just filtering to be in line with treating execute as a NONDET +/** + @title Executed is a terminal state for a proposal, once executed it cannot transition to any other state +*/ +rule W1_4_TerminalityOfExecuted(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { + uint proposalId; + require getProposal(proposalId).status == Proposals.Status.Executed; + + env e; + calldataarg args; + f(e, args); + + assert getProposal(proposalId).status == Proposals.Status.Executed; +} + +/** + @title A proposal cannot be scheduled for execution before at least ProposalExecutionMinTimelock has passed since its submission. +*/ +rule EPT_KP_1_SubmissionToSchedulingDelay { env e; uint proposalId; schedule(e, proposalId); - assert getProposalSubmissionTime(proposalId) + CONFIG.AFTER_SUBMIT_DELAY() <= e.block.timestamp; + assert getProposal(proposalId).submittedAt + CONFIG.AFTER_SUBMIT_DELAY() <= e.block.timestamp; +} + +/** + @title A proposal cannot be executed until the emergency protection timelock has passed since it was scheduled. +*/ +rule EPT_KP_2_SchedulingToExecutionDelay { + env e; + uint proposalId; + + execute(e, proposalId); + + assert getProposal(proposalId).scheduledAt + CONFIG.AFTER_SCHEDULE_DELAY() <= e.block.timestamp; } -rule EPT_2a { +/** + @title Only governance can schedule proposals. +*/ +rule EPT_2a_SchedulingGovernanceOnly { env e; uint proposalId; @@ -23,10 +61,71 @@ rule EPT_2a { assert e.msg.sender == currentContract._governance; } -rule EPT_2b { +/** + @title Only governance can submit proposals. +*/ +rule EPT_2b_SubmissionGovernanceOnly { env e; calldataarg args; submit(e, args); assert e.msg.sender == currentContract._governance; +} + +// Helper for EPT_10_ProposalTimestampConsistency because Proposal contains some other not easily comparable data +function proposalTimestampsEqual (Proposals.Proposal a, Proposals.Proposal b) returns bool { + return a.submittedAt == b.submittedAt && a.scheduledAt == b.scheduledAt && a.executedAt == b.executedAt; +} + +/** + @title Proposal timestamps reflect timelock actions +*/ +rule EPT_10_ProposalTimestampConsistency(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { + env e; + require e.block.timestamp <= max_uint40; + + if (f.selector == sig:submit(address, Executor.ExecutorCall[]).selector) { + uint proposalId; + Proposals.Proposal proposal_before = getProposal(proposalId); + calldataarg args; + uint submittedId = submit(e, args); + + assert proposalId != submittedId && proposalTimestampsEqual(proposal_before, getProposal(proposalId)) + || proposalId == submittedId && getProposal(submittedId).submittedAt == e.block.timestamp; + } else if (f.selector == sig:schedule(uint).selector) { + uint proposalId; + Proposals.Proposal proposal_before = getProposal(proposalId); + uint proposalIdToSchedule; + require proposalId != proposalIdToSchedule; + schedule(e, proposalIdToSchedule); + + assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) + && getProposal(proposalIdToSchedule).scheduledAt == e.block.timestamp; + } else if (f.selector == sig:execute(uint).selector) { + uint proposalId; + Proposals.Proposal proposal_before = getProposal(proposalId); + uint proposalIdToExecute; + require proposalId != proposalIdToExecute; + execute(e, proposalIdToExecute); + + assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) + && getProposal(proposalIdToExecute).executedAt == e.block.timestamp; + } else if (f.selector == sig:emergencyExecute(uint).selector) { + uint proposalId; + Proposals.Proposal proposal_before = getProposal(proposalId); + uint proposalIdToExecute; + require proposalId != proposalIdToExecute; + emergencyExecute(e, proposalIdToExecute); + + assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) + && getProposal(proposalIdToExecute).executedAt == e.block.timestamp; + } else { + uint proposalId; + Proposals.Proposal proposal_before = getProposal(proposalId); + + calldataarg args; + f(e, args); + + assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)); + } } \ No newline at end of file From 720c7d35c93afa7c1844a89d2803bd689855689d Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Fri, 23 Aug 2024 10:03:38 +0200 Subject: [PATCH 15/67] EPT_3 --- certora/specs/Timelock.spec | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index f921b0ba..71922df7 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -4,6 +4,7 @@ methods { function CONFIG.AFTER_SUBMIT_DELAY() external returns (Durations.Duration) envfree; function CONFIG.AFTER_SCHEDULE_DELAY() external returns (Durations.Duration) envfree; function getProposal(uint256) external returns (Proposals.Proposal) envfree; + function getEmergencyState() external returns (EmergencyProtection.EmergencyState) envfree; // TODO: Improve this to instead resolving the inner unresolved calls to anything in EPT function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; @@ -72,6 +73,23 @@ rule EPT_2b_SubmissionGovernanceOnly { assert e.msg.sender == currentContract._governance; } +/** + @title If emergency mode is active, only emergency execution committee can execute proposals +*/ +rule EPT_3_EmergencyModeExecutionRestriction(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { + uint proposalId; + uint executedAtBefore = getProposal(proposalId).executedAt; + + bool isEmergencyModeActivated = getEmergencyState().isEmergencyModeActivated; + address executionCommittee = getEmergencyState().executionCommittee; + + env e; + calldataarg args; + f(e, args); + + assert isEmergencyModeActivated && getProposal(proposalId).executedAt != executedAtBefore => e.msg.sender == executionCommittee; +} + // Helper for EPT_10_ProposalTimestampConsistency because Proposal contains some other not easily comparable data function proposalTimestampsEqual (Proposals.Proposal a, Proposals.Proposal b) returns bool { return a.submittedAt == b.submittedAt && a.scheduledAt == b.scheduledAt && a.executedAt == b.executedAt; From 4a1ef3ff05ee5f35e50f5bb4c5a63794ec01b0b7 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Fri, 23 Aug 2024 16:14:37 +0200 Subject: [PATCH 16/67] adjustments after rebase --- certora/confs/EmergencyProtectedTimelock.conf | 7 +- certora/specs/Timelock.spec | 83 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/certora/confs/EmergencyProtectedTimelock.conf b/certora/confs/EmergencyProtectedTimelock.conf index dfed2903..9a527f18 100644 --- a/certora/confs/EmergencyProtectedTimelock.conf +++ b/certora/confs/EmergencyProtectedTimelock.conf @@ -1,16 +1,13 @@ { "files": [ "contracts/EmergencyProtectedTimelock.sol", - "contracts/Configuration.sol", "contracts/Executor.sol", - "contracts/libraries/Proposals.sol", + "contracts/libraries/ExecutableProposals.sol", "contracts/libraries/EmergencyProtection.sol", "contracts/types/Timestamp.sol:Timestamps", "contracts/types/Duration.sol:Durations" ], - "link": [ - "EmergencyProtectedTimelock:CONFIG=Configuration", - ], + "link": [], "struct_link": [ "EmergencyProtectedTimelock:executor=Executor", ], diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index 71922df7..4e2b7217 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -1,10 +1,17 @@ -using Configuration as CONFIG; - methods { - function CONFIG.AFTER_SUBMIT_DELAY() external returns (Durations.Duration) envfree; - function CONFIG.AFTER_SCHEDULE_DELAY() external returns (Durations.Duration) envfree; - function getProposal(uint256) external returns (Proposals.Proposal) envfree; - function getEmergencyState() external returns (EmergencyProtection.EmergencyState) envfree; + function MAX_AFTER_SUBMIT_DELAY() external returns (Durations.Duration) envfree; + function MAX_AFTER_SCHEDULE_DELAY() external returns (Durations.Duration) envfree; + function MAX_EMERGENCY_MODE_DURATION() external returns (Durations.Duration) envfree; + function MAX_EMERGENCY_PROTECTION_DURATION() external returns (Durations.Duration) envfree; + + function getProposal(uint256) external returns (ITimelock.Proposal) envfree; + function getProposalsCount() external returns (uint256) envfree; + function getEmergencyProtectionContext() external returns (EmergencyProtection.Context) envfree; + function isEmergencyModeActive() external returns (bool) envfree; + function getAdminExecutor() external returns (address) envfree; + function getGovernance() external returns (address) envfree; + function getAfterSubmitDelay() external returns (Durations.Duration) envfree; + function getAfterScheduleDelay() external returns (Durations.Duration) envfree; // TODO: Improve this to instead resolving the inner unresolved calls to anything in EPT function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; @@ -14,18 +21,29 @@ methods { // right now we're just filtering to be in line with treating execute as a NONDET /** @title Executed is a terminal state for a proposal, once executed it cannot transition to any other state + @notice Expected to fail due to an acknowledged bug whose fix is not merged yet */ rule W1_4_TerminalityOfExecuted(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { uint proposalId; - require getProposal(proposalId).status == Proposals.Status.Executed; + requireInvariant outOfBoundsProposalDoesNotExist(proposalId); + require getProposal(proposalId).status == ExecutableProposals.Status.Executed; env e; calldataarg args; f(e, args); - assert getProposal(proposalId).status == Proposals.Status.Executed; + assert getProposal(proposalId).status == ExecutableProposals.Status.Executed; } +// invariant proposalHasSubmissionTimeIfItExists(uint proposalId) getProposal(proposalId).status != ExecutableProposals.Status.NotExist <=> getProposal(proposalId).submittedAt != 0 +// filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { +// preserved { +// requireInvariant outOfBoundsProposalDoesNotExist(proposalId); +// } +// } +invariant outOfBoundsProposalDoesNotExist(uint proposalId) proposalId == 0 || proposalId > getProposalsCount() => getProposal(proposalId).status == ExecutableProposals.Status.NotExist + filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } {} + /** @title A proposal cannot be scheduled for execution before at least ProposalExecutionMinTimelock has passed since its submission. */ @@ -35,7 +53,7 @@ rule EPT_KP_1_SubmissionToSchedulingDelay { schedule(e, proposalId); - assert getProposal(proposalId).submittedAt + CONFIG.AFTER_SUBMIT_DELAY() <= e.block.timestamp; + assert getProposal(proposalId).submittedAt + getAfterSubmitDelay() <= e.block.timestamp; } /** @@ -47,7 +65,7 @@ rule EPT_KP_2_SchedulingToExecutionDelay { execute(e, proposalId); - assert getProposal(proposalId).scheduledAt + CONFIG.AFTER_SCHEDULE_DELAY() <= e.block.timestamp; + assert getProposal(proposalId).scheduledAt + getAfterScheduleDelay() <= e.block.timestamp; } /** @@ -59,7 +77,7 @@ rule EPT_2a_SchedulingGovernanceOnly { schedule(e, proposalId); - assert e.msg.sender == currentContract._governance; + assert e.msg.sender == getGovernance(); } /** @@ -70,7 +88,7 @@ rule EPT_2b_SubmissionGovernanceOnly { calldataarg args; submit(e, args); - assert e.msg.sender == currentContract._governance; + assert e.msg.sender == getGovernance(); } /** @@ -78,21 +96,24 @@ rule EPT_2b_SubmissionGovernanceOnly { */ rule EPT_3_EmergencyModeExecutionRestriction(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { uint proposalId; - uint executedAtBefore = getProposal(proposalId).executedAt; + requireInvariant outOfBoundsProposalDoesNotExist(proposalId); + bool executedBefore = getProposal(proposalId).status == ExecutableProposals.Status.Executed; - bool isEmergencyModeActivated = getEmergencyState().isEmergencyModeActivated; - address executionCommittee = getEmergencyState().executionCommittee; + bool isEmergencyModeActivated = isEmergencyModeActive(); + address executionCommittee = getEmergencyProtectionContext().emergencyExecutionCommittee; env e; calldataarg args; f(e, args); - assert isEmergencyModeActivated && getProposal(proposalId).executedAt != executedAtBefore => e.msg.sender == executionCommittee; + bool executedAfter = getProposal(proposalId).status == ExecutableProposals.Status.Executed; + + assert isEmergencyModeActivated && !executedBefore && executedAfter => e.msg.sender == executionCommittee; } // Helper for EPT_10_ProposalTimestampConsistency because Proposal contains some other not easily comparable data -function proposalTimestampsEqual (Proposals.Proposal a, Proposals.Proposal b) returns bool { - return a.submittedAt == b.submittedAt && a.scheduledAt == b.scheduledAt && a.executedAt == b.executedAt; +function proposalTimestampsEqual (ITimelock.Proposal a, ITimelock.Proposal b) returns bool { + return a.submittedAt == b.submittedAt && a.scheduledAt == b.scheduledAt; } /** @@ -102,44 +123,28 @@ rule EPT_10_ProposalTimestampConsistency(method f) filtered { f -> f.selector != env e; require e.block.timestamp <= max_uint40; - if (f.selector == sig:submit(address, Executor.ExecutorCall[]).selector) { + if (f.selector == sig:submit(address, ExternalCalls.ExternalCall[]).selector) { uint proposalId; - Proposals.Proposal proposal_before = getProposal(proposalId); + ITimelock.Proposal proposal_before = getProposal(proposalId); calldataarg args; uint submittedId = submit(e, args); assert proposalId != submittedId && proposalTimestampsEqual(proposal_before, getProposal(proposalId)) || proposalId == submittedId && getProposal(submittedId).submittedAt == e.block.timestamp; + } else if (f.selector == sig:schedule(uint).selector) { uint proposalId; - Proposals.Proposal proposal_before = getProposal(proposalId); + ITimelock.Proposal proposal_before = getProposal(proposalId); uint proposalIdToSchedule; require proposalId != proposalIdToSchedule; schedule(e, proposalIdToSchedule); assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) && getProposal(proposalIdToSchedule).scheduledAt == e.block.timestamp; - } else if (f.selector == sig:execute(uint).selector) { - uint proposalId; - Proposals.Proposal proposal_before = getProposal(proposalId); - uint proposalIdToExecute; - require proposalId != proposalIdToExecute; - execute(e, proposalIdToExecute); - assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) - && getProposal(proposalIdToExecute).executedAt == e.block.timestamp; - } else if (f.selector == sig:emergencyExecute(uint).selector) { - uint proposalId; - Proposals.Proposal proposal_before = getProposal(proposalId); - uint proposalIdToExecute; - require proposalId != proposalIdToExecute; - emergencyExecute(e, proposalIdToExecute); - - assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) - && getProposal(proposalIdToExecute).executedAt == e.block.timestamp; } else { uint proposalId; - Proposals.Proposal proposal_before = getProposal(proposalId); + ITimelock.Proposal proposal_before = getProposal(proposalId); calldataarg args; f(e, args); From 2e76f3a27f62b4ed2ebdbc2a93d8e44468157c3c Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Fri, 23 Aug 2024 20:26:52 +0200 Subject: [PATCH 17/67] EPT_1 and EPT_5 --- certora/specs/Timelock.spec | 76 ++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index 4e2b7217..a6fb83d9 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -68,6 +68,60 @@ rule EPT_KP_2_SchedulingToExecutionDelay { assert getProposal(proposalId).scheduledAt + getAfterScheduleDelay() <= e.block.timestamp; } +function effectiveEmergencyExecutionCommittee(env e) returns address { + if (e.block.timestamp <= getEmergencyProtectionContext().emergencyProtectionEndsAfter || isEmergencyModeActive()) { + return getEmergencyProtectionContext().emergencyExecutionCommittee; + } + return 0; +} + +function effectiveEmergencyActivationCommittee(env e) returns address { + if (e.block.timestamp <= getEmergencyProtectionContext().emergencyProtectionEndsAfter) { + return getEmergencyProtectionContext().emergencyActivationCommittee; + } + return 0; +} + +/** + @title Emergency protection configuration changes are guarded by committees or admin executor +*/ +rule EPT_1_EmergencyProtectionConfigurationGuarded(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { + EmergencyProtection.Context before = getEmergencyProtectionContext(); + + env e; + require e.block.timestamp <= max_uint40; + bool isEmergencyModePassed = before.emergencyModeEndsAfter <= e.block.timestamp; + address effectiveEmergencyActivationCommittee = effectiveEmergencyActivationCommittee(e); + address effectiveEmergencyExecutionCommittee = effectiveEmergencyExecutionCommittee(e); + + calldataarg args; + f(e, args); + + EmergencyProtection.Context after = getEmergencyProtectionContext(); + + assert before == after + // emergency mode activation + || (after.emergencyModeEndsAfter != 0 && + before.emergencyActivationCommittee == after.emergencyActivationCommittee && + before.emergencyProtectionEndsAfter == after.emergencyProtectionEndsAfter && + before.emergencyExecutionCommittee == after.emergencyExecutionCommittee && + before.emergencyModeDuration == after.emergencyModeDuration && + before.emergencyGovernance == after.emergencyGovernance && + e.msg.sender == effectiveEmergencyActivationCommittee) + // emergency mode deactivation + || (after.emergencyModeEndsAfter == 0 && + after.emergencyActivationCommittee == 0 && + after.emergencyProtectionEndsAfter == 0 && + after.emergencyExecutionCommittee == 0 && + after.emergencyModeDuration == 0 && + after.emergencyGovernance == before.emergencyGovernance && + // via time passing or execution committee + (isEmergencyModePassed || e.msg.sender == effectiveEmergencyExecutionCommittee)) + // reconfiguration through proposal executed by admin executor + || e.msg.sender == getAdminExecutor(); + +} + /** @title Only governance can schedule proposals. */ @@ -100,15 +154,33 @@ rule EPT_3_EmergencyModeExecutionRestriction(method f) filtered { f -> f.selecto bool executedBefore = getProposal(proposalId).status == ExecutableProposals.Status.Executed; bool isEmergencyModeActivated = isEmergencyModeActive(); - address executionCommittee = getEmergencyProtectionContext().emergencyExecutionCommittee; env e; + address effectiveEmergencyExecutionCommittee = effectiveEmergencyExecutionCommittee(e); + calldataarg args; f(e, args); bool executedAfter = getProposal(proposalId).status == ExecutableProposals.Status.Executed; - assert isEmergencyModeActivated && !executedBefore && executedAfter => e.msg.sender == executionCommittee; + assert isEmergencyModeActivated && !executedBefore && executedAfter => e.msg.sender == effectiveEmergencyExecutionCommittee; +} + +/** + @title Emergency Protection deactivation without emergency + @notice The usefullness of this rule depends on us using the effectiveXXXCommittee functions also in all other rules + where we check that something is guarded by the committee. +*/ +rule EPT_5_EmergencyProtectionElapsed(method f) { + EmergencyProtection.Context context = getEmergencyProtectionContext(); + // protected deployment mode was activated, but not emergency mode + require context.emergencyProtectionEndsAfter != 0 && !isEmergencyModeActive(); + + env e; + // protection time has elapsed in our environment + require e.block.timestamp > context.emergencyProtectionEndsAfter; + + assert effectiveEmergencyActivationCommittee(e) == 0 && effectiveEmergencyExecutionCommittee(e) == 0; } // Helper for EPT_10_ProposalTimestampConsistency because Proposal contains some other not easily comparable data From 79d40e8b307cecadf49bc7320324f2e4e8d8eba7 Mon Sep 17 00:00:00 2001 From: Nurit Dor <57101353+nd-certora@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:42:06 +0300 Subject: [PATCH 18/67] setup for escrow --- .gitignore | 3 + certora/confs/Escrow.conf | 32 ++++ .../ERC20Like/DummyERC20MintBurn.sol | 60 ++++++++ certora/harnesses/ERC20Like/DummyWstETH.sol | 75 ++++++++++ certora/helpers/DummyWithdrawalQueue.sol | 122 ++++++++++----- certora/specs/Escrow.spec | 141 ++++++++++++++++++ 6 files changed, 392 insertions(+), 41 deletions(-) create mode 100644 certora/confs/Escrow.conf create mode 100644 certora/harnesses/ERC20Like/DummyERC20MintBurn.sol create mode 100644 certora/harnesses/ERC20Like/DummyWstETH.sol create mode 100644 certora/specs/Escrow.spec diff --git a/.gitignore b/.gitignore index b75e4b7d..a6f1a6ca 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ out/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ + +#Certora +.certora_internal/ diff --git a/certora/confs/Escrow.conf b/certora/confs/Escrow.conf new file mode 100644 index 00000000..f17f007e --- /dev/null +++ b/certora/confs/Escrow.conf @@ -0,0 +1,32 @@ +{ + "files": [ + "contracts/Escrow.sol", + "contracts/DualGovernance.sol", + "contracts/Configuration.sol", + "certora/helpers/DummyWithdrawalQueue.sol", + "certora/harnesses/ERC20Like/DummyStETH.sol", + "certora/harnesses/ERC20Like/DummyWstETH.sol", + ], + "link": [ + "Escrow:_dualGovernance=DualGovernance", + "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", + "Escrow:ST_ETH=DummyStETH", + "Escrow:WST_ETH=DummyWstETH", + "DummyWstETH:stETH=DummyStETH", + "DummyWithdrawalQueue:stETH=DummyStETH", + "Escrow:CONFIG=Configuration", + "DualGovernance:CONFIG=Configuration", + ], + + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "optimistic_fallback": true, + "loop_iter": "3", + "build_cache" : true, + "rule_sanity" : "basic", + "verify": "Escrow:certora/specs/Escrow.spec" +} \ No newline at end of file diff --git a/certora/harnesses/ERC20Like/DummyERC20MintBurn.sol b/certora/harnesses/ERC20Like/DummyERC20MintBurn.sol new file mode 100644 index 00000000..87e463e2 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20MintBurn.sol @@ -0,0 +1,60 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20MintBurn { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function _mint(address to, uint256 amount) internal { + b[to] += amount; + t += amount; + } + function _burn(address to, uint256 amount) internal { + b[to] -= amount; + t -= amount; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyWstETH.sol b/certora/harnesses/ERC20Like/DummyWstETH.sol new file mode 100644 index 00000000..2c47535b --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyWstETH.sol @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2021 Lido + +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity ^0.8.26; + +import "./DummyERC20MintBurn.sol"; + +import "../../../contracts/interfaces/IStETH.sol"; + +/** + * @title StETH token wrapper with static balances. + * @dev It's an ERC20 token that represents the account's share of the total + * supply of stETH tokens. WstETH token's balance only changes on transfers, + * unlike StETH that is also changed when oracles report staking rewards and + * penalties. It's a "power user" token for DeFi protocols which don't + * support rebasable tokens. + * + * The contract is also a trustless wrapper that accepts stETH tokens and mints + * wstETH in return. Then the user unwraps, the contract burns user's wstETH + * and sends user locked stETH in return. + * + * The contract provides the staking shortcut: user can send ETH with regular + * transfer and get wstETH in return. The contract will send ETH to Lido submit + * method, staking it and wrapping the received stETH. + * + */ +contract DummyWstETH is DummyERC20MintBurn { + IStETH public stETH; + + /** + * @param _stETH address of the StETH token to wrap + */ + constructor(IStETH _stETH) + { + stETH = _stETH; + } + + /** + * @notice Exchanges stETH to wstETH + * @param _stETHAmount amount of stETH to wrap in exchange for wstETH + * @dev Requirements: + * - `_stETHAmount` must be non-zero + * - msg.sender must approve at least `_stETHAmount` stETH to this + * contract. + * - msg.sender must have at least `_stETHAmount` of stETH. + * User should first approve _stETHAmount to the WstETH contract + * @return Amount of wstETH user receives after wrap + */ + function wrap(uint256 _stETHAmount) external returns (uint256) { + require(_stETHAmount > 0, "wstETH: can't wrap zero stETH"); + uint256 wstETHAmount = stETH.getSharesByPooledEth(_stETHAmount); + _mint(msg.sender, wstETHAmount); + stETH.transferFrom(msg.sender, address(this), _stETHAmount); + return wstETHAmount; + } + + /** + * @notice Exchanges wstETH to stETH + * @param _wstETHAmount amount of wstETH to uwrap in exchange for stETH + * @dev Requirements: + * - `_wstETHAmount` must be non-zero + * - msg.sender must have at least `_wstETHAmount` wstETH. + * @return Amount of stETH user receives after unwrap + */ + function unwrap(uint256 _wstETHAmount) external returns (uint256) { + require(_wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); + uint256 stETHAmount = stETH.getPooledEthByShares(_wstETHAmount); + _burn(msg.sender, _wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); + return stETHAmount; + } + +} diff --git a/certora/helpers/DummyWithdrawalQueue.sol b/certora/helpers/DummyWithdrawalQueue.sol index e4a8d70e..3578388e 100644 --- a/certora/helpers/DummyWithdrawalQueue.sol +++ b/certora/helpers/DummyWithdrawalQueue.sol @@ -2,80 +2,120 @@ pragma solidity ^0.8.26; import {IWithdrawalQueue, WithdrawalRequestStatus} from "../../contracts/interfaces/IWithdrawalQueue.sol"; + +import "../../contracts/interfaces/IStETH.sol"; + // This implementation is only mock which will is later summarised by NONDET and HAVOC summary contract DummyWithdrawalQueue is IWithdrawalQueue { - function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { - uint256 res; - return res; + + // The Prover will assume a contant but random value; + uint256 public MAX_STETH_WITHDRAWAL_AMOUNT; + uint256 public MIN_STETH_WITHDRAWAL_AMOUNT; + + uint256 internal lastRequestId; + + mapping(address => uint256) balances; + mapping(uint256 => address) owner; + + IStETH public stETH; + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + { + + //summary as nondet } - function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { - uint256 res; - return res; + function transferFrom(address from, address to, uint256 requestId) external { + require (owner[requestId] == from && balances[from] >= 1); + owner[requestId] = to; + balances[from] = balances[from] -1; + balances[to] = balances[to] -1; } - function getLastRequestId() external view returns (uint256) { - uint256 res; - return res; + + function balanceOf(address owner) external view returns (uint256) { + return balances[owner]; } + mapping(uint256 => uint256) amountOfStETH; function getClaimableEther( uint256[] calldata _requestIds, uint256[] calldata _hints ) external view returns (uint256[] memory claimableEthValues) { - uint256[] memory res; - return res; + //summary as nondet + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) external returns (uint256[] memory requestIds) { + for (uint256 i = 0; i < _amounts.length; ++i) { + stETH.transferFrom(msg.sender, address(this), _amounts[i]); + uint256 amountOfShares = stETH.getSharesByPooledEth(_amounts[i]); + requestIds[i] = lastRequestId + 1; + lastRequestId += 1; + //todo - update amountOfStETH + owner[requestIds[i]] = _owner; + } + } + + function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { + for (uint256 i = 0; i < requestIds.length; ++i) { + //todo; + } } + + uint256[] internal hints; + function findCheckpointHints( uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex - ) external view returns (uint256[] memory hintIds) { - uint256[] memory res; - return res; + ) external view returns (uint256[] memory ) { + return hints; } + uint256 lastCheckpointIndex; function getLastCheckpointIndex() external view returns (uint256) { - uint256 res; - return res; + return lastCheckpointIndex; } - function grantRole(bytes32 role, address account) external {} - function pauseFor(uint256 duration) external {} - function isPaused() external returns (bool) { - bool res; - return res; + + + function grantRole(bytes32 role, address account) external { + //unused + assert(false); } + function pauseFor(uint256 duration) external {} - function requestWithdrawalsWstETH(uint256[] calldata amounts, address owner) external returns (uint256[] memory) { - uint256[] memory res; - return res; + function isPaused() external returns (bool b) { + //unused + assert(false); } - function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external {} + function getLastRequestId() external view returns (uint256 r) { + //unused + assert(false); + } - function getLastFinalizedRequestId() external view returns (uint256) { - uint256 res; - return res; + function requestWithdrawalsWstETH(uint256[] calldata amounts, address owner) external returns (uint256[] memory b) { + //unused + assert(false); } - function transferFrom(address from, address to, uint256 requestId) external {} - function getWithdrawalStatus(uint256[] calldata _requestIds) - external - view - returns (WithdrawalRequestStatus[] memory statuses) - {} - function balanceOf(address owner) external view returns (uint256) { - uint256 res; - return res; + function getLastFinalizedRequestId() external view returns (uint256 c) { + //unused + assert(false); } - function requestWithdrawals( - uint256[] calldata _amounts, - address _owner - ) external returns (uint256[] memory requestIds) {} + + + } diff --git a/certora/specs/Escrow.spec b/certora/specs/Escrow.spec new file mode 100644 index 00000000..f34bc456 --- /dev/null +++ b/certora/specs/Escrow.spec @@ -0,0 +1,141 @@ +using DummyStETH as stEth; +using DummyWstETH as wst_eth; + +methods { + function _.getRageQuitSupport() external => DISPATCHER(true); + function _.isRageQuitFinalized() external => DISPATCHER(true); + function _.MASTER_COPY() external => DISPATCHER(true); + function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); + function _.initialize(address) external => DISPATCHER(true); + + //envfree + + function isWithdrawalsBatchesFinalized() external returns (bool) envfree; + + //calls to stEthn and wst_eth from spec + + function DummyStETH.balanceOf(address) external returns(uint256) envfree; + function DummyWstETH.balanceOf(address) external returns(uint256) envfree; + function DummyStETH.getPooledEthByShares(uint256) external returns (uint256) envfree; + + //calls to resealMaanger are from dualGov and unrelated + function _.resume(address sealable) external => NONDET; + function _.reseal(address[] sealables) external => NONDET; + + //calls to timelosck are from dualGov and unrelated + function _.submit(address executor, DualGovernance.ExecutorCall[] calls) external => NONDET; + function _.schedule(uint256 proposalId) external => NONDET; + function _.execute(uint256 proposalId) external => NONDET; + function _.cancelAllNonExecutedProposals() external => NONDET; + + function _.canSchedule(uint256 proposalId) external => NONDET; + function _.canExecute(uint256 proposalId) external => NONDET; + + function _.getProposalSubmissionTime(uint256 proposalId) external => NONDET; +} + +use builtin rule sanity; + + + +/** +@title Rage Quite is a final state of the contract, i.e can not change the state + +**/ +rule rageQuiteFinalState(method f) +{ + Escrow.EscrowState stateBefore = currentContract._escrowState; + + env e; + calldataarg args; + f(e,args); + + Escrow.EscrowState stateAfter = currentContract._escrowState; + + assert stateBefore == Escrow.EscrowState.RageQuitEscrow => + stateAfter == Escrow.EscrowState.RageQuitEscrow; + + +} + +/// @todo rule rageQuitNlockUnlock + + +/** +@title once requestNextWithdrawalsBatch results in batchesQueue.close() all additional calls result in close(); +**/ +rule batchesQueueCloseFinalState(method f){ + + bool startBatchesQueueStatus = isWithdrawalsBatchesFinalized(); + + env eF; + calldataarg argsF; + f(eF,argsF); + + bool nextBatchesQueueStatus = isWithdrawalsBatchesFinalized(); + assert startBatchesQueueStatus => nextBatchesQueueStatus; +} + +//todo - what else should not change be allowed; +/** +@title when ques are closed, no change in batch list. + +checked with mutation on version with issue +https://prover.certora.com/output/40726/dd696d553405430aa40ae244474aa1d0/?anonymousKey=fe11fe659d51d8b9d1c1021a8ec18b9c2e6ab2a9 + +**/ + +rule batchesQueueCloseNochange(method f){ + + bool finialize = isWithdrawalsBatchesFinalized(); + uint256 any; + uint256 before = currentContract._batchesQueue.batches[any]; + + env eF; + calldataarg argsF; + f(eF,argsF); + + assert finialize => before == currentContract._batchesQueue.batches[any]; +} + + +invariant solvency_steth() + stEth.getPooledEthByShares(currentContract._accounting.stETHTotals.lockedShares) <= stEth.balanceOf(currentContract); + + +//todo (assuming no transfer) +// wstEth.balanceOf(currentContract) == 0 ; + +rule solvency_wst_eth_test(method f) +{ + uint256 before = wst_eth.balanceOf(currentContract); + env e; + calldataarg args; + f(e,args); + uint256 after = wst_eth.balanceOf(currentContract); + assert after == before; +} +//todo - StETHAccounting.claimedETH <= nativeBalances[currentContract] +// need to prove sum of balance <= self.stETHTotals.lockedShares + +rule solvency_eth(method f) +{ + uint256 before = nativeBalances[currentContract]; + env e; + calldataarg args; + f(e,args); + uint256 after = nativeBalances[currentContract]; + assert after == before; +} + +rule solvency_st_eth(method f) +{ + uint256 before = stEth.balanceOf(currentContract); + env e; + calldataarg args; + f(e,args); + uint256 after = stEth.balanceOf(currentContract); + assert after == before; +} + +//todo - count of all unstETHIds <= withdrawalQueue.balanaceOf(currentContract) From c2453d27ced29748ccc0e8d4033b9b4baefddf2e Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Mon, 26 Aug 2024 15:39:49 +0200 Subject: [PATCH 19/67] EPT_9 and improvement to EPT_10 --- certora/specs/Timelock.spec | 54 +++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index a6fb83d9..b62b9b79 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -14,7 +14,13 @@ methods { function getAfterScheduleDelay() external returns (Durations.Duration) envfree; // TODO: Improve this to instead resolving the inner unresolved calls to anything in EPT - function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; + function _.execute(address, uint256, bytes) external => nondetBytes() expect bytes; +} + +// somehow, specifying it like this instead of NONDET avoids a revert based on the returned value in EPT_9_EmergencyModeLiveness +function nondetBytes() returns bytes { + bytes b; + return b; } // TODO: maybe we can get rid of the filter if we resolve the unresolved calls inside execute, @@ -35,12 +41,6 @@ rule W1_4_TerminalityOfExecuted(method f) filtered { f -> f.selector != sig:Exec assert getProposal(proposalId).status == ExecutableProposals.Status.Executed; } -// invariant proposalHasSubmissionTimeIfItExists(uint proposalId) getProposal(proposalId).status != ExecutableProposals.Status.NotExist <=> getProposal(proposalId).submittedAt != 0 -// filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { -// preserved { -// requireInvariant outOfBoundsProposalDoesNotExist(proposalId); -// } -// } invariant outOfBoundsProposalDoesNotExist(uint proposalId) proposalId == 0 || proposalId > getProposalsCount() => getProposal(proposalId).status == ExecutableProposals.Status.NotExist filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } {} @@ -171,7 +171,7 @@ rule EPT_3_EmergencyModeExecutionRestriction(method f) filtered { f -> f.selecto @notice The usefullness of this rule depends on us using the effectiveXXXCommittee functions also in all other rules where we check that something is guarded by the committee. */ -rule EPT_5_EmergencyProtectionElapsed(method f) { +rule EPT_5_EmergencyProtectionElapsed() { EmergencyProtection.Context context = getEmergencyProtectionContext(); // protected deployment mode was activated, but not emergency mode require context.emergencyProtectionEndsAfter != 0 && !isEmergencyModeActive(); @@ -183,6 +183,24 @@ rule EPT_5_EmergencyProtectionElapsed(method f) { assert effectiveEmergencyActivationCommittee(e) == 0 && effectiveEmergencyExecutionCommittee(e) == 0; } +/** + @title When emergency mode is active, the emergency execution committee can execute proposals successfully +*/ +rule EPT_9_EmergencyModeLiveness { + require isEmergencyModeActive(); + uint proposalId; + requireInvariant outOfBoundsProposalDoesNotExist(proposalId); + require getProposal(proposalId).status == ExecutableProposals.Status.Scheduled; + env e; + require e.msg.value == 0; + require e.block.timestamp >= getProposal(proposalId).scheduledAt; + require e.block.timestamp < max_uint40; + emergencyExecute@withrevert(e, proposalId); + bool reverted = lastReverted; + + assert e.msg.sender == effectiveEmergencyExecutionCommittee(e) => !reverted; +} + // Helper for EPT_10_ProposalTimestampConsistency because Proposal contains some other not easily comparable data function proposalTimestampsEqual (ITimelock.Proposal a, ITimelock.Proposal b) returns bool { return a.submittedAt == b.submittedAt && a.scheduledAt == b.scheduledAt; @@ -214,6 +232,26 @@ rule EPT_10_ProposalTimestampConsistency(method f) filtered { f -> f.selector != assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) && getProposal(proposalIdToSchedule).scheduledAt == e.block.timestamp; + } else if (f.selector == sig:execute(uint).selector) { + uint proposalId; + ITimelock.Proposal proposal_before = getProposal(proposalId); + uint proposalIdToExecute; + require proposalId != proposalIdToExecute; + execute(e, proposalIdToExecute); + + // for the execution methods we also check that they update the status, since executedAt is not longer included as a timestamp, + // but EPT_3_EmergencyModeExecutionRestriction depends on the execution status being recorded correctly to be meaningful + assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) + && getProposal(proposalIdToExecute).status == ExecutableProposals.Status.Executed; + } else if (f.selector == sig:emergencyExecute(uint).selector) { + uint proposalId; + ITimelock.Proposal proposal_before = getProposal(proposalId); + uint proposalIdToExecute; + require proposalId != proposalIdToExecute; + emergencyExecute(e, proposalIdToExecute); + + assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) + && getProposal(proposalIdToExecute).status == ExecutableProposals.Status.Executed; } else { uint proposalId; ITimelock.Proposal proposal_before = getProposal(proposalId); From 52dff7936b5e8ec09532258e4d4d62f1f355da8c Mon Sep 17 00:00:00 2001 From: Nurit Dor <57101353+nd-certora@users.noreply.github.com> Date: Mon, 26 Aug 2024 23:44:21 +0300 Subject: [PATCH 20/67] fix steth and withdrawlqueue mocks --- certora/harnesses/ERC20Like/DummyStETH.sol | 15 +++--- certora/helpers/DummyWithdrawalQueue.sol | 54 +++++-------------- certora/specs/Escrow.spec | 60 ++++++++++++++++++---- 3 files changed, 69 insertions(+), 60 deletions(-) diff --git a/certora/harnesses/ERC20Like/DummyStETH.sol b/certora/harnesses/ERC20Like/DummyStETH.sol index 523b6130..020b6ebd 100644 --- a/certora/harnesses/ERC20Like/DummyStETH.sol +++ b/certora/harnesses/ERC20Like/DummyStETH.sol @@ -12,11 +12,11 @@ contract DummyStETH is IStETH { } function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { - return ethAmount * 5 / 3; + return ethAmount * 3 / 5; } function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256) { - return sharesAmount * 3 / 5; + return sharesAmount * 5 / 3; } function transferShares(address to, uint256 amount) external { @@ -31,9 +31,8 @@ contract DummyStETH is IStETH { address _recipient, uint256 _sharesAmount ) external returns (uint256) { - // uint256 tokensAmount = getPooledEthByShares(_sharesAmount); - uint256 tokensAmount = _sharesAmount * 3 / 5; - _spendAllowance(_sender, msg.sender, tokensAmount); + uint256 tokensAmount = _sharesAmount * 5 / 3; + _spendAllowance(_sender, msg.sender, _sharesAmount); _transferShares(_sender, _recipient, _sharesAmount); return tokensAmount; } @@ -62,7 +61,7 @@ contract DummyStETH is IStETH { } function totalSupply() external view returns (uint256) { - return totalShares * 3 / 5; + return totalShares * 5 / 3; } function approve(address _spender, uint256 _amount) external returns (bool) { @@ -76,7 +75,7 @@ contract DummyStETH is IStETH { function balanceOf(address _account) external view returns (uint256) { // return getPooledEthByShares(_sharesOf(_account)); - return _sharesOf(_account) * 3 / 5; + return _sharesOf(_account) * 5 / 3; } function _sharesOf(address account) internal view returns (uint256) { @@ -84,7 +83,7 @@ contract DummyStETH is IStETH { } function _transfer(address sender, address recipient, uint256 amount) internal { - uint256 sharesToTransfer = amount * 5 / 3; + uint256 sharesToTransfer = amount * 3 / 5; _transferShares(sender, recipient, sharesToTransfer); } diff --git a/certora/helpers/DummyWithdrawalQueue.sol b/certora/helpers/DummyWithdrawalQueue.sol index 3578388e..e6aaa25c 100644 --- a/certora/helpers/DummyWithdrawalQueue.sol +++ b/certora/helpers/DummyWithdrawalQueue.sol @@ -1,12 +1,11 @@ pragma solidity ^0.8.26; -import {IWithdrawalQueue, WithdrawalRequestStatus} from "../../contracts/interfaces/IWithdrawalQueue.sol"; import "../../contracts/interfaces/IStETH.sol"; -// This implementation is only mock which will is later summarised by NONDET and HAVOC summary -contract DummyWithdrawalQueue is IWithdrawalQueue { +// This implementation is only mock for ESCROW contract +contract DummyWithdrawalQueue { // The Prover will assume a contant but random value; uint256 public MAX_STETH_WITHDRAWAL_AMOUNT; @@ -18,7 +17,7 @@ contract DummyWithdrawalQueue is IWithdrawalQueue { mapping(uint256 => address) owner; IStETH public stETH; - +/* function getWithdrawalStatus(uint256[] calldata _requestIds) external view @@ -27,7 +26,7 @@ contract DummyWithdrawalQueue is IWithdrawalQueue { //summary as nondet } - +*/ function transferFrom(address from, address to, uint256 requestId) external { require (owner[requestId] == from && balances[from] >= 1); owner[requestId] = to; @@ -40,24 +39,28 @@ contract DummyWithdrawalQueue is IWithdrawalQueue { return balances[owner]; } - mapping(uint256 => uint256) amountOfStETH; + mapping(uint256 => uint256) amountOfETH; function getClaimableEther( uint256[] calldata _requestIds, uint256[] calldata _hints ) external view returns (uint256[] memory claimableEthValues) { - //summary as nondet + claimableEthValues = new uint256[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + claimableEthValues[i] = amountOfETH[_requestIds[i]]; + } } function requestWithdrawals( uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds) { + requestIds = new uint256[](_amounts.length); for (uint256 i = 0; i < _amounts.length; ++i) { stETH.transferFrom(msg.sender, address(this), _amounts[i]); uint256 amountOfShares = stETH.getSharesByPooledEth(_amounts[i]); requestIds[i] = lastRequestId + 1; lastRequestId += 1; - //todo - update amountOfStETH + amountOfETH[requestIds[i]] = _amounts[i]; owner[requestIds[i]] = _owner; } } @@ -65,6 +68,8 @@ contract DummyWithdrawalQueue is IWithdrawalQueue { function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { for (uint256 i = 0; i < requestIds.length; ++i) { //todo; + (bool success,) = msg.sender.call{value: amountOfETH[requestIds[i]]}(""); + require(success); } } @@ -85,37 +90,4 @@ contract DummyWithdrawalQueue is IWithdrawalQueue { } - - - function grantRole(bytes32 role, address account) external { - //unused - assert(false); - } - function pauseFor(uint256 duration) external {} - - function isPaused() external returns (bool b) { - //unused - assert(false); - } - - function getLastRequestId() external view returns (uint256 r) { - //unused - assert(false); - } - - function requestWithdrawalsWstETH(uint256[] calldata amounts, address owner) external returns (uint256[] memory b) { - //unused - assert(false); - } - - - - function getLastFinalizedRequestId() external view returns (uint256 c) { - //unused - assert(false); - } - - - - } diff --git a/certora/specs/Escrow.spec b/certora/specs/Escrow.spec index f34bc456..bba02ca8 100644 --- a/certora/specs/Escrow.spec +++ b/certora/specs/Escrow.spec @@ -14,6 +14,9 @@ methods { //calls to stEthn and wst_eth from spec + + function DummyStETH.getTotalShares() external returns(uint256) envfree; + function DummyStETH.totalSupply() external returns(uint256) envfree; function DummyStETH.balanceOf(address) external returns(uint256) envfree; function DummyWstETH.balanceOf(address) external returns(uint256) envfree; function DummyStETH.getPooledEthByShares(uint256) external returns (uint256) envfree; @@ -32,12 +35,14 @@ methods { function _.canExecute(uint256 proposalId) external => NONDET; function _.getProposalSubmissionTime(uint256 proposalId) external => NONDET; + + + function _.getWithdrawalStatus(uint256[] _requestIds) external => NONDET; } use builtin rule sanity; - /** @title Rage Quite is a final state of the contract, i.e can not change the state @@ -78,16 +83,16 @@ rule batchesQueueCloseFinalState(method f){ //todo - what else should not change be allowed; /** -@title when ques are closed, no change in batch list. +@title when queues are closed, no change in batch list. checked with mutation on version with issue https://prover.certora.com/output/40726/dd696d553405430aa40ae244474aa1d0/?anonymousKey=fe11fe659d51d8b9d1c1021a8ec18b9c2e6ab2a9 **/ -rule batchesQueueCloseNochange(method f){ +rule batchesQueueCloseNoChange(method f){ - bool finialize = isWithdrawalsBatchesFinalized(); + bool finalize = isWithdrawalsBatchesFinalized(); uint256 any; uint256 before = currentContract._batchesQueue.batches[any]; @@ -95,16 +100,47 @@ rule batchesQueueCloseNochange(method f){ calldataarg argsF; f(eF,argsF); - assert finialize => before == currentContract._batchesQueue.batches[any]; + assert finalize => before == currentContract._batchesQueue.batches[any]; +} + + +invariant solvency_stETH() + stEth.getPooledEthByShares(currentContract._accounting.stETHTotals.lockedShares) <= stEth.balanceOf(currentContract) + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + } } -invariant solvency_steth() - stEth.getPooledEthByShares(currentContract._accounting.stETHTotals.lockedShares) <= stEth.balanceOf(currentContract); +ghost mathint sumStETHLockedShares{ + // assuming value zero at the initial state before constructor + init_state axiom sumStETHLockedShares == 0; +} + + +/* updated sumStETHLockedShares according to the change of a single account */ +hook Sstore currentContract._accounting.assets[KEY address a].stETHLockedShares Escrow.SharesValue new_balance +// the old value that balances[a] holds before the store + (Escrow.SharesValue old_balance) { + sumStETHLockedShares = sumStETHLockedShares + new_balance - old_balance; +} + +invariant totalLockedShares() + sumStETHLockedShares == currentContract._accounting.stETHTotals.lockedShares; + +invariant solvency_claimedETH() + currentContract._accounting.stETHTotals.claimedETH <= nativeBalances[currentContract] + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + } +} + -//todo (assuming no transfer) -// wstEth.balanceOf(currentContract) == 0 ; rule solvency_wst_eth_test(method f) { @@ -115,10 +151,12 @@ rule solvency_wst_eth_test(method f) uint256 after = wst_eth.balanceOf(currentContract); assert after == before; } + + //todo - StETHAccounting.claimedETH <= nativeBalances[currentContract] // need to prove sum of balance <= self.stETHTotals.lockedShares -rule solvency_eth(method f) +rule change_eth(method f) { uint256 before = nativeBalances[currentContract]; env e; @@ -128,7 +166,7 @@ rule solvency_eth(method f) assert after == before; } -rule solvency_st_eth(method f) +rule change_st_eth(method f) { uint256 before = stEth.balanceOf(currentContract); env e; From d0d9e7978c14c004910d6329905ffc979f49c85b Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Wed, 28 Aug 2024 13:06:46 +0200 Subject: [PATCH 21/67] EPT_11 --- certora/specs/Timelock.spec | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index b62b9b79..dcb3294c 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -261,4 +261,19 @@ rule EPT_10_ProposalTimestampConsistency(method f) filtered { f -> f.selector != assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)); } +} + +/** + @title Cancelled is a terminal state for a proposal, once cancelled it cannot transition to any other state +*/ +rule EPT_11_TerminalityOfCancelled(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { + uint proposalId; + requireInvariant outOfBoundsProposalDoesNotExist(proposalId); + require getProposal(proposalId).status == ExecutableProposals.Status.Cancelled; + + env e; + calldataarg args; + f(e, args); + + assert getProposal(proposalId).status == ExecutableProposals.Status.Cancelled; } \ No newline at end of file From 67c7789a9ad66a61cfe60a0e9769a755c23a284e Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Wed, 28 Aug 2024 18:23:36 +0200 Subject: [PATCH 22/67] mutants for EPT --- .../conf/EmergencyProtectedTimelock.conf | 41 +++ ...edTimelockEmergencyExecuteGuardMissing.sol | 289 +++++++++++++++++ ...gencyExecuteWrongCheckForEmergencyMode.sol | 290 ++++++++++++++++++ ...yProtectedTimelockScheduleGuardMissing.sol | 289 +++++++++++++++++ ...ncyProtectedTimelockSubmitGuardMissing.sol | 289 +++++++++++++++++ ...ModeActivationMissesProtectedModeCheck.sol | 185 +++++++++++ ...tableProposalsExecutionDelayCheckBuggy.sol | 224 ++++++++++++++ ...utableProposalsScheduleDelayCheckBuggy.sol | 225 ++++++++++++++ ...posalsScheduleMissingCancellationCheck.sol | 222 ++++++++++++++ ...ableProposalsSubmissionTimestampNotSet.sol | 223 ++++++++++++++ 10 files changed, 2277 insertions(+) create mode 100644 certora/mutation/conf/EmergencyProtectedTimelock.conf create mode 100644 certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteGuardMissing.sol create mode 100644 certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteWrongCheckForEmergencyMode.sol create mode 100644 certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockScheduleGuardMissing.sol create mode 100644 certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockSubmitGuardMissing.sol create mode 100644 certora/mutation/mutants/EmergencyProtection/EmergencyProtectionEmergencyModeActivationMissesProtectedModeCheck.sol create mode 100644 certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecutionDelayCheckBuggy.sol create mode 100644 certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleDelayCheckBuggy.sol create mode 100644 certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol create mode 100644 certora/mutation/mutants/ExecutableProposals/ExecutableProposalsSubmissionTimestampNotSet.sol diff --git a/certora/mutation/conf/EmergencyProtectedTimelock.conf b/certora/mutation/conf/EmergencyProtectedTimelock.conf new file mode 100644 index 00000000..7cc92eb8 --- /dev/null +++ b/certora/mutation/conf/EmergencyProtectedTimelock.conf @@ -0,0 +1,41 @@ +{ + "files": [ + "contracts/EmergencyProtectedTimelock.sol", + "contracts/Executor.sol", + "contracts/libraries/ExecutableProposals.sol", + "contracts/libraries/EmergencyProtection.sol", + "contracts/types/Timestamp.sol:Timestamps", + "contracts/types/Duration.sol:Durations" + ], + "link": [], + "struct_link": [ + "EmergencyProtectedTimelock:executor=Executor", + ], + "msg": "Emergency Protected Timelock", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "solc": "solc8.26", + "optimistic_loop": true, + "solc_via_ir": false, + "verify": "EmergencyProtectedTimelock:certora/specs/Timelock.spec", + "server": "production", + // mutation options below this line + "mutations": { + "manual_mutants": [ + { + "file_to_mutate": "contracts/libraries/ExecutableProposals.sol", + "mutants_location": "certora/mutation/mutants/ExecutableProposals" + }, + { + "file_to_mutate": "contracts/libraries/EmergencyProtection.sol", + "mutants_location": "certora/mutation/mutants/EmergencyProtection" + }, + { + "file_to_mutate": "contracts/EmergencyProtectedTimelock.sol", + "mutants_location": "certora/mutation/mutants/EmergencyProtectedTimelock" + }, + ] + } +} \ No newline at end of file diff --git a/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteGuardMissing.sol b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteGuardMissing.sol new file mode 100644 index 00000000..79b393f4 --- /dev/null +++ b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteGuardMissing.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; + +import {IOwnable} from "./interfaces/IOwnable.sol"; +import {ITimelock, ProposalStatus} from "./interfaces/ITimelock.sol"; + +import {TimelockState} from "./libraries/TimelockState.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {ExecutableProposals} from "./libraries/ExecutableProposals.sol"; +import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; + +/// @title EmergencyProtectedTimelock +/// @dev A timelock contract with emergency protection functionality. +/// The contract allows for submitting, scheduling, and executing proposals, +/// while providing emergency protection features to prevent unauthorized +/// execution during emergency situations. +contract EmergencyProtectedTimelock is ITimelock { + using TimelockState for TimelockState.Context; + using ExecutableProposals for ExecutableProposals.Context; + using EmergencyProtection for EmergencyProtection.Context; + + error CallerIsNotAdminExecutor(address value); + + // --- + // Sanity Check Params Immutables + // --- + struct SanityCheckParams { + Duration maxAfterSubmitDelay; + Duration maxAfterScheduleDelay; + Duration maxEmergencyModeDuration; + Duration maxEmergencyProtectionDuration; + } + + Duration public immutable MAX_AFTER_SUBMIT_DELAY; + Duration public immutable MAX_AFTER_SCHEDULE_DELAY; + + Duration public immutable MAX_EMERGENCY_MODE_DURATION; + Duration public immutable MAX_EMERGENCY_PROTECTION_DURATION; + + // --- + // Admin Executor Immutables + // --- + + address private immutable _ADMIN_EXECUTOR; + + // --- + // Aspects + // --- + + TimelockState.Context internal _timelockState; + ExecutableProposals.Context internal _proposals; + EmergencyProtection.Context internal _emergencyProtection; + + constructor(SanityCheckParams memory sanityCheckParams, address adminExecutor) { + _ADMIN_EXECUTOR = adminExecutor; + + MAX_AFTER_SUBMIT_DELAY = sanityCheckParams.maxAfterSubmitDelay; + MAX_AFTER_SCHEDULE_DELAY = sanityCheckParams.maxAfterScheduleDelay; + MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; + MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyModeDuration; + } + + // --- + // Main Timelock Functionality + // --- + + /// @dev Submits a new proposal to execute a series of calls through an executor. + /// Only the governance contract can call this function. + /// @param executor The address of the executor contract that will execute the calls. + /// @param calls An array of `ExternalCall` structs representing the calls to be executed. + /// @return newProposalId The ID of the newly created proposal. + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { + _timelockState.checkCallerIsGovernance(); + newProposalId = _proposals.submit(executor, calls); + } + + /// @dev Schedules a proposal for execution after a specified delay. + /// Only the governance contract can call this function. + /// @param proposalId The ID of the proposal to be scheduled. + function schedule(uint256 proposalId) external { + _timelockState.checkCallerIsGovernance(); + _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + /// @dev Executes a scheduled proposal. + /// Checks if emergency mode is active and prevents execution if it is. + /// @param proposalId The ID of the proposal to be executed. + function execute(uint256 proposalId) external { + _emergencyProtection.checkEmergencyMode({isActive: false}); + _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Cancels all non-executed proposals. + /// Only the governance contract can call this function. + function cancelAllNonExecutedProposals() external { + _timelockState.checkCallerIsGovernance(); + _proposals.cancelAll(); + } + + // --- + // Timelock Management + // --- + + function setGovernance(address newGovernance) external { + _checkCallerIsAdminExecutor(); + _timelockState.setGovernance(newGovernance); + } + + function setDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + _checkCallerIsAdminExecutor(); + _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); + _timelockState.setAfterScheduleDelay(afterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); + } + + /// @dev Transfers ownership of the executor contract to a new owner. + /// Only the admin executor can call this function. + /// @param executor The address of the executor contract. + /// @param owner The address of the new owner. + function transferExecutorOwnership(address executor, address owner) external { + _checkCallerIsAdminExecutor(); + IOwnable(executor).transferOwnership(owner); + } + + // --- + // Emergency Protection Functionality + // --- + + function setupEmergencyProtection( + address emergencyGovernance, + address emergencyActivationCommittee, + address emergencyExecutionCommittee, + Timestamp emergencyProtectionEndDate, + Duration emergencyModeDuration + ) external { + _checkCallerIsAdminExecutor(); + + _emergencyProtection.setEmergencyGovernance(emergencyGovernance); + _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + _emergencyProtection.setEmergencyProtectionEndDate( + emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION + ); + _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); + _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + } + + /// @dev Activates the emergency mode. + /// Only the activation committee can call this function. + function activateEmergencyMode() external { + _emergencyProtection.checkCallerIsEmergencyActivationCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: false}); + _emergencyProtection.activateEmergencyMode(); + } + + /// @dev Executes a proposal during emergency mode. + /// Checks if emergency mode is active and if the caller is part of the execution committee. + /// @param proposalId The ID of the proposal to be executed. + function emergencyExecute(uint256 proposalId) external { + _emergencyProtection.checkEmergencyMode({isActive: true}); + // mutated + //_emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _proposals.execute({proposalId: proposalId, afterScheduleDelay: Duration.wrap(0)}); + } + + /// @dev Deactivates the emergency mode. + /// If the emergency mode has not passed, only the admin executor can call this function. + function deactivateEmergencyMode() external { + _emergencyProtection.checkEmergencyMode({isActive: true}); + if (!_emergencyProtection.isEmergencyModeDurationPassed()) { + _checkCallerIsAdminExecutor(); + } + _emergencyProtection.deactivateEmergencyMode(); + _proposals.cancelAll(); + } + + /// @dev Resets the system after entering the emergency mode. + /// Only the execution committee can call this function. + function emergencyReset() external { + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.deactivateEmergencyMode(); + + _timelockState.setGovernance(_emergencyProtection.emergencyGovernance); + _proposals.cancelAll(); + } + + function getEmergencyProtectionContext() external view returns (EmergencyProtection.Context memory) { + return _emergencyProtection; + } + + function isEmergencyProtectionEnabled() public view returns (bool) { + return _emergencyProtection.isEmergencyProtectionEnabled(); + } + + function isEmergencyModeActive() public view returns (bool isActive) { + isActive = _emergencyProtection.isEmergencyModeActive(); + } + + // --- + // Timelock View Methods + // --- + + function getGovernance() external view returns (address) { + return _timelockState.governance; + } + + function getAdminExecutor() external view returns (address) { + return _ADMIN_EXECUTOR; + } + + function getAfterSubmitDelay() external view returns (Duration) { + return _timelockState.getAfterSubmitDelay(); + } + + function getAfterScheduleDelay() external view returns (Duration) { + return _timelockState.getAfterScheduleDelay(); + } + + /// @dev Retrieves the details of a proposal. + /// @param proposalId The ID of the proposal. + /// @return proposal The Proposal struct containing the details of the proposal. + function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { + proposal.id = proposalId; + (proposal.status, proposal.executor, proposal.submittedAt, proposal.scheduledAt) = + _proposals.getProposalInfo(proposalId); + proposal.calls = _proposals.getProposalCalls(proposalId); + } + + /// @notice Retrieves information about a proposal, excluding the external calls associated with it. + /// @param proposalId The ID of the proposal to retrieve information for. + /// @return id The ID of the proposal. + /// @return status The current status of the proposal. Possible values are: + /// 0 - The proposal does not exist. + /// 1 - The proposal was submitted but not scheduled. + /// 2 - The proposal was submitted and scheduled but not yet executed. + /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. + /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. + /// This is the final state of the proposal. + /// @return executor The address of the executor responsible for executing the proposal's external calls. + /// @return submittedAt The timestamp when the proposal was submitted. + /// @return scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal + /// was submitted but not yet scheduled. + function getProposalInfo( + uint256 proposalId + ) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) + { + id = proposalId; + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + } + + /// @notice Retrieves the external calls associated with the specified proposal. + /// @param proposalId The ID of the proposal to retrieve external calls for. + /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + calls = _proposals.getProposalCalls(proposalId); + } + + /// @dev Retrieves the total number of proposals. + /// @return count The total number of proposals. + function getProposalsCount() external view returns (uint256 count) { + count = _proposals.getProposalsCount(); + } + + /// @dev Checks if a proposal can be executed. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be executed. + function canExecute(uint256 proposalId) external view returns (bool) { + return !_emergencyProtection.isEmergencyModeActive() + && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Checks if a proposal can be scheduled. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be scheduled. + function canSchedule(uint256 proposalId) external view returns (bool) { + return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + function _checkCallerIsAdminExecutor() internal view { + if (msg.sender != _ADMIN_EXECUTOR) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteWrongCheckForEmergencyMode.sol b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteWrongCheckForEmergencyMode.sol new file mode 100644 index 00000000..a4183b43 --- /dev/null +++ b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockEmergencyExecuteWrongCheckForEmergencyMode.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; + +import {IOwnable} from "./interfaces/IOwnable.sol"; +import {ITimelock, ProposalStatus} from "./interfaces/ITimelock.sol"; + +import {TimelockState} from "./libraries/TimelockState.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {ExecutableProposals} from "./libraries/ExecutableProposals.sol"; +import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; + +/// @title EmergencyProtectedTimelock +/// @dev A timelock contract with emergency protection functionality. +/// The contract allows for submitting, scheduling, and executing proposals, +/// while providing emergency protection features to prevent unauthorized +/// execution during emergency situations. +contract EmergencyProtectedTimelock is ITimelock { + using TimelockState for TimelockState.Context; + using ExecutableProposals for ExecutableProposals.Context; + using EmergencyProtection for EmergencyProtection.Context; + + error CallerIsNotAdminExecutor(address value); + + // --- + // Sanity Check Params Immutables + // --- + struct SanityCheckParams { + Duration maxAfterSubmitDelay; + Duration maxAfterScheduleDelay; + Duration maxEmergencyModeDuration; + Duration maxEmergencyProtectionDuration; + } + + Duration public immutable MAX_AFTER_SUBMIT_DELAY; + Duration public immutable MAX_AFTER_SCHEDULE_DELAY; + + Duration public immutable MAX_EMERGENCY_MODE_DURATION; + Duration public immutable MAX_EMERGENCY_PROTECTION_DURATION; + + // --- + // Admin Executor Immutables + // --- + + address private immutable _ADMIN_EXECUTOR; + + // --- + // Aspects + // --- + + TimelockState.Context internal _timelockState; + ExecutableProposals.Context internal _proposals; + EmergencyProtection.Context internal _emergencyProtection; + + constructor(SanityCheckParams memory sanityCheckParams, address adminExecutor) { + _ADMIN_EXECUTOR = adminExecutor; + + MAX_AFTER_SUBMIT_DELAY = sanityCheckParams.maxAfterSubmitDelay; + MAX_AFTER_SCHEDULE_DELAY = sanityCheckParams.maxAfterScheduleDelay; + MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; + MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyModeDuration; + } + + // --- + // Main Timelock Functionality + // --- + + /// @dev Submits a new proposal to execute a series of calls through an executor. + /// Only the governance contract can call this function. + /// @param executor The address of the executor contract that will execute the calls. + /// @param calls An array of `ExternalCall` structs representing the calls to be executed. + /// @return newProposalId The ID of the newly created proposal. + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { + _timelockState.checkCallerIsGovernance(); + newProposalId = _proposals.submit(executor, calls); + } + + /// @dev Schedules a proposal for execution after a specified delay. + /// Only the governance contract can call this function. + /// @param proposalId The ID of the proposal to be scheduled. + function schedule(uint256 proposalId) external { + _timelockState.checkCallerIsGovernance(); + _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + /// @dev Executes a scheduled proposal. + /// Checks if emergency mode is active and prevents execution if it is. + /// @param proposalId The ID of the proposal to be executed. + function execute(uint256 proposalId) external { + _emergencyProtection.checkEmergencyMode({isActive: false}); + _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Cancels all non-executed proposals. + /// Only the governance contract can call this function. + function cancelAllNonExecutedProposals() external { + _timelockState.checkCallerIsGovernance(); + _proposals.cancelAll(); + } + + // --- + // Timelock Management + // --- + + function setGovernance(address newGovernance) external { + _checkCallerIsAdminExecutor(); + _timelockState.setGovernance(newGovernance); + } + + function setDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + _checkCallerIsAdminExecutor(); + _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); + _timelockState.setAfterScheduleDelay(afterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); + } + + /// @dev Transfers ownership of the executor contract to a new owner. + /// Only the admin executor can call this function. + /// @param executor The address of the executor contract. + /// @param owner The address of the new owner. + function transferExecutorOwnership(address executor, address owner) external { + _checkCallerIsAdminExecutor(); + IOwnable(executor).transferOwnership(owner); + } + + // --- + // Emergency Protection Functionality + // --- + + function setupEmergencyProtection( + address emergencyGovernance, + address emergencyActivationCommittee, + address emergencyExecutionCommittee, + Timestamp emergencyProtectionEndDate, + Duration emergencyModeDuration + ) external { + _checkCallerIsAdminExecutor(); + + _emergencyProtection.setEmergencyGovernance(emergencyGovernance); + _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + _emergencyProtection.setEmergencyProtectionEndDate( + emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION + ); + _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); + _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + } + + /// @dev Activates the emergency mode. + /// Only the activation committee can call this function. + function activateEmergencyMode() external { + _emergencyProtection.checkCallerIsEmergencyActivationCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: false}); + _emergencyProtection.activateEmergencyMode(); + } + + /// @dev Executes a proposal during emergency mode. + /// Checks if emergency mode is active and if the caller is part of the execution committee. + /// @param proposalId The ID of the proposal to be executed. + function emergencyExecute(uint256 proposalId) external { + // mutated + _emergencyProtection.checkEmergencyMode({isActive: false}); + //_emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _proposals.execute({proposalId: proposalId, afterScheduleDelay: Duration.wrap(0)}); + } + + /// @dev Deactivates the emergency mode. + /// If the emergency mode has not passed, only the admin executor can call this function. + function deactivateEmergencyMode() external { + _emergencyProtection.checkEmergencyMode({isActive: true}); + if (!_emergencyProtection.isEmergencyModeDurationPassed()) { + _checkCallerIsAdminExecutor(); + } + _emergencyProtection.deactivateEmergencyMode(); + _proposals.cancelAll(); + } + + /// @dev Resets the system after entering the emergency mode. + /// Only the execution committee can call this function. + function emergencyReset() external { + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.deactivateEmergencyMode(); + + _timelockState.setGovernance(_emergencyProtection.emergencyGovernance); + _proposals.cancelAll(); + } + + function getEmergencyProtectionContext() external view returns (EmergencyProtection.Context memory) { + return _emergencyProtection; + } + + function isEmergencyProtectionEnabled() public view returns (bool) { + return _emergencyProtection.isEmergencyProtectionEnabled(); + } + + function isEmergencyModeActive() public view returns (bool isActive) { + isActive = _emergencyProtection.isEmergencyModeActive(); + } + + // --- + // Timelock View Methods + // --- + + function getGovernance() external view returns (address) { + return _timelockState.governance; + } + + function getAdminExecutor() external view returns (address) { + return _ADMIN_EXECUTOR; + } + + function getAfterSubmitDelay() external view returns (Duration) { + return _timelockState.getAfterSubmitDelay(); + } + + function getAfterScheduleDelay() external view returns (Duration) { + return _timelockState.getAfterScheduleDelay(); + } + + /// @dev Retrieves the details of a proposal. + /// @param proposalId The ID of the proposal. + /// @return proposal The Proposal struct containing the details of the proposal. + function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { + proposal.id = proposalId; + (proposal.status, proposal.executor, proposal.submittedAt, proposal.scheduledAt) = + _proposals.getProposalInfo(proposalId); + proposal.calls = _proposals.getProposalCalls(proposalId); + } + + /// @notice Retrieves information about a proposal, excluding the external calls associated with it. + /// @param proposalId The ID of the proposal to retrieve information for. + /// @return id The ID of the proposal. + /// @return status The current status of the proposal. Possible values are: + /// 0 - The proposal does not exist. + /// 1 - The proposal was submitted but not scheduled. + /// 2 - The proposal was submitted and scheduled but not yet executed. + /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. + /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. + /// This is the final state of the proposal. + /// @return executor The address of the executor responsible for executing the proposal's external calls. + /// @return submittedAt The timestamp when the proposal was submitted. + /// @return scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal + /// was submitted but not yet scheduled. + function getProposalInfo( + uint256 proposalId + ) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) + { + id = proposalId; + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + } + + /// @notice Retrieves the external calls associated with the specified proposal. + /// @param proposalId The ID of the proposal to retrieve external calls for. + /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + calls = _proposals.getProposalCalls(proposalId); + } + + /// @dev Retrieves the total number of proposals. + /// @return count The total number of proposals. + function getProposalsCount() external view returns (uint256 count) { + count = _proposals.getProposalsCount(); + } + + /// @dev Checks if a proposal can be executed. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be executed. + function canExecute(uint256 proposalId) external view returns (bool) { + return !_emergencyProtection.isEmergencyModeActive() + && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Checks if a proposal can be scheduled. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be scheduled. + function canSchedule(uint256 proposalId) external view returns (bool) { + return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + function _checkCallerIsAdminExecutor() internal view { + if (msg.sender != _ADMIN_EXECUTOR) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockScheduleGuardMissing.sol b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockScheduleGuardMissing.sol new file mode 100644 index 00000000..39ee0b08 --- /dev/null +++ b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockScheduleGuardMissing.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; + +import {IOwnable} from "./interfaces/IOwnable.sol"; +import {ITimelock, ProposalStatus} from "./interfaces/ITimelock.sol"; + +import {TimelockState} from "./libraries/TimelockState.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {ExecutableProposals} from "./libraries/ExecutableProposals.sol"; +import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; + +/// @title EmergencyProtectedTimelock +/// @dev A timelock contract with emergency protection functionality. +/// The contract allows for submitting, scheduling, and executing proposals, +/// while providing emergency protection features to prevent unauthorized +/// execution during emergency situations. +contract EmergencyProtectedTimelock is ITimelock { + using TimelockState for TimelockState.Context; + using ExecutableProposals for ExecutableProposals.Context; + using EmergencyProtection for EmergencyProtection.Context; + + error CallerIsNotAdminExecutor(address value); + + // --- + // Sanity Check Params Immutables + // --- + struct SanityCheckParams { + Duration maxAfterSubmitDelay; + Duration maxAfterScheduleDelay; + Duration maxEmergencyModeDuration; + Duration maxEmergencyProtectionDuration; + } + + Duration public immutable MAX_AFTER_SUBMIT_DELAY; + Duration public immutable MAX_AFTER_SCHEDULE_DELAY; + + Duration public immutable MAX_EMERGENCY_MODE_DURATION; + Duration public immutable MAX_EMERGENCY_PROTECTION_DURATION; + + // --- + // Admin Executor Immutables + // --- + + address private immutable _ADMIN_EXECUTOR; + + // --- + // Aspects + // --- + + TimelockState.Context internal _timelockState; + ExecutableProposals.Context internal _proposals; + EmergencyProtection.Context internal _emergencyProtection; + + constructor(SanityCheckParams memory sanityCheckParams, address adminExecutor) { + _ADMIN_EXECUTOR = adminExecutor; + + MAX_AFTER_SUBMIT_DELAY = sanityCheckParams.maxAfterSubmitDelay; + MAX_AFTER_SCHEDULE_DELAY = sanityCheckParams.maxAfterScheduleDelay; + MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; + MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyModeDuration; + } + + // --- + // Main Timelock Functionality + // --- + + /// @dev Submits a new proposal to execute a series of calls through an executor. + /// Only the governance contract can call this function. + /// @param executor The address of the executor contract that will execute the calls. + /// @param calls An array of `ExternalCall` structs representing the calls to be executed. + /// @return newProposalId The ID of the newly created proposal. + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { + _timelockState.checkCallerIsGovernance(); + newProposalId = _proposals.submit(executor, calls); + } + + /// @dev Schedules a proposal for execution after a specified delay. + /// Only the governance contract can call this function. + /// @param proposalId The ID of the proposal to be scheduled. + function schedule(uint256 proposalId) external { + // mutated + //_timelockState.checkCallerIsGovernance(); + _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + /// @dev Executes a scheduled proposal. + /// Checks if emergency mode is active and prevents execution if it is. + /// @param proposalId The ID of the proposal to be executed. + function execute(uint256 proposalId) external { + _emergencyProtection.checkEmergencyMode({isActive: false}); + _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Cancels all non-executed proposals. + /// Only the governance contract can call this function. + function cancelAllNonExecutedProposals() external { + _timelockState.checkCallerIsGovernance(); + _proposals.cancelAll(); + } + + // --- + // Timelock Management + // --- + + function setGovernance(address newGovernance) external { + _checkCallerIsAdminExecutor(); + _timelockState.setGovernance(newGovernance); + } + + function setDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + _checkCallerIsAdminExecutor(); + _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); + _timelockState.setAfterScheduleDelay(afterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); + } + + /// @dev Transfers ownership of the executor contract to a new owner. + /// Only the admin executor can call this function. + /// @param executor The address of the executor contract. + /// @param owner The address of the new owner. + function transferExecutorOwnership(address executor, address owner) external { + _checkCallerIsAdminExecutor(); + IOwnable(executor).transferOwnership(owner); + } + + // --- + // Emergency Protection Functionality + // --- + + function setupEmergencyProtection( + address emergencyGovernance, + address emergencyActivationCommittee, + address emergencyExecutionCommittee, + Timestamp emergencyProtectionEndDate, + Duration emergencyModeDuration + ) external { + _checkCallerIsAdminExecutor(); + + _emergencyProtection.setEmergencyGovernance(emergencyGovernance); + _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + _emergencyProtection.setEmergencyProtectionEndDate( + emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION + ); + _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); + _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + } + + /// @dev Activates the emergency mode. + /// Only the activation committee can call this function. + function activateEmergencyMode() external { + _emergencyProtection.checkCallerIsEmergencyActivationCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: false}); + _emergencyProtection.activateEmergencyMode(); + } + + /// @dev Executes a proposal during emergency mode. + /// Checks if emergency mode is active and if the caller is part of the execution committee. + /// @param proposalId The ID of the proposal to be executed. + function emergencyExecute(uint256 proposalId) external { + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _proposals.execute({proposalId: proposalId, afterScheduleDelay: Duration.wrap(0)}); + } + + /// @dev Deactivates the emergency mode. + /// If the emergency mode has not passed, only the admin executor can call this function. + function deactivateEmergencyMode() external { + _emergencyProtection.checkEmergencyMode({isActive: true}); + if (!_emergencyProtection.isEmergencyModeDurationPassed()) { + _checkCallerIsAdminExecutor(); + } + _emergencyProtection.deactivateEmergencyMode(); + _proposals.cancelAll(); + } + + /// @dev Resets the system after entering the emergency mode. + /// Only the execution committee can call this function. + function emergencyReset() external { + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.deactivateEmergencyMode(); + + _timelockState.setGovernance(_emergencyProtection.emergencyGovernance); + _proposals.cancelAll(); + } + + function getEmergencyProtectionContext() external view returns (EmergencyProtection.Context memory) { + return _emergencyProtection; + } + + function isEmergencyProtectionEnabled() public view returns (bool) { + return _emergencyProtection.isEmergencyProtectionEnabled(); + } + + function isEmergencyModeActive() public view returns (bool isActive) { + isActive = _emergencyProtection.isEmergencyModeActive(); + } + + // --- + // Timelock View Methods + // --- + + function getGovernance() external view returns (address) { + return _timelockState.governance; + } + + function getAdminExecutor() external view returns (address) { + return _ADMIN_EXECUTOR; + } + + function getAfterSubmitDelay() external view returns (Duration) { + return _timelockState.getAfterSubmitDelay(); + } + + function getAfterScheduleDelay() external view returns (Duration) { + return _timelockState.getAfterScheduleDelay(); + } + + /// @dev Retrieves the details of a proposal. + /// @param proposalId The ID of the proposal. + /// @return proposal The Proposal struct containing the details of the proposal. + function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { + proposal.id = proposalId; + (proposal.status, proposal.executor, proposal.submittedAt, proposal.scheduledAt) = + _proposals.getProposalInfo(proposalId); + proposal.calls = _proposals.getProposalCalls(proposalId); + } + + /// @notice Retrieves information about a proposal, excluding the external calls associated with it. + /// @param proposalId The ID of the proposal to retrieve information for. + /// @return id The ID of the proposal. + /// @return status The current status of the proposal. Possible values are: + /// 0 - The proposal does not exist. + /// 1 - The proposal was submitted but not scheduled. + /// 2 - The proposal was submitted and scheduled but not yet executed. + /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. + /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. + /// This is the final state of the proposal. + /// @return executor The address of the executor responsible for executing the proposal's external calls. + /// @return submittedAt The timestamp when the proposal was submitted. + /// @return scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal + /// was submitted but not yet scheduled. + function getProposalInfo( + uint256 proposalId + ) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) + { + id = proposalId; + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + } + + /// @notice Retrieves the external calls associated with the specified proposal. + /// @param proposalId The ID of the proposal to retrieve external calls for. + /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + calls = _proposals.getProposalCalls(proposalId); + } + + /// @dev Retrieves the total number of proposals. + /// @return count The total number of proposals. + function getProposalsCount() external view returns (uint256 count) { + count = _proposals.getProposalsCount(); + } + + /// @dev Checks if a proposal can be executed. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be executed. + function canExecute(uint256 proposalId) external view returns (bool) { + return !_emergencyProtection.isEmergencyModeActive() + && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Checks if a proposal can be scheduled. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be scheduled. + function canSchedule(uint256 proposalId) external view returns (bool) { + return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + function _checkCallerIsAdminExecutor() internal view { + if (msg.sender != _ADMIN_EXECUTOR) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockSubmitGuardMissing.sol b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockSubmitGuardMissing.sol new file mode 100644 index 00000000..5a000678 --- /dev/null +++ b/certora/mutation/mutants/EmergencyProtectedTimelock/EmergencyProtectedTimelockSubmitGuardMissing.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; + +import {IOwnable} from "./interfaces/IOwnable.sol"; +import {ITimelock, ProposalStatus} from "./interfaces/ITimelock.sol"; + +import {TimelockState} from "./libraries/TimelockState.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {ExecutableProposals} from "./libraries/ExecutableProposals.sol"; +import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; + +/// @title EmergencyProtectedTimelock +/// @dev A timelock contract with emergency protection functionality. +/// The contract allows for submitting, scheduling, and executing proposals, +/// while providing emergency protection features to prevent unauthorized +/// execution during emergency situations. +contract EmergencyProtectedTimelock is ITimelock { + using TimelockState for TimelockState.Context; + using ExecutableProposals for ExecutableProposals.Context; + using EmergencyProtection for EmergencyProtection.Context; + + error CallerIsNotAdminExecutor(address value); + + // --- + // Sanity Check Params Immutables + // --- + struct SanityCheckParams { + Duration maxAfterSubmitDelay; + Duration maxAfterScheduleDelay; + Duration maxEmergencyModeDuration; + Duration maxEmergencyProtectionDuration; + } + + Duration public immutable MAX_AFTER_SUBMIT_DELAY; + Duration public immutable MAX_AFTER_SCHEDULE_DELAY; + + Duration public immutable MAX_EMERGENCY_MODE_DURATION; + Duration public immutable MAX_EMERGENCY_PROTECTION_DURATION; + + // --- + // Admin Executor Immutables + // --- + + address private immutable _ADMIN_EXECUTOR; + + // --- + // Aspects + // --- + + TimelockState.Context internal _timelockState; + ExecutableProposals.Context internal _proposals; + EmergencyProtection.Context internal _emergencyProtection; + + constructor(SanityCheckParams memory sanityCheckParams, address adminExecutor) { + _ADMIN_EXECUTOR = adminExecutor; + + MAX_AFTER_SUBMIT_DELAY = sanityCheckParams.maxAfterSubmitDelay; + MAX_AFTER_SCHEDULE_DELAY = sanityCheckParams.maxAfterScheduleDelay; + MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; + MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyModeDuration; + } + + // --- + // Main Timelock Functionality + // --- + + /// @dev Submits a new proposal to execute a series of calls through an executor. + /// Only the governance contract can call this function. + /// @param executor The address of the executor contract that will execute the calls. + /// @param calls An array of `ExternalCall` structs representing the calls to be executed. + /// @return newProposalId The ID of the newly created proposal. + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { + // mutated + //_timelockState.checkCallerIsGovernance(); + newProposalId = _proposals.submit(executor, calls); + } + + /// @dev Schedules a proposal for execution after a specified delay. + /// Only the governance contract can call this function. + /// @param proposalId The ID of the proposal to be scheduled. + function schedule(uint256 proposalId) external { + _timelockState.checkCallerIsGovernance(); + _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + /// @dev Executes a scheduled proposal. + /// Checks if emergency mode is active and prevents execution if it is. + /// @param proposalId The ID of the proposal to be executed. + function execute(uint256 proposalId) external { + _emergencyProtection.checkEmergencyMode({isActive: false}); + _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Cancels all non-executed proposals. + /// Only the governance contract can call this function. + function cancelAllNonExecutedProposals() external { + _timelockState.checkCallerIsGovernance(); + _proposals.cancelAll(); + } + + // --- + // Timelock Management + // --- + + function setGovernance(address newGovernance) external { + _checkCallerIsAdminExecutor(); + _timelockState.setGovernance(newGovernance); + } + + function setDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + _checkCallerIsAdminExecutor(); + _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); + _timelockState.setAfterScheduleDelay(afterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); + } + + /// @dev Transfers ownership of the executor contract to a new owner. + /// Only the admin executor can call this function. + /// @param executor The address of the executor contract. + /// @param owner The address of the new owner. + function transferExecutorOwnership(address executor, address owner) external { + _checkCallerIsAdminExecutor(); + IOwnable(executor).transferOwnership(owner); + } + + // --- + // Emergency Protection Functionality + // --- + + function setupEmergencyProtection( + address emergencyGovernance, + address emergencyActivationCommittee, + address emergencyExecutionCommittee, + Timestamp emergencyProtectionEndDate, + Duration emergencyModeDuration + ) external { + _checkCallerIsAdminExecutor(); + + _emergencyProtection.setEmergencyGovernance(emergencyGovernance); + _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + _emergencyProtection.setEmergencyProtectionEndDate( + emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION + ); + _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); + _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + } + + /// @dev Activates the emergency mode. + /// Only the activation committee can call this function. + function activateEmergencyMode() external { + _emergencyProtection.checkCallerIsEmergencyActivationCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: false}); + _emergencyProtection.activateEmergencyMode(); + } + + /// @dev Executes a proposal during emergency mode. + /// Checks if emergency mode is active and if the caller is part of the execution committee. + /// @param proposalId The ID of the proposal to be executed. + function emergencyExecute(uint256 proposalId) external { + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _proposals.execute({proposalId: proposalId, afterScheduleDelay: Duration.wrap(0)}); + } + + /// @dev Deactivates the emergency mode. + /// If the emergency mode has not passed, only the admin executor can call this function. + function deactivateEmergencyMode() external { + _emergencyProtection.checkEmergencyMode({isActive: true}); + if (!_emergencyProtection.isEmergencyModeDurationPassed()) { + _checkCallerIsAdminExecutor(); + } + _emergencyProtection.deactivateEmergencyMode(); + _proposals.cancelAll(); + } + + /// @dev Resets the system after entering the emergency mode. + /// Only the execution committee can call this function. + function emergencyReset() external { + _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); + _emergencyProtection.checkEmergencyMode({isActive: true}); + _emergencyProtection.deactivateEmergencyMode(); + + _timelockState.setGovernance(_emergencyProtection.emergencyGovernance); + _proposals.cancelAll(); + } + + function getEmergencyProtectionContext() external view returns (EmergencyProtection.Context memory) { + return _emergencyProtection; + } + + function isEmergencyProtectionEnabled() public view returns (bool) { + return _emergencyProtection.isEmergencyProtectionEnabled(); + } + + function isEmergencyModeActive() public view returns (bool isActive) { + isActive = _emergencyProtection.isEmergencyModeActive(); + } + + // --- + // Timelock View Methods + // --- + + function getGovernance() external view returns (address) { + return _timelockState.governance; + } + + function getAdminExecutor() external view returns (address) { + return _ADMIN_EXECUTOR; + } + + function getAfterSubmitDelay() external view returns (Duration) { + return _timelockState.getAfterSubmitDelay(); + } + + function getAfterScheduleDelay() external view returns (Duration) { + return _timelockState.getAfterScheduleDelay(); + } + + /// @dev Retrieves the details of a proposal. + /// @param proposalId The ID of the proposal. + /// @return proposal The Proposal struct containing the details of the proposal. + function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { + proposal.id = proposalId; + (proposal.status, proposal.executor, proposal.submittedAt, proposal.scheduledAt) = + _proposals.getProposalInfo(proposalId); + proposal.calls = _proposals.getProposalCalls(proposalId); + } + + /// @notice Retrieves information about a proposal, excluding the external calls associated with it. + /// @param proposalId The ID of the proposal to retrieve information for. + /// @return id The ID of the proposal. + /// @return status The current status of the proposal. Possible values are: + /// 0 - The proposal does not exist. + /// 1 - The proposal was submitted but not scheduled. + /// 2 - The proposal was submitted and scheduled but not yet executed. + /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. + /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. + /// This is the final state of the proposal. + /// @return executor The address of the executor responsible for executing the proposal's external calls. + /// @return submittedAt The timestamp when the proposal was submitted. + /// @return scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal + /// was submitted but not yet scheduled. + function getProposalInfo( + uint256 proposalId + ) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) + { + id = proposalId; + (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + } + + /// @notice Retrieves the external calls associated with the specified proposal. + /// @param proposalId The ID of the proposal to retrieve external calls for. + /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + calls = _proposals.getProposalCalls(proposalId); + } + + /// @dev Retrieves the total number of proposals. + /// @return count The total number of proposals. + function getProposalsCount() external view returns (uint256 count) { + count = _proposals.getProposalsCount(); + } + + /// @dev Checks if a proposal can be executed. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be executed. + function canExecute(uint256 proposalId) external view returns (bool) { + return !_emergencyProtection.isEmergencyModeActive() + && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); + } + + /// @dev Checks if a proposal can be scheduled. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be scheduled. + function canSchedule(uint256 proposalId) external view returns (bool) { + return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); + } + + function _checkCallerIsAdminExecutor() internal view { + if (msg.sender != _ADMIN_EXECUTOR) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/EmergencyProtection/EmergencyProtectionEmergencyModeActivationMissesProtectedModeCheck.sol b/certora/mutation/mutants/EmergencyProtection/EmergencyProtectionEmergencyModeActivationMissesProtectedModeCheck.sol new file mode 100644 index 00000000..7fbde780 --- /dev/null +++ b/certora/mutation/mutants/EmergencyProtection/EmergencyProtectionEmergencyModeActivationMissesProtectedModeCheck.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +/// @title EmergencyProtection +/// @dev This library manages emergency protection functionality, allowing for +/// the activation and deactivation of emergency mode by designated committees. +library EmergencyProtection { + error CallerIsNotEmergencyActivationCommittee(address caller); + error CallerIsNotEmergencyExecutionCommittee(address caller); + error EmergencyProtectionExpired(Timestamp protectedTill); + error InvalidEmergencyModeDuration(Duration value); + error InvalidEmergencyProtectionEndDate(Timestamp value); + error UnexpectedEmergencyModeState(bool value); + + event EmergencyModeActivated(); + event EmergencyModeDeactivated(); + event EmergencyGovernanceSet(address newEmergencyGovernance); + event EmergencyActivationCommitteeSet(address newActivationCommittee); + event EmergencyExecutionCommitteeSet(address newActivationCommittee); + event EmergencyModeDurationSet(Duration newEmergencyModeDuration); + event EmergencyProtectionEndDateSet(Timestamp newEmergencyProtectionEndDate); + + struct Context { + /// @dev slot0 [0..39] + Timestamp emergencyModeEndsAfter; + /// @dev slot0 [40..199] + address emergencyActivationCommittee; + /// @dev slot0 [200..240] + Timestamp emergencyProtectionEndsAfter; + /// @dev slot1 [0..159] + address emergencyExecutionCommittee; + /// @dev slot1 [160..191] + Duration emergencyModeDuration; + /// @dev slot2 [0..160] + address emergencyGovernance; + } + + // --- + // Main functionality + // --- + + /// @dev Activates the emergency mode. + /// @param self The storage reference to the Context struct. + function activateEmergencyMode(Context storage self) internal { + Timestamp now_ = Timestamps.now(); + + // mutated + //if (now_ > self.emergencyProtectionEndsAfter) { + // revert EmergencyProtectionExpired(self.emergencyProtectionEndsAfter); + //} + + self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(now_); + + emit EmergencyModeActivated(); + } + + /// @dev Deactivates the emergency mode. + /// @param self The storage reference to the Context struct. + function deactivateEmergencyMode(Context storage self) internal { + self.emergencyActivationCommittee = address(0); + self.emergencyExecutionCommittee = address(0); + self.emergencyProtectionEndsAfter = Timestamps.ZERO; + self.emergencyModeEndsAfter = Timestamps.ZERO; + self.emergencyModeDuration = Durations.ZERO; + emit EmergencyModeDeactivated(); + } + + // --- + // Setup functionality + // --- + + function setEmergencyGovernance(Context storage self, address newEmergencyGovernance) internal { + if (newEmergencyGovernance == self.emergencyGovernance) { + return; + } + self.emergencyGovernance = newEmergencyGovernance; + emit EmergencyGovernanceSet(newEmergencyGovernance); + } + + function setEmergencyProtectionEndDate( + Context storage self, + Timestamp newEmergencyProtectionEndDate, + Duration maxEmergencyProtectionDuration + ) internal { + if (newEmergencyProtectionEndDate > maxEmergencyProtectionDuration.addTo(Timestamps.now())) { + revert InvalidEmergencyProtectionEndDate(newEmergencyProtectionEndDate); + } + + if (newEmergencyProtectionEndDate == self.emergencyProtectionEndsAfter) { + return; + } + self.emergencyProtectionEndsAfter = newEmergencyProtectionEndDate; + emit EmergencyProtectionEndDateSet(newEmergencyProtectionEndDate); + } + + function setEmergencyModeDuration( + Context storage self, + Duration newEmergencyModeDuration, + Duration maxEmergencyModeDuration + ) internal { + if (newEmergencyModeDuration > maxEmergencyModeDuration) { + revert InvalidEmergencyModeDuration(newEmergencyModeDuration); + } + if (newEmergencyModeDuration == self.emergencyModeDuration) { + return; + } + + self.emergencyModeDuration = newEmergencyModeDuration; + emit EmergencyModeDurationSet(newEmergencyModeDuration); + } + + function setEmergencyActivationCommittee(Context storage self, address newActivationCommittee) internal { + if (newActivationCommittee == self.emergencyActivationCommittee) { + return; + } + self.emergencyActivationCommittee = newActivationCommittee; + emit EmergencyActivationCommitteeSet(newActivationCommittee); + } + + function setEmergencyExecutionCommittee(Context storage self, address newExecutionCommittee) internal { + if (newExecutionCommittee == self.emergencyExecutionCommittee) { + return; + } + self.emergencyExecutionCommittee = newExecutionCommittee; + emit EmergencyExecutionCommitteeSet(newExecutionCommittee); + } + + // --- + // Checks + // --- + + /// @dev Checks if the caller is the emergency activator and reverts if not. + /// @param self The storage reference to the Context struct. + function checkCallerIsEmergencyActivationCommittee(Context storage self) internal view { + if (self.emergencyActivationCommittee != msg.sender) { + revert CallerIsNotEmergencyActivationCommittee(msg.sender); + } + } + + /// @dev Checks if the caller is the emergency enactor and reverts if not. + /// @param self The storage reference to the Context struct. + function checkCallerIsEmergencyExecutionCommittee(Context storage self) internal view { + if (self.emergencyExecutionCommittee != msg.sender) { + revert CallerIsNotEmergencyExecutionCommittee(msg.sender); + } + } + + /// @dev Checks if the emergency mode matches with expected passed value and reverts if not. + /// @param self The storage reference to the Context struct. + /// @param isActive The expected value of the emergency mode. + function checkEmergencyMode(Context storage self, bool isActive) internal view { + if (isEmergencyModeActive(self) != isActive) { + revert UnexpectedEmergencyModeState(isActive); + } + } + + // --- + // Getters + // --- + + /// @dev Checks if the emergency mode is activated + /// @param self The storage reference to the Context struct. + /// @return Whether the emergency mode is activated or not. + function isEmergencyModeActive(Context storage self) internal view returns (bool) { + return self.emergencyModeEndsAfter.isNotZero(); + } + + /// @dev Checks if the emergency mode has passed. + /// @param self The storage reference to the Context struct. + /// @return Whether the emergency mode has passed or not. + function isEmergencyModeDurationPassed(Context storage self) internal view returns (bool) { + Timestamp endsAfter = self.emergencyModeEndsAfter; + return endsAfter.isNotZero() && Timestamps.now() > endsAfter; + } + + /// @dev Checks if the emergency protection is enabled. + /// @param self The storage reference to the Context struct. + /// @return Whether the emergency protection is enabled or not. + function isEmergencyProtectionEnabled(Context storage self) internal view returns (bool) { + return Timestamps.now() <= self.emergencyProtectionEndsAfter || self.emergencyModeEndsAfter.isNotZero(); + } +} diff --git a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecutionDelayCheckBuggy.sol b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecutionDelayCheckBuggy.sol new file mode 100644 index 00000000..63260867 --- /dev/null +++ b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecutionDelayCheckBuggy.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; + +/// @dev Describes the lifecycle state of a proposal +enum Status { + /// Proposal has not been submitted yet + NotExist, + /// Proposal has been successfully submitted but not scheduled yet. This state is only reachable from NotExist + Submitted, + /// Proposal has been successfully scheduled after submission. This state is only reachable from Submitted + Scheduled, + /// Proposal has been successfully executed after being scheduled. This state is only reachable from Scheduled + /// and is the final state of the proposal + Executed, + /// Proposal was cancelled before execution. Cancelled proposals cannot be resubmitted or rescheduled. + /// This state is only reachable from Submitted or Scheduled and is the final state of the proposal. + /// @dev A proposal is considered cancelled if it was not executed and its ID is less than the ID of the last + /// submitted proposal at the time the cancelAll() method was called. To check if a proposal is in the Cancelled + /// state, use the _isProposalMarkedCancelled() view function. + Cancelled +} + +/// @dev Manages a collection of proposals with associated external calls stored as Proposal struct. +/// Proposals are uniquely identified by sequential IDs, starting from one. +library ExecutableProposals { + using ExternalCalls for ExternalCall[]; + + /// @dev Efficiently stores proposal data within a single EVM word. + /// This struct allows gas-efficient loading from storage using a single EVM sload operation. + struct ProposalData { + /// + /// @dev slot 0: [0..7] + /// The current status of the proposal. See Status for details. + Status status; + /// + /// @dev slot 0: [8..167] + /// The address of the associated executor used for executing the proposal's calls. + address executor; + /// + /// @dev slot 0: [168..207] + /// The timestamp when the proposal was submitted. + Timestamp submittedAt; + /// + /// @dev slot 0: [208..247] + /// The timestamp when the proposal was scheduled for execution. Equals zero if the proposal hasn't been scheduled yet. + Timestamp scheduledAt; + } + + struct Proposal { + /// @dev Proposal data packed into a struct for efficient loading into memory. + ProposalData data; + /// @dev The list of external calls associated with the proposal. + ExternalCall[] calls; + } + + error EmptyCalls(); + error ProposalNotFound(uint256 proposalId); + error ProposalNotScheduled(uint256 proposalId); + error ProposalNotSubmitted(uint256 proposalId); + error AfterSubmitDelayNotPassed(uint256 proposalId); + error AfterScheduleDelayNotPassed(uint256 proposalId); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalScheduled(uint256 indexed id); + event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalsCancelledTill(uint256 proposalId); + + struct Context { + uint64 proposalsCount; + uint64 lastCancelledProposalId; + mapping(uint256 proposalId => Proposal) proposals; + } + + // --- + // Proposal lifecycle + // --- + + function submit( + Context storage self, + address executor, + ExternalCall[] memory calls + ) internal returns (uint256 newProposalId) { + if (calls.length == 0) { + revert EmptyCalls(); + } + + /// @dev: proposal ids are one-based. The first item has id = 1 + newProposalId = ++self.proposalsCount; + Proposal storage newProposal = self.proposals[newProposalId]; + + newProposal.data.executor = executor; + newProposal.data.status = Status.Submitted; + newProposal.data.submittedAt = Timestamps.now(); + + uint256 callsCount = calls.length; + for (uint256 i = 0; i < callsCount; ++i) { + newProposal.calls.push(calls[i]); + } + + emit ProposalSubmitted(newProposalId, executor, calls); + } + + function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { + ProposalData memory proposalState = self.proposals[proposalId].data; + + if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { + revert ProposalNotSubmitted(proposalId); + } + + if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + revert AfterSubmitDelayNotPassed(proposalId); + } + + proposalState.status = Status.Scheduled; + proposalState.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalState; + + emit ProposalScheduled(proposalId); + } + + function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { + Proposal memory proposal = self.proposals[proposalId]; + + if (proposal.data.status != Status.Scheduled || _isProposalMarkedCancelled(self, proposalId, proposal.data)) { + revert ProposalNotScheduled(proposalId); + } + + // mutated + if (afterScheduleDelay.addTo(proposal.data.submittedAt) > Timestamps.now()) { + //if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { + revert AfterScheduleDelayNotPassed(proposalId); + } + + self.proposals[proposalId].data.status = Status.Executed; + + address executor = proposal.data.executor; + ExternalCall[] memory calls = proposal.calls; + + bytes[] memory results = calls.execute(IExternalExecutor(executor)); + + emit ProposalExecuted(proposalId, results); + } + + function cancelAll(Context storage self) internal { + uint64 lastCancelledProposalId = self.proposalsCount; + self.lastCancelledProposalId = lastCancelledProposalId; + emit ProposalsCancelledTill(lastCancelledProposalId); + } + + // --- + // Getters + // --- + + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + } + + function canSchedule( + Context storage self, + uint256 proposalId, + Duration afterSubmitDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + } + + function getProposalsCount(Context storage self) internal view returns (uint256) { + return self.proposalsCount; + } + + function getProposalInfo( + Context storage self, + uint256 proposalId + ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ProposalData memory proposalData = self.proposals[proposalId].data; + _checkProposalExists(proposalId, proposalData); + + status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + executor = address(proposalData.executor); + submittedAt = proposalData.submittedAt; + scheduledAt = proposalData.scheduledAt; + } + + function getProposalCalls( + Context storage self, + uint256 proposalId + ) internal view returns (ExternalCall[] memory calls) { + Proposal memory proposal = self.proposals[proposalId]; + _checkProposalExists(proposalId, proposal.data); + calls = proposal.calls; + } + + // --- + // Private methods + // --- + + function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { + if (proposalData.status == Status.NotExist) { + revert ProposalNotFound(proposalId); + } + } + + function _isProposalMarkedCancelled( + Context storage self, + uint256 proposalId, + ProposalData memory proposalData + ) private view returns (bool) { + return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + } +} diff --git a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleDelayCheckBuggy.sol b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleDelayCheckBuggy.sol new file mode 100644 index 00000000..10b263ae --- /dev/null +++ b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleDelayCheckBuggy.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; + +/// @dev Describes the lifecycle state of a proposal +enum Status { + /// Proposal has not been submitted yet + NotExist, + /// Proposal has been successfully submitted but not scheduled yet. This state is only reachable from NotExist + Submitted, + /// Proposal has been successfully scheduled after submission. This state is only reachable from Submitted + Scheduled, + /// Proposal has been successfully executed after being scheduled. This state is only reachable from Scheduled + /// and is the final state of the proposal + Executed, + /// Proposal was cancelled before execution. Cancelled proposals cannot be resubmitted or rescheduled. + /// This state is only reachable from Submitted or Scheduled and is the final state of the proposal. + /// @dev A proposal is considered cancelled if it was not executed and its ID is less than the ID of the last + /// submitted proposal at the time the cancelAll() method was called. To check if a proposal is in the Cancelled + /// state, use the _isProposalMarkedCancelled() view function. + Cancelled +} + +/// @dev Manages a collection of proposals with associated external calls stored as Proposal struct. +/// Proposals are uniquely identified by sequential IDs, starting from one. +library ExecutableProposals { + using ExternalCalls for ExternalCall[]; + + /// @dev Efficiently stores proposal data within a single EVM word. + /// This struct allows gas-efficient loading from storage using a single EVM sload operation. + struct ProposalData { + /// + /// @dev slot 0: [0..7] + /// The current status of the proposal. See Status for details. + Status status; + /// + /// @dev slot 0: [8..167] + /// The address of the associated executor used for executing the proposal's calls. + address executor; + /// + /// @dev slot 0: [168..207] + /// The timestamp when the proposal was submitted. + Timestamp submittedAt; + /// + /// @dev slot 0: [208..247] + /// The timestamp when the proposal was scheduled for execution. Equals zero if the proposal hasn't been scheduled yet. + Timestamp scheduledAt; + } + + struct Proposal { + /// @dev Proposal data packed into a struct for efficient loading into memory. + ProposalData data; + /// @dev The list of external calls associated with the proposal. + ExternalCall[] calls; + } + + error EmptyCalls(); + error ProposalNotFound(uint256 proposalId); + error ProposalNotScheduled(uint256 proposalId); + error ProposalNotSubmitted(uint256 proposalId); + error AfterSubmitDelayNotPassed(uint256 proposalId); + error AfterScheduleDelayNotPassed(uint256 proposalId); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalScheduled(uint256 indexed id); + event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalsCancelledTill(uint256 proposalId); + + struct Context { + uint64 proposalsCount; + uint64 lastCancelledProposalId; + mapping(uint256 proposalId => Proposal) proposals; + } + + // --- + // Proposal lifecycle + // --- + + function submit( + Context storage self, + address executor, + ExternalCall[] memory calls + ) internal returns (uint256 newProposalId) { + if (calls.length == 0) { + revert EmptyCalls(); + } + + /// @dev: proposal ids are one-based. The first item has id = 1 + newProposalId = ++self.proposalsCount; + Proposal storage newProposal = self.proposals[newProposalId]; + + newProposal.data.executor = executor; + newProposal.data.status = Status.Submitted; + newProposal.data.submittedAt = Timestamps.now(); + + uint256 callsCount = calls.length; + for (uint256 i = 0; i < callsCount; ++i) { + newProposal.calls.push(calls[i]); + } + + emit ProposalSubmitted(newProposalId, executor, calls); + } + + function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { + ProposalData memory proposalState = self.proposals[proposalId].data; + + if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { + revert ProposalNotSubmitted(proposalId); + } + + // mutated + if (afterSubmitDelay.addTo(proposalState.submittedAt) <= Timestamps.now()) { + // if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + + revert AfterSubmitDelayNotPassed(proposalId); + } + + proposalState.status = Status.Scheduled; + proposalState.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalState; + + emit ProposalScheduled(proposalId); + } + + function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { + Proposal memory proposal = self.proposals[proposalId]; + + if (proposal.data.status != Status.Scheduled || _isProposalMarkedCancelled(self, proposalId, proposal.data)) { + revert ProposalNotScheduled(proposalId); + } + + if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { + revert AfterScheduleDelayNotPassed(proposalId); + } + + self.proposals[proposalId].data.status = Status.Executed; + + address executor = proposal.data.executor; + ExternalCall[] memory calls = proposal.calls; + + bytes[] memory results = calls.execute(IExternalExecutor(executor)); + + emit ProposalExecuted(proposalId, results); + } + + function cancelAll(Context storage self) internal { + uint64 lastCancelledProposalId = self.proposalsCount; + self.lastCancelledProposalId = lastCancelledProposalId; + emit ProposalsCancelledTill(lastCancelledProposalId); + } + + // --- + // Getters + // --- + + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + } + + function canSchedule( + Context storage self, + uint256 proposalId, + Duration afterSubmitDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + } + + function getProposalsCount(Context storage self) internal view returns (uint256) { + return self.proposalsCount; + } + + function getProposalInfo( + Context storage self, + uint256 proposalId + ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ProposalData memory proposalData = self.proposals[proposalId].data; + _checkProposalExists(proposalId, proposalData); + + status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + executor = address(proposalData.executor); + submittedAt = proposalData.submittedAt; + scheduledAt = proposalData.scheduledAt; + } + + function getProposalCalls( + Context storage self, + uint256 proposalId + ) internal view returns (ExternalCall[] memory calls) { + Proposal memory proposal = self.proposals[proposalId]; + _checkProposalExists(proposalId, proposal.data); + calls = proposal.calls; + } + + // --- + // Private methods + // --- + + function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { + if (proposalData.status == Status.NotExist) { + revert ProposalNotFound(proposalId); + } + } + + function _isProposalMarkedCancelled( + Context storage self, + uint256 proposalId, + ProposalData memory proposalData + ) private view returns (bool) { + return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + } +} diff --git a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol new file mode 100644 index 00000000..175096c9 --- /dev/null +++ b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; + +/// @dev Describes the lifecycle state of a proposal +enum Status { + /// Proposal has not been submitted yet + NotExist, + /// Proposal has been successfully submitted but not scheduled yet. This state is only reachable from NotExist + Submitted, + /// Proposal has been successfully scheduled after submission. This state is only reachable from Submitted + Scheduled, + /// Proposal has been successfully executed after being scheduled. This state is only reachable from Scheduled + /// and is the final state of the proposal + Executed, + /// Proposal was cancelled before execution. Cancelled proposals cannot be resubmitted or rescheduled. + /// This state is only reachable from Submitted or Scheduled and is the final state of the proposal. + /// @dev A proposal is considered cancelled if it was not executed and its ID is less than the ID of the last + /// submitted proposal at the time the cancelAll() method was called. To check if a proposal is in the Cancelled + /// state, use the _isProposalMarkedCancelled() view function. + Cancelled +} + +/// @dev Manages a collection of proposals with associated external calls stored as Proposal struct. +/// Proposals are uniquely identified by sequential IDs, starting from one. +library ExecutableProposals { + using ExternalCalls for ExternalCall[]; + + /// @dev Efficiently stores proposal data within a single EVM word. + /// This struct allows gas-efficient loading from storage using a single EVM sload operation. + struct ProposalData { + /// + /// @dev slot 0: [0..7] + /// The current status of the proposal. See Status for details. + Status status; + /// + /// @dev slot 0: [8..167] + /// The address of the associated executor used for executing the proposal's calls. + address executor; + /// + /// @dev slot 0: [168..207] + /// The timestamp when the proposal was submitted. + Timestamp submittedAt; + /// + /// @dev slot 0: [208..247] + /// The timestamp when the proposal was scheduled for execution. Equals zero if the proposal hasn't been scheduled yet. + Timestamp scheduledAt; + } + + struct Proposal { + /// @dev Proposal data packed into a struct for efficient loading into memory. + ProposalData data; + /// @dev The list of external calls associated with the proposal. + ExternalCall[] calls; + } + + error EmptyCalls(); + error ProposalNotFound(uint256 proposalId); + error ProposalNotScheduled(uint256 proposalId); + error ProposalNotSubmitted(uint256 proposalId); + error AfterSubmitDelayNotPassed(uint256 proposalId); + error AfterScheduleDelayNotPassed(uint256 proposalId); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalScheduled(uint256 indexed id); + event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalsCancelledTill(uint256 proposalId); + + struct Context { + uint64 proposalsCount; + uint64 lastCancelledProposalId; + mapping(uint256 proposalId => Proposal) proposals; + } + + // --- + // Proposal lifecycle + // --- + + function submit( + Context storage self, + address executor, + ExternalCall[] memory calls + ) internal returns (uint256 newProposalId) { + if (calls.length == 0) { + revert EmptyCalls(); + } + + /// @dev: proposal ids are one-based. The first item has id = 1 + newProposalId = ++self.proposalsCount; + Proposal storage newProposal = self.proposals[newProposalId]; + + newProposal.data.executor = executor; + newProposal.data.status = Status.Submitted; + newProposal.data.submittedAt = Timestamps.now(); + + uint256 callsCount = calls.length; + for (uint256 i = 0; i < callsCount; ++i) { + newProposal.calls.push(calls[i]); + } + + emit ProposalSubmitted(newProposalId, executor, calls); + } + + function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { + ProposalData memory proposalState = self.proposals[proposalId].data; + + // if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { + // revert ProposalNotSubmitted(proposalId); + // } + + if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + revert AfterSubmitDelayNotPassed(proposalId); + } + + proposalState.status = Status.Scheduled; + proposalState.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalState; + + emit ProposalScheduled(proposalId); + } + + function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { + Proposal memory proposal = self.proposals[proposalId]; + + if (proposal.data.status != Status.Scheduled || _isProposalMarkedCancelled(self, proposalId, proposal.data)) { + revert ProposalNotScheduled(proposalId); + } + + if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { + revert AfterScheduleDelayNotPassed(proposalId); + } + + self.proposals[proposalId].data.status = Status.Executed; + + address executor = proposal.data.executor; + ExternalCall[] memory calls = proposal.calls; + + bytes[] memory results = calls.execute(IExternalExecutor(executor)); + + emit ProposalExecuted(proposalId, results); + } + + function cancelAll(Context storage self) internal { + uint64 lastCancelledProposalId = self.proposalsCount; + self.lastCancelledProposalId = lastCancelledProposalId; + emit ProposalsCancelledTill(lastCancelledProposalId); + } + + // --- + // Getters + // --- + + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + } + + function canSchedule( + Context storage self, + uint256 proposalId, + Duration afterSubmitDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + } + + function getProposalsCount(Context storage self) internal view returns (uint256) { + return self.proposalsCount; + } + + function getProposalInfo( + Context storage self, + uint256 proposalId + ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ProposalData memory proposalData = self.proposals[proposalId].data; + _checkProposalExists(proposalId, proposalData); + + status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + executor = address(proposalData.executor); + submittedAt = proposalData.submittedAt; + scheduledAt = proposalData.scheduledAt; + } + + function getProposalCalls( + Context storage self, + uint256 proposalId + ) internal view returns (ExternalCall[] memory calls) { + Proposal memory proposal = self.proposals[proposalId]; + _checkProposalExists(proposalId, proposal.data); + calls = proposal.calls; + } + + // --- + // Private methods + // --- + + function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { + if (proposalData.status == Status.NotExist) { + revert ProposalNotFound(proposalId); + } + } + + function _isProposalMarkedCancelled( + Context storage self, + uint256 proposalId, + ProposalData memory proposalData + ) private view returns (bool) { + return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + } +} diff --git a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsSubmissionTimestampNotSet.sol b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsSubmissionTimestampNotSet.sol new file mode 100644 index 00000000..6b4b7bd2 --- /dev/null +++ b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsSubmissionTimestampNotSet.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; + +/// @dev Describes the lifecycle state of a proposal +enum Status { + /// Proposal has not been submitted yet + NotExist, + /// Proposal has been successfully submitted but not scheduled yet. This state is only reachable from NotExist + Submitted, + /// Proposal has been successfully scheduled after submission. This state is only reachable from Submitted + Scheduled, + /// Proposal has been successfully executed after being scheduled. This state is only reachable from Scheduled + /// and is the final state of the proposal + Executed, + /// Proposal was cancelled before execution. Cancelled proposals cannot be resubmitted or rescheduled. + /// This state is only reachable from Submitted or Scheduled and is the final state of the proposal. + /// @dev A proposal is considered cancelled if it was not executed and its ID is less than the ID of the last + /// submitted proposal at the time the cancelAll() method was called. To check if a proposal is in the Cancelled + /// state, use the _isProposalMarkedCancelled() view function. + Cancelled +} + +/// @dev Manages a collection of proposals with associated external calls stored as Proposal struct. +/// Proposals are uniquely identified by sequential IDs, starting from one. +library ExecutableProposals { + using ExternalCalls for ExternalCall[]; + + /// @dev Efficiently stores proposal data within a single EVM word. + /// This struct allows gas-efficient loading from storage using a single EVM sload operation. + struct ProposalData { + /// + /// @dev slot 0: [0..7] + /// The current status of the proposal. See Status for details. + Status status; + /// + /// @dev slot 0: [8..167] + /// The address of the associated executor used for executing the proposal's calls. + address executor; + /// + /// @dev slot 0: [168..207] + /// The timestamp when the proposal was submitted. + Timestamp submittedAt; + /// + /// @dev slot 0: [208..247] + /// The timestamp when the proposal was scheduled for execution. Equals zero if the proposal hasn't been scheduled yet. + Timestamp scheduledAt; + } + + struct Proposal { + /// @dev Proposal data packed into a struct for efficient loading into memory. + ProposalData data; + /// @dev The list of external calls associated with the proposal. + ExternalCall[] calls; + } + + error EmptyCalls(); + error ProposalNotFound(uint256 proposalId); + error ProposalNotScheduled(uint256 proposalId); + error ProposalNotSubmitted(uint256 proposalId); + error AfterSubmitDelayNotPassed(uint256 proposalId); + error AfterScheduleDelayNotPassed(uint256 proposalId); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalScheduled(uint256 indexed id); + event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalsCancelledTill(uint256 proposalId); + + struct Context { + uint64 proposalsCount; + uint64 lastCancelledProposalId; + mapping(uint256 proposalId => Proposal) proposals; + } + + // --- + // Proposal lifecycle + // --- + + function submit( + Context storage self, + address executor, + ExternalCall[] memory calls + ) internal returns (uint256 newProposalId) { + if (calls.length == 0) { + revert EmptyCalls(); + } + + /// @dev: proposal ids are one-based. The first item has id = 1 + newProposalId = ++self.proposalsCount; + Proposal storage newProposal = self.proposals[newProposalId]; + + newProposal.data.executor = executor; + newProposal.data.status = Status.Submitted; + // mutated + //newProposal.data.submittedAt = Timestamps.now(); + + uint256 callsCount = calls.length; + for (uint256 i = 0; i < callsCount; ++i) { + newProposal.calls.push(calls[i]); + } + + emit ProposalSubmitted(newProposalId, executor, calls); + } + + function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { + ProposalData memory proposalState = self.proposals[proposalId].data; + + if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { + revert ProposalNotSubmitted(proposalId); + } + + if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + revert AfterSubmitDelayNotPassed(proposalId); + } + + proposalState.status = Status.Scheduled; + proposalState.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalState; + + emit ProposalScheduled(proposalId); + } + + function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { + Proposal memory proposal = self.proposals[proposalId]; + + if (proposal.data.status != Status.Scheduled || _isProposalMarkedCancelled(self, proposalId, proposal.data)) { + revert ProposalNotScheduled(proposalId); + } + + if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { + revert AfterScheduleDelayNotPassed(proposalId); + } + + self.proposals[proposalId].data.status = Status.Executed; + + address executor = proposal.data.executor; + ExternalCall[] memory calls = proposal.calls; + + bytes[] memory results = calls.execute(IExternalExecutor(executor)); + + emit ProposalExecuted(proposalId, results); + } + + function cancelAll(Context storage self) internal { + uint64 lastCancelledProposalId = self.proposalsCount; + self.lastCancelledProposalId = lastCancelledProposalId; + emit ProposalsCancelledTill(lastCancelledProposalId); + } + + // --- + // Getters + // --- + + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + } + + function canSchedule( + Context storage self, + uint256 proposalId, + Duration afterSubmitDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + } + + function getProposalsCount(Context storage self) internal view returns (uint256) { + return self.proposalsCount; + } + + function getProposalInfo( + Context storage self, + uint256 proposalId + ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ProposalData memory proposalData = self.proposals[proposalId].data; + _checkProposalExists(proposalId, proposalData); + + status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + executor = address(proposalData.executor); + submittedAt = proposalData.submittedAt; + scheduledAt = proposalData.scheduledAt; + } + + function getProposalCalls( + Context storage self, + uint256 proposalId + ) internal view returns (ExternalCall[] memory calls) { + Proposal memory proposal = self.proposals[proposalId]; + _checkProposalExists(proposalId, proposal.data); + calls = proposal.calls; + } + + // --- + // Private methods + // --- + + function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { + if (proposalData.status == Status.NotExist) { + revert ProposalNotFound(proposalId); + } + } + + function _isProposalMarkedCancelled( + Context storage self, + uint256 proposalId, + ProposalData memory proposalData + ) private view returns (bool) { + return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + } +} From 8b7dc658734846d1552ac27ad3bf2fea6e659c7d Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Thu, 29 Aug 2024 13:55:03 +0200 Subject: [PATCH 23/67] one more mutant --- ...oposalsExecuteMissingCancellationCheck.sol | 223 ++++++++++++++++++ ...posalsScheduleMissingCancellationCheck.sol | 1 + 2 files changed, 224 insertions(+) create mode 100644 certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecuteMissingCancellationCheck.sol diff --git a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecuteMissingCancellationCheck.sol b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecuteMissingCancellationCheck.sol new file mode 100644 index 00000000..41d67bf8 --- /dev/null +++ b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsExecuteMissingCancellationCheck.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; + +/// @dev Describes the lifecycle state of a proposal +enum Status { + /// Proposal has not been submitted yet + NotExist, + /// Proposal has been successfully submitted but not scheduled yet. This state is only reachable from NotExist + Submitted, + /// Proposal has been successfully scheduled after submission. This state is only reachable from Submitted + Scheduled, + /// Proposal has been successfully executed after being scheduled. This state is only reachable from Scheduled + /// and is the final state of the proposal + Executed, + /// Proposal was cancelled before execution. Cancelled proposals cannot be resubmitted or rescheduled. + /// This state is only reachable from Submitted or Scheduled and is the final state of the proposal. + /// @dev A proposal is considered cancelled if it was not executed and its ID is less than the ID of the last + /// submitted proposal at the time the cancelAll() method was called. To check if a proposal is in the Cancelled + /// state, use the _isProposalMarkedCancelled() view function. + Cancelled +} + +/// @dev Manages a collection of proposals with associated external calls stored as Proposal struct. +/// Proposals are uniquely identified by sequential IDs, starting from one. +library ExecutableProposals { + using ExternalCalls for ExternalCall[]; + + /// @dev Efficiently stores proposal data within a single EVM word. + /// This struct allows gas-efficient loading from storage using a single EVM sload operation. + struct ProposalData { + /// + /// @dev slot 0: [0..7] + /// The current status of the proposal. See Status for details. + Status status; + /// + /// @dev slot 0: [8..167] + /// The address of the associated executor used for executing the proposal's calls. + address executor; + /// + /// @dev slot 0: [168..207] + /// The timestamp when the proposal was submitted. + Timestamp submittedAt; + /// + /// @dev slot 0: [208..247] + /// The timestamp when the proposal was scheduled for execution. Equals zero if the proposal hasn't been scheduled yet. + Timestamp scheduledAt; + } + + struct Proposal { + /// @dev Proposal data packed into a struct for efficient loading into memory. + ProposalData data; + /// @dev The list of external calls associated with the proposal. + ExternalCall[] calls; + } + + error EmptyCalls(); + error ProposalNotFound(uint256 proposalId); + error ProposalNotScheduled(uint256 proposalId); + error ProposalNotSubmitted(uint256 proposalId); + error AfterSubmitDelayNotPassed(uint256 proposalId); + error AfterScheduleDelayNotPassed(uint256 proposalId); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalScheduled(uint256 indexed id); + event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalsCancelledTill(uint256 proposalId); + + struct Context { + uint64 proposalsCount; + uint64 lastCancelledProposalId; + mapping(uint256 proposalId => Proposal) proposals; + } + + // --- + // Proposal lifecycle + // --- + + function submit( + Context storage self, + address executor, + ExternalCall[] memory calls + ) internal returns (uint256 newProposalId) { + if (calls.length == 0) { + revert EmptyCalls(); + } + + /// @dev: proposal ids are one-based. The first item has id = 1 + newProposalId = ++self.proposalsCount; + Proposal storage newProposal = self.proposals[newProposalId]; + + newProposal.data.executor = executor; + newProposal.data.status = Status.Submitted; + newProposal.data.submittedAt = Timestamps.now(); + + uint256 callsCount = calls.length; + for (uint256 i = 0; i < callsCount; ++i) { + newProposal.calls.push(calls[i]); + } + + emit ProposalSubmitted(newProposalId, executor, calls); + } + + function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { + ProposalData memory proposalState = self.proposals[proposalId].data; + + if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { + revert ProposalNotSubmitted(proposalId); + } + + if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + revert AfterSubmitDelayNotPassed(proposalId); + } + + proposalState.status = Status.Scheduled; + proposalState.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalState; + + emit ProposalScheduled(proposalId); + } + + function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { + Proposal memory proposal = self.proposals[proposalId]; + + // mutated + // if (proposal.data.status != Status.Scheduled || _isProposalMarkedCancelled(self, proposalId, proposal.data)) { + // revert ProposalNotScheduled(proposalId); + // } + + if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { + revert AfterScheduleDelayNotPassed(proposalId); + } + + self.proposals[proposalId].data.status = Status.Executed; + + address executor = proposal.data.executor; + ExternalCall[] memory calls = proposal.calls; + + bytes[] memory results = calls.execute(IExternalExecutor(executor)); + + emit ProposalExecuted(proposalId, results); + } + + function cancelAll(Context storage self) internal { + uint64 lastCancelledProposalId = self.proposalsCount; + self.lastCancelledProposalId = lastCancelledProposalId; + emit ProposalsCancelledTill(lastCancelledProposalId); + } + + // --- + // Getters + // --- + + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + } + + function canSchedule( + Context storage self, + uint256 proposalId, + Duration afterSubmitDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + } + + function getProposalsCount(Context storage self) internal view returns (uint256) { + return self.proposalsCount; + } + + function getProposalInfo( + Context storage self, + uint256 proposalId + ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ProposalData memory proposalData = self.proposals[proposalId].data; + _checkProposalExists(proposalId, proposalData); + + status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + executor = address(proposalData.executor); + submittedAt = proposalData.submittedAt; + scheduledAt = proposalData.scheduledAt; + } + + function getProposalCalls( + Context storage self, + uint256 proposalId + ) internal view returns (ExternalCall[] memory calls) { + Proposal memory proposal = self.proposals[proposalId]; + _checkProposalExists(proposalId, proposal.data); + calls = proposal.calls; + } + + // --- + // Private methods + // --- + + function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { + if (proposalData.status == Status.NotExist) { + revert ProposalNotFound(proposalId); + } + } + + function _isProposalMarkedCancelled( + Context storage self, + uint256 proposalId, + ProposalData memory proposalData + ) private view returns (bool) { + return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + } +} diff --git a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol index 175096c9..8c9f0304 100644 --- a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol +++ b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsScheduleMissingCancellationCheck.sol @@ -108,6 +108,7 @@ library ExecutableProposals { function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { ProposalData memory proposalState = self.proposals[proposalId].data; + // mutated // if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { // revert ProposalNotSubmitted(proposalId); // } From 1ca74aed03f4d83a92146f9c3c71ad0f709e9e80 Mon Sep 17 00:00:00 2001 From: Nurit Dor <57101353+nd-certora@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:30:28 +0300 Subject: [PATCH 24/67] all rules but solvency --- certora/confs/Escrow.conf | 7 +- certora/helpers/DummyWithdrawalQueue.sol | 75 ++++++-- certora/specs/Escrow.spec | 221 ++++++++++++++++++----- 3 files changed, 242 insertions(+), 61 deletions(-) diff --git a/certora/confs/Escrow.conf b/certora/confs/Escrow.conf index f17f007e..06cc5834 100644 --- a/certora/confs/Escrow.conf +++ b/certora/confs/Escrow.conf @@ -2,20 +2,19 @@ "files": [ "contracts/Escrow.sol", "contracts/DualGovernance.sol", - "contracts/Configuration.sol", + "contracts/DualGovernanceConfigProvider.sol:ImmutableDualGovernanceConfigProvider", "certora/helpers/DummyWithdrawalQueue.sol", "certora/harnesses/ERC20Like/DummyStETH.sol", "certora/harnesses/ERC20Like/DummyWstETH.sol", ], "link": [ - "Escrow:_dualGovernance=DualGovernance", + "Escrow:DUAL_GOVERNANCE=DualGovernance", "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", "Escrow:ST_ETH=DummyStETH", "Escrow:WST_ETH=DummyWstETH", "DummyWstETH:stETH=DummyStETH", "DummyWithdrawalQueue:stETH=DummyStETH", - "Escrow:CONFIG=Configuration", - "DualGovernance:CONFIG=Configuration", + "DualGovernance:_configProvider=ImmutableDualGovernanceConfigProvider", ], "msg": "sanity", diff --git a/certora/helpers/DummyWithdrawalQueue.sol b/certora/helpers/DummyWithdrawalQueue.sol index e6aaa25c..a6d7bb90 100644 --- a/certora/helpers/DummyWithdrawalQueue.sol +++ b/certora/helpers/DummyWithdrawalQueue.sol @@ -12,24 +12,57 @@ contract DummyWithdrawalQueue { uint256 public MIN_STETH_WITHDRAWAL_AMOUNT; uint256 internal lastRequestId; + uint256 internal lastFinalizedRequestId; mapping(address => uint256) balances; - mapping(uint256 => address) owner; + IStETH public stETH; -/* + + struct WithdrawalRequestStatus { + uint256 amountOfStETH; // + uint256 amountOfShares; + address owner; + uint256 timestamp; + bool isFinalized; + bool isClaimed; + } + + mapping(uint256 => WithdrawalRequestStatus) requests; + + + function getLastFinalizedRequestId() public view returns (uint256) { + return lastFinalizedRequestId; + } + + uint256 randomNumOfFinalzied; + // if reduction true we simulate reduce by half + function finalize(uint256 upToRequestId, bool reduction) external { + for(uint256 i = lastFinalizedRequestId; i <= upToRequestId ; i++) { + require(!requests[i].isFinalized); + requests[i].isFinalized = true; + if (reduction) { + requests[i].amountOfStETH = requests[i].amountOfStETH / 2; + } + } + lastFinalizedRequestId = upToRequestId; + } + function getWithdrawalStatus(uint256[] calldata _requestIds) external view returns (WithdrawalRequestStatus[] memory statuses) { - - //summary as nondet + statuses = new WithdrawalRequestStatus[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + require(_requestIds[i] <= lastRequestId); + statuses[i] = requests[_requestIds[i]]; + } } -*/ + function transferFrom(address from, address to, uint256 requestId) external { - require (owner[requestId] == from && balances[from] >= 1); - owner[requestId] = to; + require (requests[requestId].owner == from && balances[from] >= 1); + requests[requestId].owner = to; balances[from] = balances[from] -1; balances[to] = balances[to] -1; } @@ -39,14 +72,21 @@ contract DummyWithdrawalQueue { return balances[owner]; } - mapping(uint256 => uint256) amountOfETH; + function getClaimableEther( uint256[] calldata _requestIds, uint256[] calldata _hints ) external view returns (uint256[] memory claimableEthValues) { claimableEthValues = new uint256[](_requestIds.length); for (uint256 i = 0; i < _requestIds.length; ++i) { - claimableEthValues[i] = amountOfETH[_requestIds[i]]; + uint256 _requestId = _requestIds[i]; + require (_requestId != 0 && _requestId <= lastRequestId) ; + if (_requestId > lastFinalizedRequestId || requests[_requestId].isClaimed) { + claimableEthValues[i] = 0; + } + else { + claimableEthValues[i] = requests[_requestIds[i]].amountOfStETH; + } } } @@ -58,17 +98,24 @@ contract DummyWithdrawalQueue { for (uint256 i = 0; i < _amounts.length; ++i) { stETH.transferFrom(msg.sender, address(this), _amounts[i]); uint256 amountOfShares = stETH.getSharesByPooledEth(_amounts[i]); - requestIds[i] = lastRequestId + 1; lastRequestId += 1; - amountOfETH[requestIds[i]] = _amounts[i]; - owner[requestIds[i]] = _owner; + requestIds[i] = lastRequestId; + requests[lastRequestId] = + WithdrawalRequestStatus( + _amounts[i], + amountOfShares, + _owner, + block.timestamp, + false, + false); } } function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { for (uint256 i = 0; i < requestIds.length; ++i) { - //todo; - (bool success,) = msg.sender.call{value: amountOfETH[requestIds[i]]}(""); + require( ! requests[requestIds[i]].isClaimed && requests[requestIds[i]].isFinalized); + requests[requestIds[i]].isClaimed = true; + (bool success,) = msg.sender.call{value: requests[requestIds[i]].amountOfStETH }(""); require(success); } } diff --git a/certora/specs/Escrow.spec b/certora/specs/Escrow.spec index bba02ca8..ef007b78 100644 --- a/certora/specs/Escrow.spec +++ b/certora/specs/Escrow.spec @@ -1,32 +1,36 @@ using DummyStETH as stEth; using DummyWstETH as wst_eth; +using Escrow as escrow; +using DualGovernance as dualGovernance; +using ImmutableDualGovernanceConfigProvider as config; methods { + // calls to Escrow from dualGovernance function _.getRageQuitSupport() external => DISPATCHER(true); function _.isRageQuitFinalized() external => DISPATCHER(true); - function _.MASTER_COPY() external => DISPATCHER(true); function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); - function _.initialize(address) external => DISPATCHER(true); + function _.initialize(Durations.Duration) external => DISPATCHER(true); + function _.setMinAssetsLockDuration(Durations.Duration newMinAssetsLockDuration) external => DISPATCHER(true); //envfree - function isWithdrawalsBatchesFinalized() external returns (bool) envfree; - - //calls to stEthn and wst_eth from spec - + function getRageQuitSupport() external returns (Escrow.PercentD16) envfree; + //calls to stEth and wst_eth from spec function DummyStETH.getTotalShares() external returns(uint256) envfree; function DummyStETH.totalSupply() external returns(uint256) envfree; function DummyStETH.balanceOf(address) external returns(uint256) envfree; function DummyWstETH.balanceOf(address) external returns(uint256) envfree; function DummyStETH.getPooledEthByShares(uint256) external returns (uint256) envfree; - //calls to resealMaanger are from dualGov and unrelated + //calls to resealManager are from dualGov are unrelated function _.resume(address sealable) external => NONDET; + function _.reseal(address sealable) external => NONDET; function _.reseal(address[] sealables) external => NONDET; - //calls to timelosck are from dualGov and unrelated - function _.submit(address executor, DualGovernance.ExecutorCall[] calls) external => NONDET; + //calls to timelock are from dualGov are unrelated + function _.submit(address executor, DualGovernance.ExternalCall[] calls) external => NONDET; + function _.schedule(uint256 proposalId) external => NONDET; function _.execute(uint256 proposalId) external => NONDET; function _.cancelAllNonExecutedProposals() external => NONDET; @@ -36,40 +40,104 @@ methods { function _.getProposalSubmissionTime(uint256 proposalId) external => NONDET; - - function _.getWithdrawalStatus(uint256[] _requestIds) external => NONDET; } use builtin rule sanity; /** -@title Rage Quite is a final state of the contract, i.e can not change the state +@title Ragequit is a final state of the contract, i.e can not change the state **/ -rule rageQuiteFinalState(method f) +function isRageQuitState() returns bool { + return require_uint8(currentContract._escrowState.state) == 2 /*EscrowState.State.RageQuitEscrow*/; +} +/** +@title If the state of an escrow is RageQuitEscrow, we can execute any method and it will still be in the same state afterwards +**/ +rule E_State_1_rageQuitFinalState(method f) { - Escrow.EscrowState stateBefore = currentContract._escrowState; + bool rageQuitStateBefore = isRageQuitState(); env e; calldataarg args; f(e,args); - Escrow.EscrowState stateAfter = currentContract._escrowState; + bool rageQuitStateAfter = isRageQuitState(); + + assert rageQuitStateBefore => rageQuitStateAfter ; + +} + +rule E_KP_5_rageQuitStarter(method f) +{ + bool rageQuitStateBefore = isRageQuitState(); + + env e; + calldataarg args; + f(e,args); + + bool rageQuitStateAfter = isRageQuitState(); + + assert !rageQuitStateBefore && rageQuitStateAfter => + e.msg.sender == dualGovernance; + // && + // enought support && time=> rageQuitStarted + +} + +/** @title It's not possible to lock funds in or unlock funds from an escrow that is already in the rage quit state. +locking/unlocking implies chaning the stETHLockedShares or unstETHLockedShares of an account + +this can happen only on withdrawEth +**/ + +rule E_KP_3_rageQuitNolockUnlock(method f, address holder) +{ + bool rageQuitStateBefore = isRageQuitState(); + + uint256 beforeStShares = currentContract._accounting.assets[holder].stETHLockedShares; + uint256 beforeUnStShares = currentContract._accounting.assets[holder].unstETHLockedShares; - assert stateBefore == Escrow.EscrowState.RageQuitEscrow => - stateAfter == Escrow.EscrowState.RageQuitEscrow; - + env e; + calldataarg args; + f(e,args); + assert rageQuitStateBefore => + (beforeStShares == currentContract._accounting.assets[holder].stETHLockedShares && + beforeUnStShares == currentContract._accounting.assets[holder].unstETHLockedShares ) || + f.selector == sig:withdrawETH().selector; } -/// @todo rule rageQuitNlockUnlock +/** +@title An agent cannot unlock their funds until SignallingEscrowMinLockTime has passed since this user last locked funds. +**/ +//TODO - there is a violation : https://prover.certora.com/output/40726/de13f7a8cc0a43ea9d0a2626098cb465/?anonymousKey=cd50592304ac7f689618e4afa778f954271896cd +// need to acknowledge it is ok +rule E_KP_4_unlockMinTime(method f, address holder) +{ + bool rageQuitStateBefore = isRageQuitState(); + + uint256 beforeStShares = currentContract._accounting.assets[holder].stETHLockedShares; + uint256 beforeUnStShares = currentContract._accounting.assets[holder].unstETHLockedShares; + uint256 lastTimestamp = currentContract._accounting.assets[holder].lastAssetsLockTimestamp; + env e; + calldataarg args; + f(e,args); + + uint256 min_time = currentContract._escrowState.minAssetsLockDuration; + + assert (!rageQuitStateBefore && e.block.timestamp < lastTimestamp + min_time) + => + beforeStShares <= currentContract._accounting.assets[holder].stETHLockedShares && + beforeUnStShares <= currentContract._accounting.assets[holder].unstETHLockedShares; +} /** @title once requestNextWithdrawalsBatch results in batchesQueue.close() all additional calls result in close(); **/ -rule batchesQueueCloseFinalState(method f){ +rule W2_2_batchesQueueCloseFinalState(method f){ bool startBatchesQueueStatus = isWithdrawalsBatchesFinalized(); @@ -81,45 +149,92 @@ rule batchesQueueCloseFinalState(method f){ assert startBatchesQueueStatus => nextBatchesQueueStatus; } -//todo - what else should not change be allowed; + + /** -@title when queues are closed, no change in batch list. +@title W2-2 DOS when queues are closed, no change in batch list. checked with mutation on version with issue https://prover.certora.com/output/40726/dd696d553405430aa40ae244474aa1d0/?anonymousKey=fe11fe659d51d8b9d1c1021a8ec18b9c2e6ab2a9 + **/ -rule batchesQueueCloseNoChange(method f){ +rule W2_2_batchesQueueCloseNoChange(method f){ bool finalize = isWithdrawalsBatchesFinalized(); uint256 any; - uint256 before = currentContract._batchesQueue.batches[any]; - + uint256 beforeFirst = currentContract._batchesQueue.batches[any].firstUnstETHId; + uint256 beforeSecond = currentContract._batchesQueue.batches[any].lastUnstETHId; env eF; calldataarg argsF; f(eF,argsF); - assert finalize => before == currentContract._batchesQueue.batches[any]; + assert finalize => + beforeFirst == currentContract._batchesQueue.batches[any].firstUnstETHId && + beforeSecond == currentContract._batchesQueue.batches[any].lastUnstETHId; } -invariant solvency_stETH() - stEth.getPooledEthByShares(currentContract._accounting.stETHTotals.lockedShares) <= stEth.balanceOf(currentContract) +/** +W1-1 Evading Ragequit second seal: + +@title when is ragequit the ragequit support is at least SECOND_SEAL_RAGE_QUIT_SUPPORT + +**/ + +invariant W1_1_rageQuitSupportMinValue() + isRageQuitState() => getRageQuitSupport() <= config.SECOND_SEAL_RAGE_QUIT_SUPPORT + // startRageQuit is only called from DualGoverance (rule ragequitStarter) + // and those functions are checked through dual governance + filtered { f-> f.selector != sig:startRageQuit(Durations.Duration, Durations.Duration).selector} + +/** + @title Reage quit support value + +ignoring imprecisions due to fixed-point arithmetic, the rage quit support of an escrow is equal to +(S+W+U+F) / (T+F) + where +S - is the ETH amount locked in the escrow in the form of stET + +W - is the ETH amount locked in the escrow in the form of wstETH: _accounting.stETHTotals.lockedShares +U - is the ETH amount locked in the escrow in the form of unfinalized Withdrawal NFTs: _accounting.unstETHTotals.unfinalizedShares (sum of all nft deposited) +F - is the ETH amount locked in the escrow in the form of finalized Withdrawal NFTs: _accounting.unstETHTotals.unstETHFinalizedETH (out of unstETHUnfinalizedShares ) +T - is the total supply of stETH. + **/ + + + rule E_KP_1_rageQuitSupportValue() { + // this mostly checks for overflow/underflow + mathint actual = getRageQuitSupport(); + uint256 S_W = currentContract._accounting.stETHTotals.lockedShares; + uint256 U = currentContract._accounting.unstETHTotals.unfinalizedShares; + mathint F = currentContract._accounting.unstETHTotals.finalizedETH; + mathint T = stEth.totalSupply(); + mathint expected = + ((100 * 10 ^ 16) *(stEth.getPooledEthByShares( assert_uint256(S_W + U) ) + F) ) + / ( T + F ); + assert actual == expected; + } + +/************* Solvency Rules **********/ +/************* E-KP-2 : total holding of each token ***********/ +/** +@title total holding of wst_eth is zero as all wst_eth are converted to st_eth +**/ +invariant zeroWstEthBalance() + wst_eth.balanceOf(currentContract) == 0 filtered { f -> f.contract != stEth && f.contract != wst_eth} { preserved with (env e) { require e.msg.sender != currentContract; } } - ghost mathint sumStETHLockedShares{ // assuming value zero at the initial state before constructor init_state axiom sumStETHLockedShares == 0; } - /* updated sumStETHLockedShares according to the change of a single account */ hook Sstore currentContract._accounting.assets[KEY address a].stETHLockedShares Escrow.SharesValue new_balance // the old value that balances[a] holds before the store @@ -127,11 +242,18 @@ hook Sstore currentContract._accounting.assets[KEY address a].stETHLockedShares sumStETHLockedShares = sumStETHLockedShares + new_balance - old_balance; } +hook Sload Escrow.SharesValue value currentContract._accounting.assets[KEY address a].stETHLockedShares { + require value <= sumStETHLockedShares; +} + invariant totalLockedShares() - sumStETHLockedShares == currentContract._accounting.stETHTotals.lockedShares; + sumStETHLockedShares <= currentContract._accounting.stETHTotals.lockedShares; -invariant solvency_claimedETH() - currentContract._accounting.stETHTotals.claimedETH <= nativeBalances[currentContract] + /** @title total holding of stEth before rageQuit start + **/ +invariant solvency_stETH_before_ragequit() + !isRageQuitState() => stEth.getPooledEthByShares(currentContract._accounting.stETHTotals.lockedShares + ) <= stEth.balanceOf(currentContract) filtered { f -> f.contract != stEth && f.contract != wst_eth} { preserved with (env e) { @@ -139,23 +261,35 @@ invariant solvency_claimedETH() } } +////// Nurit : From here work in progress +/* +invariant solvency_stETH_before_ragequit() + !isRageQuitState() => + currentContract._accounting.unstETHTotals.unfinalizedShares + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + } +} +*/ +invariant solvency_claimedETH() + isRageQuitState() => currentContract._accounting.stETHTotals.claimedETH * sumStETHLockedShares /*/ currentContract._accounting.stETHTotals.lockedShares*/ <= nativeBalances[currentContract] * currentContract._accounting.stETHTotals.lockedShares -rule solvency_wst_eth_test(method f) -{ - uint256 before = wst_eth.balanceOf(currentContract); - env e; - calldataarg args; - f(e,args); - uint256 after = wst_eth.balanceOf(currentContract); - assert after == before; + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + } } + + //todo - StETHAccounting.claimedETH <= nativeBalances[currentContract] // need to prove sum of balance <= self.stETHTotals.lockedShares - +/* rule change_eth(method f) { uint256 before = nativeBalances[currentContract]; @@ -175,5 +309,6 @@ rule change_st_eth(method f) uint256 after = stEth.balanceOf(currentContract); assert after == before; } - +*/ //todo - count of all unstETHIds <= withdrawalQueue.balanaceOf(currentContract) + From 385108113553e7bc31c71a49756b7ce5cfcd562b Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Thu, 29 Aug 2024 15:56:11 +0100 Subject: [PATCH 25/67] Delete extra stuff --- certora/confs/DualGovernance_sanity.conf | 37 ------ .../EmergencyActivationCommittee_sanity.conf | 13 --- .../EmergencyExecutionCommittee_sanity.conf | 13 --- .../EmergencyProtectedTimelock_sanity.conf | 22 ---- certora/confs/Escrow_sanity.conf | 37 ------ certora/confs/Executor_sanity.conf | 12 -- certora/confs/ResealManager_sanity.conf | 17 --- certora/confs/TiebreakerCore_sanity.conf | 13 --- .../confs/TiebreakerSubCommittee_sanity.conf | 17 --- .../DualGovernance_builtin_assertions.conf | 22 ---- ...nce_sanity_with_all_default_summaries.conf | 25 ----- .../DualGovernance_sanity_with_erc20cvl.conf | 25 ----- ...overnance_sanity_with_erc20dispatched.conf | 26 ----- ...ctivationCommittee_builtin_assertions.conf | 21 ---- ...tee_sanity_with_all_default_summaries.conf | 24 ---- ...ivationCommittee_sanity_with_erc20cvl.conf | 24 ---- ...Committee_sanity_with_erc20dispatched.conf | 25 ----- ...ExecutionCommittee_builtin_assertions.conf | 21 ---- ...tee_sanity_with_all_default_summaries.conf | 24 ---- ...ecutionCommittee_sanity_with_erc20cvl.conf | 24 ---- ...Committee_sanity_with_erc20dispatched.conf | 25 ----- ...yProtectedTimelock_builtin_assertions.conf | 21 ---- ...ock_sanity_with_all_default_summaries.conf | 24 ---- ...rotectedTimelock_sanity_with_erc20cvl.conf | 24 ---- ...dTimelock_sanity_with_erc20dispatched.conf | 25 ----- .../extra/Escrow_builtin_assertions.conf | 21 ---- ...row_sanity_with_all_default_summaries.conf | 24 ---- .../extra/Escrow_sanity_with_erc20cvl.conf | 24 ---- .../Escrow_sanity_with_erc20dispatched.conf | 25 ----- .../extra/Executor_builtin_assertions.conf | 20 ---- ...tor_sanity_with_all_default_summaries.conf | 23 ---- .../extra/Executor_sanity_with_erc20cvl.conf | 23 ---- .../Executor_sanity_with_erc20dispatched.conf | 24 ---- .../ResealManager_builtin_assertions.conf | 21 ---- ...ger_sanity_with_all_default_summaries.conf | 24 ---- .../ResealManager_sanity_with_erc20cvl.conf | 24 ---- ...alManager_sanity_with_erc20dispatched.conf | 25 ----- .../TiebreakerCore_builtin_assertions.conf | 21 ---- ...ore_sanity_with_all_default_summaries.conf | 24 ---- .../TiebreakerCore_sanity_with_erc20cvl.conf | 24 ---- ...eakerCore_sanity_with_erc20dispatched.conf | 25 ----- ...reakerSubCommittee_builtin_assertions.conf | 21 ---- ...tee_sanity_with_all_default_summaries.conf | 24 ---- ...akerSubCommittee_sanity_with_erc20cvl.conf | 24 ---- ...Committee_sanity_with_erc20dispatched.conf | 25 ----- certora/specs/DeX/curve.spec | 6 - certora/specs/DeX/pancakeswap.spec | 7 -- certora/specs/ERC1155/erc1155.spec | 16 --- certora/specs/ERC1967/erc1967.spec | 7 -- certora/specs/ERC20/WETHcvl.spec | 35 ------ certora/specs/ERC20/erc20cvl.spec | 68 ------------ certora/specs/ERC20/erc20dispatched.spec | 16 --- certora/specs/ERC721/erc721.spec | 9 -- certora/specs/PriceAggregators/chainlink.spec | 4 - certora/specs/PriceAggregators/tellor.spec | 3 - certora/specs/Staking/eigenlayer.spec | 15 --- certora/specs/Staking/lido.spec | 16 --- certora/specs/Staking/wrappedETH.spec | 13 --- certora/specs/generic.spec | 105 ------------------ certora/specs/setup/builtin_assertions.spec | 8 -- certora/specs/setup/sanity.spec | 13 --- .../specs/setup/sanity_DualGovernance.spec | 21 ---- certora/specs/setup/sanity_Escrow.spec | 21 ---- certora/specs/setup/sanity_Timelock.spec | 17 --- .../sanity_with_all_default_summaries.spec | 14 --- certora/specs/setup/sanity_with_erc20cvl.spec | 4 - .../setup/sanity_with_erc20dispatched.spec | 3 - 67 files changed, 1448 deletions(-) delete mode 100644 certora/confs/DualGovernance_sanity.conf delete mode 100644 certora/confs/EmergencyActivationCommittee_sanity.conf delete mode 100644 certora/confs/EmergencyExecutionCommittee_sanity.conf delete mode 100644 certora/confs/EmergencyProtectedTimelock_sanity.conf delete mode 100644 certora/confs/Escrow_sanity.conf delete mode 100644 certora/confs/Executor_sanity.conf delete mode 100644 certora/confs/ResealManager_sanity.conf delete mode 100644 certora/confs/TiebreakerCore_sanity.conf delete mode 100644 certora/confs/TiebreakerSubCommittee_sanity.conf delete mode 100644 certora/confs/extra/DualGovernance_builtin_assertions.conf delete mode 100644 certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf delete mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf delete mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf delete mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/Escrow_builtin_assertions.conf delete mode 100644 certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/Escrow_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/Executor_builtin_assertions.conf delete mode 100644 certora/confs/extra/Executor_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/Executor_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/Executor_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/ResealManager_builtin_assertions.conf delete mode 100644 certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/TiebreakerCore_builtin_assertions.conf delete mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf delete mode 100644 certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf delete mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf delete mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf delete mode 100644 certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf delete mode 100644 certora/specs/DeX/curve.spec delete mode 100644 certora/specs/DeX/pancakeswap.spec delete mode 100644 certora/specs/ERC1155/erc1155.spec delete mode 100644 certora/specs/ERC1967/erc1967.spec delete mode 100644 certora/specs/ERC20/WETHcvl.spec delete mode 100644 certora/specs/ERC20/erc20cvl.spec delete mode 100644 certora/specs/ERC20/erc20dispatched.spec delete mode 100644 certora/specs/ERC721/erc721.spec delete mode 100644 certora/specs/PriceAggregators/chainlink.spec delete mode 100644 certora/specs/PriceAggregators/tellor.spec delete mode 100644 certora/specs/Staking/eigenlayer.spec delete mode 100644 certora/specs/Staking/lido.spec delete mode 100644 certora/specs/Staking/wrappedETH.spec delete mode 100644 certora/specs/generic.spec delete mode 100644 certora/specs/setup/builtin_assertions.spec delete mode 100644 certora/specs/setup/sanity.spec delete mode 100644 certora/specs/setup/sanity_DualGovernance.spec delete mode 100644 certora/specs/setup/sanity_Escrow.spec delete mode 100644 certora/specs/setup/sanity_Timelock.spec delete mode 100644 certora/specs/setup/sanity_with_all_default_summaries.spec delete mode 100644 certora/specs/setup/sanity_with_erc20cvl.spec delete mode 100644 certora/specs/setup/sanity_with_erc20dispatched.spec diff --git a/certora/confs/DualGovernance_sanity.conf b/certora/confs/DualGovernance_sanity.conf deleted file mode 100644 index fd9dcd54..00000000 --- a/certora/confs/DualGovernance_sanity.conf +++ /dev/null @@ -1,37 +0,0 @@ -{ - "files": [ - "contracts/DualGovernance.sol", - "contracts/libraries/DualGovernanceState.sol", - "contracts/Escrow.sol", - "contracts/Configuration.sol", - "contracts/ConfigurationProvider.sol", - "contracts/EmergencyProtectedTimelock.sol", - "contracts/ResealManager.sol", - "contracts/types/Duration.sol:Durations", - "certora/harnesses/ERC20Like/DummyStETH.sol", - ], - "link": [ - "ConfigurationProvider:CONFIG=Configuration", - "DualGovernance:CONFIG=Configuration", - "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", - "EmergencyProtectedTimelock:CONFIG=Configuration", - "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", - "DualGovernance:_resealManager=ResealManager", - "Escrow:ST_ETH=DummyStETH", - ], - "struct_link": [ - "DualGovernance:rageQuitEscrow=Escrow", - "DualGovernance:signallingEscrow=Escrow", - "DualGovernance:resealManager=ResealManager", - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "loop_iter": "3", - "smt_timeout": "3600", - "verify": "DualGovernance:certora/specs/setup/sanity_DualGovernance.spec" -} \ No newline at end of file diff --git a/certora/confs/EmergencyActivationCommittee_sanity.conf b/certora/confs/EmergencyActivationCommittee_sanity.conf deleted file mode 100644 index 2b1e71f0..00000000 --- a/certora/confs/EmergencyActivationCommittee_sanity.conf +++ /dev/null @@ -1,13 +0,0 @@ -{ - "files": [ - "contracts/committees/EmergencyActivationCommittee.sol" - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity.spec" -} \ No newline at end of file diff --git a/certora/confs/EmergencyExecutionCommittee_sanity.conf b/certora/confs/EmergencyExecutionCommittee_sanity.conf deleted file mode 100644 index 08d85aba..00000000 --- a/certora/confs/EmergencyExecutionCommittee_sanity.conf +++ /dev/null @@ -1,13 +0,0 @@ -{ - "files": [ - "contracts/committees/EmergencyExecutionCommittee.sol" - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity.spec" -} \ No newline at end of file diff --git a/certora/confs/EmergencyProtectedTimelock_sanity.conf b/certora/confs/EmergencyProtectedTimelock_sanity.conf deleted file mode 100644 index 3c3af124..00000000 --- a/certora/confs/EmergencyProtectedTimelock_sanity.conf +++ /dev/null @@ -1,22 +0,0 @@ -{ - "files": [ - "contracts/EmergencyProtectedTimelock.sol", - "contracts/Configuration.sol", - "contracts/Executor.sol", - ], - "link": [ - "EmergencyProtectedTimelock:CONFIG=Configuration", - ], - "struct_link": [ - "EmergencyProtectedTimelock:executor=Executor", - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_Timelock.spec" -} \ No newline at end of file diff --git a/certora/confs/Escrow_sanity.conf b/certora/confs/Escrow_sanity.conf deleted file mode 100644 index ff021046..00000000 --- a/certora/confs/Escrow_sanity.conf +++ /dev/null @@ -1,37 +0,0 @@ -{ - "files": [ - "contracts/Escrow.sol", - "contracts/DualGovernance.sol", - "contracts/EmergencyProtectedTimelock.sol", - "contracts/libraries/DualGovernanceState.sol", - "contracts/Configuration.sol", - "contracts/ConfigurationProvider.sol", - "contracts/EmergencyProtectedTimelock.sol", -// "certora/helpers/DummyWithdrawalQueue.sol", // it causes sanity issue, the dummy needs to be fixed before it is used - "certora/harnesses/ERC20Like/DummyStETH.sol", - "contracts/types/Duration.sol:Durations", - ], - "link": [ - "DualGovernance:TIMELOCK=EmergencyProtectedTimelock", - "Escrow:_dualGovernance=DualGovernance", - "ConfigurationProvider:CONFIG=Configuration", -// "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", - "Escrow:ST_ETH=DummyStETH", - "Escrow:CONFIG=Configuration", - "DualGovernance:CONFIG=Configuration", - ], - "struct_link": [ - "DualGovernance:rageQuitEscrow=Escrow", - "DualGovernance:signallingEscrow=Escrow" - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "optimistic_fallback": true, - "loop_iter": "3", - "verify": "Escrow:certora/specs/setup/sanity_Escrow.spec" -} \ No newline at end of file diff --git a/certora/confs/Executor_sanity.conf b/certora/confs/Executor_sanity.conf deleted file mode 100644 index c3ed904c..00000000 --- a/certora/confs/Executor_sanity.conf +++ /dev/null @@ -1,12 +0,0 @@ -{ - "files": [ - "contracts/Executor.sol" - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "verify": "Executor:certora/specs/setup/sanity.spec" -} \ No newline at end of file diff --git a/certora/confs/ResealManager_sanity.conf b/certora/confs/ResealManager_sanity.conf deleted file mode 100644 index 0c4d62e9..00000000 --- a/certora/confs/ResealManager_sanity.conf +++ /dev/null @@ -1,17 +0,0 @@ -{ - "files": [ - "contracts/ResealManager.sol", - "contracts/EmergencyProtectedTimelock.sol", - ], - "link": [ - "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock" - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "verify": "ResealManager:certora/specs/setup/sanity.spec" -} \ No newline at end of file diff --git a/certora/confs/TiebreakerCore_sanity.conf b/certora/confs/TiebreakerCore_sanity.conf deleted file mode 100644 index 24d2fb66..00000000 --- a/certora/confs/TiebreakerCore_sanity.conf +++ /dev/null @@ -1,13 +0,0 @@ -{ - "files": [ - "contracts/committees/TiebreakerCore.sol" - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "verify": "TiebreakerCore:certora/specs/setup/sanity.spec" -} \ No newline at end of file diff --git a/certora/confs/TiebreakerSubCommittee_sanity.conf b/certora/confs/TiebreakerSubCommittee_sanity.conf deleted file mode 100644 index 0d75b70d..00000000 --- a/certora/confs/TiebreakerSubCommittee_sanity.conf +++ /dev/null @@ -1,17 +0,0 @@ -{ - "files": [ - "contracts/committees/TiebreakerSubCommittee.sol", - "contracts/committees/TiebreakerCore.sol", - ], - "link": [ - "TiebreakerSubCommittee:TIEBREAKER_CORE=TiebreakerCore", - ], - "msg": "sanity", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_builtin_assertions.conf b/certora/confs/extra/DualGovernance_builtin_assertions.conf deleted file mode 100644 index 566adeb5..00000000 --- a/certora/confs/extra/DualGovernance_builtin_assertions.conf +++ /dev/null @@ -1,22 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/DualGovernance.sol", - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, -// "loop_iter": "3", - "solc_via_ir": false, - "verify": "DualGovernance:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf b/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf deleted file mode 100644 index 24ec9e84..00000000 --- a/certora/confs/extra/DualGovernance_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/DualGovernance.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "loop_iter": "3", - "solc_via_ir": false, - "verify": "DualGovernance:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf b/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf deleted file mode 100644 index 26911379..00000000 --- a/certora/confs/extra/DualGovernance_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/DualGovernance.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "loop_iter": "3", - "solc_via_ir": false, - "verify": "DualGovernance:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf b/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf deleted file mode 100644 index 73b46140..00000000 --- a/certora/confs/extra/DualGovernance_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,26 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/DualGovernance.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "loop_iter": "3", - "solc_via_ir": false, - "verify": "DualGovernance:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf b/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf deleted file mode 100644 index e38009fe..00000000 --- a/certora/confs/extra/EmergencyActivationCommittee_builtin_assertions.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/committees/EmergencyActivationCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyActivationCommittee:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf deleted file mode 100644 index 0a2b260e..00000000 --- a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/EmergencyActivationCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf deleted file mode 100644 index bfbf3f71..00000000 --- a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/EmergencyActivationCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf deleted file mode 100644 index 117f617f..00000000 --- a/certora/confs/extra/EmergencyActivationCommittee_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/committees/EmergencyActivationCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyActivationCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf b/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf deleted file mode 100644 index 0da6f51c..00000000 --- a/certora/confs/extra/EmergencyExecutionCommittee_builtin_assertions.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/committees/EmergencyExecutionCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyExecutionCommittee:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf deleted file mode 100644 index 98a5ac3f..00000000 --- a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/EmergencyExecutionCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf deleted file mode 100644 index 5aea5b35..00000000 --- a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/EmergencyExecutionCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf deleted file mode 100644 index 9714a454..00000000 --- a/certora/confs/extra/EmergencyExecutionCommittee_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/committees/EmergencyExecutionCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyExecutionCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf b/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf deleted file mode 100644 index 29901693..00000000 --- a/certora/confs/extra/EmergencyProtectedTimelock_builtin_assertions.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/EmergencyProtectedTimelock.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyProtectedTimelock:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf deleted file mode 100644 index 98d79e66..00000000 --- a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/EmergencyProtectedTimelock.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf deleted file mode 100644 index 7ea22d9d..00000000 --- a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/EmergencyProtectedTimelock.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf b/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf deleted file mode 100644 index fc91c53d..00000000 --- a/certora/confs/extra/EmergencyProtectedTimelock_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/EmergencyProtectedTimelock.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "EmergencyProtectedTimelock:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_builtin_assertions.conf b/certora/confs/extra/Escrow_builtin_assertions.conf deleted file mode 100644 index 0b11e69f..00000000 --- a/certora/confs/extra/Escrow_builtin_assertions.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/Escrow.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "Escrow:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf b/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf deleted file mode 100644 index 1c086d0c..00000000 --- a/certora/confs/extra/Escrow_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/Escrow.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "Escrow:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf b/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf deleted file mode 100644 index 6193c3f5..00000000 --- a/certora/confs/extra/Escrow_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/Escrow.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "Escrow:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf b/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf deleted file mode 100644 index a7f404b0..00000000 --- a/certora/confs/extra/Escrow_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/Escrow.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "Escrow:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Executor_builtin_assertions.conf b/certora/confs/extra/Executor_builtin_assertions.conf deleted file mode 100644 index bc661b3e..00000000 --- a/certora/confs/extra/Executor_builtin_assertions.conf +++ /dev/null @@ -1,20 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/Executor.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "solc_via_ir": false, - "verify": "Executor:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf b/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf deleted file mode 100644 index c140f316..00000000 --- a/certora/confs/extra/Executor_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,23 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/Executor.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "solc_via_ir": false, - "verify": "Executor:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_erc20cvl.conf b/certora/confs/extra/Executor_sanity_with_erc20cvl.conf deleted file mode 100644 index 4d07e19b..00000000 --- a/certora/confs/extra/Executor_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,23 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/Executor.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "solc_via_ir": false, - "verify": "Executor:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf b/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf deleted file mode 100644 index eda809cf..00000000 --- a/certora/confs/extra/Executor_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/Executor.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "solc_via_ir": false, - "verify": "Executor:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_builtin_assertions.conf b/certora/confs/extra/ResealManager_builtin_assertions.conf deleted file mode 100644 index a6a71379..00000000 --- a/certora/confs/extra/ResealManager_builtin_assertions.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/ResealManager.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "ResealManager:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf b/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf deleted file mode 100644 index a716708f..00000000 --- a/certora/confs/extra/ResealManager_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/ResealManager.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "ResealManager:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf b/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf deleted file mode 100644 index 78247dd5..00000000 --- a/certora/confs/extra/ResealManager_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/ResealManager.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "ResealManager:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf b/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf deleted file mode 100644 index c55c2b47..00000000 --- a/certora/confs/extra/ResealManager_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/ResealManager.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "ResealManager:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_builtin_assertions.conf b/certora/confs/extra/TiebreakerCore_builtin_assertions.conf deleted file mode 100644 index 5a1f0fce..00000000 --- a/certora/confs/extra/TiebreakerCore_builtin_assertions.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/committees/TiebreakerCore.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerCore:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf b/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf deleted file mode 100644 index ea96237e..00000000 --- a/certora/confs/extra/TiebreakerCore_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/TiebreakerCore.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerCore:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf b/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf deleted file mode 100644 index 4ad26619..00000000 --- a/certora/confs/extra/TiebreakerCore_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/TiebreakerCore.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerCore:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf b/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf deleted file mode 100644 index 30d41a83..00000000 --- a/certora/confs/extra/TiebreakerCore_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/committees/TiebreakerCore.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerCore:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf b/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf deleted file mode 100644 index 016c8101..00000000 --- a/certora/confs/extra/TiebreakerSubCommittee_builtin_assertions.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "contracts/committees/TiebreakerSubCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "builtin_assertions", - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerSubCommittee:certora/specs/setup/builtin_assertions.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf deleted file mode 100644 index 26571a3c..00000000 --- a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_all_default_summaries.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/TiebreakerSubCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_all_default_summaries", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_all_default_summaries.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf deleted file mode 100644 index 84be27a7..00000000 --- a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20cvl.conf +++ /dev/null @@ -1,24 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyWeth.sol", - "certora/harnesses/Utilities.sol", - "contracts/committees/TiebreakerSubCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20cvl", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_erc20cvl.spec" -} \ No newline at end of file diff --git a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf b/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf deleted file mode 100644 index 79633981..00000000 --- a/certora/confs/extra/TiebreakerSubCommittee_sanity_with_erc20dispatched.conf +++ /dev/null @@ -1,25 +0,0 @@ -{ - "assert_autofinder_success": true, - "files": [ - "certora/harnesses/ERC20Like/DummyERC20A.sol", - "certora/harnesses/ERC20Like/DummyERC20B.sol", - "certora/harnesses/ERC20Like/DummyWeth.sol", - "contracts/committees/TiebreakerSubCommittee.sol" - ], - "java_args": [ - " -ea -Dlevel.setup.helpers=info" - ], - "msg": "sanity_with_erc20dispatched", - "optimistic_fallback": true, - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "process": "emv", - "prover_args": [ - " -verifyCache -verifyTACDumps -testMode -checkRuleDigest -callTraceHardFail on" - ], - "solc": "solc8.26", - "optimistic_loop": true, - "solc_via_ir": false, - "verify": "TiebreakerSubCommittee:certora/specs/setup/sanity_with_erc20dispatched.spec" -} \ No newline at end of file diff --git a/certora/specs/DeX/curve.spec b/certora/specs/DeX/curve.spec deleted file mode 100644 index d2e062f4..00000000 --- a/certora/specs/DeX/curve.spec +++ /dev/null @@ -1,6 +0,0 @@ -methods { - function _.exchange_underlying(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); - function _.exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external => HAVOC_ECF; // expect (uint256); - function _.get_virtual_price() external => NONDET; // expect (uint256); - function _.get_dy(uint256 i, iunt256 j, uint256 dx) external => NONDET; // expect (uint256); -} \ No newline at end of file diff --git a/certora/specs/DeX/pancakeswap.spec b/certora/specs/DeX/pancakeswap.spec deleted file mode 100644 index 1767b85c..00000000 --- a/certora/specs/DeX/pancakeswap.spec +++ /dev/null @@ -1,7 +0,0 @@ -methods { - // interface IPancackeV3SwapRouter - function _.WETH9() external => HAVOC_ECF; // expect (address); // xxx not marked as view but suspect it is... - function _.unwrapWETH9(uint256 amountMinimum, address recipient) external => HAVOC_ECF; // payable, expect void; - // xxx to use this, must import IPancackeV3SwapRouter - // function _.exactInputSingle(IPancackeV3SwapRouter.ExactInputSingleParams /* calldata */ params) external => HAVOC_ECF; // payable, expect (uint256 amountOut); -} \ No newline at end of file diff --git a/certora/specs/ERC1155/erc1155.spec b/certora/specs/ERC1155/erc1155.spec deleted file mode 100644 index 07948f01..00000000 --- a/certora/specs/ERC1155/erc1155.spec +++ /dev/null @@ -1,16 +0,0 @@ -methods { - function _.onERC1155Received( - address operator, - address from, - uint256 id, - uint256 value, - bytes /* calldata */ data - ) external => HAVOC_ECF; // expect (bytes4); - function _.onERC1155BatchReceived( - address operator, - address from, - uint256[] /* calldata */ ids, - uint256[] /* calldata */ values, - bytes /* calldata */ data - ) external => HAVOC_ECF; // expect (bytes4); -} \ No newline at end of file diff --git a/certora/specs/ERC1967/erc1967.spec b/certora/specs/ERC1967/erc1967.spec deleted file mode 100644 index 0bab6c78..00000000 --- a/certora/specs/ERC1967/erc1967.spec +++ /dev/null @@ -1,7 +0,0 @@ -methods { - // avoids linking messages upon upgradeToAndCall - function _._upgradeToAndCall(address,bytes,bool) external => HAVOC_ECF; - function _._upgradeToAndCallUUPS(address,bytes,bool) external => HAVOC_ECF; - // view function - function _.proxiableUUID() external => NONDET; // expect bytes32 -} diff --git a/certora/specs/ERC20/WETHcvl.spec b/certora/specs/ERC20/WETHcvl.spec deleted file mode 100644 index 8b1d8b70..00000000 --- a/certora/specs/ERC20/WETHcvl.spec +++ /dev/null @@ -1,35 +0,0 @@ -using DummyWeth as weth; // we are limited by the fact that we cannot do transfers from CVL -using Utilities as utils; - -methods { - // Utilities - function Utilities.justRevert() external envfree; - - // WETH - function _.deposit() external with (env e) => wethDeposit(calledContract, e.msg.sender, e.msg.value) expect void; - function _.withdraw(uint256 amount) external with (env e) => wethWithdraw(calledContract, e.msg.sender, amount) expect void; -} - -function wethDeposit(address target, address caller, uint256 value) { - // should be reverting if target != weth. Instead, we will use a contract to revert - if (target != weth) { - utils.justRevert(); // check this works xxx - } else { - // money will be transferred because of the payability of deposit - env e2; - require e2.msg.sender == caller; - require e2.msg.value == value; - weth.deposit(e2); - } -} - -function wethWithdraw(address target, address caller, uint256 amount) { - // should be reverting if target != weth. Instead, we will use a contract to revert - if (target != weth) { - utils.justRevert(); // check this works xxx - } else { - env e2; - require e2.msg.sender == caller; - weth.withdraw(e2, amount); - } -} \ No newline at end of file diff --git a/certora/specs/ERC20/erc20cvl.spec b/certora/specs/ERC20/erc20cvl.spec deleted file mode 100644 index 03f5a422..00000000 --- a/certora/specs/ERC20/erc20cvl.spec +++ /dev/null @@ -1,68 +0,0 @@ -methods { - // ERC20 standard - function _.name() external => NONDET; // can we use PER_CALLEE_CONSTANT? - function _.symbol() external => NONDET; // can we use PER_CALLEE_CONSTANT? - function _.decimals() external => PER_CALLEE_CONSTANT; - function _.totalSupply() external => totalSupplyCVL(calledContract) expect uint256; - function _.balanceOf(address a) external => balanceOfCVL(calledContract, a) expect uint256; - function _.allowance(address a, address b) external => allowanceCVL(calledContract, a, b) expect uint256; - function _.approve(address a, uint256 x) external with (env e) => approveCVL(calledContract, e.msg.sender, a, x) expect bool; - function _.transfer(address a, uint256 x) external with (env e) => transferCVL(calledContract, e.msg.sender, a, x) expect bool; - function _.transferFrom(address a, address b, uint256 x) external with (env e) => transferFromCVL(calledContract, e.msg.sender, a, b, x) expect bool; - -} - - -/// CVL simple implementations of IERC20: -/// token => totalSupply -ghost mapping(address => uint256) totalSupplyByToken; -/// token => account => balance -ghost mapping(address => mapping(address => uint256)) balanceByToken; -/// token => owner => spender => allowance -ghost mapping(address => mapping(address => mapping(address => uint256))) allowanceByToken; - -// function tokenBalanceOf(address token, address account) returns uint256 { -// return balanceByToken[token][account]; -// } - -function totalSupplyCVL(address token) returns uint256 { - return totalSupplyByToken[token]; -} - -function balanceOfCVL(address token, address a) returns uint256 { - return balanceByToken[token][a]; -} - -function allowanceCVL(address token, address a, address b) returns uint256 { - return allowanceByToken[token][a][b]; -} - -function approveCVL(address token, address approver, address spender, uint256 amount) returns bool { - // should be randomly reverting xxx - bool nondetSuccess; - if (!nondetSuccess) return false; - - allowanceByToken[token][approver][spender] = amount; - return true; -} - -function transferFromCVL(address token, address spender, address from, address to, uint256 amount) returns bool { - // should be randomly reverting xxx - bool nondetSuccess; - if (!nondetSuccess) return false; - - if (allowanceByToken[token][from][spender] < amount) return false; - allowanceByToken[token][from][spender] = assert_uint256(allowanceByToken[token][from][spender] - amount); - return transferCVL(token, from, to, amount); -} - -function transferCVL(address token, address from, address to, uint256 amount) returns bool { - // should be randomly reverting xxx - bool nondetSuccess; - if (!nondetSuccess) return false; - - if(balanceByToken[token][from] < amount) return false; - balanceByToken[token][from] = assert_uint256(balanceByToken[token][from] - amount); - balanceByToken[token][to] = require_uint256(balanceByToken[token][to] + amount); // We neglect overflows. - return true; -} \ No newline at end of file diff --git a/certora/specs/ERC20/erc20dispatched.spec b/certora/specs/ERC20/erc20dispatched.spec deleted file mode 100644 index c40394bb..00000000 --- a/certora/specs/ERC20/erc20dispatched.spec +++ /dev/null @@ -1,16 +0,0 @@ -methods { - // ERC20 standard - function _.name() external => DISPATCHER(true); - function _.symbol() external => DISPATCHER(true); - function _.decimals() external => DISPATCHER(true); - function _.totalSupply() external => DISPATCHER(true); - function _.balanceOf(address) external => DISPATCHER(true); - function _.allowance(address,address) external => DISPATCHER(true); - function _.approve(address,uint256) external => DISPATCHER(true); - function _.transfer(address,uint256) external => DISPATCHER(true); - function _.transferFrom(address,address,uint256) external => DISPATCHER(true); - - // WETH - function _.deposit() external => DISPATCHER(true); - function _.withdraw(uint256) external => DISPATCHER(true); -} diff --git a/certora/specs/ERC721/erc721.spec b/certora/specs/ERC721/erc721.spec deleted file mode 100644 index 474b69f3..00000000 --- a/certora/specs/ERC721/erc721.spec +++ /dev/null @@ -1,9 +0,0 @@ -methods { - // likely unsound, but assumes no callback - function _.onERC721Received( - address operator, - address from, - uint256 tokenId, - bytes data - ) external => NONDET; /* expects bytes4 */ -} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/chainlink.spec b/certora/specs/PriceAggregators/chainlink.spec deleted file mode 100644 index 889e39e6..00000000 --- a/certora/specs/PriceAggregators/chainlink.spec +++ /dev/null @@ -1,4 +0,0 @@ -methods { - function _.getRoundData(uint80) external => NONDET; - function _.latestRoundData() external => NONDET; -} \ No newline at end of file diff --git a/certora/specs/PriceAggregators/tellor.spec b/certora/specs/PriceAggregators/tellor.spec deleted file mode 100644 index 03f90c77..00000000 --- a/certora/specs/PriceAggregators/tellor.spec +++ /dev/null @@ -1,3 +0,0 @@ -methods { - function _.getTellorCurrentValue(uint256) external => NONDET; -} \ No newline at end of file diff --git a/certora/specs/Staking/eigenlayer.spec b/certora/specs/Staking/eigenlayer.spec deleted file mode 100644 index 1aa91c7f..00000000 --- a/certora/specs/Staking/eigenlayer.spec +++ /dev/null @@ -1,15 +0,0 @@ -methods { - // Strategy manager - function _.withdrawalRootPending(bytes32 _withdrawalRoot) external => NONDET; // expect (bool); - function _.numWithdrawalsQueued(address _user) external => NONDET; // expect (uint96); - function _.pauserRegistry() external => HAVOC_ECF; // expect (IPauserRegistry); - function _.paused(uint8 index) external => NONDET; // expect (bool); - function _.unpause(uint256 newPausedStatus) external => HAVOC_ECF; // expect void - - // interface IEigenPod - function _.withdrawBeforeRestaking() external => HAVOC_ECF; // expect void - - // interface IEigenPodManager - function _.getPod(address podOwner) external => NONDET; // expect address; // (IEigenPod) - function _.createPod() external => HAVOC_ECF; // expect (address); -} \ No newline at end of file diff --git a/certora/specs/Staking/lido.spec b/certora/specs/Staking/lido.spec deleted file mode 100644 index 69f4a4e3..00000000 --- a/certora/specs/Staking/lido.spec +++ /dev/null @@ -1,16 +0,0 @@ -methods { - // Lido - function _.getTotalPooledEther() external => NONDET; // expect (uint256); - function _.getTotalShares() external => NONDET; // expect (uint256); - function _.submit(address _referral) external => HAVOC_ECF; // payable, expect (uint256); - - // may be shared with other contracts XXX - function _.nonces(address _user) external => NONDET; // expect (uint256); - function _.DOMAIN_SEPARATOR() external => NONDET; // expect (bytes32); - - // Lido Withdrawal Queue - function _.MAX_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); - function _.MIN_STETH_WITHDRAWAL_AMOUNT() external => NONDET; // expect (uint256); - function _.requestWithdrawals(uint256[] /* calldata */ _amount, address _depositor) external => HAVOC_ECF; // expect (uint256[] memory); - function _.claimWithdrawals(uint256[] /* calldata */ _requestIds, uint256[] /* calldata */ _hints) external => HAVOC_ECF; // expect void; -} \ No newline at end of file diff --git a/certora/specs/Staking/wrappedETH.spec b/certora/specs/Staking/wrappedETH.spec deleted file mode 100644 index 235c82d3..00000000 --- a/certora/specs/Staking/wrappedETH.spec +++ /dev/null @@ -1,13 +0,0 @@ -import "../shared.spec"; - -using Utilities as utils; - -methods { - // e.g. cbETH (Coinbase Wrapped Staked ETH), WBETH (Wrapped Beacon ETH) - function _.mint(address _to, uint256 _amount) external => HAVOC_ECF; // expect void; - function _.exchangeRate() external => NONDET; // expect (uint256 _exchangeRate); - - // WBETH - function _.deposit(address referral) external with (env e) => pay_and_havoc(calledContract, e); // payable, expect void -} - diff --git a/certora/specs/generic.spec b/certora/specs/generic.spec deleted file mode 100644 index 22c0ad8e..00000000 --- a/certora/specs/generic.spec +++ /dev/null @@ -1,105 +0,0 @@ -/* -This rule find which functions are privileged. -A function is privileged if there is only one address that can call it. - -The rules finds this by finding which functions can be called by two different users. -*/ -rule privilegedOperation(method f, address privileged) { - env e1; - calldataarg arg; - require e1.msg.sender == privileged; - - storage initialStorage = lastStorage; - f@withrevert(e1, arg); // privileged succeeds executing candidate privileged operation. - bool firstSucceeded = !lastReverted; - - env e2; - calldataarg arg2; - require e2.msg.sender != privileged; - f@withrevert(e2, arg2) at initialStorage; // unprivileged - bool secondSucceeded = !lastReverted; - - assert !(firstSucceeded && secondSucceeded); -} - -rule timeoutChecker(method f) { - storage before = lastStorage; - env e; calldataarg arg; - f(e,arg); - assert before == lastStorage; -} - -/* -This rule find which functions that can be called, may fail due to someone else calling a function right before. - -This is n expensive rule - might fail on the demo site on big contracts -*/ -rule simpleFrontRunning(method f, address privileged) filtered { f-> !f.isView } { - env e1; - calldataarg arg; - require e1.msg.sender == privileged; - storage initialStorage = lastStorage; - f@withrevert(e1, arg); - bool firstSucceeded = !lastReverted; - env e2; - calldataarg arg2; - require e2.msg.sender != e1.msg.sender; - f(e2, arg2) at initialStorage; - f@withrevert(e1, arg); - bool succeeded = !lastReverted; - assert succeeded; -} - -rule noRevert(method f) { - env e; - calldataarg arg; - require e.msg.value == 0; - f@withrevert(e, arg); - assert !lastReverted; -} - - -rule alwaysRevert(method f) { - env e; - calldataarg arg; - f@withrevert(e, arg); - assert lastReverted; -} - -/* failing CALL should lead to a revert */ -ghost bool saw_failing_call; - -hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { - saw_failing_call = saw_failing_call || rc == 0; -} - -rule failing_CALL_leads_to_revert(method f) { - saw_failing_call = false; - env e; - calldataarg arg; - f@withrevert(e, arg); - bool reverted = lastReverted; - assert saw_failing_call => reverted; -} - -// All usages -use builtin rule sanity; -use builtin rule hasDelegateCalls; -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; - -/** - -// Integrate rules from generic.spec in importing specs like this: - -use builtin rule sanity filtered { f -> f.contract == currentContract } -use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; -use rule privilegedOperation filtered { f -> f.contract == currentContract } -use rule timeoutChecker filtered { f -> f.contract == currentContract } -use rule simpleFrontRunning filtered { f -> f.contract == currentContract } -use rule noRevert filtered { f -> f.contract == currentContract } -use rule alwaysRevert filtered { f -> f.contract == currentContract } -use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } - **/ diff --git a/certora/specs/setup/builtin_assertions.spec b/certora/specs/setup/builtin_assertions.spec deleted file mode 100644 index e6937f83..00000000 --- a/certora/specs/setup/builtin_assertions.spec +++ /dev/null @@ -1,8 +0,0 @@ -rule check_builtin_assertions(method f) - filtered { f -> f.contract == currentContract } -{ - env e; - calldataarg arg; - f(e, arg); - assert true; -} diff --git a/certora/specs/setup/sanity.spec b/certora/specs/setup/sanity.spec deleted file mode 100644 index 8ee60b15..00000000 --- a/certora/specs/setup/sanity.spec +++ /dev/null @@ -1,13 +0,0 @@ -import "../generic.spec"; - -use builtin rule sanity filtered { f -> f.contract == currentContract } - -use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; -use rule privilegedOperation filtered { f -> f.contract == currentContract } -use rule timeoutChecker filtered { f -> f.contract == currentContract } -use rule simpleFrontRunning filtered { f -> f.contract == currentContract } -use rule noRevert filtered { f -> f.contract == currentContract } -use rule alwaysRevert filtered { f -> f.contract == currentContract } -use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_DualGovernance.spec b/certora/specs/setup/sanity_DualGovernance.spec deleted file mode 100644 index 7fbf87e7..00000000 --- a/certora/specs/setup/sanity_DualGovernance.spec +++ /dev/null @@ -1,21 +0,0 @@ -import "../generic.spec"; - -methods { - function _.getRageQuitSupport() external => DISPATCHER(true); - function _.isRageQuitFinalized() external => DISPATCHER(true); - function _.MASTER_COPY() external => DISPATCHER(true); - function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); - function _.initialize(address) external => DISPATCHER(true); -} - -use builtin rule sanity filtered { f -> f.contract == currentContract } - -use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; -use rule privilegedOperation filtered { f -> f.contract == currentContract } -use rule timeoutChecker filtered { f -> f.contract == currentContract } -use rule simpleFrontRunning filtered { f -> f.contract == currentContract } -use rule noRevert filtered { f -> f.contract == currentContract } -use rule alwaysRevert filtered { f -> f.contract == currentContract } -use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_Escrow.spec b/certora/specs/setup/sanity_Escrow.spec deleted file mode 100644 index 7fbf87e7..00000000 --- a/certora/specs/setup/sanity_Escrow.spec +++ /dev/null @@ -1,21 +0,0 @@ -import "../generic.spec"; - -methods { - function _.getRageQuitSupport() external => DISPATCHER(true); - function _.isRageQuitFinalized() external => DISPATCHER(true); - function _.MASTER_COPY() external => DISPATCHER(true); - function _.startRageQuit(Durations.Duration, Durations.Duration) external => DISPATCHER(true); - function _.initialize(address) external => DISPATCHER(true); -} - -use builtin rule sanity filtered { f -> f.contract == currentContract } - -use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; -use rule privilegedOperation filtered { f -> f.contract == currentContract } -use rule timeoutChecker filtered { f -> f.contract == currentContract } -use rule simpleFrontRunning filtered { f -> f.contract == currentContract } -use rule noRevert filtered { f -> f.contract == currentContract } -use rule alwaysRevert filtered { f -> f.contract == currentContract } -use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_Timelock.spec b/certora/specs/setup/sanity_Timelock.spec deleted file mode 100644 index a05b3a43..00000000 --- a/certora/specs/setup/sanity_Timelock.spec +++ /dev/null @@ -1,17 +0,0 @@ -import "../generic.spec"; - -methods { - function _.transferOwnership(address) external => DISPATCHER(true); -} - -use builtin rule sanity filtered { f -> f.contract == currentContract } - -use builtin rule hasDelegateCalls filtered { f -> f.contract == currentContract } -use builtin rule msgValueInLoopRule; -use builtin rule viewReentrancy; -use rule privilegedOperation filtered { f -> f.contract == currentContract } -use rule timeoutChecker filtered { f -> f.contract == currentContract } -use rule simpleFrontRunning filtered { f -> f.contract == currentContract } -use rule noRevert filtered { f -> f.contract == currentContract } -use rule alwaysRevert filtered { f -> f.contract == currentContract } -use rule failing_CALL_leads_to_revert filtered { f -> f.contract == currentContract } \ No newline at end of file diff --git a/certora/specs/setup/sanity_with_all_default_summaries.spec b/certora/specs/setup/sanity_with_all_default_summaries.spec deleted file mode 100644 index 9c6f8055..00000000 --- a/certora/specs/setup/sanity_with_all_default_summaries.spec +++ /dev/null @@ -1,14 +0,0 @@ -import "../ERC20/erc20cvl.spec"; -import "../ERC20/WETHcvl.spec"; -import "../ERC721/erc721.spec"; -import "../ERC1967/erc1967.spec"; -import "../PriceAggregators/chainlink.spec"; -import "../PriceAggregators/tellor.spec"; - -import "../problems.spec"; -import "../unresolved.spec"; -import "../optimizations.spec"; - -import "../generic.spec"; // pick additional rules from here - -use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20cvl.spec b/certora/specs/setup/sanity_with_erc20cvl.spec deleted file mode 100644 index 628255b1..00000000 --- a/certora/specs/setup/sanity_with_erc20cvl.spec +++ /dev/null @@ -1,4 +0,0 @@ -import "../ERC20/erc20cvl.spec"; -import "../ERC20/WETHcvl.spec"; - -use builtin rule sanity filtered { f -> f.contract == currentContract } diff --git a/certora/specs/setup/sanity_with_erc20dispatched.spec b/certora/specs/setup/sanity_with_erc20dispatched.spec deleted file mode 100644 index 4648a1e1..00000000 --- a/certora/specs/setup/sanity_with_erc20dispatched.spec +++ /dev/null @@ -1,3 +0,0 @@ -import "../ERC20/erc20dispatched.spec"; - -use builtin rule sanity filtered { f -> f.contract == currentContract } From ebce035fb2d9fb72ab16a6f56dbcac4d03e7ba86 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Thu, 22 Aug 2024 18:12:05 +0100 Subject: [PATCH 26/67] Initial draft of w2_1a_indexes_match --- certora/confs/DualGovernance.conf | 35 +++++++++++ certora/harnesses/DualGovernanceHarness.sol | 21 +++++++ certora/harnesses/ERC20Like/DummyWstETH.sol | 68 +++++++++++++++++++++ certora/specs/DualGovernance.spec | 30 +++++++++ 4 files changed, 154 insertions(+) create mode 100644 certora/confs/DualGovernance.conf create mode 100644 certora/harnesses/DualGovernanceHarness.sol create mode 100644 certora/harnesses/ERC20Like/DummyWstETH.sol create mode 100644 certora/specs/DualGovernance.spec diff --git a/certora/confs/DualGovernance.conf b/certora/confs/DualGovernance.conf new file mode 100644 index 00000000..35c0069b --- /dev/null +++ b/certora/confs/DualGovernance.conf @@ -0,0 +1,35 @@ +{ + "files": [ + "contracts/libraries/DualGovernanceStateMachine.sol", + "contracts/Escrow.sol", + "contracts/EmergencyProtectedTimelock.sol", + "contracts/ResealManager.sol", + "contracts/types/Duration.sol:Durations", + "certora/harnesses/ERC20Like/DummyStETH.sol", + "certora/harnesses/ERC20Like/DummyWstETH.sol", + "certora/harnesses/DualGovernanceHarness.sol", + ], + "link": [ + "DualGovernanceHarness:TIMELOCK=EmergencyProtectedTimelock", + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", + "DualGovernanceHarness:RESEAL_MANAGER=ResealManager", + "Escrow:ST_ETH=DummyStETH", + "Escrow:WST_ETH=DummyWstETH", + "Escrow:DUAL_GOVERNANCE=DualGovernanceHarness" + ], + "struct_link": [ + "DualGovernanceHarness:rageQuitEscrow=Escrow", + "DualGovernanceHarness:signallingEscrow=Escrow", + "DualGovernanceHarness:resealManager=ResealManager", + ], + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "process": "emv", + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "5", + "smt_timeout": "3600", + "build_cache": true, + "verify": "DualGovernanceHarness:certora/specs/DualGovernance.spec" +} \ No newline at end of file diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol new file mode 100644 index 00000000..320dd787 --- /dev/null +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "../../contracts/libraries/Proposers.sol"; +import "../../contracts/DualGovernance.sol"; + +contract DualGovernanceHarness is DualGovernance { + using Proposers for Proposers.Context; + using Tiebreaker for Tiebreaker.Context; + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + + constructor( + ExternalDependencies memory dependencies, + SanityCheckParams memory sanityCheckParams + ) DualGovernance(dependencies, sanityCheckParams) {} + + // Return is uint32 which is the same as IndexOneBased + function getProposerIndexFromExecutor(address proposer) external view returns (uint32) { + return IndexOneBased.unwrap(_proposers.executors[proposer].proposerIndex); + } +} diff --git a/certora/harnesses/ERC20Like/DummyWstETH.sol b/certora/harnesses/ERC20Like/DummyWstETH.sol new file mode 100644 index 00000000..05d20af1 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyWstETH.sol @@ -0,0 +1,68 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +import "../../../contracts/interfaces/IWstETH.sol"; + +// Based on DummyERC20A.sol +contract DummyWstETH is IWstETH { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + // TODO wrap, unwrap, getSTETHByWstETH are the functions IWstETH adds + // and these may need better implementations + function wrap(uint256 stETHAmount) external returns (uint256) { + return stETHAmount; + } + + function unwrap(uint256 wstETHAmount) external returns (uint256) { + return wstETHAmount; + } + + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256) { + return wstethAmount; + } + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec new file mode 100644 index 00000000..a7baa6f0 --- /dev/null +++ b/certora/specs/DualGovernance.spec @@ -0,0 +1,30 @@ +methods { + function getProposer(address account) external returns (Proposers.Proposer memory) envfree; + function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; + // This is reached by Escrow.withdrawETH() and makes a lowlevel + // call on amount causing a HAVOC. This is not very relevant to these + // rules, so NONDETing + function Address.sendValue(address recipient, uint256 amount) internal => NONDET; +} +// for any registered proposer, his index should be ≤ the length of +// the array of proposers +// “for each entry in the struct in the array, show that the index inside is the same as the real array index” +// NOTE: this has not been addressed by customer, so this should fail now. +rule w2_1a_indexes_match (method f) { + env e; + calldataarg args; + Proposers.Proposer[] proposers = getProposers(e); + require proposers.length <= 5; // loop unrolling + uint256 idx; + require idx <= proposers.length; + mathint get_proposers_length = proposers.length; + address proposer_addr = proposers[idx].account; + require getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; + require getProposerIndexFromExecutor(proposer_addr) - 1 == idx; + + f(e, args); + // Strategy 1: check proposerIndex is <= proposers array length + assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; + // Strategy 2: check proposerIndex == real array index + assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; +} \ No newline at end of file From 8556e1dfe91877eddba1934830f4b9f121e7f9c8 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Thu, 22 Aug 2024 18:42:06 +0100 Subject: [PATCH 27/67] Fix some havocs in DG spec --- certora/specs/DualGovernance.spec | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index a7baa6f0..9d085e5a 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -1,10 +1,31 @@ methods { function getProposer(address account) external returns (Proposers.Proposer memory) envfree; function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; + // TODO check these NONDETs. So far they seem pretty irrelevant to the + // rules in scope for this contract. // This is reached by Escrow.withdrawETH() and makes a lowlevel - // call on amount causing a HAVOC. This is not very relevant to these - // rules, so NONDETing + // call on amount causing a HAVOC. function Address.sendValue(address recipient, uint256 amount) internal => NONDET; + // This is reached by ResealManager.reseal and makes a low-level call + // on target which havocs all contracts. (And we can't NONDET functions + // that return bytes). + function Address.functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) => CVLFunctionCallWithValue(target, data, value); + // This function belongs to ISealble which we do not have an implementation + // of and it causes a havoc of all contracts. + // It is reached by ResealManager.reaseal/resume + function _.getResumeSinceTimestamp() external => CONSTANT; + // This function belongs to IOnable which we do not have an implementation + // of and it causes a havoc of all contracts. It is reached by EPT. + // transferExecutorOwnership + function _.transferOwnership(address newOwner) external; +} + +// Ideally we would return a ghost but then we run into the tool bug +// where a ghost declared bytes is actually given type "hashblob" +// and this bug won't be fixed :) +function CVLFunctionCallWithValue(address target, bytes data, uint256 value) returns bytes { + bytes ret; + return ret; } // for any registered proposer, his index should be ≤ the length of // the array of proposers From 283645d33a0dbf6b282072897952b618b43d8238 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Fri, 23 Aug 2024 11:14:36 +0100 Subject: [PATCH 28/67] WIP, can't declare DualGovernanceStateMachine.State --- certora/confs/DualGovernance.conf | 13 ++++++++ certora/harnesses/DualGovernanceHarness.sol | 8 +++++ certora/specs/DualGovernance.spec | 37 ++++++++++++++++++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/certora/confs/DualGovernance.conf b/certora/confs/DualGovernance.conf index 35c0069b..c1ae1ead 100644 --- a/certora/confs/DualGovernance.conf +++ b/certora/confs/DualGovernance.conf @@ -2,6 +2,7 @@ "files": [ "contracts/libraries/DualGovernanceStateMachine.sol", "contracts/Escrow.sol", + "contracts/Executor.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", "contracts/types/Duration.sol:Durations", @@ -21,10 +22,22 @@ "DualGovernanceHarness:rageQuitEscrow=Escrow", "DualGovernanceHarness:signallingEscrow=Escrow", "DualGovernanceHarness:resealManager=ResealManager", + "EmergencyProtectedTimelock:executor=Executor", ], "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], + "parametric_contracts": [ + "DualGovernanceHarness", + // I get timeouts in EPT.execute and EPT.emergencyExecute + // "EmergencyProtectedTimelock", + // "ResealManager", + // // Leaving out until WithdrawalManager + // "Escrow", + // // Not sure these are needed + // "DummyStETH", + // "DummyWstETH", + ], "process": "emv", "solc": "solc8.26", "optimistic_loop": true, diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 320dd787..4610c009 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -3,9 +3,13 @@ pragma solidity 0.8.26; import "../../contracts/libraries/Proposers.sol"; import "../../contracts/DualGovernance.sol"; +// This is to make a type available for a NONDET summary +import {IExternalExecutor} from "../../contracts/interfaces/IExternalExecutor.sol"; +// import "../../contracts/libraries/DualGovernanceStateMachine.sol"; contract DualGovernanceHarness is DualGovernance { using Proposers for Proposers.Context; + using Proposers for Proposers.Proposer; using Tiebreaker for Tiebreaker.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; @@ -18,4 +22,8 @@ contract DualGovernanceHarness is DualGovernance { function getProposerIndexFromExecutor(address proposer) external view returns (uint32) { return IndexOneBased.unwrap(_proposers.executors[proposer].proposerIndex); } + + function getState() external view returns (State) { + return _stateMachine.state; + } } diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 9d085e5a..69194a1f 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -1,6 +1,9 @@ methods { + // envfrees function getProposer(address account) external returns (Proposers.Proposer memory) envfree; function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; + function getState() external returns (DualGovernanceStateMachine.State) envfree; + // TODO check these NONDETs. So far they seem pretty irrelevant to the // rules in scope for this contract. // This is reached by Escrow.withdrawETH() and makes a lowlevel @@ -17,7 +20,12 @@ methods { // This function belongs to IOnable which we do not have an implementation // of and it causes a havoc of all contracts. It is reached by EPT. // transferExecutorOwnership - function _.transferOwnership(address newOwner) external; + function _.transferOwnership(address newOwner) external => NONDET; + // This is in is reached by 2 calls in EPT and reaches a call to + // functionCallWithValue. (It may be subsumed by the summary to + // Address.functionCallWithValue) + function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; + } // Ideally we would return a ghost but then we run into the tool bug @@ -48,4 +56,31 @@ rule w2_1a_indexes_match (method f) { assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; // Strategy 2: check proposerIndex == real array index assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; +} + +// Proposals cannot be executed in the Veto Signaling (both parent state and +// Deactivation sub-state) and Rage Quit states. +rule dg_kp_1_proposal_execution { + assert false; + uint256 proposal_id; + scheduleProposal(proposalId); + DualGovernanceStateMachine.State state = getState(); + assert state != state.VetoSignaling && state != state.RageQuit; +} + +// Proposals cannot be submitted in the Veto Signaling Deactivation sub-state or in the Veto Cooldown state. +rule dg_kp_2_proposal_submission { + assert false; +} + +// If a proposal was submitted after the last time the Veto Signaling state was +// activated, then it cannot be executed in the Veto Cooldown state. +rule dg_kp_3_cooldown_execution { + assert false; +} + +// One rage quit cannot start until the previous rage quit has finalized. In +// other words, there can only be at most one active rage quit escrow at a time. +rule dg_kp_4_single_ragequit { + assert false; } \ No newline at end of file From bfd8850d5dee2b9012b2a328653032807244c5d5 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Fri, 23 Aug 2024 13:47:28 +0100 Subject: [PATCH 29/67] Workaround for type name issue. kp 1, 2 --- certora/confs/DualGovernance.conf | 1 - certora/harnesses/DualGovernanceHarness.sol | 46 +++++++++++++++++++-- certora/specs/DualGovernance.spec | 26 +++++++++--- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/certora/confs/DualGovernance.conf b/certora/confs/DualGovernance.conf index c1ae1ead..b088836c 100644 --- a/certora/confs/DualGovernance.conf +++ b/certora/confs/DualGovernance.conf @@ -5,7 +5,6 @@ "contracts/Executor.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", - "contracts/types/Duration.sol:Durations", "certora/harnesses/ERC20Like/DummyStETH.sol", "certora/harnesses/ERC20Like/DummyWstETH.sol", "certora/harnesses/DualGovernanceHarness.sol", diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 4610c009..74c2f071 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -5,7 +5,7 @@ import "../../contracts/libraries/Proposers.sol"; import "../../contracts/DualGovernance.sol"; // This is to make a type available for a NONDET summary import {IExternalExecutor} from "../../contracts/interfaces/IExternalExecutor.sol"; -// import "../../contracts/libraries/DualGovernanceStateMachine.sol"; +import {State, DualGovernanceStateMachine} from "../../contracts/libraries/DualGovernanceStateMachine.sol"; contract DualGovernanceHarness is DualGovernance { using Proposers for Proposers.Context; @@ -13,6 +13,17 @@ contract DualGovernanceHarness is DualGovernance { using Tiebreaker for Tiebreaker.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + // Needed because DualGovernanceStateMachine.State is not + // referrable without redeclaring this here. + enum DGHarnessState { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit + } + constructor( ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams @@ -23,7 +34,36 @@ contract DualGovernanceHarness is DualGovernance { return IndexOneBased.unwrap(_proposers.executors[proposer].proposerIndex); } - function getState() external view returns (State) { - return _stateMachine.state; + function asDGHarnessState(State state) public returns (DGHarnessState) { + uint256 state_underlying = uint256(state); + return DGHarnessState(state_underlying); + } + + function getState() external returns (DGHarnessState) { + return asDGHarnessState(_stateMachine.state); + } + + function isUnset(DGHarnessState state) public returns (bool) { + return state == DGHarnessState.Unset; + } + + function isNormal(DGHarnessState state) public returns (bool) { + return state == DGHarnessState.Normal; + } + + function isVetoSignalling(DGHarnessState state) public returns (bool) { + return state == DGHarnessState.VetoSignalling; + } + + function isVetoSignallingDeactivation(DGHarnessState state) public returns (bool) { + return state == DGHarnessState.VetoSignallingDeactivation; + } + + function isVetoCooldown(DGHarnessState state) public returns (bool) { + return state == DGHarnessState.VetoCooldown; + } + + function isRageQuit(DGHarnessState state) public returns (bool) { + return state == DGHarnessState.RageQuit; } } diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 69194a1f..f7e34e55 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -2,7 +2,13 @@ methods { // envfrees function getProposer(address account) external returns (Proposers.Proposer memory) envfree; function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; - function getState() external returns (DualGovernanceStateMachine.State) envfree; + function getState() external returns (DualGovernanceHarness.DGHarnessState) envfree; + function isUnset(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isNormal(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoSignalling(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoSignallingDeactivation(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoCooldown(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; // TODO check these NONDETs. So far they seem pretty irrelevant to the // rules in scope for this contract. @@ -61,16 +67,24 @@ rule w2_1a_indexes_match (method f) { // Proposals cannot be executed in the Veto Signaling (both parent state and // Deactivation sub-state) and Rage Quit states. rule dg_kp_1_proposal_execution { - assert false; + env e; uint256 proposal_id; - scheduleProposal(proposalId); - DualGovernanceStateMachine.State state = getState(); - assert state != state.VetoSignaling && state != state.RageQuit; + scheduleProposal(e, proposal_id); + DualGovernanceHarness.DGHarnessState state = getState(); + assert !isVetoSignalling(state) && !isRageQuit(state); + // This throws a type error wherein CLV claims DGHarnessState.VetoSignaling + // does not exist -- it seems like it starts to assume the type is a struct + // if you nest more than one deep + // DualGovernanceHarness.DGHarnessState.VetoSignaling && state != DualGovernanceHarness.DGHarnessState.RageQuit; } // Proposals cannot be submitted in the Veto Signaling Deactivation sub-state or in the Veto Cooldown state. rule dg_kp_2_proposal_submission { - assert false; + env e; + DualGovernanceHarness.ExternalCall[] calls; + submitProposal(e, calls); + DualGovernanceHarness.DGHarnessState state = getState(); + assert !isVetoSignallingDeactivation(state) && !isVetoCooldown(state); } // If a proposal was submitted after the last time the Veto Signaling state was From 3c7104cae6685ccadcc4c39d4e3428ecbe54acb0 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Fri, 23 Aug 2024 14:45:20 +0100 Subject: [PATCH 30/67] dg_kp_3 --- certora/harnesses/DualGovernanceHarness.sol | 13 +++++++++++++ certora/specs/DualGovernance.spec | 14 +++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 74c2f071..46a77900 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; import "../../contracts/libraries/Proposers.sol"; import "../../contracts/DualGovernance.sol"; +import {Status as ProposalStatus} from "../../contracts/libraries/ExecutableProposals.sol"; // This is to make a type available for a NONDET summary import {IExternalExecutor} from "../../contracts/interfaces/IExternalExecutor.sol"; import {State, DualGovernanceStateMachine} from "../../contracts/libraries/DualGovernanceStateMachine.sol"; @@ -34,6 +35,18 @@ contract DualGovernanceHarness is DualGovernance { return IndexOneBased.unwrap(_proposers.executors[proposer].proposerIndex); } + function getProposalInfoHarnessed(uint256 proposalId) + external + view + returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) + { + return TIMELOCK.getProposalInfo(proposalId); + } + + function getVetoSignallingActivatedAt() external view returns (Timestamp) { + return _stateMachine.vetoSignallingActivatedAt; + } + function asDGHarnessState(State state) public returns (DGHarnessState) { uint256 state_underlying = uint256(state); return DGHarnessState(state_underlying); diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index f7e34e55..de43a70f 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -9,6 +9,7 @@ methods { function isVetoSignallingDeactivation(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; function isVetoCooldown(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; // TODO check these NONDETs. So far they seem pretty irrelevant to the // rules in scope for this contract. @@ -90,7 +91,18 @@ rule dg_kp_2_proposal_submission { // If a proposal was submitted after the last time the Veto Signaling state was // activated, then it cannot be executed in the Veto Cooldown state. rule dg_kp_3_cooldown_execution { - assert false; + env e; + uint256 proposalId; + uint256 id; + ExecutableProposals.Status proposal_status; + address executor; + DualGovernanceHarness.Timestamp submittedAt; + DualGovernanceHarness.Timestamp scheduledAt; + (id, proposal_status, executor, submittedAt, scheduledAt) = + getProposalInfoHarnessed(e, proposalId); + require isVetoCooldown(getState()); + scheduleProposal(e, proposalId); + assert submittedAt < getVetoSignallingActivatedAt(); } // One rage quit cannot start until the previous rage quit has finalized. In From e6f1b3181fdbb085b3ad21323da3767ca165f5be Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Fri, 23 Aug 2024 15:27:41 +0100 Subject: [PATCH 31/67] Working on kp3 kp4 --- certora/confs/DualGovernance.conf | 2 ++ certora/helpers/EscrowA.sol | 19 +++++++++++++++++++ certora/helpers/EscrowB.sol | 19 +++++++++++++++++++ certora/specs/DualGovernance.spec | 10 ++++++++-- 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 certora/helpers/EscrowA.sol create mode 100644 certora/helpers/EscrowB.sol diff --git a/certora/confs/DualGovernance.conf b/certora/confs/DualGovernance.conf index b088836c..16a945cd 100644 --- a/certora/confs/DualGovernance.conf +++ b/certora/confs/DualGovernance.conf @@ -5,6 +5,8 @@ "contracts/Executor.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", + "certora/helpers/EscrowA.sol", + "certora/helpers/EscrowB.sol", "certora/harnesses/ERC20Like/DummyStETH.sol", "certora/harnesses/ERC20Like/DummyWstETH.sol", "certora/harnesses/DualGovernanceHarness.sol", diff --git a/certora/helpers/EscrowA.sol b/certora/helpers/EscrowA.sol new file mode 100644 index 00000000..abe135ff --- /dev/null +++ b/certora/helpers/EscrowA.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "../../contracts/Escrow.sol"; +import {State as EscrowStateInner} from "../../contracts/libraries/EscrowState.sol"; + +contract EscrowA is Escrow { + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) Escrow(stETH, wstETH, withdrawalQueue, dualGovernance, minWithdrawalsBatchSize) {} + + function isRageQuitState() external returns (bool) { + return _escrowState.state == EscrowStateInner.RageQuitEscrow; + } +} diff --git a/certora/helpers/EscrowB.sol b/certora/helpers/EscrowB.sol new file mode 100644 index 00000000..14d314bc --- /dev/null +++ b/certora/helpers/EscrowB.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import "../../contracts/Escrow.sol"; +import {State as EscrowStateInner} from "../../contracts/libraries/EscrowState.sol"; + +contract EscrowB is Escrow { + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) Escrow(stETH, wstETH, withdrawalQueue, dualGovernance, minWithdrawalsBatchSize) {} + + function isRageQuitState() external returns (bool) { + return _escrowState.state == EscrowStateInner.RageQuitEscrow; + } +} diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index de43a70f..e7fc0b34 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -1,3 +1,6 @@ +using EscrowA as EscrowA; +using EscrowB as EscrowB; + methods { // envfrees function getProposer(address account) external returns (Proposers.Proposer memory) envfree; @@ -107,6 +110,9 @@ rule dg_kp_3_cooldown_execution { // One rage quit cannot start until the previous rage quit has finalized. In // other words, there can only be at most one active rage quit escrow at a time. -rule dg_kp_4_single_ragequit { - assert false; +rule dg_kp_4_single_ragequit (method f) { + env e; + calldataarg args; + f(e, args); + assert EscrowA == EscrowB || !(EscrowA.isRageQuitState(e) && EscrowB.isRageQuitState(e)); } \ No newline at end of file From 609576b64286f74067acf71bbc5cdff043f9c8f6 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 26 Aug 2024 11:04:51 +0100 Subject: [PATCH 32/67] kp 3,4 done. better escrow setup --- certora/confs/DualGovernance.conf | 15 +++--- certora/harnesses/DualGovernanceHarness.sol | 8 +++ certora/specs/DualGovernance.spec | 58 +++++++++++++++++++-- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/certora/confs/DualGovernance.conf b/certora/confs/DualGovernance.conf index 16a945cd..779c1502 100644 --- a/certora/confs/DualGovernance.conf +++ b/certora/confs/DualGovernance.conf @@ -1,7 +1,7 @@ { "files": [ "contracts/libraries/DualGovernanceStateMachine.sol", - "contracts/Escrow.sol", + // "contracts/Escrow.sol", "contracts/Executor.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", @@ -15,13 +15,16 @@ "DualGovernanceHarness:TIMELOCK=EmergencyProtectedTimelock", "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", "DualGovernanceHarness:RESEAL_MANAGER=ResealManager", - "Escrow:ST_ETH=DummyStETH", - "Escrow:WST_ETH=DummyWstETH", - "Escrow:DUAL_GOVERNANCE=DualGovernanceHarness" + "EscrowA:ST_ETH=DummyStETH", + "EscrowA:WST_ETH=DummyWstETH", + "EscrowA:DUAL_GOVERNANCE=DualGovernanceHarness", + "EscrowB:ST_ETH=DummyStETH", + "EscrowB:WST_ETH=DummyWstETH", + "EscrowB:DUAL_GOVERNANCE=DualGovernanceHarness" ], "struct_link": [ - "DualGovernanceHarness:rageQuitEscrow=Escrow", - "DualGovernanceHarness:signallingEscrow=Escrow", + // "DualGovernanceHarness:rageQuitEscrow=Escrow", + // "DualGovernanceHarness:signallingEscrow=Escrow", "DualGovernanceHarness:resealManager=ResealManager", "EmergencyProtectedTimelock:executor=Executor", ], diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 46a77900..4d4e5903 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -56,6 +56,14 @@ contract DualGovernanceHarness is DualGovernance { return asDGHarnessState(_stateMachine.state); } + // function getStateTransition() external returns (DGHarnessState oldState, DGHarnessState newState) { + // (State oldState, State newState) = _stateMachine.getStateTransition( + // _configProvider.getDualGovernanceConfig(), + // ESCROW_MASTER_COPY + // ); + // return (asDGHarnessState(oldState), asDGHarnessState(newState)); + // } + function isUnset(DGHarnessState state) public returns (bool) { return state == DGHarnessState.Unset; } diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index e7fc0b34..3dc5b801 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -13,6 +13,17 @@ methods { function isVetoCooldown(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; + function getRageQuitEscrow() external returns (address) envfree; + + // envfrees escrow + function EscrowA.isRageQuitState() external returns (bool) envfree; + function EscrowB.isRageQuitState() external returns (bool) envfree; + + // route escrow functions to implementations while + // still allowing escrow addresses to vary + function _.startRageQuit(DualGovernanceHarness.Duration, DualGovernance.Duration) external => DISPATCHER(true); + function _.initialize(DualGovernanceHarness.Duration) external => DISPATCHER(true); + function _.setMinAssetsLockDuration(DualGovernanceHarness.Duration newMinAssetsLockDuration) external => DISPATCHER(true); // TODO check these NONDETs. So far they seem pretty irrelevant to the // rules in scope for this contract. @@ -93,7 +104,8 @@ rule dg_kp_2_proposal_submission { // If a proposal was submitted after the last time the Veto Signaling state was // activated, then it cannot be executed in the Veto Cooldown state. -rule dg_kp_3_cooldown_execution { +rule dg_kp_3_cooldown_execution (method f) { + calldataarg args; env e; uint256 proposalId; uint256 id; @@ -103,9 +115,24 @@ rule dg_kp_3_cooldown_execution { DualGovernanceHarness.Timestamp scheduledAt; (id, proposal_status, executor, submittedAt, scheduledAt) = getProposalInfoHarnessed(e, proposalId); - require isVetoCooldown(getState()); + scheduleProposal(e, proposalId); - assert submittedAt < getVetoSignallingActivatedAt(); + + // This requires affects the state that was stepped into at the start of + // the scheduleProposal call + require isVetoCooldown(getState()); + DualGovernanceHarness.Timestamp vetoSignallingActivatedAt = + getVetoSignallingActivatedAt(); + assert submittedAt <= vetoSignallingActivatedAt; +} + +function escrowAddressIsRageQuit(address escrow) returns bool { + if (escrow == EscrowA) { + return EscrowA.isRageQuitState(); + } else if (escrow == EscrowB) { + return EscrowB.isRageQuitState(); + } + return false; } // One rage quit cannot start until the previous rage quit has finalized. In @@ -113,6 +140,27 @@ rule dg_kp_3_cooldown_execution { rule dg_kp_4_single_ragequit (method f) { env e; calldataarg args; + require getRageQuitEscrow() != 0 => escrowAddressIsRageQuit(getRageQuitEscrow()); + require EscrowA == EscrowB || !(EscrowA.isRageQuitState() && EscrowB.isRageQuitState()); f(e, args); - assert EscrowA == EscrowB || !(EscrowA.isRageQuitState(e) && EscrowB.isRageQuitState(e)); -} \ No newline at end of file + assert EscrowA == EscrowB || !(EscrowA.isRageQuitState() && EscrowB.isRageQuitState()); +} + +// PP-1: Regardless of the state in which a proposal is submitted, if the +// stakers are able to amass and maintain a certain amount of rage quit +// support before the ProposalExecutionMinTimelock expires, they can extend +// the timelock for a proportional time, according to the dynamic timelock +// calculation. +// expected complexity: low + +// PP-2: It's not possible to prevent a proposal from being executed +// indefinitely without triggering a rage quit. +// expected complexity: extra high + +// PP-3: It's not possible to block proposal submission indefinitely. +// expected complexity: high + +// PP-4: Until the Veto Signaling Deactivation sub-state transitions to Veto +// Cooldown, there is always a possibility (given enough rage quit support) of +// canceling Deactivation and returning to the parent state (possibly +// triggering a rage quit immediately afterwards). \ No newline at end of file From 397272d21256ac62bdff95dc5151d0eb9fc7f758 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 26 Aug 2024 14:45:24 +0100 Subject: [PATCH 33/67] Check in WIP on PP_KP_1 --- certora/harnesses/DualGovernanceHarness.sol | 11 +++ certora/scripts/monday.sh | 2 + certora/specs/DualGovernance.spec | 75 ++++++++++++++++++--- 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 certora/scripts/monday.sh diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 4d4e5903..28101620 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -8,11 +8,16 @@ import {Status as ProposalStatus} from "../../contracts/libraries/ExecutableProp import {IExternalExecutor} from "../../contracts/interfaces/IExternalExecutor.sol"; import {State, DualGovernanceStateMachine} from "../../contracts/libraries/DualGovernanceStateMachine.sol"; +// The following two are both for isDynamicTimelockDurationPassed +import {DualGovernanceConfig} from "../../contracts/libraries/DualGovernanceConfig.sol"; +import {PercentD16} from "../../contracts/types/PercentD16.sol"; + contract DualGovernanceHarness is DualGovernance { using Proposers for Proposers.Context; using Proposers for Proposers.Proposer; using Tiebreaker for Tiebreaker.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + using DualGovernanceConfig for DualGovernanceConfig.Context; // Needed because DualGovernanceStateMachine.State is not // referrable without redeclaring this here. @@ -64,6 +69,12 @@ contract DualGovernanceHarness is DualGovernance { // return (asDGHarnessState(oldState), asDGHarnessState(newState)); // } + function isDynamicTimelockPassed(uint256 rageQuitSupport) public returns (bool) { + return _configProvider.getDualGovernanceConfig().isDynamicTimelockDurationPassed( + _stateMachine.vetoSignallingActivatedAt, PercentD16.wrap(rageQuitSupport) + ); + } + function isUnset(DGHarnessState state) public returns (bool) { return state == DGHarnessState.Unset; } diff --git a/certora/scripts/monday.sh b/certora/scripts/monday.sh new file mode 100644 index 00000000..fc97404b --- /dev/null +++ b/certora/scripts/monday.sh @@ -0,0 +1,2 @@ +#!/bin/sh +update-monday --api-key $SRC/monday_token/token.txt --board 1593099164 --job $1 \ No newline at end of file diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 3dc5b801..d9f6c968 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -15,6 +15,16 @@ methods { function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; function getRageQuitEscrow() external returns (address) envfree; + // DualGovernanceConfig summaries + function _.isFirstSealRageQuitSupportCrossed( + DualGovernanceConfig.Context memory configContext, + DualGovernanceHarness.PercentD16 rageQuitSupport) internal => + isFirstRageQuitCrossedGhost(rageQuitSupport) expect bool; + function _.isSecondSealRageQuitSupportCrossed( + DualGovernanceConfig.Context memory configContext, + DualGovernanceHarness.PercentD16 rageQuitSupport) internal => + isSecondRageQuitCrossedGhost(rageQuitSupport) expect bool; + // envfrees escrow function EscrowA.isRageQuitState() external returns (bool) envfree; function EscrowB.isRageQuitState() external returns (bool) envfree; @@ -56,6 +66,38 @@ function CVLFunctionCallWithValue(address target, bytes data, uint256 value) ret bytes ret; return ret; } + +function escrowAddressIsRageQuit(address escrow) returns bool { + if (escrow == EscrowA) { + return EscrowA.isRageQuitState(); + } else if (escrow == EscrowB) { + return EscrowB.isRageQuitState(); + } + return false; +} + +// Ghosts for support thresholds so we do not need to link +// in DualGovernanceConfig which has some nonlinear functions +ghost uint256 rageQuitFirstSealGhost { + init_state axiom rageQuitFirstSealGhost > 0; +} +ghost uint256 rageQuitSecondSealGhost { + init_state axiom rageQuitSecondSealGhost > 0 && + rageQuitFirstSealGhost < rageQuitSecondSealGhost; +} +function isFirstRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { + require rageQuitFirstSealGhost > 0; + require rageQuitSecondSealGhost > 0; + require rageQuitFirstSealGhost < rageQuitSecondSealGhost; + return rageQuitSupport > rageQuitFirstSealGhost; +} +function isSecondRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { + require rageQuitFirstSealGhost > 0; + require rageQuitSecondSealGhost > 0; + require rageQuitFirstSealGhost < rageQuitSecondSealGhost; + return rageQuitSupport > rageQuitSecondSealGhost; +} + // for any registered proposer, his index should be ≤ the length of // the array of proposers // “for each entry in the struct in the array, show that the index inside is the same as the real array index” @@ -126,15 +168,6 @@ rule dg_kp_3_cooldown_execution (method f) { assert submittedAt <= vetoSignallingActivatedAt; } -function escrowAddressIsRageQuit(address escrow) returns bool { - if (escrow == EscrowA) { - return EscrowA.isRageQuitState(); - } else if (escrow == EscrowB) { - return EscrowB.isRageQuitState(); - } - return false; -} - // One rage quit cannot start until the previous rage quit has finalized. In // other words, there can only be at most one active rage quit escrow at a time. rule dg_kp_4_single_ragequit (method f) { @@ -152,11 +185,35 @@ rule dg_kp_4_single_ragequit (method f) { // the timelock for a proportional time, according to the dynamic timelock // calculation. // expected complexity: low +rule pp_kp_1_ragequit_extends (method f) { + env e; + calldataarg args; + f(e, args); + // Note: the only two states where execution is possible are Normal + // and VetoCooldown + // assuming there is enough ragequit support and the max timelock + // has not exceeded: + // - we do not transition into normal state + // - if timelock is extended with ragequit support, we + // cannot transition into VetoCooldown + require getVetoSignallingEscrow(e) == EscrowA; + uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); + require isFirstRageQuitCrossedGhost(rageQuitSupport); + require !isDynamicTimelockPassed(e, rageQuitSupport); + // we cannot transition to normal state above first seal ragequit support + assert !isNormal(getState()); + assert !isVetoCooldown(getState()); +} +// Alternative: maybe it's more useful to just prove that dynamicDelayDuration +// is monotonically increasing with increased rageQuitSupport. // PP-2: It's not possible to prevent a proposal from being executed // indefinitely without triggering a rage quit. // expected complexity: extra high +// One option: assume rageQuitSupport == max, show secondSealRageQuit support +// is crossed. Seems trivial though. + // PP-3: It's not possible to block proposal submission indefinitely. // expected complexity: high From 72e5a936559f614b46c98d7a4b70e4ab176d9571 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Tue, 27 Aug 2024 10:25:00 +0100 Subject: [PATCH 34/67] Mainly add escrows linked spec and conf for pp1_v2 --- certora/confs/DualGovernance.conf | 7 +- .../confs/DualGovernanceEscrowsLinked.conf | 53 +++++ certora/harnesses/DualGovernanceHarness.sol | 8 + certora/specs/DualGovernance.spec | 6 + .../specs/DualGovernanceEscrowsLinked.spec | 204 ++++++++++++++++++ 5 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 certora/confs/DualGovernanceEscrowsLinked.conf create mode 100644 certora/specs/DualGovernanceEscrowsLinked.spec diff --git a/certora/confs/DualGovernance.conf b/certora/confs/DualGovernance.conf index 779c1502..a22a92e0 100644 --- a/certora/confs/DualGovernance.conf +++ b/certora/confs/DualGovernance.conf @@ -7,12 +7,14 @@ "contracts/ResealManager.sol", "certora/helpers/EscrowA.sol", "certora/helpers/EscrowB.sol", + "contracts/DualGovernanceConfigProvider.sol:ImmutableDualGovernanceConfigProvider", "certora/harnesses/ERC20Like/DummyStETH.sol", "certora/harnesses/ERC20Like/DummyWstETH.sol", "certora/harnesses/DualGovernanceHarness.sol", ], "link": [ "DualGovernanceHarness:TIMELOCK=EmergencyProtectedTimelock", + "DualGovernanceHarness:_configProvider=ImmutableDualGovernanceConfigProvider", "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", "DualGovernanceHarness:RESEAL_MANAGER=ResealManager", "EscrowA:ST_ETH=DummyStETH", @@ -23,8 +25,8 @@ "EscrowB:DUAL_GOVERNANCE=DualGovernanceHarness" ], "struct_link": [ - // "DualGovernanceHarness:rageQuitEscrow=Escrow", - // "DualGovernanceHarness:signallingEscrow=Escrow", + // "DualGovernanceHarness:rageQuitEscrow=EscrowA", + // "DualGovernanceHarness:signallingEscrow=EscrowB", "DualGovernanceHarness:resealManager=ResealManager", "EmergencyProtectedTimelock:executor=Executor", ], @@ -42,6 +44,7 @@ // "DummyStETH", // "DummyWstETH", ], + "rule_sanity": "basic", "process": "emv", "solc": "solc8.26", "optimistic_loop": true, diff --git a/certora/confs/DualGovernanceEscrowsLinked.conf b/certora/confs/DualGovernanceEscrowsLinked.conf new file mode 100644 index 00000000..938e0d66 --- /dev/null +++ b/certora/confs/DualGovernanceEscrowsLinked.conf @@ -0,0 +1,53 @@ +{ + "files": [ + "contracts/libraries/DualGovernanceStateMachine.sol", + // "contracts/Escrow.sol", + "contracts/Executor.sol", + "contracts/EmergencyProtectedTimelock.sol", + "contracts/ResealManager.sol", + "certora/helpers/EscrowA.sol", + "certora/helpers/EscrowB.sol", + "certora/harnesses/ERC20Like/DummyStETH.sol", + "certora/harnesses/ERC20Like/DummyWstETH.sol", + "certora/harnesses/DualGovernanceHarness.sol", + ], + "link": [ + "DualGovernanceHarness:TIMELOCK=EmergencyProtectedTimelock", + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", + "DualGovernanceHarness:RESEAL_MANAGER=ResealManager", + "EscrowA:ST_ETH=DummyStETH", + "EscrowA:WST_ETH=DummyWstETH", + "EscrowA:DUAL_GOVERNANCE=DualGovernanceHarness", + "EscrowB:ST_ETH=DummyStETH", + "EscrowB:WST_ETH=DummyWstETH", + "EscrowB:DUAL_GOVERNANCE=DualGovernanceHarness" + ], + "struct_link": [ + "DualGovernanceHarness:rageQuitEscrow=EscrowA", + "DualGovernanceHarness:signallingEscrow=EscrowB", + "DualGovernanceHarness:resealManager=ResealManager", + "EmergencyProtectedTimelock:executor=Executor", + ], + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "parametric_contracts": [ + "DualGovernanceHarness", + // I get timeouts in EPT.execute and EPT.emergencyExecute + // "EmergencyProtectedTimelock", + // "ResealManager", + // // Leaving out until WithdrawalManager + // "Escrow", + // // Not sure these are needed + // "DummyStETH", + // "DummyWstETH", + ], + "rule_sanity": "basic", + "process": "emv", + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "5", + "smt_timeout": "3600", + "build_cache": true, + "verify": "DualGovernanceHarness:certora/specs/DualGovernanceEscrowsLinked.spec" +} \ No newline at end of file diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 28101620..9e71e540 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -69,6 +69,14 @@ contract DualGovernanceHarness is DualGovernance { // return (asDGHarnessState(oldState), asDGHarnessState(newState)); // } + function getFirstSeal() external view returns (uint256) { + return PercentD16.unwrap(_configProvider.getDualGovernanceConfig().firstSealRageQuitSupport); + } + + function getSecondSeal() external view returns (uint256) { + return PercentD16.unwrap(_configProvider.getDualGovernanceConfig().secondSealRageQuitSupport); + } + function isDynamicTimelockPassed(uint256 rageQuitSupport) public returns (bool) { return _configProvider.getDualGovernanceConfig().isDynamicTimelockDurationPassed( _stateMachine.vetoSignallingActivatedAt, PercentD16.wrap(rageQuitSupport) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index d9f6c968..746eb0ce 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -34,6 +34,7 @@ methods { function _.startRageQuit(DualGovernanceHarness.Duration, DualGovernance.Duration) external => DISPATCHER(true); function _.initialize(DualGovernanceHarness.Duration) external => DISPATCHER(true); function _.setMinAssetsLockDuration(DualGovernanceHarness.Duration newMinAssetsLockDuration) external => DISPATCHER(true); + function _.getRageQuitSupport() external => DISPATCHER(true); // TODO check these NONDETs. So far they seem pretty irrelevant to the // rules in scope for this contract. @@ -188,6 +189,10 @@ rule dg_kp_4_single_ragequit (method f) { rule pp_kp_1_ragequit_extends (method f) { env e; calldataarg args; + // Assume not initially in VetoCooldown as we stay in this state + // unless vetoCooldownDuration has passed + require !isVetoCooldown(getState()); + f(e, args); // Note: the only two states where execution is possible are Normal // and VetoCooldown @@ -204,6 +209,7 @@ rule pp_kp_1_ragequit_extends (method f) { assert !isNormal(getState()); assert !isVetoCooldown(getState()); } + // Alternative: maybe it's more useful to just prove that dynamicDelayDuration // is monotonically increasing with increased rageQuitSupport. diff --git a/certora/specs/DualGovernanceEscrowsLinked.spec b/certora/specs/DualGovernanceEscrowsLinked.spec new file mode 100644 index 00000000..de1cea61 --- /dev/null +++ b/certora/specs/DualGovernanceEscrowsLinked.spec @@ -0,0 +1,204 @@ +using EscrowA as EscrowA; +using EscrowB as EscrowB; + +methods { + // envfrees + function getProposer(address account) external returns (Proposers.Proposer memory) envfree; + function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; + function getState() external returns (DualGovernanceHarness.DGHarnessState) envfree; + function isUnset(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isNormal(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoSignalling(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoSignallingDeactivation(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoCooldown(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; + function getRageQuitEscrow() external returns (address) envfree; + + // DualGovernanceConfig summaries + // function _.isFirstSealRageQuitSupportCrossed( + // DualGovernanceConfig.Context memory configContext, + // DualGovernanceHarness.PercentD16 rageQuitSupport) internal => + // isFirstRageQuitCrossedGhost(rageQuitSupport) expect bool; + // function _.isSecondSealRageQuitSupportCrossed( + // DualGovernanceConfig.Context memory configContext, + // DualGovernanceHarness.PercentD16 rageQuitSupport) internal => + // isSecondRageQuitCrossedGhost(rageQuitSupport) expect bool; + + function EscrowA.getRageQuitSupport() external returns (DualGovernanceHarness.PercentD16) => CVLRagequitSupport(); + function EscrowB.getRageQuitSupport() external returns (DualGovernanceHarness.PercentD16) => CVLRagequitSupport(); + + // envfrees escrow + function EscrowA.isRageQuitState() external returns (bool) envfree; + function EscrowB.isRageQuitState() external returns (bool) envfree; + + // route escrow functions to implementations while + // still allowing escrow addresses to vary + function _.startRageQuit(DualGovernanceHarness.Duration, DualGovernance.Duration) external => DISPATCHER(true); + function _.initialize(DualGovernanceHarness.Duration) external => DISPATCHER(true); + function _.setMinAssetsLockDuration(DualGovernanceHarness.Duration newMinAssetsLockDuration) external => DISPATCHER(true); + function _.getRageQuitSupport() external => DISPATCHER(true); + + // TODO check these NONDETs. So far they seem pretty irrelevant to the + // rules in scope for this contract. + // This is reached by Escrow.withdrawETH() and makes a lowlevel + // call on amount causing a HAVOC. + function Address.sendValue(address recipient, uint256 amount) internal => NONDET; + // This is reached by ResealManager.reseal and makes a low-level call + // on target which havocs all contracts. (And we can't NONDET functions + // that return bytes). + function Address.functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) => CVLFunctionCallWithValue(target, data, value); + // This function belongs to ISealble which we do not have an implementation + // of and it causes a havoc of all contracts. + // It is reached by ResealManager.reaseal/resume + function _.getResumeSinceTimestamp() external => CONSTANT; + // This function belongs to IOnable which we do not have an implementation + // of and it causes a havoc of all contracts. It is reached by EPT. + // transferExecutorOwnership + function _.transferOwnership(address newOwner) external => NONDET; + // This is in is reached by 2 calls in EPT and reaches a call to + // functionCallWithValue. (It may be subsumed by the summary to + // Address.functionCallWithValue) + function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; + +} + +// Ideally we would return a ghost but then we run into the tool bug +// where a ghost declared bytes is actually given type "hashblob" +// and this bug won't be fixed :) +function CVLFunctionCallWithValue(address target, bytes data, uint256 value) returns bytes { + bytes ret; + return ret; +} + +// function escrowAddressIsRageQuit(address escrow) returns bool { +// if (escrow == EscrowA) { +// return EscrowA.isRageQuitState(); +// } else if (escrow == EscrowB) { +// return EscrowB.isRageQuitState(); +// } +// return false; +// } + +// Ghosts for support thresholds so we do not need to link +// in DualGovernanceConfig which has some nonlinear functions +// ghost uint256 rageQuitFirstSealGhost { +// init_state axiom rageQuitFirstSealGhost > 0; +// } +// ghost uint256 rageQuitSecondSealGhost { +// init_state axiom rageQuitSecondSealGhost > 0 && +// rageQuitFirstSealGhost < rageQuitSecondSealGhost; +// } +// function isFirstRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { +// require rageQuitFirstSealGhost > 0; +// require rageQuitSecondSealGhost > 0; +// require rageQuitFirstSealGhost < rageQuitSecondSealGhost; +// return rageQuitSupport > rageQuitFirstSealGhost; +// } +// function isSecondRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { +// require rageQuitFirstSealGhost > 0; +// require rageQuitSecondSealGhost > 0; +// require rageQuitFirstSealGhost < rageQuitSecondSealGhost; +// return rageQuitSupport > rageQuitSecondSealGhost; +// } + +persistent ghost uint256 ghost_ragequit_support; +function CVLRagequitSupport() returns uint256 { + return ghost_ragequit_support; +} + +// PP-1: Regardless of the state in which a proposal is submitted, if the +// stakers are able to amass and maintain a certain amount of rage quit +// support before the ProposalExecutionMinTimelock expires, they can extend +// the timelock for a proportional time, according to the dynamic timelock +// calculation. +// expected complexity: low +rule pp_kp_1_ragequit_extends_v2 { + // Get proposal in submitted state + env e1; + uint256 proposalId; + uint256 id; + ExecutableProposals.Status proposal_status; + address executor; + DualGovernanceHarness.Timestamp submittedAt; + DualGovernanceHarness.Timestamp scheduledAt; + (id, proposal_status, executor, submittedAt, scheduledAt) = + getProposalInfoHarnessed(e1, proposalId); + // state is Submitted + require assert_uint8(proposal_status) == 1; + require submittedAt < e1.block.timestamp; + + // setup different rage quits for different executions + uint256 firstSeal = getFirstSeal(e1); + uint256 secondSeal = getSecondSeal(e1); + require firstSeal > 0; + require secondSeal > firstSeal; + + // 2 rageQuitSupport values both between the first and second seal + uint256 rageQuitSupport1; + uint256 rageQuitSupport2; + require rageQuitSupport1 > firstSeal && rageQuitSupport1 < secondSeal; + require rageQuitSupport2 > firstSeal && rageQuitSupport2 < secondSeal; + require rageQuitSupport1 > rageQuitSupport2; + + + storage initialState = lastStorage; + + // NOTE: I think the way this works I will get a sanity + // failure by requesting 2 different values from getRageQuitSupport + // instead I can move this to a separate file and use a summary to make + // getRageQuitSupport return a ghost and then impose the requirements on + // the ghost. + + // Execution 1: set R = rageQuitSupport1 then: + // - take a state step from an arbitrary later timestamp + // - advance to an arbitrary later timestamp after that and schedule + env ex1_step; + // Here assuming EscrowA is vetoSignalling Escrow + // require EscrowA.getRageQuitSupport(ex1_step) == rageQuitSupport1; + ghost_ragequit_support = rageQuitSupport1; + require ex1_step.block.timestamp > e1.block.timestamp; + // advance state + activateNextState(ex1_step); + env ex1_schedule; + require ex1_schedule.block.timestamp > ex1_step.block.timestamp; + scheduleProposal@withrevert(ex1_schedule, proposalId); + bool ex1_sched_reverted = lastReverted; + + // Execution 2: similar to Execution 1 but with R=rageQuitSupport2 + env ex2_step; + // Here assuming EscrowA is vetoSignalling Escrow + // require EscrowA.getRageQuitSupport(ex2_step) at initialState == rageQuitSupport2; + ghost_ragequit_support = rageQuitSupport2; + require ex2_step.block.timestamp > e1.block.timestamp; + // advance state + activateNextState(ex2_step) at initialState; + env ex2_schedule; + require ex2_schedule.block.timestamp > ex2_step.block.timestamp; + scheduleProposal@withrevert(ex2_schedule, proposalId); + bool ex2_sched_reverted = lastReverted; + + // the Good one: + // satisfy ex1_sched_reverted && !ex2_sched_reverted; + // the bad one: + satisfy !ex1_sched_reverted && ex2_sched_reverted; + +} + +// Alternative: maybe it's more useful to just prove that dynamicDelayDuration +// is monotonically increasing with increased rageQuitSupport. + +// PP-2: It's not possible to prevent a proposal from being executed +// indefinitely without triggering a rage quit. +// expected complexity: extra high + +// One option: assume rageQuitSupport == max, show secondSealRageQuit support +// is crossed. Seems trivial though. + +// PP-3: It's not possible to block proposal submission indefinitely. +// expected complexity: high + +// PP-4: Until the Veto Signaling Deactivation sub-state transitions to Veto +// Cooldown, there is always a possibility (given enough rage quit support) of +// canceling Deactivation and returning to the parent state (possibly +// triggering a rage quit immediately afterwards). \ No newline at end of file From 4af07313741099ced6b4f4202c97104fe0c42f99 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Tue, 27 Aug 2024 11:03:32 +0100 Subject: [PATCH 35/67] WIP on pp2, pp1 --- certora/specs/DualGovernance.spec | 32 ++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 746eb0ce..24f04d58 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -35,6 +35,8 @@ methods { function _.initialize(DualGovernanceHarness.Duration) external => DISPATCHER(true); function _.setMinAssetsLockDuration(DualGovernanceHarness.Duration newMinAssetsLockDuration) external => DISPATCHER(true); function _.getRageQuitSupport() external => DISPATCHER(true); + function _.isRageQuitFinalized() external => DISPATCHER(true); + // TODO check these NONDETs. So far they seem pretty irrelevant to the // rules in scope for this contract. @@ -186,14 +188,14 @@ rule dg_kp_4_single_ragequit (method f) { // the timelock for a proportional time, according to the dynamic timelock // calculation. // expected complexity: low -rule pp_kp_1_ragequit_extends (method f) { +rule pp_kp_1_ragequit_extends { env e; - calldataarg args; // Assume not initially in VetoCooldown as we stay in this state // unless vetoCooldownDuration has passed require !isVetoCooldown(getState()); - f(e, args); + activateNextState(e); + // Note: the only two states where execution is possible are Normal // and VetoCooldown // assuming there is enough ragequit support and the max timelock @@ -216,6 +218,30 @@ rule pp_kp_1_ragequit_extends (method f) { // PP-2: It's not possible to prevent a proposal from being executed // indefinitely without triggering a rage quit. // expected complexity: extra high +rule pp_kp_2_ragequit_triggers { + env e; + calldataarg args; + + require getVetoSignallingEscrow(e) == EscrowA; + uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); + require rageQuitFirstSealGhost > 0; + require rageQuitSecondSealGhost > rageQuitFirstSealGhost; + // Max out the rageQuitSupport to try to cause the greatest delay + require rageQuitSupport == max_uint256; + + // Assume we have waited long enough if in VetoSignalling + // (Check if this is actually needed) + require isDynamicTimelockPassed(e, rageQuitSupport); + + DualGovernanceHarness.DGHarnessState old_state = getState(); + activateNextState(e); + DualGovernanceHarness.DGHarnessState new_state = getState(); + + // Show that from normal state we step towards RageQuit + assert isNormal(old_state) => isVetoSignalling(new_state); + assert isVetoSignalling(old_state) => isRageQuit(new_state); + +} // One option: assume rageQuitSupport == max, show secondSealRageQuit support // is crossed. Seems trivial though. From 9b53ac3f8f6a2d38d2bcb6ddf180a04c27ef81eb Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Tue, 27 Aug 2024 11:41:27 +0200 Subject: [PATCH 36/67] PP_4 --- certora/specs/DualGovernance.spec | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 24f04d58..2011edc7 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -252,4 +252,18 @@ rule pp_kp_2_ragequit_triggers { // PP-4: Until the Veto Signaling Deactivation sub-state transitions to Veto // Cooldown, there is always a possibility (given enough rage quit support) of // canceling Deactivation and returning to the parent state (possibly -// triggering a rage quit immediately afterwards). \ No newline at end of file +// triggering a rage quit immediately afterwards). +rule pp_kp_4_veto_signalling_deactivation_cancellable() { + env e; + require getVetoSignallingEscrow(e) == EscrowA; + uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); + require isVetoSignallingDeactivation(getState()); + require isSecondRageQuitCrossedGhost(rageQuitSupport); + activateNextState(e); + + // the only way out of veto signalling deactivation that does not go back to the parent is veto cooldown, + // so if we can't go here, there is no way to bypass the rage quit support + assert !isVetoCooldown(getState()); + // and we also don't want to be stuck in deactivation, but have a way back to the parent + satisfy isVetoSignalling(getState()); +} \ No newline at end of file From 52ef49775c11013c87e1bc40854939a71410ed82 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Tue, 27 Aug 2024 12:52:01 +0100 Subject: [PATCH 37/67] pp_kp_2 passing --- certora/specs/DualGovernance.spec | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 2011edc7..a40e8b68 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -218,7 +218,7 @@ rule pp_kp_1_ragequit_extends { // PP-2: It's not possible to prevent a proposal from being executed // indefinitely without triggering a rage quit. // expected complexity: extra high -rule pp_kp_2_ragequit_triggers { +rule pp_kp_2_ragequit_trigger { env e; calldataarg args; @@ -226,11 +226,10 @@ rule pp_kp_2_ragequit_triggers { uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); require rageQuitFirstSealGhost > 0; require rageQuitSecondSealGhost > rageQuitFirstSealGhost; - // Max out the rageQuitSupport to try to cause the greatest delay - require rageQuitSupport == max_uint256; + // have large ragequit support to try to maximize delay + require rageQuitSupport > rageQuitSecondSealGhost; // Assume we have waited long enough if in VetoSignalling - // (Check if this is actually needed) require isDynamicTimelockPassed(e, rageQuitSupport); DualGovernanceHarness.DGHarnessState old_state = getState(); @@ -240,11 +239,10 @@ rule pp_kp_2_ragequit_triggers { // Show that from normal state we step towards RageQuit assert isNormal(old_state) => isVetoSignalling(new_state); assert isVetoSignalling(old_state) => isRageQuit(new_state); - } // One option: assume rageQuitSupport == max, show secondSealRageQuit support -// is crossed. Seems trivial though. +// is crossed. // PP-3: It's not possible to block proposal submission indefinitely. // expected complexity: high From 12897ef3b358f9b5742a58db6eefca914257dd9b Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Tue, 27 Aug 2024 16:21:21 +0200 Subject: [PATCH 38/67] PP-3 --- certora/harnesses/DualGovernanceHarness.sol | 14 +++++++++++++- certora/specs/DualGovernance.spec | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 9e71e540..451c4bc9 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -40,7 +40,9 @@ contract DualGovernanceHarness is DualGovernance { return IndexOneBased.unwrap(_proposers.executors[proposer].proposerIndex); } - function getProposalInfoHarnessed(uint256 proposalId) + function getProposalInfoHarnessed( + uint256 proposalId + ) external view returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) @@ -83,6 +85,16 @@ contract DualGovernanceHarness is DualGovernance { ); } + function isVetoSignallingDeactivationMaxDurationPassed() public returns (bool) { + return _configProvider.getDualGovernanceConfig().isVetoSignallingDeactivationMaxDurationPassed( + _stateMachine.enteredAt + ); + } + + function isVetoCooldownDurationPassed() public returns (bool) { + return _configProvider.getDualGovernanceConfig().isVetoCooldownDurationPassed(_stateMachine.enteredAt); + } + function isUnset(DGHarnessState state) public returns (bool) { return state == DGHarnessState.Unset; } diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index a40e8b68..22dbcd36 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -246,6 +246,22 @@ rule pp_kp_2_ragequit_trigger { // PP-3: It's not possible to block proposal submission indefinitely. // expected complexity: high +rule pp_kp_3_no_indefinite_proposal_submission_block { + env e; + + require getVetoSignallingEscrow(e) == EscrowA; + uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); + // Assume we have waited long enough + require isVetoSignallingDeactivationMaxDurationPassed(e) && isVetoCooldownDurationPassed(e); + + DualGovernanceHarness.DGHarnessState old_state = getState(); + activateNextState(e); + DualGovernanceHarness.DGHarnessState new_state = getState(); + + // Show that from any state in which proposal submission is disallowed, we must step on given our waiting time + assert isVetoCooldown(old_state) => isNormal(new_state) || isVetoSignalling(new_state); + assert isVetoSignallingDeactivation(old_state) => isVetoCooldown(new_state) || isVetoSignalling(new_state) || isRageQuit(new_state); +} // PP-4: Until the Veto Signaling Deactivation sub-state transitions to Veto // Cooldown, there is always a possibility (given enough rage quit support) of From 28f4d5e8520dcf1e607010a80089a934853e9df6 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Tue, 27 Aug 2024 18:09:55 +0200 Subject: [PATCH 39/67] dg_states_1 and dg_states_2 --- certora/specs/DualGovernance.spec | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 22dbcd36..56eab4f1 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -280,4 +280,28 @@ rule pp_kp_4_veto_signalling_deactivation_cancellable() { assert !isVetoCooldown(getState()); // and we also don't want to be stuck in deactivation, but have a way back to the parent satisfy isVetoSignalling(getState()); +} + +// If proposal submission succeeds, the system was in on of these states: Normal, Veto Signalling, Rage Quit +rule dg_states_1_proposal_submission_states() { + env e; + calldataarg args; + submitProposal(e, args); + // we take the state after and not before, because state transitions are triggered at the start of actions, + // not at the end of the ones that caused them to become possible + DualGovernanceHarness.DGHarnessState state = getState(); + + assert isNormal(state) || isVetoSignalling(state) || isRageQuit(state); +} + +// If proposal scheduling succeeds, the system was in one of these states: Normal, Veto Cooldown +rule dg_states_2_proposal_scheduling_states() { + env e; + calldataarg args; + scheduleProposal(e, args); + // we take the state after and not before, because state transitions are triggered at the start of actions, + // not at the end of the ones that caused them to become possible + DualGovernanceHarness.DGHarnessState state = getState(); + + assert isNormal(state) || isVetoCooldown(state); } \ No newline at end of file From a2108feb627dd29590c15b9689ef86156e05285f Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Wed, 28 Aug 2024 10:17:23 +0200 Subject: [PATCH 40/67] dg_transitions_1 --- certora/specs/DualGovernance.spec | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 56eab4f1..09a0cf63 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -304,4 +304,20 @@ rule dg_states_2_proposal_scheduling_states() { DualGovernanceHarness.DGHarnessState state = getState(); assert isNormal(state) || isVetoCooldown(state); +} + +// Only specified transitions are possible +rule dg_transitions_1_only_legal_transitions() { + env e; + DualGovernanceHarness.DGHarnessState old_state = getState(); + activateNextState(e); + DualGovernanceHarness.DGHarnessState new_state = getState(); + // we are not interested in the cases where no transition happened + require old_state != new_state; + + assert isNormal(new_state) => isVetoCooldown(old_state); + assert isVetoSignalling(new_state) => isNormal(old_state) || isVetoCooldown(old_state) || isVetoSignallingDeactivation(old_state) || isRageQuit(old_state); + assert isRageQuit(new_state) => isVetoSignalling(old_state) || isVetoSignallingDeactivation(old_state); + assert isVetoCooldown(new_state) => isRageQuit(old_state) || isVetoSignallingDeactivation(old_state); + assert isVetoSignallingDeactivation(new_state) => isVetoSignalling(old_state); } \ No newline at end of file From 6a2127a2f0be320d6954511ba5a9188ffadc8d09 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Wed, 28 Aug 2024 12:22:15 +0100 Subject: [PATCH 41/67] Update ERC20 dummys --- .../ERC20Like/DummyERC20MintBurn.sol | 61 +++++++++ certora/harnesses/ERC20Like/DummyStETH.sol | 15 +-- certora/harnesses/ERC20Like/DummyWstETH.sol | 127 +++++++++--------- certora/specs/DualGovernance.spec | 2 + 4 files changed, 136 insertions(+), 69 deletions(-) create mode 100644 certora/harnesses/ERC20Like/DummyERC20MintBurn.sol diff --git a/certora/harnesses/ERC20Like/DummyERC20MintBurn.sol b/certora/harnesses/ERC20Like/DummyERC20MintBurn.sol new file mode 100644 index 00000000..ae30daa9 --- /dev/null +++ b/certora/harnesses/ERC20Like/DummyERC20MintBurn.sol @@ -0,0 +1,61 @@ +// Represents a symbolic/dummy ERC20 token + +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.0; + +contract DummyERC20MintBurn { + uint256 t; + mapping(address => uint256) b; + mapping(address => mapping(address => uint256)) a; + + string public name; + string public symbol; + uint256 public decimals; + + function myAddress() external view returns (address) { + return address(this); + } + + function totalSupply() external view returns (uint256) { + return t; + } + + function balanceOf(address account) external view returns (uint256) { + return b[account]; + } + + function _mint(address to, uint256 amount) internal { + b[to] += amount; + t += amount; + } + + function _burn(address to, uint256 amount) internal { + b[to] -= amount; + t -= amount; + } + + function transfer(address recipient, uint256 amount) external returns (bool) { + b[msg.sender] -= amount; + b[recipient] += amount; + + return true; + } + + function allowance(address owner, address spender) external view returns (uint256) { + return a[owner][spender]; + } + + function approve(address spender, uint256 amount) external returns (bool) { + a[msg.sender][spender] = amount; + + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { + b[sender] -= amount; + b[recipient] += amount; + a[sender][msg.sender] -= amount; + + return true; + } +} diff --git a/certora/harnesses/ERC20Like/DummyStETH.sol b/certora/harnesses/ERC20Like/DummyStETH.sol index 523b6130..020b6ebd 100644 --- a/certora/harnesses/ERC20Like/DummyStETH.sol +++ b/certora/harnesses/ERC20Like/DummyStETH.sol @@ -12,11 +12,11 @@ contract DummyStETH is IStETH { } function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { - return ethAmount * 5 / 3; + return ethAmount * 3 / 5; } function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256) { - return sharesAmount * 3 / 5; + return sharesAmount * 5 / 3; } function transferShares(address to, uint256 amount) external { @@ -31,9 +31,8 @@ contract DummyStETH is IStETH { address _recipient, uint256 _sharesAmount ) external returns (uint256) { - // uint256 tokensAmount = getPooledEthByShares(_sharesAmount); - uint256 tokensAmount = _sharesAmount * 3 / 5; - _spendAllowance(_sender, msg.sender, tokensAmount); + uint256 tokensAmount = _sharesAmount * 5 / 3; + _spendAllowance(_sender, msg.sender, _sharesAmount); _transferShares(_sender, _recipient, _sharesAmount); return tokensAmount; } @@ -62,7 +61,7 @@ contract DummyStETH is IStETH { } function totalSupply() external view returns (uint256) { - return totalShares * 3 / 5; + return totalShares * 5 / 3; } function approve(address _spender, uint256 _amount) external returns (bool) { @@ -76,7 +75,7 @@ contract DummyStETH is IStETH { function balanceOf(address _account) external view returns (uint256) { // return getPooledEthByShares(_sharesOf(_account)); - return _sharesOf(_account) * 3 / 5; + return _sharesOf(_account) * 5 / 3; } function _sharesOf(address account) internal view returns (uint256) { @@ -84,7 +83,7 @@ contract DummyStETH is IStETH { } function _transfer(address sender, address recipient, uint256 amount) internal { - uint256 sharesToTransfer = amount * 5 / 3; + uint256 sharesToTransfer = amount * 3 / 5; _transferShares(sender, recipient, sharesToTransfer); } diff --git a/certora/harnesses/ERC20Like/DummyWstETH.sol b/certora/harnesses/ERC20Like/DummyWstETH.sol index 05d20af1..dafd6337 100644 --- a/certora/harnesses/ERC20Like/DummyWstETH.sol +++ b/certora/harnesses/ERC20Like/DummyWstETH.sol @@ -1,68 +1,73 @@ -// Represents a symbolic/dummy ERC20 token - -// SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.8.0; - -import "../../../contracts/interfaces/IWstETH.sol"; - -// Based on DummyERC20A.sol -contract DummyWstETH is IWstETH { - uint256 t; - mapping(address => uint256) b; - mapping(address => mapping(address => uint256)) a; - - string public name; - string public symbol; - uint256 public decimals; - - // TODO wrap, unwrap, getSTETHByWstETH are the functions IWstETH adds - // and these may need better implementations - function wrap(uint256 stETHAmount) external returns (uint256) { - return stETHAmount; +// SPDX-FileCopyrightText: 2021 Lido + +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity ^0.8.26; + +import "./DummyERC20MintBurn.sol"; + +import "../../../contracts/interfaces/IStETH.sol"; + +/** + * @title StETH token wrapper with static balances. + * @dev It's an ERC20 token that represents the account's share of the total + * supply of stETH tokens. WstETH token's balance only changes on transfers, + * unlike StETH that is also changed when oracles report staking rewards and + * penalties. It's a "power user" token for DeFi protocols which don't + * support rebasable tokens. + * + * The contract is also a trustless wrapper that accepts stETH tokens and mints + * wstETH in return. Then the user unwraps, the contract burns user's wstETH + * and sends user locked stETH in return. + * + * The contract provides the staking shortcut: user can send ETH with regular + * transfer and get wstETH in return. The contract will send ETH to Lido submit + * method, staking it and wrapping the received stETH. + * + */ +contract DummyWstETH is DummyERC20MintBurn { + IStETH public stETH; + + /** + * @param _stETH address of the StETH token to wrap + */ + constructor(IStETH _stETH) { + stETH = _stETH; } - function unwrap(uint256 wstETHAmount) external returns (uint256) { + /** + * @notice Exchanges stETH to wstETH + * @param _stETHAmount amount of stETH to wrap in exchange for wstETH + * @dev Requirements: + * - `_stETHAmount` must be non-zero + * - msg.sender must approve at least `_stETHAmount` stETH to this + * contract. + * - msg.sender must have at least `_stETHAmount` of stETH. + * User should first approve _stETHAmount to the WstETH contract + * @return Amount of wstETH user receives after wrap + */ + function wrap(uint256 _stETHAmount) external returns (uint256) { + require(_stETHAmount > 0, "wstETH: can't wrap zero stETH"); + uint256 wstETHAmount = stETH.getSharesByPooledEth(_stETHAmount); + _mint(msg.sender, wstETHAmount); + stETH.transferFrom(msg.sender, address(this), _stETHAmount); return wstETHAmount; } - function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256) { - return wstethAmount; - } - - function myAddress() external view returns (address) { - return address(this); - } - - function totalSupply() external view returns (uint256) { - return t; - } - - function balanceOf(address account) external view returns (uint256) { - return b[account]; - } - - function transfer(address recipient, uint256 amount) external returns (bool) { - b[msg.sender] -= amount; - b[recipient] += amount; - - return true; - } - - function allowance(address owner, address spender) external view returns (uint256) { - return a[owner][spender]; - } - - function approve(address spender, uint256 amount) external returns (bool) { - a[msg.sender][spender] = amount; - - return true; - } - - function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { - b[sender] -= amount; - b[recipient] += amount; - a[sender][msg.sender] -= amount; - - return true; + /** + * @notice Exchanges wstETH to stETH + * @param _wstETHAmount amount of wstETH to uwrap in exchange for stETH + * @dev Requirements: + * - `_wstETHAmount` must be non-zero + * - msg.sender must have at least `_wstETHAmount` wstETH. + * @return Amount of stETH user receives after unwrap + */ + function unwrap(uint256 _wstETHAmount) external returns (uint256) { + require(_wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); + uint256 stETHAmount = stETH.getPooledEthByShares(_wstETHAmount); + _burn(msg.sender, _wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); + return stETHAmount; } } diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 09a0cf63..6af574e1 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -227,6 +227,7 @@ rule pp_kp_2_ragequit_trigger { require rageQuitFirstSealGhost > 0; require rageQuitSecondSealGhost > rageQuitFirstSealGhost; // have large ragequit support to try to maximize delay + // TODO relax this assumption require rageQuitSupport > rageQuitSecondSealGhost; // Assume we have waited long enough if in VetoSignalling @@ -239,6 +240,7 @@ rule pp_kp_2_ragequit_trigger { // Show that from normal state we step towards RageQuit assert isNormal(old_state) => isVetoSignalling(new_state); assert isVetoSignalling(old_state) => isRageQuit(new_state); + // TODO consider old_state is deactivation } // One option: assume rageQuitSupport == max, show secondSealRageQuit support From 3b2e72d9d194e559fac610068bbb9d483ca21b63 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Wed, 28 Aug 2024 16:11:58 +0100 Subject: [PATCH 42/67] improve pp_kp_2 --- certora/harnesses/DualGovernanceHarness.sol | 19 +++++++++++++---- certora/specs/DualGovernance.spec | 23 ++++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index 451c4bc9..e9651f08 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -8,9 +8,10 @@ import {Status as ProposalStatus} from "../../contracts/libraries/ExecutableProp import {IExternalExecutor} from "../../contracts/interfaces/IExternalExecutor.sol"; import {State, DualGovernanceStateMachine} from "../../contracts/libraries/DualGovernanceStateMachine.sol"; -// The following two are both for isDynamicTimelockDurationPassed +// The following are for methods about checking if max durations have passed import {DualGovernanceConfig} from "../../contracts/libraries/DualGovernanceConfig.sol"; import {PercentD16} from "../../contracts/types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../../contracts/types/Timestamp.sol"; contract DualGovernanceHarness is DualGovernance { using Proposers for Proposers.Context; @@ -40,9 +41,7 @@ contract DualGovernanceHarness is DualGovernance { return IndexOneBased.unwrap(_proposers.executors[proposer].proposerIndex); } - function getProposalInfoHarnessed( - uint256 proposalId - ) + function getProposalInfoHarnessed(uint256 proposalId) external view returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) @@ -85,6 +84,18 @@ contract DualGovernanceHarness is DualGovernance { ); } + function isVetoSignallingReactivationPassed() public returns (bool) { + return _configProvider.getDualGovernanceConfig().isVetoSignallingReactivationDurationPassed( + Timestamps.max(_stateMachine.vetoSignallingReactivationTime, _stateMachine.vetoSignallingActivatedAt) + ); + } + + function isVetoSignallingDeactivationPassed() public returns (bool) { + return _configProvider.getDualGovernanceConfig().isVetoSignallingDeactivationMaxDurationPassed( + _stateMachine.enteredAt + ); + } + function isVetoSignallingDeactivationMaxDurationPassed() public returns (bool) { return _configProvider.getDualGovernanceConfig().isVetoSignallingDeactivationMaxDurationPassed( _stateMachine.enteredAt diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 6af574e1..17d21afb 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -227,20 +227,33 @@ rule pp_kp_2_ragequit_trigger { require rageQuitFirstSealGhost > 0; require rageQuitSecondSealGhost > rageQuitFirstSealGhost; // have large ragequit support to try to maximize delay - // TODO relax this assumption - require rageQuitSupport > rageQuitSecondSealGhost; + require rageQuitSupport > rageQuitFirstSealGhost; + // Assumptions about waiting long enough: // Assume we have waited long enough if in VetoSignalling require isDynamicTimelockPassed(e, rageQuitSupport); + // Assume we wait enough time for deactivation if needed + require isVetoSignallingReactivationPassed(e); + // Assume we have waited enough time to exit deactivation if needed + require isVetoSignallingDeactivationPassed(e); DualGovernanceHarness.DGHarnessState old_state = getState(); activateNextState(e); DualGovernanceHarness.DGHarnessState new_state = getState(); - // Show that from normal state we step towards RageQuit + // from normal we eventually make forward progress into veto signalling assert isNormal(old_state) => isVetoSignalling(new_state); - assert isVetoSignalling(old_state) => isRageQuit(new_state); - // TODO consider old_state is deactivation + // from veto signalling we either make forward progress + // into rageQuit or vetoSignallingDeactivation + // (and we show forward progress is eventually made + // from vetoSignallingDeactivation) + assert isVetoSignalling(old_state) => + isRageQuit(new_state) || isVetoSignallingDeactivation(new_state); + // From VetoSignallingDeactivation we make forward progress + // into rageQuit or vetoSignallingCooldown + // (and proposal execution is possible from cooldown) + assert isVetoSignallingDeactivation(old_state) => + isRageQuit(new_state) || isVetoCooldown(new_state); } // One option: assume rageQuitSupport == max, show secondSealRageQuit support From 2536050e6464a087d279395d7bebfaf980cc6430 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Thu, 29 Aug 2024 15:47:34 +0100 Subject: [PATCH 43/67] Initial mutation testing commit --- certora/mutation/conf/DualGovernance.conf | 70 ++++ .../DualGovernance-NoScheduleCheck.sol | 331 +++++++++++++++++ .../DualGovernance-NoSubmitCheck.sol | 331 +++++++++++++++++ .../DualGovernanceFindingW2-1.sol | 336 ++++++++++++++++++ ...overnanceStateMachine-BadScheduleCheck.sol | 266 ++++++++++++++ ...lGovernanceStateMachine-BadSubmitCheck.sol | 266 ++++++++++++++ ...teMachine-BadVetoCooldownDurationCheck.sol | 267 ++++++++++++++ ...StateMachine-DeactivateAndBypassParent.sol | 265 ++++++++++++++ ...anceStateMachine-IndefiniteWNoRagequit.sol | 266 ++++++++++++++ ...ceStateMachine-MultipleRageQuitEscrows.sol | 269 ++++++++++++++ ...ernanceStateMachine-RagequitFromNormal.sol | 268 ++++++++++++++ ...ceStateMachine-SubmitCheckBadTimestamp.sol | 266 ++++++++++++++ .../Proposers/ProposersFindingW2-1.sol | 173 +++++++++ certora/specs/DualGovernance.spec | 6 - 14 files changed, 3374 insertions(+), 6 deletions(-) create mode 100644 certora/mutation/conf/DualGovernance.conf create mode 100644 certora/mutation/mutants/DualGovernance/DualGovernance-NoScheduleCheck.sol create mode 100644 certora/mutation/mutants/DualGovernance/DualGovernance-NoSubmitCheck.sol create mode 100644 certora/mutation/mutants/DualGovernance/DualGovernanceFindingW2-1.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadScheduleCheck.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadSubmitCheck.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-DeactivateAndBypassParent.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-IndefiniteWNoRagequit.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-RagequitFromNormal.sol create mode 100644 certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol create mode 100644 certora/mutation/mutants/Proposers/ProposersFindingW2-1.sol diff --git a/certora/mutation/conf/DualGovernance.conf b/certora/mutation/conf/DualGovernance.conf new file mode 100644 index 00000000..ff089d35 --- /dev/null +++ b/certora/mutation/conf/DualGovernance.conf @@ -0,0 +1,70 @@ +{ + "files": [ + "contracts/libraries/DualGovernanceStateMachine.sol", + // "contracts/Escrow.sol", + "contracts/Executor.sol", + "contracts/EmergencyProtectedTimelock.sol", + "contracts/ResealManager.sol", + "certora/helpers/EscrowA.sol", + "certora/helpers/EscrowB.sol", + "contracts/DualGovernanceConfigProvider.sol:ImmutableDualGovernanceConfigProvider", + "certora/harnesses/ERC20Like/DummyStETH.sol", + "certora/harnesses/ERC20Like/DummyWstETH.sol", + "certora/harnesses/DualGovernanceHarness.sol", + ], + "link": [ + "DualGovernanceHarness:TIMELOCK=EmergencyProtectedTimelock", + "DualGovernanceHarness:_configProvider=ImmutableDualGovernanceConfigProvider", + "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", + "DualGovernanceHarness:RESEAL_MANAGER=ResealManager", + "EscrowA:ST_ETH=DummyStETH", + "EscrowA:WST_ETH=DummyWstETH", + "EscrowA:DUAL_GOVERNANCE=DualGovernanceHarness", + "EscrowB:ST_ETH=DummyStETH", + "EscrowB:WST_ETH=DummyWstETH", + "EscrowB:DUAL_GOVERNANCE=DualGovernanceHarness" + ], + "struct_link": [ + // "DualGovernanceHarness:rageQuitEscrow=EscrowA", + // "DualGovernanceHarness:signallingEscrow=EscrowB", + "DualGovernanceHarness:resealManager=ResealManager", + "EmergencyProtectedTimelock:executor=Executor", + ], + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "parametric_contracts": [ + "DualGovernanceHarness", + // "EmergencyProtectedTimelock", + // "ResealManager", + // // Leaving out until WithdrawalManager + // "Escrow", + // // Not sure these are needed + // "DummyStETH", + // "DummyWstETH", + ], + "rule_sanity": "basic", + "process": "emv", + "solc": "solc8.26", + "optimistic_loop": true, + "loop_iter": "5", + "smt_timeout": "3600", + "build_cache": true, + "verify": "DualGovernanceHarness:certora/specs/DualGovernance.spec", + "mutations": { + "manual_mutants": [ + { + "file_to_mutate": "contracts/DualGovernance.sol", + "mutants_location": "certora/mutation/mutants/DualGovernance" + }, + { + "file_to_mutate": "contracts/libraries/Proposers.sol", + "mutants_location": "certora/mutation/mutants/Proposers" + }, + { + "file_to_mutate": "contracts/libraries/DualGovernanceStateMachine.sol", + "mutants_location": "certora/mutation/mutants/DualGovernanceStateMachine" + }, + ] + } +} \ No newline at end of file diff --git a/certora/mutation/mutants/DualGovernance/DualGovernance-NoScheduleCheck.sol b/certora/mutation/mutants/DualGovernance/DualGovernance-NoScheduleCheck.sol new file mode 100644 index 00000000..564b898a --- /dev/null +++ b/certora/mutation/mutants/DualGovernance/DualGovernance-NoScheduleCheck.sol @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; + +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; + +import {Proposers} from "./libraries/Proposers.sol"; +import {Tiebreaker} from "./libraries/Tiebreaker.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; +import {IDualGovernanceConfigProvider} from "./DualGovernanceConfigProvider.sol"; + +import {Escrow} from "./Escrow.sol"; + +contract DualGovernance is IDualGovernance { + using Proposers for Proposers.Context; + using Tiebreaker for Tiebreaker.Context; + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + + // --- + // Errors + // --- + + error NotAdminProposer(); + error UnownedAdminExecutor(); + error CallerIsNotResealCommittee(address caller); + error CallerIsNotAdminExecutor(address caller); + error InvalidConfigProvider(IDualGovernanceConfigProvider configProvider); + error ProposalSubmissionBlocked(); + error ProposalSchedulingBlocked(uint256 proposalId); + error ResealIsNotAllowedInNormalState(); + + // --- + // Events + // --- + + event EscrowMasterCopyDeployed(address escrowMasterCopy); + event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); + + // --- + // Tiebreaker Sanity Check Param Immutables + // --- + + struct SanityCheckParams { + uint256 minWithdrawalsBatchSize; + Duration minTiebreakerActivationTimeout; + Duration maxTiebreakerActivationTimeout; + uint256 maxSealableWithdrawalBlockersCount; + } + + Duration public immutable MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + Duration public immutable MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + uint256 public immutable MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + + // --- + // External Parts Immutables + + struct ExternalDependencies { + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + ITimelock timelock; + IResealManager resealManager; + IDualGovernanceConfigProvider configProvider; + } + + ITimelock public immutable TIMELOCK; + IResealManager public immutable RESEAL_MANAGER; + address public immutable ESCROW_MASTER_COPY; + + // --- + // Aspects + // --- + + Proposers.Context internal _proposers; + Tiebreaker.Context internal _tiebreaker; + DualGovernanceStateMachine.Context internal _stateMachine; + + // --- + // Standalone State Variables + // --- + IDualGovernanceConfigProvider internal _configProvider; + address internal _resealCommittee; + + constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { + TIMELOCK = dependencies.timelock; + RESEAL_MANAGER = dependencies.resealManager; + + MIN_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.minTiebreakerActivationTimeout; + MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; + MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; + + _setConfigProvider(dependencies.configProvider); + + ESCROW_MASTER_COPY = address( + new Escrow({ + dualGovernance: this, + stETH: dependencies.stETH, + wstETH: dependencies.wstETH, + withdrawalQueue: dependencies.withdrawalQueue, + minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize + }) + ); + emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); + + _stateMachine.initialize(dependencies.configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + } + + // --- + // Proposals Flow + // --- + + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + if (!_stateMachine.canSubmitProposal()) { + revert ProposalSubmissionBlocked(); + } + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); + proposalId = TIMELOCK.submit(proposer.executor, calls); + } + + function scheduleProposal(uint256 proposalId) external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + // MUTATION + // Comment out following lines + // if (!_stateMachine.canScheduleProposal(submittedAt)) { + // revert ProposalSchedulingBlocked(proposalId); + // } + TIMELOCK.schedule(proposalId); + } + + function cancelAllPendingProposals() external { + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); + if (proposer.executor != TIMELOCK.getAdminExecutor()) { + revert NotAdminProposer(); + } + TIMELOCK.cancelAllNonExecutedProposals(); + } + + function canSubmitProposal() public view returns (bool) { + return _stateMachine.canSubmitProposal(); + } + + function canScheduleProposal(uint256 proposalId) external view returns (bool) { + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); + } + + // --- + // Dual Governance State + // --- + + function activateNextState() external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + } + + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { + _checkCallerIsAdminExecutor(); + _setConfigProvider(newConfigProvider); + + /// @dev the minAssetsLockDuration is kept as a storage variable in the signalling Escrow instance + /// to sync the new value with current signalling escrow, it's value must be manually updated + _stateMachine.signallingEscrow.setMinAssetsLockDuration( + newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration + ); + } + + function getConfigProvider() external view returns (IDualGovernanceConfigProvider) { + return _configProvider; + } + + function getVetoSignallingEscrow() external view returns (address) { + return address(_stateMachine.signallingEscrow); + } + + function getRageQuitEscrow() external view returns (address) { + return address(_stateMachine.rageQuitEscrow); + } + + function getCurrentState() external view returns (State currentState) { + currentState = _stateMachine.getCurrentState(); + } + + function getCurrentStateContext() external view returns (DualGovernanceStateMachine.Context memory) { + return _stateMachine.getCurrentContext(); + } + + function getDynamicDelayDuration() external view returns (Duration) { + return _stateMachine.getDynamicDelayDuration(_configProvider.getDualGovernanceConfig()); + } + + // --- + // Proposers & Executors Management + // --- + + function registerProposer(address proposer, address executor) external { + _checkCallerIsAdminExecutor(); + _proposers.register(proposer, executor); + } + + function unregisterProposer(address proposer) external { + _checkCallerIsAdminExecutor(); + _proposers.unregister(proposer); + + /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer + if (!_proposers.isExecutor(TIMELOCK.getAdminExecutor())) { + revert UnownedAdminExecutor(); + } + } + + function isProposer(address account) external view returns (bool) { + return _proposers.isProposer(account); + } + + function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { + proposer = _proposers.getProposer(account); + } + + function getProposers() external view returns (Proposers.Proposer[] memory proposers) { + proposers = _proposers.getAllProposers(); + } + + function isExecutor(address account) external view returns (bool) { + return _proposers.isExecutor(account); + } + + // --- + // Tiebreaker Protection + // --- + + function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); + } + + function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); + } + + function setTiebreakerCommittee(address tiebreakerCommittee) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); + } + + function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerActivationTimeout( + MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT + ); + } + + function tiebreakerResumeSealable(address sealable) external { + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + RESEAL_MANAGER.resume(sealable); + } + + function tiebreakerScheduleProposal(uint256 proposalId) external { + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + TIMELOCK.schedule(proposalId); + } + + struct TiebreakerState { + address tiebreakerCommittee; + Duration tiebreakerActivationTimeout; + address[] sealableWithdrawalBlockers; + } + + function getTiebreakerState() external view returns (TiebreakerState memory tiebreakerState) { + ( + tiebreakerState.tiebreakerCommittee, + tiebreakerState.tiebreakerActivationTimeout, + tiebreakerState.sealableWithdrawalBlockers + ) = _tiebreaker.getTiebreakerInfo(); + } + + // --- + // Reseal executor + // --- + + function resealSealable(address sealable) external { + if (msg.sender != _resealCommittee) { + revert CallerIsNotResealCommittee(msg.sender); + } + if (_stateMachine.getCurrentState() == State.Normal) { + revert ResealIsNotAllowedInNormalState(); + } + RESEAL_MANAGER.reseal(sealable); + } + + function setResealCommittee(address resealCommittee) external { + _checkCallerIsAdminExecutor(); + _resealCommittee = resealCommittee; + } + + // --- + // Private methods + // --- + + function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { + if (address(newConfigProvider) == address(0)) { + revert InvalidConfigProvider(newConfigProvider); + } + + if (newConfigProvider == _configProvider) { + return; + } + + _configProvider = IDualGovernanceConfigProvider(newConfigProvider); + emit ConfigProviderSet(newConfigProvider); + } + + function _checkCallerIsAdminExecutor() internal view { + if (TIMELOCK.getAdminExecutor() != msg.sender) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/DualGovernance/DualGovernance-NoSubmitCheck.sol b/certora/mutation/mutants/DualGovernance/DualGovernance-NoSubmitCheck.sol new file mode 100644 index 00000000..279e33ba --- /dev/null +++ b/certora/mutation/mutants/DualGovernance/DualGovernance-NoSubmitCheck.sol @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; + +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; + +import {Proposers} from "./libraries/Proposers.sol"; +import {Tiebreaker} from "./libraries/Tiebreaker.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; +import {IDualGovernanceConfigProvider} from "./DualGovernanceConfigProvider.sol"; + +import {Escrow} from "./Escrow.sol"; + +contract DualGovernance is IDualGovernance { + using Proposers for Proposers.Context; + using Tiebreaker for Tiebreaker.Context; + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + + // --- + // Errors + // --- + + error NotAdminProposer(); + error UnownedAdminExecutor(); + error CallerIsNotResealCommittee(address caller); + error CallerIsNotAdminExecutor(address caller); + error InvalidConfigProvider(IDualGovernanceConfigProvider configProvider); + error ProposalSubmissionBlocked(); + error ProposalSchedulingBlocked(uint256 proposalId); + error ResealIsNotAllowedInNormalState(); + + // --- + // Events + // --- + + event EscrowMasterCopyDeployed(address escrowMasterCopy); + event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); + + // --- + // Tiebreaker Sanity Check Param Immutables + // --- + + struct SanityCheckParams { + uint256 minWithdrawalsBatchSize; + Duration minTiebreakerActivationTimeout; + Duration maxTiebreakerActivationTimeout; + uint256 maxSealableWithdrawalBlockersCount; + } + + Duration public immutable MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + Duration public immutable MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + uint256 public immutable MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + + // --- + // External Parts Immutables + + struct ExternalDependencies { + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + ITimelock timelock; + IResealManager resealManager; + IDualGovernanceConfigProvider configProvider; + } + + ITimelock public immutable TIMELOCK; + IResealManager public immutable RESEAL_MANAGER; + address public immutable ESCROW_MASTER_COPY; + + // --- + // Aspects + // --- + + Proposers.Context internal _proposers; + Tiebreaker.Context internal _tiebreaker; + DualGovernanceStateMachine.Context internal _stateMachine; + + // --- + // Standalone State Variables + // --- + IDualGovernanceConfigProvider internal _configProvider; + address internal _resealCommittee; + + constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { + TIMELOCK = dependencies.timelock; + RESEAL_MANAGER = dependencies.resealManager; + + MIN_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.minTiebreakerActivationTimeout; + MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; + MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; + + _setConfigProvider(dependencies.configProvider); + + ESCROW_MASTER_COPY = address( + new Escrow({ + dualGovernance: this, + stETH: dependencies.stETH, + wstETH: dependencies.wstETH, + withdrawalQueue: dependencies.withdrawalQueue, + minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize + }) + ); + emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); + + _stateMachine.initialize(dependencies.configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + } + + // --- + // Proposals Flow + // --- + + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + // MUTATION: + // Comment out following lines: + // if (!_stateMachine.canSubmitProposal()) { + // revert ProposalSubmissionBlocked(); + // } + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); + proposalId = TIMELOCK.submit(proposer.executor, calls); + } + + function scheduleProposal(uint256 proposalId) external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + if (!_stateMachine.canScheduleProposal(submittedAt)) { + revert ProposalSchedulingBlocked(proposalId); + } + TIMELOCK.schedule(proposalId); + } + + function cancelAllPendingProposals() external { + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); + if (proposer.executor != TIMELOCK.getAdminExecutor()) { + revert NotAdminProposer(); + } + TIMELOCK.cancelAllNonExecutedProposals(); + } + + function canSubmitProposal() public view returns (bool) { + return _stateMachine.canSubmitProposal(); + } + + function canScheduleProposal(uint256 proposalId) external view returns (bool) { + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); + } + + // --- + // Dual Governance State + // --- + + function activateNextState() external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + } + + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { + _checkCallerIsAdminExecutor(); + _setConfigProvider(newConfigProvider); + + /// @dev the minAssetsLockDuration is kept as a storage variable in the signalling Escrow instance + /// to sync the new value with current signalling escrow, it's value must be manually updated + _stateMachine.signallingEscrow.setMinAssetsLockDuration( + newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration + ); + } + + function getConfigProvider() external view returns (IDualGovernanceConfigProvider) { + return _configProvider; + } + + function getVetoSignallingEscrow() external view returns (address) { + return address(_stateMachine.signallingEscrow); + } + + function getRageQuitEscrow() external view returns (address) { + return address(_stateMachine.rageQuitEscrow); + } + + function getCurrentState() external view returns (State currentState) { + currentState = _stateMachine.getCurrentState(); + } + + function getCurrentStateContext() external view returns (DualGovernanceStateMachine.Context memory) { + return _stateMachine.getCurrentContext(); + } + + function getDynamicDelayDuration() external view returns (Duration) { + return _stateMachine.getDynamicDelayDuration(_configProvider.getDualGovernanceConfig()); + } + + // --- + // Proposers & Executors Management + // --- + + function registerProposer(address proposer, address executor) external { + _checkCallerIsAdminExecutor(); + _proposers.register(proposer, executor); + } + + function unregisterProposer(address proposer) external { + _checkCallerIsAdminExecutor(); + _proposers.unregister(proposer); + + /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer + if (!_proposers.isExecutor(TIMELOCK.getAdminExecutor())) { + revert UnownedAdminExecutor(); + } + } + + function isProposer(address account) external view returns (bool) { + return _proposers.isProposer(account); + } + + function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { + proposer = _proposers.getProposer(account); + } + + function getProposers() external view returns (Proposers.Proposer[] memory proposers) { + proposers = _proposers.getAllProposers(); + } + + function isExecutor(address account) external view returns (bool) { + return _proposers.isExecutor(account); + } + + // --- + // Tiebreaker Protection + // --- + + function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); + } + + function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); + } + + function setTiebreakerCommittee(address tiebreakerCommittee) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); + } + + function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerActivationTimeout( + MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT + ); + } + + function tiebreakerResumeSealable(address sealable) external { + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + RESEAL_MANAGER.resume(sealable); + } + + function tiebreakerScheduleProposal(uint256 proposalId) external { + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + TIMELOCK.schedule(proposalId); + } + + struct TiebreakerState { + address tiebreakerCommittee; + Duration tiebreakerActivationTimeout; + address[] sealableWithdrawalBlockers; + } + + function getTiebreakerState() external view returns (TiebreakerState memory tiebreakerState) { + ( + tiebreakerState.tiebreakerCommittee, + tiebreakerState.tiebreakerActivationTimeout, + tiebreakerState.sealableWithdrawalBlockers + ) = _tiebreaker.getTiebreakerInfo(); + } + + // --- + // Reseal executor + // --- + + function resealSealable(address sealable) external { + if (msg.sender != _resealCommittee) { + revert CallerIsNotResealCommittee(msg.sender); + } + if (_stateMachine.getCurrentState() == State.Normal) { + revert ResealIsNotAllowedInNormalState(); + } + RESEAL_MANAGER.reseal(sealable); + } + + function setResealCommittee(address resealCommittee) external { + _checkCallerIsAdminExecutor(); + _resealCommittee = resealCommittee; + } + + // --- + // Private methods + // --- + + function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { + if (address(newConfigProvider) == address(0)) { + revert InvalidConfigProvider(newConfigProvider); + } + + if (newConfigProvider == _configProvider) { + return; + } + + _configProvider = IDualGovernanceConfigProvider(newConfigProvider); + emit ConfigProviderSet(newConfigProvider); + } + + function _checkCallerIsAdminExecutor() internal view { + if (TIMELOCK.getAdminExecutor() != msg.sender) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/DualGovernance/DualGovernanceFindingW2-1.sol b/certora/mutation/mutants/DualGovernance/DualGovernanceFindingW2-1.sol new file mode 100644 index 00000000..6899d6eb --- /dev/null +++ b/certora/mutation/mutants/DualGovernance/DualGovernanceFindingW2-1.sol @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; + +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; + +import {Proposers} from "./libraries/Proposers.sol"; +import {Tiebreaker} from "./libraries/Tiebreaker.sol"; +import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; +import {IDualGovernanceConfigProvider} from "./DualGovernanceConfigProvider.sol"; + +import {Escrow} from "./Escrow.sol"; + +// MUTATION +// At time of writing there is actually no mutation. This mutant +// is a placeholder saving version of DualGovernance when +// finding W2-1 was identified. At time of writing this +// finding has not yet been fixed. The finding is also affected +// by Proposers.sol, so a placeholder is kept for that as well. + +contract DualGovernance is IDualGovernance { + using Proposers for Proposers.Context; + using Tiebreaker for Tiebreaker.Context; + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + + // --- + // Errors + // --- + + error NotAdminProposer(); + error UnownedAdminExecutor(); + error CallerIsNotResealCommittee(address caller); + error CallerIsNotAdminExecutor(address caller); + error InvalidConfigProvider(IDualGovernanceConfigProvider configProvider); + error ProposalSubmissionBlocked(); + error ProposalSchedulingBlocked(uint256 proposalId); + error ResealIsNotAllowedInNormalState(); + + // --- + // Events + // --- + + event EscrowMasterCopyDeployed(address escrowMasterCopy); + event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); + + // --- + // Tiebreaker Sanity Check Param Immutables + // --- + + struct SanityCheckParams { + uint256 minWithdrawalsBatchSize; + Duration minTiebreakerActivationTimeout; + Duration maxTiebreakerActivationTimeout; + uint256 maxSealableWithdrawalBlockersCount; + } + + Duration public immutable MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + Duration public immutable MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + uint256 public immutable MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + + // --- + // External Parts Immutables + + struct ExternalDependencies { + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + ITimelock timelock; + IResealManager resealManager; + IDualGovernanceConfigProvider configProvider; + } + + ITimelock public immutable TIMELOCK; + IResealManager public immutable RESEAL_MANAGER; + address public immutable ESCROW_MASTER_COPY; + + // --- + // Aspects + // --- + + Proposers.Context internal _proposers; + Tiebreaker.Context internal _tiebreaker; + DualGovernanceStateMachine.Context internal _stateMachine; + + // --- + // Standalone State Variables + // --- + IDualGovernanceConfigProvider internal _configProvider; + address internal _resealCommittee; + + constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { + TIMELOCK = dependencies.timelock; + RESEAL_MANAGER = dependencies.resealManager; + + MIN_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.minTiebreakerActivationTimeout; + MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; + MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; + + _setConfigProvider(dependencies.configProvider); + + ESCROW_MASTER_COPY = address( + new Escrow({ + dualGovernance: this, + stETH: dependencies.stETH, + wstETH: dependencies.wstETH, + withdrawalQueue: dependencies.withdrawalQueue, + minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize + }) + ); + emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); + + _stateMachine.initialize(dependencies.configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + } + + // --- + // Proposals Flow + // --- + + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + if (!_stateMachine.canSubmitProposal()) { + revert ProposalSubmissionBlocked(); + } + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); + proposalId = TIMELOCK.submit(proposer.executor, calls); + } + + function scheduleProposal(uint256 proposalId) external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + if (!_stateMachine.canScheduleProposal(submittedAt)) { + revert ProposalSchedulingBlocked(proposalId); + } + TIMELOCK.schedule(proposalId); + } + + function cancelAllPendingProposals() external { + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); + if (proposer.executor != TIMELOCK.getAdminExecutor()) { + revert NotAdminProposer(); + } + TIMELOCK.cancelAllNonExecutedProposals(); + } + + function canSubmitProposal() public view returns (bool) { + return _stateMachine.canSubmitProposal(); + } + + function canScheduleProposal(uint256 proposalId) external view returns (bool) { + ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = + TIMELOCK.getProposalInfo(proposalId); + return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); + } + + // --- + // Dual Governance State + // --- + + function activateNextState() external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + } + + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { + _checkCallerIsAdminExecutor(); + _setConfigProvider(newConfigProvider); + + /// @dev the minAssetsLockDuration is kept as a storage variable in the signalling Escrow instance + /// to sync the new value with current signalling escrow, it's value must be manually updated + _stateMachine.signallingEscrow.setMinAssetsLockDuration( + newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration + ); + } + + function getConfigProvider() external view returns (IDualGovernanceConfigProvider) { + return _configProvider; + } + + function getVetoSignallingEscrow() external view returns (address) { + return address(_stateMachine.signallingEscrow); + } + + function getRageQuitEscrow() external view returns (address) { + return address(_stateMachine.rageQuitEscrow); + } + + function getCurrentState() external view returns (State currentState) { + currentState = _stateMachine.getCurrentState(); + } + + function getCurrentStateContext() external view returns (DualGovernanceStateMachine.Context memory) { + return _stateMachine.getCurrentContext(); + } + + function getDynamicDelayDuration() external view returns (Duration) { + return _stateMachine.getDynamicDelayDuration(_configProvider.getDualGovernanceConfig()); + } + + // --- + // Proposers & Executors Management + // --- + + function registerProposer(address proposer, address executor) external { + _checkCallerIsAdminExecutor(); + _proposers.register(proposer, executor); + } + + function unregisterProposer(address proposer) external { + _checkCallerIsAdminExecutor(); + _proposers.unregister(proposer); + + /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer + if (!_proposers.isExecutor(TIMELOCK.getAdminExecutor())) { + revert UnownedAdminExecutor(); + } + } + + function isProposer(address account) external view returns (bool) { + return _proposers.isProposer(account); + } + + function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { + proposer = _proposers.getProposer(account); + } + + function getProposers() external view returns (Proposers.Proposer[] memory proposers) { + proposers = _proposers.getAllProposers(); + } + + function isExecutor(address account) external view returns (bool) { + return _proposers.isExecutor(account); + } + + // --- + // Tiebreaker Protection + // --- + + function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); + } + + function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); + } + + function setTiebreakerCommittee(address tiebreakerCommittee) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); + } + + function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { + _checkCallerIsAdminExecutor(); + _tiebreaker.setTiebreakerActivationTimeout( + MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT + ); + } + + function tiebreakerResumeSealable(address sealable) external { + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + RESEAL_MANAGER.resume(sealable); + } + + function tiebreakerScheduleProposal(uint256 proposalId) external { + _tiebreaker.checkCallerIsTiebreakerCommittee(); + _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + TIMELOCK.schedule(proposalId); + } + + struct TiebreakerState { + address tiebreakerCommittee; + Duration tiebreakerActivationTimeout; + address[] sealableWithdrawalBlockers; + } + + function getTiebreakerState() external view returns (TiebreakerState memory tiebreakerState) { + ( + tiebreakerState.tiebreakerCommittee, + tiebreakerState.tiebreakerActivationTimeout, + tiebreakerState.sealableWithdrawalBlockers + ) = _tiebreaker.getTiebreakerInfo(); + } + + // --- + // Reseal executor + // --- + + function resealSealable(address sealable) external { + if (msg.sender != _resealCommittee) { + revert CallerIsNotResealCommittee(msg.sender); + } + if (_stateMachine.getCurrentState() == State.Normal) { + revert ResealIsNotAllowedInNormalState(); + } + RESEAL_MANAGER.reseal(sealable); + } + + function setResealCommittee(address resealCommittee) external { + _checkCallerIsAdminExecutor(); + _resealCommittee = resealCommittee; + } + + // --- + // Private methods + // --- + + function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { + if (address(newConfigProvider) == address(0)) { + revert InvalidConfigProvider(newConfigProvider); + } + + if (newConfigProvider == _configProvider) { + return; + } + + _configProvider = IDualGovernanceConfigProvider(newConfigProvider); + emit ConfigProviderSet(newConfigProvider); + } + + function _checkCallerIsAdminExecutor() internal view { + if (TIMELOCK.getAdminExecutor() != msg.sender) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadScheduleCheck.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadScheduleCheck.sol new file mode 100644 index 00000000..f63d01d9 --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadScheduleCheck.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + // Mutation + // Add following line + if (state == State.VetoSignalling) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadSubmitCheck.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadSubmitCheck.sol new file mode 100644 index 00000000..77b7e19e --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadSubmitCheck.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + // MUTATION + // change submit check + return state == State.VetoSignallingDeactivation && state != State.VetoCooldown; + // return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol new file mode 100644 index 00000000..c594bf46 --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + // MUTATION: bad cooldown duration check + if (!config.isVetoCooldownDurationPassed(Timestamp.wrap(0))) { + return State.VetoCooldown; + } + // if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + // return State.VetoCooldown; + // } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-DeactivateAndBypassParent.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-DeactivateAndBypassParent.sol new file mode 100644 index 00000000..406c7329 --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-DeactivateAndBypassParent.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + // MUTATION + // do not cross back into ragequit + // if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + // return State.RageQuit; + // } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-IndefiniteWNoRagequit.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-IndefiniteWNoRagequit.sol new file mode 100644 index 00000000..63a31653 --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-IndefiniteWNoRagequit.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + // MUTATION + // Change argument passed for ragequit support + return config.isFirstSealRageQuitSupportCrossed(PercentD16.wrap(12)) ? State.VetoSignalling : State.Normal; + // return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + // ? State.VetoSignalling + // : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol new file mode 100644 index 00000000..a72e629c --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + // MUTATION + // Add following lines (meant to cause newly deployed + // signalling escrow to also enter ragequit) + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-RagequitFromNormal.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-RagequitFromNormal.sol new file mode 100644 index 00000000..f4b856f2 --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-RagequitFromNormal.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + // MUTATION + // go to ragequit instead of VetoSignalling + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.RageQuit + : State.Normal; + // return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + // ? State.VetoSignalling + // : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol new file mode 100644 index 00000000..add1c0fc --- /dev/null +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "../interfaces/IEscrow.sol"; + +import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; + +enum State { + Unset, + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit +} + +library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + struct Context { + /// + /// @dev slot 0: [0..7] + /// The current state of the Dual Governance FSM + State state; + /// + /// @dev slot 0: [8..47] + /// The timestamp when the Dual Governance FSM entered the current state + Timestamp enteredAt; + /// + /// @dev slot 0: [48..87] + /// The time the VetoSignalling FSM state was entered the last time + Timestamp vetoSignallingActivatedAt; + /// + /// @dev slot 0: [88..247] + /// The address of the currently used Veto Signalling Escrow + IEscrow signallingEscrow; + /// + /// @dev slot 0: [248..255] + /// The number of the Rage Quit round. Initial value is 0. + uint8 rageQuitRound; + /// + /// @dev slot 1: [0..39] + /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened + Timestamp vetoSignallingReactivationTime; + /// + /// @dev slot 1: [40..79] + /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state + Timestamp normalOrVetoCooldownExitedAt; + /// + /// @dev slot 1: [80..239] + /// The address of the Escrow used during the last (may be ongoing) Rage Quit process + IEscrow rageQuitEscrow; + } + + error AlreadyInitialized(); + + event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event DualGovernanceStateChanged(State from, State to, Context state); + + function initialize( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + if (self.state != State.Unset) { + revert AlreadyInitialized(); + } + + self.state = State.Normal; + self.enteredAt = Timestamps.now(); + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + + emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + } + + function activateNextState( + Context storage self, + DualGovernanceConfig.Context memory config, + address escrowMasterCopy + ) internal { + (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + + if (currentState == newState) { + return; + } + + self.state = newState; + self.enteredAt = Timestamps.now(); + + if (currentState == State.Normal || currentState == State.VetoCooldown) { + self.normalOrVetoCooldownExitedAt = Timestamps.now(); + } + + if (newState == State.Normal && self.rageQuitRound != 0) { + self.rageQuitRound = 0; + } else if (newState == State.VetoSignalling) { + if (currentState == State.VetoSignallingDeactivation) { + self.vetoSignallingReactivationTime = Timestamps.now(); + } else { + self.vetoSignallingActivatedAt = Timestamps.now(); + } + } else if (newState == State.RageQuit) { + IEscrow signallingEscrow = self.signallingEscrow; + uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); + self.rageQuitRound = uint8(rageQuitRound); + signallingEscrow.startRageQuit( + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + ); + self.rageQuitEscrow = signallingEscrow; + _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + } + + emit DualGovernanceStateChanged(currentState, newState, self); + } + + function getCurrentContext(Context storage self) internal pure returns (Context memory) { + return self; + } + + function getCurrentState(Context storage self) internal view returns (State) { + return self.state; + } + + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + return self.normalOrVetoCooldownExitedAt; + } + + function getDynamicDelayDuration( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (Duration) { + return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + } + + function canSubmitProposal(Context storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + // MUTATION + // change the check on the following line + return Timestamps.now() <= self.vetoSignallingActivatedAt; + // return proposalSubmissionTime <= self.vetoSignallingActivatedAt; + } + return false; + } + + function _deployNewSignallingEscrow( + Context storage self, + address escrowMasterCopy, + Duration minAssetsLockDuration + ) private { + IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + newSignallingEscrow.initialize(minAssetsLockDuration); + self.signallingEscrow = newSignallingEscrow; + emit NewSignallingEscrowDeployed(newSignallingEscrow); + } +} + +library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + function getStateTransition( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (State currentState, State nextStatus) { + currentState = self.state; + if (currentState == State.Normal) { + nextStatus = _fromNormalState(self, config); + } else if (currentState == State.VetoSignalling) { + nextStatus = _fromVetoSignallingState(self, config); + } else if (currentState == State.VetoSignallingDeactivation) { + nextStatus = _fromVetoSignallingDeactivationState(self, config); + } else if (currentState == State.VetoCooldown) { + nextStatus = _fromVetoCooldownState(self, config); + } else if (currentState == State.RageQuit) { + nextStatus = _fromRageQuitState(self, config); + } else { + assert(false); + } + } + + function _fromNormalState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromVetoSignallingState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + return config.isVetoSignallingReactivationDurationPassed( + Timestamps.max(self.vetoSignallingReactivationTime, self.vetoSignallingActivatedAt) + ) ? State.VetoSignallingDeactivation : State.VetoSignalling; + } + + function _fromVetoSignallingDeactivationState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + + if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + return State.VetoSignalling; + } + + if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + return State.RageQuit; + } + + if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + + return State.VetoSignallingDeactivation; + } + + function _fromVetoCooldownState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { + return State.VetoCooldown; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; + } + + function _fromRageQuitState( + DualGovernanceStateMachine.Context storage self, + DualGovernanceConfig.Context memory config + ) private view returns (State) { + if (!self.rageQuitEscrow.isRageQuitFinalized()) { + return State.RageQuit; + } + return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; + } +} diff --git a/certora/mutation/mutants/Proposers/ProposersFindingW2-1.sol b/certora/mutation/mutants/Proposers/ProposersFindingW2-1.sol new file mode 100644 index 00000000..1d514f53 --- /dev/null +++ b/certora/mutation/mutants/Proposers/ProposersFindingW2-1.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; + +/// @title Proposers Library +/// @dev This library manages proposers and their assigned executors in a governance system, providing functions to register, +/// unregister, and verify proposers and their roles. It ensures proper assignment and validation of proposers and executors. + +// MUTATION +// At time of writing there is actually no mutation. This mutant +// is a placeholder saving version of Proposers when +// finding W2-1 was identified. At time of writing this +// finding has not yet been fixed. +library Proposers { + // --- + // Errors + // --- + error InvalidExecutor(address executor); + error InvalidProposerAccount(address account); + error ProposerNotRegistered(address proposer); + error ProposerAlreadyRegistered(address proposer); + + // --- + // Events + // --- + + event AdminExecutorSet(address indexed adminExecutor); + event ProposerRegistered(address indexed proposer, address indexed executor); + event ProposerUnregistered(address indexed proposer, address indexed executor); + + // --- + // Data Types + // --- + + /// @notice The info about the registered proposer and associated executor + /// @param account Address of the proposer + /// @param executor Address of the executor associated with proposer. When proposer submits proposals, they execution + /// will be done with this address. + struct Proposer { + address account; + address executor; + } + + /// @notice The internal info about the proposer's executor data + /// @param proposerIndex The one-based index of the proposer associated with the `executor` from + /// the `Context.proposers` array + /// @param executor The address of the executor associated with the proposer + struct ExecutorData { + /// @dev slot0: [0..31] + IndexOneBased proposerIndex; + /// @dev slot0: [32..191] + address executor; + } + + /// @notice The context of the Proposers library + /// @param proposers The list of the registered proposers + /// @param executors The mapping with the executor info of the registered proposers + /// @param executorRefsCounts The mapping with the count of how many proposers is associated + /// with given executor address + struct Context { + address[] proposers; + mapping(address proposer => ExecutorData) executors; + mapping(address executor => uint256 usagesCount) executorRefsCounts; + } + + // --- + // Main Functionality + // --- + + /// @dev Registers a proposer with an assigned executor. + /// @param self The storage state of the Proposers library. + /// @param proposerAccount The address of the proposer to register. + /// @param executor The address of the assigned executor. + function register(Context storage self, address proposerAccount, address executor) internal { + if (proposerAccount == address(0)) { + revert InvalidProposerAccount(proposerAccount); + } + + if (executor == address(0)) { + revert InvalidExecutor(executor); + } + + if (_isRegisteredProposer(self.executors[proposerAccount])) { + revert ProposerAlreadyRegistered(proposerAccount); + } + + self.proposers.push(proposerAccount); + self.executors[proposerAccount] = + ExecutorData({proposerIndex: IndicesOneBased.fromOneBasedValue(self.proposers.length), executor: executor}); + self.executorRefsCounts[executor] += 1; + + emit ProposerRegistered(proposerAccount, executor); + } + + /// @dev Unregisters a proposer. + /// @param self The storage state of the Proposers library. + /// @param proposerAccount The address of the proposer to unregister. + function unregister(Context storage self, address proposerAccount) internal { + ExecutorData memory executorData = self.executors[proposerAccount]; + + _checkRegisteredProposer(proposerAccount, executorData); + + IndexOneBased lastProposerIndex = IndicesOneBased.fromOneBasedValue(self.proposers.length); + if (executorData.proposerIndex != lastProposerIndex) { + self.proposers[executorData.proposerIndex.toZeroBasedValue()] = + self.proposers[lastProposerIndex.toZeroBasedValue()]; + } + + self.proposers.pop(); + delete self.executors[proposerAccount]; + self.executorRefsCounts[executorData.executor] -= 1; + + emit ProposerUnregistered(proposerAccount, executorData.executor); + } + + // --- + // Getters + // --- + + /// @dev Retrieves the details of a specific proposer. + /// @param self The storage state of the Proposers library. + /// @param proposerAccount The address of the proposer. + /// @return proposer The struct representing the details of the proposer. + function getProposer( + Context storage self, + address proposerAccount + ) internal view returns (Proposer memory proposer) { + ExecutorData memory executorData = self.executors[proposerAccount]; + _checkRegisteredProposer(proposerAccount, executorData); + + proposer.account = proposerAccount; + proposer.executor = executorData.executor; + } + + /// @dev Retrieves all registered proposers. + /// @param self The storage state of the Proposers library. + /// @return proposers An array of structs representing all registered proposers. + function getAllProposers(Context storage self) internal view returns (Proposer[] memory proposers) { + proposers = new Proposer[](self.proposers.length); + for (uint256 i = 0; i < proposers.length; ++i) { + proposers[i] = getProposer(self, self.proposers[i]); + } + } + + /// @dev Checks if an account is a registered proposer. + /// @param self The storage state of the Proposers library. + /// @param account The address to check. + /// @return A boolean indicating whether the account is a registered proposer. + function isProposer(Context storage self, address account) internal view returns (bool) { + return _isRegisteredProposer(self.executors[account]); + } + + /// @dev Checks if an account is an executor. + /// @param self The storage state of the Proposers library. + /// @param account The address to check. + /// @return A boolean indicating whether the account is an executor. + function isExecutor(Context storage self, address account) internal view returns (bool) { + return self.executorRefsCounts[account] > 0; + } + + /// @dev Checks that proposer with given executorData is registered proposer + function _checkRegisteredProposer(address proposerAccount, ExecutorData memory executorData) internal pure { + if (!_isRegisteredProposer(executorData)) { + revert ProposerNotRegistered(proposerAccount); + } + } + + /// @dev Returns if the executorData belongs to registered proposer + function _isRegisteredProposer(ExecutorData memory executorData) internal pure returns (bool) { + return executorData.proposerIndex.isNotEmpty(); + } +} diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 17d21afb..ad46b8ed 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -212,9 +212,6 @@ rule pp_kp_1_ragequit_extends { assert !isVetoCooldown(getState()); } -// Alternative: maybe it's more useful to just prove that dynamicDelayDuration -// is monotonically increasing with increased rageQuitSupport. - // PP-2: It's not possible to prevent a proposal from being executed // indefinitely without triggering a rage quit. // expected complexity: extra high @@ -256,9 +253,6 @@ rule pp_kp_2_ragequit_trigger { isRageQuit(new_state) || isVetoCooldown(new_state); } -// One option: assume rageQuitSupport == max, show secondSealRageQuit support -// is crossed. - // PP-3: It's not possible to block proposal submission indefinitely. // expected complexity: high rule pp_kp_3_no_indefinite_proposal_submission_block { From b48bad6e382c515e55744a9940dec5dabb19acb3 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Thu, 29 Aug 2024 18:41:37 +0200 Subject: [PATCH 44/67] remove todo comments --- certora/specs/Timelock.spec | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index dcb3294c..2fcf1e84 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -13,7 +13,7 @@ methods { function getAfterSubmitDelay() external returns (Durations.Duration) envfree; function getAfterScheduleDelay() external returns (Durations.Duration) envfree; - // TODO: Improve this to instead resolving the inner unresolved calls to anything in EPT + // We do not model the calls executed through proposals function _.execute(address, uint256, bytes) external => nondetBytes() expect bytes; } @@ -23,8 +23,7 @@ function nondetBytes() returns bytes { return b; } -// TODO: maybe we can get rid of the filter if we resolve the unresolved calls inside execute, -// right now we're just filtering to be in line with treating execute as a NONDET + /** @title Executed is a terminal state for a proposal, once executed it cannot transition to any other state @notice Expected to fail due to an acknowledged bug whose fix is not merged yet From ff003211116909cb31c2336facca62256076578c Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Fri, 30 Aug 2024 11:49:35 +0100 Subject: [PATCH 45/67] Mainly save NONDET that fixes timeout --- certora/specs/DualGovernance.spec | 44 ++++++++++++++++++------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index ad46b8ed..9fc082cc 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -60,6 +60,13 @@ methods { // Address.functionCallWithValue) function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; + // This NONDET is meant to address a timeout of dg_kp_2 + // for which EPT is a significant bottleneck but not + // really needed for verifying DG. We also have separate rules for EPT. + function EmergencyProtectedTimelock.submit(address executor, + DualGovernanceHarness.ExternalCall[] calls) external returns (uint256) => NONDET; + + } // Ideally we would return a ghost but then we run into the tool bug @@ -105,24 +112,24 @@ function isSecondRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { // the array of proposers // “for each entry in the struct in the array, show that the index inside is the same as the real array index” // NOTE: this has not been addressed by customer, so this should fail now. -rule w2_1a_indexes_match (method f) { - env e; - calldataarg args; - Proposers.Proposer[] proposers = getProposers(e); - require proposers.length <= 5; // loop unrolling - uint256 idx; - require idx <= proposers.length; - mathint get_proposers_length = proposers.length; - address proposer_addr = proposers[idx].account; - require getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; - require getProposerIndexFromExecutor(proposer_addr) - 1 == idx; - - f(e, args); - // Strategy 1: check proposerIndex is <= proposers array length - assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; - // Strategy 2: check proposerIndex == real array index - assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; -} +// rule w2_1a_indexes_match (method f) { +// env e; +// calldataarg args; +// Proposers.Proposer[] proposers = getProposers(e); +// require proposers.length <= 5; // loop unrolling +// uint256 idx; +// require idx <= proposers.length; +// mathint get_proposers_length = proposers.length; +// address proposer_addr = proposers[idx].account; +// require getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; +// require getProposerIndexFromExecutor(proposer_addr) - 1 == idx; +// +// f(e, args); +// // Strategy 1: check proposerIndex is <= proposers array length +// assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; +// // Strategy 2: check proposerIndex == real array index +// assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; +// } // Proposals cannot be executed in the Veto Signaling (both parent state and // Deactivation sub-state) and Rage Quit states. @@ -138,6 +145,7 @@ rule dg_kp_1_proposal_execution { // DualGovernanceHarness.DGHarnessState.VetoSignaling && state != DualGovernanceHarness.DGHarnessState.RageQuit; } +// NOTE: moved this to other spec file while fixing timeout // Proposals cannot be submitted in the Veto Signaling Deactivation sub-state or in the Veto Cooldown state. rule dg_kp_2_proposal_submission { env e; From e33314e4fccc021fea9fd582f071afd5e9ff709f Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Fri, 30 Aug 2024 16:10:12 +0100 Subject: [PATCH 46/67] Fixes for mutation testing --- certora/harnesses/DualGovernanceHarness.sol | 16 ++++ ...teMachine-BadVetoCooldownDurationCheck.sol | 2 +- ...ceStateMachine-MultipleRageQuitEscrows.sol | 10 +- ...ceStateMachine-NormalFromDeactivation.sol} | 31 ++++--- ...ceStateMachine-SubmitCheckBadTimestamp.sol | 2 +- certora/specs/DualGovernance.spec | 91 ++++++++----------- 6 files changed, 78 insertions(+), 74 deletions(-) rename certora/mutation/mutants/DualGovernanceStateMachine/{DualGovernanceStateMachine-RagequitFromNormal.sol => DualGovernanceStateMachine-NormalFromDeactivation.sol} (92%) diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index e9651f08..a92ffd16 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -78,6 +78,22 @@ contract DualGovernanceHarness is DualGovernance { return PercentD16.unwrap(_configProvider.getDualGovernanceConfig().secondSealRageQuitSupport); } + function getFirstSealRageQuitSupportCrossed() external view returns (bool) { + return _configProvider.getDualGovernanceConfig().isFirstSealRageQuitSupportCrossed( + _stateMachine.signallingEscrow.getRageQuitSupport() + ); + } + + function getSecondSealRageQuitSupportCrossed() external view returns (bool) { + return _configProvider.getDualGovernanceConfig().isSecondSealRageQuitSupportCrossed( + _stateMachine.signallingEscrow.getRageQuitSupport() + ); + } + + function getRageQuitSupportHarnessed() external view returns (PercentD16) { + return _stateMachine.signallingEscrow.getRageQuitSupport(); + } + function isDynamicTimelockPassed(uint256 rageQuitSupport) public returns (bool) { return _configProvider.getDualGovernanceConfig().isDynamicTimelockDurationPassed( _stateMachine.vetoSignallingActivatedAt, PercentD16.wrap(rageQuitSupport) diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol index c594bf46..0381672b 100644 --- a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-BadVetoCooldownDurationCheck.sol @@ -242,7 +242,7 @@ library DualGovernanceStateTransitions { DualGovernanceConfig.Context memory config ) private view returns (State) { // MUTATION: bad cooldown duration check - if (!config.isVetoCooldownDurationPassed(Timestamp.wrap(0))) { + if (config.isVetoCooldownDurationPassed(self.enteredAt)) { return State.VetoCooldown; } // if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol index a72e629c..50565aec 100644 --- a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-MultipleRageQuitEscrows.sol @@ -115,13 +115,11 @@ library DualGovernanceStateMachine { ); self.rageQuitEscrow = signallingEscrow; _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); - // MUTATION - // Add following lines (meant to cause newly deployed - // signalling escrow to also enter ragequit) - signallingEscrow.startRageQuit( - config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) - ); } + // MUTATION + // Add following lines (meant to cause both escrows to enter ragequit) + self.signallingEscrow.startRageQuit(config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(0)); + self.rageQuitEscrow.startRageQuit(config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(0)); emit DualGovernanceStateChanged(currentState, newState, self); } diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-RagequitFromNormal.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-NormalFromDeactivation.sol similarity index 92% rename from certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-RagequitFromNormal.sol rename to certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-NormalFromDeactivation.sol index f4b856f2..604765a3 100644 --- a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-RagequitFromNormal.sol +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-NormalFromDeactivation.sol @@ -193,10 +193,11 @@ library DualGovernanceStateTransitions { DualGovernanceConfig.Context memory config ) private view returns (State) { // MUTATION - // go to ragequit instead of VetoSignalling - return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) - ? State.RageQuit - : State.Normal; + // go to ragequit always + return State.RageQuit; + // return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + // ? State.RageQuit + // : State.Normal; // return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) // ? State.VetoSignalling // : State.Normal; @@ -227,19 +228,21 @@ library DualGovernanceStateTransitions { ) private view returns (State) { PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { - return State.VetoSignalling; - } + // MUTATION: always go to normal state + // if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + // return State.VetoSignalling; + // } - if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { - return State.RageQuit; - } + // if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + // return State.RageQuit; + // } - if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { - return State.VetoCooldown; - } + // if (config.isVetoSignallingDeactivationMaxDurationPassed(self.enteredAt)) { + // return State.VetoCooldown; + // } - return State.VetoSignallingDeactivation; + // return State.VetoSignallingDeactivation; + return State.Normal; } function _fromVetoCooldownState( diff --git a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol index add1c0fc..bed17800 100644 --- a/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol +++ b/certora/mutation/mutants/DualGovernanceStateMachine/DualGovernanceStateMachine-SubmitCheckBadTimestamp.sol @@ -150,7 +150,7 @@ library DualGovernanceStateMachine { if (state == State.VetoCooldown) { // MUTATION // change the check on the following line - return Timestamps.now() <= self.vetoSignallingActivatedAt; + return proposalSubmissionTime <= Timestamps.now(); // return proposalSubmissionTime <= self.vetoSignallingActivatedAt; } return false; diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 9fc082cc..4756bd2b 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -14,16 +14,9 @@ methods { function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; function getRageQuitEscrow() external returns (address) envfree; - - // DualGovernanceConfig summaries - function _.isFirstSealRageQuitSupportCrossed( - DualGovernanceConfig.Context memory configContext, - DualGovernanceHarness.PercentD16 rageQuitSupport) internal => - isFirstRageQuitCrossedGhost(rageQuitSupport) expect bool; - function _.isSecondSealRageQuitSupportCrossed( - DualGovernanceConfig.Context memory configContext, - DualGovernanceHarness.PercentD16 rageQuitSupport) internal => - isSecondRageQuitCrossedGhost(rageQuitSupport) expect bool; + function getVetoSignallingEscrow() external returns (address) envfree; + function getFirstSeal() external returns (uint256) envfree; + function getSecondSeal() external returns (uint256) envfree; // envfrees escrow function EscrowA.isRageQuitState() external returns (bool) envfree; @@ -66,6 +59,7 @@ methods { function EmergencyProtectedTimelock.submit(address executor, DualGovernanceHarness.ExternalCall[] calls) external returns (uint256) => NONDET; + } @@ -86,26 +80,8 @@ function escrowAddressIsRageQuit(address escrow) returns bool { return false; } -// Ghosts for support thresholds so we do not need to link -// in DualGovernanceConfig which has some nonlinear functions -ghost uint256 rageQuitFirstSealGhost { - init_state axiom rageQuitFirstSealGhost > 0; -} -ghost uint256 rageQuitSecondSealGhost { - init_state axiom rageQuitSecondSealGhost > 0 && - rageQuitFirstSealGhost < rageQuitSecondSealGhost; -} -function isFirstRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { - require rageQuitFirstSealGhost > 0; - require rageQuitSecondSealGhost > 0; - require rageQuitFirstSealGhost < rageQuitSecondSealGhost; - return rageQuitSupport > rageQuitFirstSealGhost; -} -function isSecondRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { - require rageQuitFirstSealGhost > 0; - require rageQuitSecondSealGhost > 0; - require rageQuitFirstSealGhost < rageQuitSecondSealGhost; - return rageQuitSupport > rageQuitSecondSealGhost; +function rageQuitThresholdAssumptions() returns bool { + return getFirstSeal() > 0 && getSecondSeal() > getFirstSeal(); } // for any registered proposer, his index should be ≤ the length of @@ -184,10 +160,9 @@ rule dg_kp_3_cooldown_execution (method f) { rule dg_kp_4_single_ragequit (method f) { env e; calldataarg args; - require getRageQuitEscrow() != 0 => escrowAddressIsRageQuit(getRageQuitEscrow()); - require EscrowA == EscrowB || !(EscrowA.isRageQuitState() && EscrowB.isRageQuitState()); + require !escrowAddressIsRageQuit(getVetoSignallingEscrow()); f(e, args); - assert EscrowA == EscrowB || !(EscrowA.isRageQuitState() && EscrowB.isRageQuitState()); + assert !escrowAddressIsRageQuit(getVetoSignallingEscrow()); } // PP-1: Regardless of the state in which a proposal is submitted, if the @@ -211,10 +186,12 @@ rule pp_kp_1_ragequit_extends { // - we do not transition into normal state // - if timelock is extended with ragequit support, we // cannot transition into VetoCooldown - require getVetoSignallingEscrow(e) == EscrowA; - uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); - require isFirstRageQuitCrossedGhost(rageQuitSupport); + // require getVetoSignallingEscrow(e) == EscrowA; + uint256 rageQuitSupport = getRageQuitSupportHarnessed(e); require !isDynamicTimelockPassed(e, rageQuitSupport); + require rageQuitThresholdAssumptions(); + require getFirstSealRageQuitSupportCrossed(e); + // we cannot transition to normal state above first seal ragequit support assert !isNormal(getState()); assert !isVetoCooldown(getState()); @@ -227,12 +204,9 @@ rule pp_kp_2_ragequit_trigger { env e; calldataarg args; - require getVetoSignallingEscrow(e) == EscrowA; - uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); - require rageQuitFirstSealGhost > 0; - require rageQuitSecondSealGhost > rageQuitFirstSealGhost; - // have large ragequit support to try to maximize delay - require rageQuitSupport > rageQuitFirstSealGhost; + uint256 rageQuitSupport = getRageQuitSupportHarnessed(e); + require rageQuitThresholdAssumptions(); + require getFirstSealRageQuitSupportCrossed(e); // Assumptions about waiting long enough: // Assume we have waited long enough if in VetoSignalling @@ -266,11 +240,12 @@ rule pp_kp_2_ragequit_trigger { rule pp_kp_3_no_indefinite_proposal_submission_block { env e; - require getVetoSignallingEscrow(e) == EscrowA; - uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); + uint256 rageQuitSupport = getRageQuitSupportHarnessed(e); // Assume we have waited long enough + require rageQuitThresholdAssumptions(); require isVetoSignallingDeactivationMaxDurationPassed(e) && isVetoCooldownDurationPassed(e); + DualGovernanceHarness.DGHarnessState old_state = getState(); activateNextState(e); DualGovernanceHarness.DGHarnessState new_state = getState(); @@ -286,10 +261,9 @@ rule pp_kp_3_no_indefinite_proposal_submission_block { // triggering a rage quit immediately afterwards). rule pp_kp_4_veto_signalling_deactivation_cancellable() { env e; - require getVetoSignallingEscrow(e) == EscrowA; - uint256 rageQuitSupport = EscrowA.getRageQuitSupport(e); require isVetoSignallingDeactivation(getState()); - require isSecondRageQuitCrossedGhost(rageQuitSupport); + require rageQuitThresholdAssumptions(); + require getSecondSealRageQuitSupportCrossed(e); activateNextState(e); // the only way out of veto signalling deactivation that does not go back to the parent is veto cooldown, @@ -332,9 +306,22 @@ rule dg_transitions_1_only_legal_transitions() { // we are not interested in the cases where no transition happened require old_state != new_state; - assert isNormal(new_state) => isVetoCooldown(old_state); - assert isVetoSignalling(new_state) => isNormal(old_state) || isVetoCooldown(old_state) || isVetoSignallingDeactivation(old_state) || isRageQuit(old_state); - assert isRageQuit(new_state) => isVetoSignalling(old_state) || isVetoSignallingDeactivation(old_state); - assert isVetoCooldown(new_state) => isRageQuit(old_state) || isVetoSignallingDeactivation(old_state); - assert isVetoSignallingDeactivation(new_state) => isVetoSignalling(old_state); + require rageQuitThresholdAssumptions(); + + if(isNormal(old_state)) { + assert isVetoSignalling(new_state); + } else if(isVetoSignalling(old_state)) { + assert isRageQuit(new_state) || isVetoSignallingDeactivation(new_state); + } else if(isVetoSignallingDeactivation(old_state)) { + assert isVetoSignalling(new_state) || + isVetoCooldown(new_state) || + isRageQuit(new_state); + } else if(isVetoCooldown(old_state)) { + assert isNormal(new_state) || isVetoSignalling(new_state); + } else if(isRageQuit(old_state)) { + assert isVetoSignalling(new_state) || isVetoCooldown(new_state); + } else { + // unset state should not be reachable + assert false; + } } \ No newline at end of file From a0ecdba88744817da592208057f648c94892993d Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Fri, 30 Aug 2024 16:35:02 +0100 Subject: [PATCH 47/67] uncomment finding rule --- certora/specs/DualGovernance.spec | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 4756bd2b..63704ee1 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -88,24 +88,24 @@ function rageQuitThresholdAssumptions() returns bool { // the array of proposers // “for each entry in the struct in the array, show that the index inside is the same as the real array index” // NOTE: this has not been addressed by customer, so this should fail now. -// rule w2_1a_indexes_match (method f) { -// env e; -// calldataarg args; -// Proposers.Proposer[] proposers = getProposers(e); -// require proposers.length <= 5; // loop unrolling -// uint256 idx; -// require idx <= proposers.length; -// mathint get_proposers_length = proposers.length; -// address proposer_addr = proposers[idx].account; -// require getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; -// require getProposerIndexFromExecutor(proposer_addr) - 1 == idx; -// -// f(e, args); -// // Strategy 1: check proposerIndex is <= proposers array length -// assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; -// // Strategy 2: check proposerIndex == real array index -// assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; -// } +rule w2_1a_indexes_match (method f) { + env e; + calldataarg args; + Proposers.Proposer[] proposers = getProposers(e); + require proposers.length <= 5; // loop unrolling + uint256 idx; + require idx <= proposers.length; + mathint get_proposers_length = proposers.length; + address proposer_addr = proposers[idx].account; + require getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; + require getProposerIndexFromExecutor(proposer_addr) - 1 == idx; + + f(e, args); + // Strategy 1: check proposerIndex is <= proposers array length + assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; + // Strategy 2: check proposerIndex == real array index + assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; +} // Proposals cannot be executed in the Veto Signaling (both parent state and // Deactivation sub-state) and Rage Quit states. From d3db6b44b7f4b271c443a817e9d46202e90131c2 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Fri, 30 Aug 2024 18:51:02 +0200 Subject: [PATCH 48/67] some first mutants for escrow --- certora/mutation/conf/Escrow.conf | 44 ++ .../Escrow/EscrowLockStETHMissingGuard.sol | 473 ++++++++++++++++++ .../EscrowStartRageQuitMissingGuard.sol | 473 ++++++++++++++++++ .../EscrowUnlockStETHMissingTimeGuard.sol | 473 ++++++++++++++++++ ...crowStateResetFromRageQuitToSignalling.sol | 159 ++++++ 5 files changed, 1622 insertions(+) create mode 100644 certora/mutation/conf/Escrow.conf create mode 100644 certora/mutation/mutants/Escrow/EscrowLockStETHMissingGuard.sol create mode 100644 certora/mutation/mutants/Escrow/EscrowStartRageQuitMissingGuard.sol create mode 100644 certora/mutation/mutants/Escrow/EscrowUnlockStETHMissingTimeGuard.sol create mode 100644 certora/mutation/mutants/EscrowState/EscrowStateResetFromRageQuitToSignalling.sol diff --git a/certora/mutation/conf/Escrow.conf b/certora/mutation/conf/Escrow.conf new file mode 100644 index 00000000..f97c7556 --- /dev/null +++ b/certora/mutation/conf/Escrow.conf @@ -0,0 +1,44 @@ +{ + "files": [ + "contracts/Escrow.sol", + "contracts/DualGovernance.sol", + "contracts/DualGovernanceConfigProvider.sol:ImmutableDualGovernanceConfigProvider", + "certora/helpers/DummyWithdrawalQueue.sol", + "certora/harnesses/ERC20Like/DummyStETH.sol", + "certora/harnesses/ERC20Like/DummyWstETH.sol", + ], + "link": [ + "Escrow:DUAL_GOVERNANCE=DualGovernance", + "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", + "Escrow:ST_ETH=DummyStETH", + "Escrow:WST_ETH=DummyWstETH", + "DummyWstETH:stETH=DummyStETH", + "DummyWithdrawalQueue:stETH=DummyStETH", + "DualGovernance:_configProvider=ImmutableDualGovernanceConfigProvider", + ], + + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + "solc": "solc8.26", + "optimistic_loop": true, + "optimistic_fallback": true, + "loop_iter": "3", + "build_cache" : true, + "verify": "Escrow:certora/specs/Escrow.spec", + "server": "production", + // mutation options below this line + "mutations": { + "manual_mutants": [ + { + "file_to_mutate": "contracts/libraries/EscrowState.sol", + "mutants_location": "certora/mutation/mutants/EscrowState" + }, + { + "file_to_mutate": "contracts/Escrow.sol", + "mutants_location": "certora/mutation/mutants/Escrow" + }, + ] + } +} \ No newline at end of file diff --git a/certora/mutation/mutants/Escrow/EscrowLockStETHMissingGuard.sol b/certora/mutation/mutants/Escrow/EscrowLockStETHMissingGuard.sol new file mode 100644 index 00000000..c8954474 --- /dev/null +++ b/certora/mutation/mutants/Escrow/EscrowLockStETHMissingGuard.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ETHValue, ETHValues} from "./types/ETHValue.sol"; +import {SharesValue, SharesValues} from "./types/SharesValue.sol"; +import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; + +import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; + +import {EscrowState} from "./libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; + +/// @notice Summary of the total locked assets in the Escrow +/// @param stETHLockedShares Total number of stETH shares locked in the Escrow +/// @param stETHClaimedETH Total amount of ETH claimed from the stETH locked in the Escrow +/// @param unstETHUnfinalizedShares Total number of shares from unstETH NFTs that have not yet been +/// marked as finalized +/// @param unstETHFinalizedETH Total claimable amount of ETH from unstETH NFTs that have been marked +/// as finalized +struct LockedAssetsTotals { + uint256 stETHLockedShares; + uint256 stETHClaimedETH; + uint256 unstETHUnfinalizedShares; + uint256 unstETHFinalizedETH; +} + +struct VetoerState { + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; +} + +contract Escrow is IEscrow { + using EscrowState for EscrowState.Context; + using AssetsAccounting for AssetsAccounting.Context; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; + + // --- + // Errors + // --- + + error UnclaimedBatches(); + error UnexpectedUnstETHId(); + error UnfinalizedUnstETHIds(); + error NonProxyCallsForbidden(); + error BatchesQueueIsNotClosed(); + error InvalidBatchSize(uint256 size); + error CallerIsNotDualGovernance(address caller); + error InvalidHintsLength(uint256 actual, uint256 expected); + error InvalidETHSender(address actual, address expected); + + // --- + // Events + // --- + + event ConfigProviderSet(address newConfigProvider); + + // --- + // Sanity check params immutables + // --- + + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + + // --- + // Dependencies immutables + // --- + + IStETH public immutable ST_ETH; + IWstETH public immutable WST_ETH; + IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; + + // --- + // Implementation immutables + + address private immutable _SELF; + IDualGovernance public immutable DUAL_GOVERNANCE; + + // --- + // Aspects + // --- + + EscrowState.Context internal _escrowState; + AssetsAccounting.Context private _accounting; + WithdrawalsBatchesQueue.Context private _batchesQueue; + + // --- + // Construction & initializing + // --- + + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) { + _SELF = address(this); + DUAL_GOVERNANCE = dualGovernance; + + ST_ETH = stETH; + WST_ETH = wstETH; + WITHDRAWAL_QUEUE = withdrawalQueue; + + MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; + } + + function initialize(Duration minAssetsLockDuration) external { + if (address(this) == _SELF) { + revert NonProxyCallsForbidden(); + } + _checkCallerIsDualGovernance(); + + _escrowState.initialize(minAssetsLockDuration); + + ST_ETH.approve(address(WST_ETH), type(uint256).max); + ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); + } + + // --- + // Lock & unlock stETH + // --- + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + // mutated + //_escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockStETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + + DUAL_GOVERNANCE.activateNextState(); + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); + ST_ETH.transferShares(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock wstETH + // --- + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WST_ETH.transferFrom(msg.sender, address(this), amount); + lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockWstETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); + WST_ETH.transfer(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock unstETH + // --- + function lockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { + _escrowState.checkSignallingEscrow(); + + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); + } + + // --- + // Convert to NFT + // --- + + function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + _escrowState.checkSignallingEscrow(); + + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + + // --- + // Start rage quit + // --- + + function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { + _checkCallerIsDualGovernance(); + _escrowState.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); + } + + // --- + // Request withdrawal batches + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { + _escrowState.checkRageQuitEscrow(); + + if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { + revert InvalidBatchSize(batchSize); + } + + uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); + uint256 minStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + + if (stETHRemaining < minStETHWithdrawalRequestAmount) { + return _batchesQueue.close(); + } + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: minStETHWithdrawalRequestAmount, + maxRequestAmount: maxStETHWithdrawalRequestAmount, + remainingAmount: Math.min(stETHRemaining, maxStETHWithdrawalRequestAmount * batchSize) + }); + + _batchesQueue.addUnstETHIds(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); + } + + // --- + // Claim requested withdrawal batches + // --- + + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + _claimNextWithdrawalsBatch( + unstETHIds[0], + unstETHIds, + WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) + ); + } + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); + + _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); + } + + // --- + // Start rage quit extension delay + // --- + + function startRageQuitExtensionDelay() external { + if (!_batchesQueue.isClosed()) { + revert BatchesQueueIsNotClosed(); + } + + /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionDelay can only begin + /// when the last locked unstETH id is finalized in the WithdrawalQueue. + /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: + /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created + /// during the request for withdrawal batches. + /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. + /// - The finalization of unstETH NFTs occurs in FIFO order. + if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { + revert UnfinalizedUnstETHIds(); + } + + if (!_batchesQueue.isAllBatchesClaimed()) { + revert UnclaimedBatches(); + } + + _escrowState.startRageQuitExtensionDelay(); + } + + // --- + // Claim locked unstETH NFTs + // --- + + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); + assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); + } + + // --- + // Escrow management + // --- + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // Withdraw logic + // --- + + function withdrawETH() external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + function withdrawETH(uint256[] calldata unstETHIds) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); + totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); + totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); + } + + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + state.unstETHIdsCount = assets.unstETHIds.length; + state.stETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); + } + + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + return _batchesQueue.getNextWithdrawalsBatches(limit); + } + + function isWithdrawalsBatchesFinalized() external view returns (bool) { + return _batchesQueue.isClosed(); + } + + function isRageQuitExtensionDelayStarted() external view returns (bool) { + return _escrowState.isRageQuitExtensionDelayStarted(); + } + + function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp) { + return _escrowState.rageQuitExtensionDelayStartedAt; + } + + function getRageQuitSupport() external view returns (PercentD16) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + function isRageQuitFinalized() external view returns (bool) { + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionDelayPassed(); + } + + // --- + // Receive ETH + // --- + + receive() external payable { + if (msg.sender != address(WITHDRAWAL_QUEUE)) { + revert InvalidETHSender(msg.sender, address(WITHDRAWAL_QUEUE)); + } + } + + // --- + // Internal methods + // --- + + function _claimNextWithdrawalsBatch( + uint256 fromUnstETHId, + uint256[] memory unstETHIds, + uint256[] memory hints + ) internal { + if (fromUnstETHId != unstETHIds[0]) { + revert UnexpectedUnstETHId(); + } + + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); + } + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + _accounting.accountClaimedStETH(ethBalanceAfter - ethBalanceBefore); + } + + function _checkCallerIsDualGovernance() internal view { + if (msg.sender != address(DUAL_GOVERNANCE)) { + revert CallerIsNotDualGovernance(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/Escrow/EscrowStartRageQuitMissingGuard.sol b/certora/mutation/mutants/Escrow/EscrowStartRageQuitMissingGuard.sol new file mode 100644 index 00000000..8c5a846d --- /dev/null +++ b/certora/mutation/mutants/Escrow/EscrowStartRageQuitMissingGuard.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ETHValue, ETHValues} from "./types/ETHValue.sol"; +import {SharesValue, SharesValues} from "./types/SharesValue.sol"; +import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; + +import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; + +import {EscrowState} from "./libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; + +/// @notice Summary of the total locked assets in the Escrow +/// @param stETHLockedShares Total number of stETH shares locked in the Escrow +/// @param stETHClaimedETH Total amount of ETH claimed from the stETH locked in the Escrow +/// @param unstETHUnfinalizedShares Total number of shares from unstETH NFTs that have not yet been +/// marked as finalized +/// @param unstETHFinalizedETH Total claimable amount of ETH from unstETH NFTs that have been marked +/// as finalized +struct LockedAssetsTotals { + uint256 stETHLockedShares; + uint256 stETHClaimedETH; + uint256 unstETHUnfinalizedShares; + uint256 unstETHFinalizedETH; +} + +struct VetoerState { + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; +} + +contract Escrow is IEscrow { + using EscrowState for EscrowState.Context; + using AssetsAccounting for AssetsAccounting.Context; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; + + // --- + // Errors + // --- + + error UnclaimedBatches(); + error UnexpectedUnstETHId(); + error UnfinalizedUnstETHIds(); + error NonProxyCallsForbidden(); + error BatchesQueueIsNotClosed(); + error InvalidBatchSize(uint256 size); + error CallerIsNotDualGovernance(address caller); + error InvalidHintsLength(uint256 actual, uint256 expected); + error InvalidETHSender(address actual, address expected); + + // --- + // Events + // --- + + event ConfigProviderSet(address newConfigProvider); + + // --- + // Sanity check params immutables + // --- + + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + + // --- + // Dependencies immutables + // --- + + IStETH public immutable ST_ETH; + IWstETH public immutable WST_ETH; + IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; + + // --- + // Implementation immutables + + address private immutable _SELF; + IDualGovernance public immutable DUAL_GOVERNANCE; + + // --- + // Aspects + // --- + + EscrowState.Context internal _escrowState; + AssetsAccounting.Context private _accounting; + WithdrawalsBatchesQueue.Context private _batchesQueue; + + // --- + // Construction & initializing + // --- + + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) { + _SELF = address(this); + DUAL_GOVERNANCE = dualGovernance; + + ST_ETH = stETH; + WST_ETH = wstETH; + WITHDRAWAL_QUEUE = withdrawalQueue; + + MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; + } + + function initialize(Duration minAssetsLockDuration) external { + if (address(this) == _SELF) { + revert NonProxyCallsForbidden(); + } + _checkCallerIsDualGovernance(); + + _escrowState.initialize(minAssetsLockDuration); + + ST_ETH.approve(address(WST_ETH), type(uint256).max); + ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); + } + + // --- + // Lock & unlock stETH + // --- + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockStETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + + DUAL_GOVERNANCE.activateNextState(); + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); + ST_ETH.transferShares(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock wstETH + // --- + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WST_ETH.transferFrom(msg.sender, address(this), amount); + lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockWstETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); + WST_ETH.transfer(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock unstETH + // --- + function lockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { + _escrowState.checkSignallingEscrow(); + + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); + } + + // --- + // Convert to NFT + // --- + + function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + _escrowState.checkSignallingEscrow(); + + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + + // --- + // Start rage quit + // --- + + function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { + // mutated + //_checkCallerIsDualGovernance(); + _escrowState.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); + } + + // --- + // Request withdrawal batches + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { + _escrowState.checkRageQuitEscrow(); + + if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { + revert InvalidBatchSize(batchSize); + } + + uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); + uint256 minStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + + if (stETHRemaining < minStETHWithdrawalRequestAmount) { + return _batchesQueue.close(); + } + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: minStETHWithdrawalRequestAmount, + maxRequestAmount: maxStETHWithdrawalRequestAmount, + remainingAmount: Math.min(stETHRemaining, maxStETHWithdrawalRequestAmount * batchSize) + }); + + _batchesQueue.addUnstETHIds(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); + } + + // --- + // Claim requested withdrawal batches + // --- + + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + _claimNextWithdrawalsBatch( + unstETHIds[0], + unstETHIds, + WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) + ); + } + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); + + _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); + } + + // --- + // Start rage quit extension delay + // --- + + function startRageQuitExtensionDelay() external { + if (!_batchesQueue.isClosed()) { + revert BatchesQueueIsNotClosed(); + } + + /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionDelay can only begin + /// when the last locked unstETH id is finalized in the WithdrawalQueue. + /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: + /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created + /// during the request for withdrawal batches. + /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. + /// - The finalization of unstETH NFTs occurs in FIFO order. + if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { + revert UnfinalizedUnstETHIds(); + } + + if (!_batchesQueue.isAllBatchesClaimed()) { + revert UnclaimedBatches(); + } + + _escrowState.startRageQuitExtensionDelay(); + } + + // --- + // Claim locked unstETH NFTs + // --- + + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); + assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); + } + + // --- + // Escrow management + // --- + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // Withdraw logic + // --- + + function withdrawETH() external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + function withdrawETH(uint256[] calldata unstETHIds) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); + totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); + totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); + } + + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + state.unstETHIdsCount = assets.unstETHIds.length; + state.stETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); + } + + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + return _batchesQueue.getNextWithdrawalsBatches(limit); + } + + function isWithdrawalsBatchesFinalized() external view returns (bool) { + return _batchesQueue.isClosed(); + } + + function isRageQuitExtensionDelayStarted() external view returns (bool) { + return _escrowState.isRageQuitExtensionDelayStarted(); + } + + function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp) { + return _escrowState.rageQuitExtensionDelayStartedAt; + } + + function getRageQuitSupport() external view returns (PercentD16) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + function isRageQuitFinalized() external view returns (bool) { + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionDelayPassed(); + } + + // --- + // Receive ETH + // --- + + receive() external payable { + if (msg.sender != address(WITHDRAWAL_QUEUE)) { + revert InvalidETHSender(msg.sender, address(WITHDRAWAL_QUEUE)); + } + } + + // --- + // Internal methods + // --- + + function _claimNextWithdrawalsBatch( + uint256 fromUnstETHId, + uint256[] memory unstETHIds, + uint256[] memory hints + ) internal { + if (fromUnstETHId != unstETHIds[0]) { + revert UnexpectedUnstETHId(); + } + + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); + } + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + _accounting.accountClaimedStETH(ethBalanceAfter - ethBalanceBefore); + } + + function _checkCallerIsDualGovernance() internal view { + if (msg.sender != address(DUAL_GOVERNANCE)) { + revert CallerIsNotDualGovernance(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/Escrow/EscrowUnlockStETHMissingTimeGuard.sol b/certora/mutation/mutants/Escrow/EscrowUnlockStETHMissingTimeGuard.sol new file mode 100644 index 00000000..d26b4732 --- /dev/null +++ b/certora/mutation/mutants/Escrow/EscrowUnlockStETHMissingTimeGuard.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ETHValue, ETHValues} from "./types/ETHValue.sol"; +import {SharesValue, SharesValues} from "./types/SharesValue.sol"; +import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; + +import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; + +import {EscrowState} from "./libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; + +/// @notice Summary of the total locked assets in the Escrow +/// @param stETHLockedShares Total number of stETH shares locked in the Escrow +/// @param stETHClaimedETH Total amount of ETH claimed from the stETH locked in the Escrow +/// @param unstETHUnfinalizedShares Total number of shares from unstETH NFTs that have not yet been +/// marked as finalized +/// @param unstETHFinalizedETH Total claimable amount of ETH from unstETH NFTs that have been marked +/// as finalized +struct LockedAssetsTotals { + uint256 stETHLockedShares; + uint256 stETHClaimedETH; + uint256 unstETHUnfinalizedShares; + uint256 unstETHFinalizedETH; +} + +struct VetoerState { + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; +} + +contract Escrow is IEscrow { + using EscrowState for EscrowState.Context; + using AssetsAccounting for AssetsAccounting.Context; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; + + // --- + // Errors + // --- + + error UnclaimedBatches(); + error UnexpectedUnstETHId(); + error UnfinalizedUnstETHIds(); + error NonProxyCallsForbidden(); + error BatchesQueueIsNotClosed(); + error InvalidBatchSize(uint256 size); + error CallerIsNotDualGovernance(address caller); + error InvalidHintsLength(uint256 actual, uint256 expected); + error InvalidETHSender(address actual, address expected); + + // --- + // Events + // --- + + event ConfigProviderSet(address newConfigProvider); + + // --- + // Sanity check params immutables + // --- + + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + + // --- + // Dependencies immutables + // --- + + IStETH public immutable ST_ETH; + IWstETH public immutable WST_ETH; + IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; + + // --- + // Implementation immutables + + address private immutable _SELF; + IDualGovernance public immutable DUAL_GOVERNANCE; + + // --- + // Aspects + // --- + + EscrowState.Context internal _escrowState; + AssetsAccounting.Context private _accounting; + WithdrawalsBatchesQueue.Context private _batchesQueue; + + // --- + // Construction & initializing + // --- + + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) { + _SELF = address(this); + DUAL_GOVERNANCE = dualGovernance; + + ST_ETH = stETH; + WST_ETH = wstETH; + WITHDRAWAL_QUEUE = withdrawalQueue; + + MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; + } + + function initialize(Duration minAssetsLockDuration) external { + if (address(this) == _SELF) { + revert NonProxyCallsForbidden(); + } + _checkCallerIsDualGovernance(); + + _escrowState.initialize(minAssetsLockDuration); + + ST_ETH.approve(address(WST_ETH), type(uint256).max); + ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); + } + + // --- + // Lock & unlock stETH + // --- + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockStETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + + DUAL_GOVERNANCE.activateNextState(); + // mutated + //_accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); + ST_ETH.transferShares(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock wstETH + // --- + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WST_ETH.transferFrom(msg.sender, address(this), amount); + lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockWstETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); + WST_ETH.transfer(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock unstETH + // --- + function lockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { + _escrowState.checkSignallingEscrow(); + + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); + } + + // --- + // Convert to NFT + // --- + + function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + _escrowState.checkSignallingEscrow(); + + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + + // --- + // Start rage quit + // --- + + function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { + _checkCallerIsDualGovernance(); + _escrowState.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); + } + + // --- + // Request withdrawal batches + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { + _escrowState.checkRageQuitEscrow(); + + if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { + revert InvalidBatchSize(batchSize); + } + + uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); + uint256 minStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + + if (stETHRemaining < minStETHWithdrawalRequestAmount) { + return _batchesQueue.close(); + } + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: minStETHWithdrawalRequestAmount, + maxRequestAmount: maxStETHWithdrawalRequestAmount, + remainingAmount: Math.min(stETHRemaining, maxStETHWithdrawalRequestAmount * batchSize) + }); + + _batchesQueue.addUnstETHIds(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); + } + + // --- + // Claim requested withdrawal batches + // --- + + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + _claimNextWithdrawalsBatch( + unstETHIds[0], + unstETHIds, + WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) + ); + } + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); + + _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); + } + + // --- + // Start rage quit extension delay + // --- + + function startRageQuitExtensionDelay() external { + if (!_batchesQueue.isClosed()) { + revert BatchesQueueIsNotClosed(); + } + + /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionDelay can only begin + /// when the last locked unstETH id is finalized in the WithdrawalQueue. + /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: + /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created + /// during the request for withdrawal batches. + /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. + /// - The finalization of unstETH NFTs occurs in FIFO order. + if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { + revert UnfinalizedUnstETHIds(); + } + + if (!_batchesQueue.isAllBatchesClaimed()) { + revert UnclaimedBatches(); + } + + _escrowState.startRageQuitExtensionDelay(); + } + + // --- + // Claim locked unstETH NFTs + // --- + + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); + assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); + } + + // --- + // Escrow management + // --- + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // Withdraw logic + // --- + + function withdrawETH() external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + function withdrawETH(uint256[] calldata unstETHIds) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); + totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); + totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); + } + + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + state.unstETHIdsCount = assets.unstETHIds.length; + state.stETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); + } + + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + return _batchesQueue.getNextWithdrawalsBatches(limit); + } + + function isWithdrawalsBatchesFinalized() external view returns (bool) { + return _batchesQueue.isClosed(); + } + + function isRageQuitExtensionDelayStarted() external view returns (bool) { + return _escrowState.isRageQuitExtensionDelayStarted(); + } + + function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp) { + return _escrowState.rageQuitExtensionDelayStartedAt; + } + + function getRageQuitSupport() external view returns (PercentD16) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + function isRageQuitFinalized() external view returns (bool) { + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionDelayPassed(); + } + + // --- + // Receive ETH + // --- + + receive() external payable { + if (msg.sender != address(WITHDRAWAL_QUEUE)) { + revert InvalidETHSender(msg.sender, address(WITHDRAWAL_QUEUE)); + } + } + + // --- + // Internal methods + // --- + + function _claimNextWithdrawalsBatch( + uint256 fromUnstETHId, + uint256[] memory unstETHIds, + uint256[] memory hints + ) internal { + if (fromUnstETHId != unstETHIds[0]) { + revert UnexpectedUnstETHId(); + } + + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); + } + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + _accounting.accountClaimedStETH(ethBalanceAfter - ethBalanceBefore); + } + + function _checkCallerIsDualGovernance() internal view { + if (msg.sender != address(DUAL_GOVERNANCE)) { + revert CallerIsNotDualGovernance(msg.sender); + } + } +} diff --git a/certora/mutation/mutants/EscrowState/EscrowStateResetFromRageQuitToSignalling.sol b/certora/mutation/mutants/EscrowState/EscrowStateResetFromRageQuitToSignalling.sol new file mode 100644 index 00000000..1fcbaa9c --- /dev/null +++ b/certora/mutation/mutants/EscrowState/EscrowStateResetFromRageQuitToSignalling.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +/// @notice The state of Escrow representing the current set of actions allowed to be called +/// on the Escrow instance. +/// @param NotInitialized The default (uninitialized) state of the Escrow contract. Only the master +/// copy of the Escrow contract is expected to be in this state. +/// @param SignallingEscrow In this state, the Escrow contract functions as an on-chain oracle for measuring stakers' disagreement +/// with DAO decisions. Users are allowed to lock and unlock funds in the Escrow contract in this state. +/// @param RageQuitEscrow The final state of the Escrow contract. In this state, the Escrow instance acts as an accumulator +/// for withdrawn funds locked during the VetoSignalling phase. +enum State { + NotInitialized, + SignallingEscrow, + RageQuitEscrow +} + +/// @notice Represents the logic to manipulate the state of the Escrow +library EscrowState { + // --- + // Errors + // --- + + error ClaimingIsFinished(); + error UnexpectedState(State value); + error RageQuitExtraTimelockNotStarted(); + error WithdrawalsTimelockNotPassed(); + error BatchesCreationNotInProgress(); + + // --- + // Events + // --- + + event RageQuitTimelockStarted(); + event EscrowStateChanged(State from, State to); + event RageQuitStarted(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock); + event MinAssetsLockDurationSet(Duration newAssetsLockDuration); + + /// @notice Stores the context of the state of the Escrow instance + /// @param state The current state of the Escrow instance + /// @param minAssetsLockDuration The minimum time required to pass before tokens can be unlocked from the Escrow + /// contract instance + /// @param rageQuitExtensionDelay The period of time that starts after all withdrawal batches are formed, which delays + /// the exit from the RageQuit state of the DualGovernance. The main purpose of the rage quit extension delay is to provide + /// enough time for users who locked their unstETH to claim it. + struct Context { + /// @dev slot0: [0..7] + State state; + /// @dev slot0: [8..39] + Duration minAssetsLockDuration; + /// @dev slot0: [40..71] + Duration rageQuitExtensionDelay; + /// @dev slot0: [72..111] + Timestamp rageQuitExtensionDelayStartedAt; + /// @dev slot0: [112..143] + Duration rageQuitWithdrawalsTimelock; + } + + function initialize(Context storage self, Duration minAssetsLockDuration) internal { + _checkState(self, State.NotInitialized); + _setState(self, State.SignallingEscrow); + _setMinAssetsLockDuration(self, minAssetsLockDuration); + } + + function startRageQuit( + Context storage self, + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock + ) internal { + _checkState(self, State.SignallingEscrow); + _setState(self, State.RageQuitEscrow); + self.rageQuitExtensionDelay = rageQuitExtensionDelay; + self.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + emit RageQuitStarted(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + } + + function startRageQuitExtensionDelay(Context storage self) internal { + self.rageQuitExtensionDelayStartedAt = Timestamps.now(); + // mutated + _setState(self, State.SignallingEscrow); + emit RageQuitTimelockStarted(); + } + + function setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) internal { + if (self.minAssetsLockDuration == newMinAssetsLockDuration) { + return; + } + _setMinAssetsLockDuration(self, newMinAssetsLockDuration); + } + + // --- + // Checks + // --- + + function checkSignallingEscrow(Context storage self) internal view { + _checkState(self, State.SignallingEscrow); + } + + function checkRageQuitEscrow(Context storage self) internal view { + _checkState(self, State.RageQuitEscrow); + } + + function checkBatchesClaimingInProgress(Context storage self) internal view { + if (!self.rageQuitExtensionDelayStartedAt.isZero()) { + revert ClaimingIsFinished(); + } + } + + function checkWithdrawalsTimelockPassed(Context storage self) internal view { + if (self.rageQuitExtensionDelayStartedAt.isZero()) { + revert RageQuitExtraTimelockNotStarted(); + } + Duration withdrawalsTimelock = self.rageQuitExtensionDelay + self.rageQuitWithdrawalsTimelock; + if (Timestamps.now() <= withdrawalsTimelock.addTo(self.rageQuitExtensionDelayStartedAt)) { + revert WithdrawalsTimelockNotPassed(); + } + } + + // --- + // Getters + // --- + function isRageQuitExtensionDelayStarted(Context storage self) internal view returns (bool) { + return self.rageQuitExtensionDelayStartedAt.isNotZero(); + } + + function isRageQuitExtensionDelayPassed(Context storage self) internal view returns (bool) { + Timestamp rageQuitExtensionDelayStartedAt = self.rageQuitExtensionDelayStartedAt; + return rageQuitExtensionDelayStartedAt.isNotZero() + && Timestamps.now() > self.rageQuitExtensionDelay.addTo(rageQuitExtensionDelayStartedAt); + } + + function isRageQuitEscrow(Context storage self) internal view returns (bool) { + return self.state == State.RageQuitEscrow; + } + + // --- + // Private Methods + // --- + + function _checkState(Context storage self, State state) private view { + if (self.state != state) { + revert UnexpectedState(state); + } + } + + function _setState(Context storage self, State newState) private { + State prevState = self.state; + self.state = newState; + emit EscrowStateChanged(prevState, newState); + } + + function _setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) private { + self.minAssetsLockDuration = newMinAssetsLockDuration; + emit MinAssetsLockDurationSet(newMinAssetsLockDuration); + } +} From de64e5b8138ff7d039060556f626ca86d48d749f Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 2 Sep 2024 11:24:53 +0100 Subject: [PATCH 49/67] Cleanup before PR --- certora/confs/DualGovernance.conf | 5 - .../confs/DualGovernanceEscrowsLinked.conf | 53 ----- certora/harnesses/DualGovernanceHarness.sol | 8 - certora/mutation/conf/DualGovernance.conf | 5 - certora/specs/DualGovernance.spec | 58 ++--- .../specs/DualGovernanceEscrowsLinked.spec | 204 ------------------ 6 files changed, 31 insertions(+), 302 deletions(-) delete mode 100644 certora/confs/DualGovernanceEscrowsLinked.conf delete mode 100644 certora/specs/DualGovernanceEscrowsLinked.spec diff --git a/certora/confs/DualGovernance.conf b/certora/confs/DualGovernance.conf index a22a92e0..b5ba435c 100644 --- a/certora/confs/DualGovernance.conf +++ b/certora/confs/DualGovernance.conf @@ -1,7 +1,6 @@ { "files": [ "contracts/libraries/DualGovernanceStateMachine.sol", - // "contracts/Escrow.sol", "contracts/Executor.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", @@ -25,8 +24,6 @@ "EscrowB:DUAL_GOVERNANCE=DualGovernanceHarness" ], "struct_link": [ - // "DualGovernanceHarness:rageQuitEscrow=EscrowA", - // "DualGovernanceHarness:signallingEscrow=EscrowB", "DualGovernanceHarness:resealManager=ResealManager", "EmergencyProtectedTimelock:executor=Executor", ], @@ -35,10 +32,8 @@ ], "parametric_contracts": [ "DualGovernanceHarness", - // I get timeouts in EPT.execute and EPT.emergencyExecute // "EmergencyProtectedTimelock", // "ResealManager", - // // Leaving out until WithdrawalManager // "Escrow", // // Not sure these are needed // "DummyStETH", diff --git a/certora/confs/DualGovernanceEscrowsLinked.conf b/certora/confs/DualGovernanceEscrowsLinked.conf deleted file mode 100644 index 938e0d66..00000000 --- a/certora/confs/DualGovernanceEscrowsLinked.conf +++ /dev/null @@ -1,53 +0,0 @@ -{ - "files": [ - "contracts/libraries/DualGovernanceStateMachine.sol", - // "contracts/Escrow.sol", - "contracts/Executor.sol", - "contracts/EmergencyProtectedTimelock.sol", - "contracts/ResealManager.sol", - "certora/helpers/EscrowA.sol", - "certora/helpers/EscrowB.sol", - "certora/harnesses/ERC20Like/DummyStETH.sol", - "certora/harnesses/ERC20Like/DummyWstETH.sol", - "certora/harnesses/DualGovernanceHarness.sol", - ], - "link": [ - "DualGovernanceHarness:TIMELOCK=EmergencyProtectedTimelock", - "ResealManager:EMERGENCY_PROTECTED_TIMELOCK=EmergencyProtectedTimelock", - "DualGovernanceHarness:RESEAL_MANAGER=ResealManager", - "EscrowA:ST_ETH=DummyStETH", - "EscrowA:WST_ETH=DummyWstETH", - "EscrowA:DUAL_GOVERNANCE=DualGovernanceHarness", - "EscrowB:ST_ETH=DummyStETH", - "EscrowB:WST_ETH=DummyWstETH", - "EscrowB:DUAL_GOVERNANCE=DualGovernanceHarness" - ], - "struct_link": [ - "DualGovernanceHarness:rageQuitEscrow=EscrowA", - "DualGovernanceHarness:signallingEscrow=EscrowB", - "DualGovernanceHarness:resealManager=ResealManager", - "EmergencyProtectedTimelock:executor=Executor", - ], - "packages": [ - "@openzeppelin=lib/openzeppelin-contracts" - ], - "parametric_contracts": [ - "DualGovernanceHarness", - // I get timeouts in EPT.execute and EPT.emergencyExecute - // "EmergencyProtectedTimelock", - // "ResealManager", - // // Leaving out until WithdrawalManager - // "Escrow", - // // Not sure these are needed - // "DummyStETH", - // "DummyWstETH", - ], - "rule_sanity": "basic", - "process": "emv", - "solc": "solc8.26", - "optimistic_loop": true, - "loop_iter": "5", - "smt_timeout": "3600", - "build_cache": true, - "verify": "DualGovernanceHarness:certora/specs/DualGovernanceEscrowsLinked.spec" -} \ No newline at end of file diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index a92ffd16..a40938de 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -62,14 +62,6 @@ contract DualGovernanceHarness is DualGovernance { return asDGHarnessState(_stateMachine.state); } - // function getStateTransition() external returns (DGHarnessState oldState, DGHarnessState newState) { - // (State oldState, State newState) = _stateMachine.getStateTransition( - // _configProvider.getDualGovernanceConfig(), - // ESCROW_MASTER_COPY - // ); - // return (asDGHarnessState(oldState), asDGHarnessState(newState)); - // } - function getFirstSeal() external view returns (uint256) { return PercentD16.unwrap(_configProvider.getDualGovernanceConfig().firstSealRageQuitSupport); } diff --git a/certora/mutation/conf/DualGovernance.conf b/certora/mutation/conf/DualGovernance.conf index ff089d35..b0faf87e 100644 --- a/certora/mutation/conf/DualGovernance.conf +++ b/certora/mutation/conf/DualGovernance.conf @@ -1,7 +1,6 @@ { "files": [ "contracts/libraries/DualGovernanceStateMachine.sol", - // "contracts/Escrow.sol", "contracts/Executor.sol", "contracts/EmergencyProtectedTimelock.sol", "contracts/ResealManager.sol", @@ -25,8 +24,6 @@ "EscrowB:DUAL_GOVERNANCE=DualGovernanceHarness" ], "struct_link": [ - // "DualGovernanceHarness:rageQuitEscrow=EscrowA", - // "DualGovernanceHarness:signallingEscrow=EscrowB", "DualGovernanceHarness:resealManager=ResealManager", "EmergencyProtectedTimelock:executor=Executor", ], @@ -37,9 +34,7 @@ "DualGovernanceHarness", // "EmergencyProtectedTimelock", // "ResealManager", - // // Leaving out until WithdrawalManager // "Escrow", - // // Not sure these are needed // "DummyStETH", // "DummyWstETH", ], diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 63704ee1..8c352495 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -31,22 +31,28 @@ methods { function _.isRageQuitFinalized() external => DISPATCHER(true); - // TODO check these NONDETs. So far they seem pretty irrelevant to the - // rules in scope for this contract. + // The NONDETs and summaries here essentially introduce an assumption + // that the summarized/NONDETed function does not influence the + // state of the contracts explicitly added to the scene. // This is reached by Escrow.withdrawETH() and makes a lowlevel - // call on amount causing a HAVOC. + // call on recipient causing a HAVOC. The call in Address passes + // an empty payload, so it will call the `receive` function of + // the recipient if there is one. function Address.sendValue(address recipient, uint256 amount) internal => NONDET; // This is reached by ResealManager.reseal and makes a low-level call // on target which havocs all contracts. (And we can't NONDET functions - // that return bytes). + // that return bytes). The implementation of Address is meant + // to be a safer alternative to directly using call, according to its + // comments. function Address.functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) => CVLFunctionCallWithValue(target, data, value); // This function belongs to ISealble which we do not have an implementation - // of and it causes a havoc of all contracts. - // It is reached by ResealManager.reaseal/resume + // of and it causes a havoc of all contracts. It is reached by + // ResealManager.reseal/resume. This is a view function so it must be safe. function _.getResumeSinceTimestamp() external => CONSTANT; // This function belongs to IOnable which we do not have an implementation // of and it causes a havoc of all contracts. It is reached by EPT. - // transferExecutorOwnership + // transferExecutorOwnership. It is not a view functionion, + // but from the description it likely only affects its own state. function _.transferOwnership(address newOwner) external => NONDET; // This is in is reached by 2 calls in EPT and reaches a call to // functionCallWithValue. (It may be subsumed by the summary to @@ -58,9 +64,6 @@ methods { // really needed for verifying DG. We also have separate rules for EPT. function EmergencyProtectedTimelock.submit(address executor, DualGovernanceHarness.ExternalCall[] calls) external returns (uint256) => NONDET; - - - } // Ideally we would return a ghost but then we run into the tool bug @@ -77,6 +80,8 @@ function escrowAddressIsRageQuit(address escrow) returns bool { } else if (escrow == EscrowB) { return EscrowB.isRageQuitState(); } + // EscrowA and EscrowB are the only ones in the scene so this should + // not be reached. return false; } @@ -86,8 +91,9 @@ function rageQuitThresholdAssumptions() returns bool { // for any registered proposer, his index should be ≤ the length of // the array of proposers -// “for each entry in the struct in the array, show that the index inside is the same as the real array index” -// NOTE: this has not been addressed by customer, so this should fail now. +// “for each entry in the struct in the array, show that the index inside is +// the same as the real array index” +// NOTE: this has not yet been addressed by Lido, so this should fail now. rule w2_1a_indexes_match (method f) { env e; calldataarg args; @@ -101,9 +107,9 @@ rule w2_1a_indexes_match (method f) { require getProposerIndexFromExecutor(proposer_addr) - 1 == idx; f(e, args); - // Strategy 1: check proposerIndex is <= proposers array length + // check proposerIndex is <= proposers array length assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; - // Strategy 2: check proposerIndex == real array index + // check proposerIndex == real array index assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; } @@ -115,14 +121,10 @@ rule dg_kp_1_proposal_execution { scheduleProposal(e, proposal_id); DualGovernanceHarness.DGHarnessState state = getState(); assert !isVetoSignalling(state) && !isRageQuit(state); - // This throws a type error wherein CLV claims DGHarnessState.VetoSignaling - // does not exist -- it seems like it starts to assume the type is a struct - // if you nest more than one deep - // DualGovernanceHarness.DGHarnessState.VetoSignaling && state != DualGovernanceHarness.DGHarnessState.RageQuit; } -// NOTE: moved this to other spec file while fixing timeout -// Proposals cannot be submitted in the Veto Signaling Deactivation sub-state or in the Veto Cooldown state. +// Proposals cannot be submitted in the Veto Signaling Deactivation sub-state +// or in the Veto Cooldown state. rule dg_kp_2_proposal_submission { env e; DualGovernanceHarness.ExternalCall[] calls; @@ -147,7 +149,7 @@ rule dg_kp_3_cooldown_execution (method f) { scheduleProposal(e, proposalId); - // This requires affects the state that was stepped into at the start of + // This requires refers to the state that was stepped into during the // the scheduleProposal call require isVetoCooldown(getState()); DualGovernanceHarness.Timestamp vetoSignallingActivatedAt = @@ -186,7 +188,6 @@ rule pp_kp_1_ragequit_extends { // - we do not transition into normal state // - if timelock is extended with ragequit support, we // cannot transition into VetoCooldown - // require getVetoSignallingEscrow(e) == EscrowA; uint256 rageQuitSupport = getRageQuitSupportHarnessed(e); require !isDynamicTimelockPassed(e, rageQuitSupport); require rageQuitThresholdAssumptions(); @@ -278,20 +279,23 @@ rule dg_states_1_proposal_submission_states() { env e; calldataarg args; submitProposal(e, args); - // we take the state after and not before, because state transitions are triggered at the start of actions, - // not at the end of the ones that caused them to become possible + // we take the state after and not before, because state transitions are + // triggered at the start of actions, not at the end of the ones that + // caused them to become possible. DualGovernanceHarness.DGHarnessState state = getState(); assert isNormal(state) || isVetoSignalling(state) || isRageQuit(state); } -// If proposal scheduling succeeds, the system was in one of these states: Normal, Veto Cooldown +// If proposal scheduling succeeds, the system was in one of these states: +// Normal, Veto Cooldown rule dg_states_2_proposal_scheduling_states() { env e; calldataarg args; scheduleProposal(e, args); - // we take the state after and not before, because state transitions are triggered at the start of actions, - // not at the end of the ones that caused them to become possible + // we take the state after and not before, because state transitions are + // triggered at the start of actions, not at the end of the ones that + // caused them to become possible. DualGovernanceHarness.DGHarnessState state = getState(); assert isNormal(state) || isVetoCooldown(state); diff --git a/certora/specs/DualGovernanceEscrowsLinked.spec b/certora/specs/DualGovernanceEscrowsLinked.spec deleted file mode 100644 index de1cea61..00000000 --- a/certora/specs/DualGovernanceEscrowsLinked.spec +++ /dev/null @@ -1,204 +0,0 @@ -using EscrowA as EscrowA; -using EscrowB as EscrowB; - -methods { - // envfrees - function getProposer(address account) external returns (Proposers.Proposer memory) envfree; - function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; - function getState() external returns (DualGovernanceHarness.DGHarnessState) envfree; - function isUnset(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isNormal(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isVetoSignalling(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isVetoSignallingDeactivation(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isVetoCooldown(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; - function getRageQuitEscrow() external returns (address) envfree; - - // DualGovernanceConfig summaries - // function _.isFirstSealRageQuitSupportCrossed( - // DualGovernanceConfig.Context memory configContext, - // DualGovernanceHarness.PercentD16 rageQuitSupport) internal => - // isFirstRageQuitCrossedGhost(rageQuitSupport) expect bool; - // function _.isSecondSealRageQuitSupportCrossed( - // DualGovernanceConfig.Context memory configContext, - // DualGovernanceHarness.PercentD16 rageQuitSupport) internal => - // isSecondRageQuitCrossedGhost(rageQuitSupport) expect bool; - - function EscrowA.getRageQuitSupport() external returns (DualGovernanceHarness.PercentD16) => CVLRagequitSupport(); - function EscrowB.getRageQuitSupport() external returns (DualGovernanceHarness.PercentD16) => CVLRagequitSupport(); - - // envfrees escrow - function EscrowA.isRageQuitState() external returns (bool) envfree; - function EscrowB.isRageQuitState() external returns (bool) envfree; - - // route escrow functions to implementations while - // still allowing escrow addresses to vary - function _.startRageQuit(DualGovernanceHarness.Duration, DualGovernance.Duration) external => DISPATCHER(true); - function _.initialize(DualGovernanceHarness.Duration) external => DISPATCHER(true); - function _.setMinAssetsLockDuration(DualGovernanceHarness.Duration newMinAssetsLockDuration) external => DISPATCHER(true); - function _.getRageQuitSupport() external => DISPATCHER(true); - - // TODO check these NONDETs. So far they seem pretty irrelevant to the - // rules in scope for this contract. - // This is reached by Escrow.withdrawETH() and makes a lowlevel - // call on amount causing a HAVOC. - function Address.sendValue(address recipient, uint256 amount) internal => NONDET; - // This is reached by ResealManager.reseal and makes a low-level call - // on target which havocs all contracts. (And we can't NONDET functions - // that return bytes). - function Address.functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) => CVLFunctionCallWithValue(target, data, value); - // This function belongs to ISealble which we do not have an implementation - // of and it causes a havoc of all contracts. - // It is reached by ResealManager.reaseal/resume - function _.getResumeSinceTimestamp() external => CONSTANT; - // This function belongs to IOnable which we do not have an implementation - // of and it causes a havoc of all contracts. It is reached by EPT. - // transferExecutorOwnership - function _.transferOwnership(address newOwner) external => NONDET; - // This is in is reached by 2 calls in EPT and reaches a call to - // functionCallWithValue. (It may be subsumed by the summary to - // Address.functionCallWithValue) - function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; - -} - -// Ideally we would return a ghost but then we run into the tool bug -// where a ghost declared bytes is actually given type "hashblob" -// and this bug won't be fixed :) -function CVLFunctionCallWithValue(address target, bytes data, uint256 value) returns bytes { - bytes ret; - return ret; -} - -// function escrowAddressIsRageQuit(address escrow) returns bool { -// if (escrow == EscrowA) { -// return EscrowA.isRageQuitState(); -// } else if (escrow == EscrowB) { -// return EscrowB.isRageQuitState(); -// } -// return false; -// } - -// Ghosts for support thresholds so we do not need to link -// in DualGovernanceConfig which has some nonlinear functions -// ghost uint256 rageQuitFirstSealGhost { -// init_state axiom rageQuitFirstSealGhost > 0; -// } -// ghost uint256 rageQuitSecondSealGhost { -// init_state axiom rageQuitSecondSealGhost > 0 && -// rageQuitFirstSealGhost < rageQuitSecondSealGhost; -// } -// function isFirstRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { -// require rageQuitFirstSealGhost > 0; -// require rageQuitSecondSealGhost > 0; -// require rageQuitFirstSealGhost < rageQuitSecondSealGhost; -// return rageQuitSupport > rageQuitFirstSealGhost; -// } -// function isSecondRageQuitCrossedGhost(uint256 rageQuitSupport) returns bool { -// require rageQuitFirstSealGhost > 0; -// require rageQuitSecondSealGhost > 0; -// require rageQuitFirstSealGhost < rageQuitSecondSealGhost; -// return rageQuitSupport > rageQuitSecondSealGhost; -// } - -persistent ghost uint256 ghost_ragequit_support; -function CVLRagequitSupport() returns uint256 { - return ghost_ragequit_support; -} - -// PP-1: Regardless of the state in which a proposal is submitted, if the -// stakers are able to amass and maintain a certain amount of rage quit -// support before the ProposalExecutionMinTimelock expires, they can extend -// the timelock for a proportional time, according to the dynamic timelock -// calculation. -// expected complexity: low -rule pp_kp_1_ragequit_extends_v2 { - // Get proposal in submitted state - env e1; - uint256 proposalId; - uint256 id; - ExecutableProposals.Status proposal_status; - address executor; - DualGovernanceHarness.Timestamp submittedAt; - DualGovernanceHarness.Timestamp scheduledAt; - (id, proposal_status, executor, submittedAt, scheduledAt) = - getProposalInfoHarnessed(e1, proposalId); - // state is Submitted - require assert_uint8(proposal_status) == 1; - require submittedAt < e1.block.timestamp; - - // setup different rage quits for different executions - uint256 firstSeal = getFirstSeal(e1); - uint256 secondSeal = getSecondSeal(e1); - require firstSeal > 0; - require secondSeal > firstSeal; - - // 2 rageQuitSupport values both between the first and second seal - uint256 rageQuitSupport1; - uint256 rageQuitSupport2; - require rageQuitSupport1 > firstSeal && rageQuitSupport1 < secondSeal; - require rageQuitSupport2 > firstSeal && rageQuitSupport2 < secondSeal; - require rageQuitSupport1 > rageQuitSupport2; - - - storage initialState = lastStorage; - - // NOTE: I think the way this works I will get a sanity - // failure by requesting 2 different values from getRageQuitSupport - // instead I can move this to a separate file and use a summary to make - // getRageQuitSupport return a ghost and then impose the requirements on - // the ghost. - - // Execution 1: set R = rageQuitSupport1 then: - // - take a state step from an arbitrary later timestamp - // - advance to an arbitrary later timestamp after that and schedule - env ex1_step; - // Here assuming EscrowA is vetoSignalling Escrow - // require EscrowA.getRageQuitSupport(ex1_step) == rageQuitSupport1; - ghost_ragequit_support = rageQuitSupport1; - require ex1_step.block.timestamp > e1.block.timestamp; - // advance state - activateNextState(ex1_step); - env ex1_schedule; - require ex1_schedule.block.timestamp > ex1_step.block.timestamp; - scheduleProposal@withrevert(ex1_schedule, proposalId); - bool ex1_sched_reverted = lastReverted; - - // Execution 2: similar to Execution 1 but with R=rageQuitSupport2 - env ex2_step; - // Here assuming EscrowA is vetoSignalling Escrow - // require EscrowA.getRageQuitSupport(ex2_step) at initialState == rageQuitSupport2; - ghost_ragequit_support = rageQuitSupport2; - require ex2_step.block.timestamp > e1.block.timestamp; - // advance state - activateNextState(ex2_step) at initialState; - env ex2_schedule; - require ex2_schedule.block.timestamp > ex2_step.block.timestamp; - scheduleProposal@withrevert(ex2_schedule, proposalId); - bool ex2_sched_reverted = lastReverted; - - // the Good one: - // satisfy ex1_sched_reverted && !ex2_sched_reverted; - // the bad one: - satisfy !ex1_sched_reverted && ex2_sched_reverted; - -} - -// Alternative: maybe it's more useful to just prove that dynamicDelayDuration -// is monotonically increasing with increased rageQuitSupport. - -// PP-2: It's not possible to prevent a proposal from being executed -// indefinitely without triggering a rage quit. -// expected complexity: extra high - -// One option: assume rageQuitSupport == max, show secondSealRageQuit support -// is crossed. Seems trivial though. - -// PP-3: It's not possible to block proposal submission indefinitely. -// expected complexity: high - -// PP-4: Until the Veto Signaling Deactivation sub-state transitions to Veto -// Cooldown, there is always a possibility (given enough rage quit support) of -// canceling Deactivation and returning to the parent state (possibly -// triggering a rage quit immediately afterwards). \ No newline at end of file From 69df5ad8d170cbf4f59b9b5d262522c7347340a9 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Mon, 2 Sep 2024 11:47:12 +0100 Subject: [PATCH 50/67] Delete monday update script --- certora/scripts/monday.sh | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 certora/scripts/monday.sh diff --git a/certora/scripts/monday.sh b/certora/scripts/monday.sh deleted file mode 100644 index fc97404b..00000000 --- a/certora/scripts/monday.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -update-monday --api-key $SRC/monday_token/token.txt --board 1593099164 --job $1 \ No newline at end of file From 076cc61efb9f8c978897997b07ef7477d9daac77 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Mon, 2 Sep 2024 13:11:57 +0200 Subject: [PATCH 51/67] mutant for E_KP_1 --- .../Escrow/EscrowBuggyGetRageQuitSupport.sol | 474 ++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 certora/mutation/mutants/Escrow/EscrowBuggyGetRageQuitSupport.sol diff --git a/certora/mutation/mutants/Escrow/EscrowBuggyGetRageQuitSupport.sol b/certora/mutation/mutants/Escrow/EscrowBuggyGetRageQuitSupport.sol new file mode 100644 index 00000000..270bba60 --- /dev/null +++ b/certora/mutation/mutants/Escrow/EscrowBuggyGetRageQuitSupport.sol @@ -0,0 +1,474 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ETHValue, ETHValues} from "./types/ETHValue.sol"; +import {SharesValue, SharesValues} from "./types/SharesValue.sol"; +import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; + +import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; + +import {EscrowState} from "./libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; + +/// @notice Summary of the total locked assets in the Escrow +/// @param stETHLockedShares Total number of stETH shares locked in the Escrow +/// @param stETHClaimedETH Total amount of ETH claimed from the stETH locked in the Escrow +/// @param unstETHUnfinalizedShares Total number of shares from unstETH NFTs that have not yet been +/// marked as finalized +/// @param unstETHFinalizedETH Total claimable amount of ETH from unstETH NFTs that have been marked +/// as finalized +struct LockedAssetsTotals { + uint256 stETHLockedShares; + uint256 stETHClaimedETH; + uint256 unstETHUnfinalizedShares; + uint256 unstETHFinalizedETH; +} + +struct VetoerState { + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; +} + +contract Escrow is IEscrow { + using EscrowState for EscrowState.Context; + using AssetsAccounting for AssetsAccounting.Context; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; + + // --- + // Errors + // --- + + error UnclaimedBatches(); + error UnexpectedUnstETHId(); + error UnfinalizedUnstETHIds(); + error NonProxyCallsForbidden(); + error BatchesQueueIsNotClosed(); + error InvalidBatchSize(uint256 size); + error CallerIsNotDualGovernance(address caller); + error InvalidHintsLength(uint256 actual, uint256 expected); + error InvalidETHSender(address actual, address expected); + + // --- + // Events + // --- + + event ConfigProviderSet(address newConfigProvider); + + // --- + // Sanity check params immutables + // --- + + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + + // --- + // Dependencies immutables + // --- + + IStETH public immutable ST_ETH; + IWstETH public immutable WST_ETH; + IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; + + // --- + // Implementation immutables + + address private immutable _SELF; + IDualGovernance public immutable DUAL_GOVERNANCE; + + // --- + // Aspects + // --- + + EscrowState.Context internal _escrowState; + AssetsAccounting.Context private _accounting; + WithdrawalsBatchesQueue.Context private _batchesQueue; + + // --- + // Construction & initializing + // --- + + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) { + _SELF = address(this); + DUAL_GOVERNANCE = dualGovernance; + + ST_ETH = stETH; + WST_ETH = wstETH; + WITHDRAWAL_QUEUE = withdrawalQueue; + + MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; + } + + function initialize(Duration minAssetsLockDuration) external { + if (address(this) == _SELF) { + revert NonProxyCallsForbidden(); + } + _checkCallerIsDualGovernance(); + + _escrowState.initialize(minAssetsLockDuration); + + ST_ETH.approve(address(WST_ETH), type(uint256).max); + ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); + } + + // --- + // Lock & unlock stETH + // --- + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockStETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + + DUAL_GOVERNANCE.activateNextState(); + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); + ST_ETH.transferShares(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock wstETH + // --- + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WST_ETH.transferFrom(msg.sender, address(this), amount); + lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockWstETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); + WST_ETH.transfer(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock unstETH + // --- + function lockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { + _escrowState.checkSignallingEscrow(); + + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); + } + + // --- + // Convert to NFT + // --- + + function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + _escrowState.checkSignallingEscrow(); + + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + + // --- + // Start rage quit + // --- + + function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { + _checkCallerIsDualGovernance(); + _escrowState.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); + } + + // --- + // Request withdrawal batches + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { + _escrowState.checkRageQuitEscrow(); + + if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { + revert InvalidBatchSize(batchSize); + } + + uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); + uint256 minStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + + if (stETHRemaining < minStETHWithdrawalRequestAmount) { + return _batchesQueue.close(); + } + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: minStETHWithdrawalRequestAmount, + maxRequestAmount: maxStETHWithdrawalRequestAmount, + remainingAmount: Math.min(stETHRemaining, maxStETHWithdrawalRequestAmount * batchSize) + }); + + _batchesQueue.addUnstETHIds(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); + } + + // --- + // Claim requested withdrawal batches + // --- + + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + _claimNextWithdrawalsBatch( + unstETHIds[0], + unstETHIds, + WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) + ); + } + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); + + _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); + } + + // --- + // Start rage quit extension delay + // --- + + function startRageQuitExtensionDelay() external { + if (!_batchesQueue.isClosed()) { + revert BatchesQueueIsNotClosed(); + } + + /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionDelay can only begin + /// when the last locked unstETH id is finalized in the WithdrawalQueue. + /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: + /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created + /// during the request for withdrawal batches. + /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. + /// - The finalization of unstETH NFTs occurs in FIFO order. + if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { + revert UnfinalizedUnstETHIds(); + } + + if (!_batchesQueue.isAllBatchesClaimed()) { + revert UnclaimedBatches(); + } + + _escrowState.startRageQuitExtensionDelay(); + } + + // --- + // Claim locked unstETH NFTs + // --- + + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); + assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); + } + + // --- + // Escrow management + // --- + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // Withdraw logic + // --- + + function withdrawETH() external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + function withdrawETH(uint256[] calldata unstETHIds) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); + totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); + totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); + } + + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + state.unstETHIdsCount = assets.unstETHIds.length; + state.stETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); + } + + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + return _batchesQueue.getNextWithdrawalsBatches(limit); + } + + function isWithdrawalsBatchesFinalized() external view returns (bool) { + return _batchesQueue.isClosed(); + } + + function isRageQuitExtensionDelayStarted() external view returns (bool) { + return _escrowState.isRageQuitExtensionDelayStarted(); + } + + function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp) { + return _escrowState.rageQuitExtensionDelayStartedAt; + } + + function getRageQuitSupport() external view returns (PercentD16) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + // mutated + uint256 unfinalizedShares = (stETHTotals.lockedShares + stETHTotals.lockedShares).toUint256(); + //uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + function isRageQuitFinalized() external view returns (bool) { + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionDelayPassed(); + } + + // --- + // Receive ETH + // --- + + receive() external payable { + if (msg.sender != address(WITHDRAWAL_QUEUE)) { + revert InvalidETHSender(msg.sender, address(WITHDRAWAL_QUEUE)); + } + } + + // --- + // Internal methods + // --- + + function _claimNextWithdrawalsBatch( + uint256 fromUnstETHId, + uint256[] memory unstETHIds, + uint256[] memory hints + ) internal { + if (fromUnstETHId != unstETHIds[0]) { + revert UnexpectedUnstETHId(); + } + + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); + } + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + _accounting.accountClaimedStETH(ethBalanceAfter - ethBalanceBefore); + } + + function _checkCallerIsDualGovernance() internal view { + if (msg.sender != address(DUAL_GOVERNANCE)) { + revert CallerIsNotDualGovernance(msg.sender); + } + } +} From d5d33a2998dab7cd5cd635d72e46a4785f8596c4 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Mon, 2 Sep 2024 16:10:15 +0200 Subject: [PATCH 52/67] address simple review comments --- certora/confs/EmergencyProtectedTimelock.conf | 4 ++-- .../conf/EmergencyProtectedTimelock.conf | 1 - certora/specs/Timelock.spec | 20 ++++++++++++------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/certora/confs/EmergencyProtectedTimelock.conf b/certora/confs/EmergencyProtectedTimelock.conf index 9a527f18..30471d7d 100644 --- a/certora/confs/EmergencyProtectedTimelock.conf +++ b/certora/confs/EmergencyProtectedTimelock.conf @@ -7,7 +7,6 @@ "contracts/types/Timestamp.sol:Timestamps", "contracts/types/Duration.sol:Durations" ], - "link": [], "struct_link": [ "EmergencyProtectedTimelock:executor=Executor", ], @@ -20,5 +19,6 @@ "optimistic_loop": true, "solc_via_ir": false, "verify": "EmergencyProtectedTimelock:certora/specs/Timelock.spec", - "rule_sanity": "basic" + "rule_sanity": "basic", + "server": "production" } \ No newline at end of file diff --git a/certora/mutation/conf/EmergencyProtectedTimelock.conf b/certora/mutation/conf/EmergencyProtectedTimelock.conf index 7cc92eb8..f64c6391 100644 --- a/certora/mutation/conf/EmergencyProtectedTimelock.conf +++ b/certora/mutation/conf/EmergencyProtectedTimelock.conf @@ -7,7 +7,6 @@ "contracts/types/Timestamp.sol:Timestamps", "contracts/types/Duration.sol:Durations" ], - "link": [], "struct_link": [ "EmergencyProtectedTimelock:executor=Executor", ], diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index 2fcf1e84..6399577e 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -17,12 +17,16 @@ methods { function _.execute(address, uint256, bytes) external => nondetBytes() expect bytes; } -// somehow, specifying it like this instead of NONDET avoids a revert based on the returned value in EPT_9_EmergencyModeLiveness +// returns default empty bytes object, since we don't need to know anything about the returned value of execute in any of our rules +// specifying it like this instead of NONDET avoids a revert based on the returned value in EPT_9_EmergencyModeLiveness function nondetBytes() returns bytes { bytes b; return b; } +function proposalIsExecuted(uint proposalId) returns bool { + return getProposal(proposalId).status == ExecutableProposals.Status.Executed; +} /** @title Executed is a terminal state for a proposal, once executed it cannot transition to any other state @@ -31,13 +35,13 @@ function nondetBytes() returns bytes { rule W1_4_TerminalityOfExecuted(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { uint proposalId; requireInvariant outOfBoundsProposalDoesNotExist(proposalId); - require getProposal(proposalId).status == ExecutableProposals.Status.Executed; + require proposalIsExecuted(proposalId); env e; calldataarg args; f(e, args); - assert getProposal(proposalId).status == ExecutableProposals.Status.Executed; + assert proposalIsExecuted(proposalId); } invariant outOfBoundsProposalDoesNotExist(uint proposalId) proposalId == 0 || proposalId > getProposalsCount() => getProposal(proposalId).status == ExecutableProposals.Status.NotExist @@ -83,6 +87,8 @@ function effectiveEmergencyActivationCommittee(env e) returns address { /** @title Emergency protection configuration changes are guarded by committees or admin executor + We check here that the part of the state that should only be alterable by the respective emergency committees + or through an admin proposal is indeed not changed on any method call other than ones correctly authorized */ rule EPT_1_EmergencyProtectionConfigurationGuarded(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { EmergencyProtection.Context before = getEmergencyProtectionContext(); @@ -150,7 +156,7 @@ rule EPT_2b_SubmissionGovernanceOnly { rule EPT_3_EmergencyModeExecutionRestriction(method f) filtered { f -> f.selector != sig:Executor.execute(address, uint256, bytes).selector } { uint proposalId; requireInvariant outOfBoundsProposalDoesNotExist(proposalId); - bool executedBefore = getProposal(proposalId).status == ExecutableProposals.Status.Executed; + bool executedBefore = proposalIsExecuted(proposalId); bool isEmergencyModeActivated = isEmergencyModeActive(); @@ -160,7 +166,7 @@ rule EPT_3_EmergencyModeExecutionRestriction(method f) filtered { f -> f.selecto calldataarg args; f(e, args); - bool executedAfter = getProposal(proposalId).status == ExecutableProposals.Status.Executed; + bool executedAfter = proposalIsExecuted(proposalId); assert isEmergencyModeActivated && !executedBefore && executedAfter => e.msg.sender == effectiveEmergencyExecutionCommittee; } @@ -241,7 +247,7 @@ rule EPT_10_ProposalTimestampConsistency(method f) filtered { f -> f.selector != // for the execution methods we also check that they update the status, since executedAt is not longer included as a timestamp, // but EPT_3_EmergencyModeExecutionRestriction depends on the execution status being recorded correctly to be meaningful assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) - && getProposal(proposalIdToExecute).status == ExecutableProposals.Status.Executed; + && proposalIsExecuted(proposalIdToExecute); } else if (f.selector == sig:emergencyExecute(uint).selector) { uint proposalId; ITimelock.Proposal proposal_before = getProposal(proposalId); @@ -250,7 +256,7 @@ rule EPT_10_ProposalTimestampConsistency(method f) filtered { f -> f.selector != emergencyExecute(e, proposalIdToExecute); assert proposalTimestampsEqual(proposal_before, getProposal(proposalId)) - && getProposal(proposalIdToExecute).status == ExecutableProposals.Status.Executed; + && proposalIsExecuted(proposalIdToExecute); } else { uint proposalId; ITimelock.Proposal proposal_before = getProposal(proposalId); From 17a13e622e9a5372c71798614769e727394c164b Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Mon, 2 Sep 2024 17:10:06 +0200 Subject: [PATCH 53/67] add comment for EPT_10 --- certora/specs/Timelock.spec | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index 6399577e..ac205008 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -218,6 +218,9 @@ rule EPT_10_ProposalTimestampConsistency(method f) filtered { f -> f.selector != env e; require e.block.timestamp <= max_uint40; + // For each function that should update a timestamp, we need to check that the correct timestamp of the correct proposal was updated, + // while any proposal that was not the one the function acted on should remain unchanged. + // For any other function, all proposals should have their timestamps unchanged. if (f.selector == sig:submit(address, ExternalCalls.ExternalCall[]).selector) { uint proposalId; ITimelock.Proposal proposal_before = getProposal(proposalId); From 95b540bb50c853acb1bc7c4e1a8b82ad477d04c3 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Mon, 2 Sep 2024 17:30:44 +0200 Subject: [PATCH 54/67] add an unchanged copy of ExecutableProposals to mutants to preserve the buggy state that W1_4 catches --- ...WithCancellationMarkingBugStillPresent.sol | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 certora/mutation/mutants/ExecutableProposals/ExecutableProposalsWithCancellationMarkingBugStillPresent.sol diff --git a/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsWithCancellationMarkingBugStillPresent.sol b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsWithCancellationMarkingBugStillPresent.sol new file mode 100644 index 00000000..f94b0533 --- /dev/null +++ b/certora/mutation/mutants/ExecutableProposals/ExecutableProposalsWithCancellationMarkingBugStillPresent.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + +import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; + +/// @dev Describes the lifecycle state of a proposal +enum Status { + /// Proposal has not been submitted yet + NotExist, + /// Proposal has been successfully submitted but not scheduled yet. This state is only reachable from NotExist + Submitted, + /// Proposal has been successfully scheduled after submission. This state is only reachable from Submitted + Scheduled, + /// Proposal has been successfully executed after being scheduled. This state is only reachable from Scheduled + /// and is the final state of the proposal + Executed, + /// Proposal was cancelled before execution. Cancelled proposals cannot be resubmitted or rescheduled. + /// This state is only reachable from Submitted or Scheduled and is the final state of the proposal. + /// @dev A proposal is considered cancelled if it was not executed and its ID is less than the ID of the last + /// submitted proposal at the time the cancelAll() method was called. To check if a proposal is in the Cancelled + /// state, use the _isProposalMarkedCancelled() view function. + Cancelled +} + +/// @dev Manages a collection of proposals with associated external calls stored as Proposal struct. +/// Proposals are uniquely identified by sequential IDs, starting from one. +library ExecutableProposals { + using ExternalCalls for ExternalCall[]; + + /// @dev Efficiently stores proposal data within a single EVM word. + /// This struct allows gas-efficient loading from storage using a single EVM sload operation. + struct ProposalData { + /// + /// @dev slot 0: [0..7] + /// The current status of the proposal. See Status for details. + Status status; + /// + /// @dev slot 0: [8..167] + /// The address of the associated executor used for executing the proposal's calls. + address executor; + /// + /// @dev slot 0: [168..207] + /// The timestamp when the proposal was submitted. + Timestamp submittedAt; + /// + /// @dev slot 0: [208..247] + /// The timestamp when the proposal was scheduled for execution. Equals zero if the proposal hasn't been scheduled yet. + Timestamp scheduledAt; + } + + struct Proposal { + /// @dev Proposal data packed into a struct for efficient loading into memory. + ProposalData data; + /// @dev The list of external calls associated with the proposal. + ExternalCall[] calls; + } + + error EmptyCalls(); + error ProposalNotFound(uint256 proposalId); + error ProposalNotScheduled(uint256 proposalId); + error ProposalNotSubmitted(uint256 proposalId); + error AfterSubmitDelayNotPassed(uint256 proposalId); + error AfterScheduleDelayNotPassed(uint256 proposalId); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalScheduled(uint256 indexed id); + event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalsCancelledTill(uint256 proposalId); + + struct Context { + uint64 proposalsCount; + uint64 lastCancelledProposalId; + mapping(uint256 proposalId => Proposal) proposals; + } + + // --- + // Proposal lifecycle + // --- + + function submit( + Context storage self, + address executor, + ExternalCall[] memory calls + ) internal returns (uint256 newProposalId) { + if (calls.length == 0) { + revert EmptyCalls(); + } + + /// @dev: proposal ids are one-based. The first item has id = 1 + newProposalId = ++self.proposalsCount; + Proposal storage newProposal = self.proposals[newProposalId]; + + newProposal.data.executor = executor; + newProposal.data.status = Status.Submitted; + newProposal.data.submittedAt = Timestamps.now(); + + uint256 callsCount = calls.length; + for (uint256 i = 0; i < callsCount; ++i) { + newProposal.calls.push(calls[i]); + } + + emit ProposalSubmitted(newProposalId, executor, calls); + } + + function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { + ProposalData memory proposalState = self.proposals[proposalId].data; + + if (proposalState.status != Status.Submitted || _isProposalMarkedCancelled(self, proposalId, proposalState)) { + revert ProposalNotSubmitted(proposalId); + } + + if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + revert AfterSubmitDelayNotPassed(proposalId); + } + + proposalState.status = Status.Scheduled; + proposalState.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalState; + + emit ProposalScheduled(proposalId); + } + + function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { + Proposal memory proposal = self.proposals[proposalId]; + + if (proposal.data.status != Status.Scheduled || _isProposalMarkedCancelled(self, proposalId, proposal.data)) { + revert ProposalNotScheduled(proposalId); + } + + if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { + revert AfterScheduleDelayNotPassed(proposalId); + } + + self.proposals[proposalId].data.status = Status.Executed; + + address executor = proposal.data.executor; + ExternalCall[] memory calls = proposal.calls; + + bytes[] memory results = calls.execute(IExternalExecutor(executor)); + + emit ProposalExecuted(proposalId, results); + } + + function cancelAll(Context storage self) internal { + uint64 lastCancelledProposalId = self.proposalsCount; + self.lastCancelledProposalId = lastCancelledProposalId; + emit ProposalsCancelledTill(lastCancelledProposalId); + } + + // --- + // Getters + // --- + + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + } + + function canSchedule( + Context storage self, + uint256 proposalId, + Duration afterSubmitDelay + ) internal view returns (bool) { + ProposalData memory proposalState = self.proposals[proposalId].data; + if (_isProposalMarkedCancelled(self, proposalId, proposalState)) return false; + return proposalState.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + } + + function getProposalsCount(Context storage self) internal view returns (uint256) { + return self.proposalsCount; + } + + function getProposalInfo( + Context storage self, + uint256 proposalId + ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ProposalData memory proposalData = self.proposals[proposalId].data; + _checkProposalExists(proposalId, proposalData); + + status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + executor = address(proposalData.executor); + submittedAt = proposalData.submittedAt; + scheduledAt = proposalData.scheduledAt; + } + + function getProposalCalls( + Context storage self, + uint256 proposalId + ) internal view returns (ExternalCall[] memory calls) { + Proposal memory proposal = self.proposals[proposalId]; + _checkProposalExists(proposalId, proposal.data); + calls = proposal.calls; + } + + // --- + // Private methods + // --- + + function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { + if (proposalData.status == Status.NotExist) { + revert ProposalNotFound(proposalId); + } + } + + function _isProposalMarkedCancelled( + Context storage self, + uint256 proposalId, + ProposalData memory proposalData + ) private view returns (bool) { + // mutation: keeping this line from buggy version, should be changed in upcoming fix + return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + } +} From d8b5222e6428e297378a37db1ef2c8a0ca0ec5f6 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Tue, 3 Sep 2024 11:42:57 +0100 Subject: [PATCH 55/67] PR fixups --- certora/harnesses/DualGovernanceHarness.sol | 5 ++ certora/specs/DualGovernance.spec | 56 +++++++++------------ 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/certora/harnesses/DualGovernanceHarness.sol b/certora/harnesses/DualGovernanceHarness.sol index a40938de..84496cbb 100644 --- a/certora/harnesses/DualGovernanceHarness.sol +++ b/certora/harnesses/DualGovernanceHarness.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import "../../contracts/libraries/Proposers.sol"; import "../../contracts/DualGovernance.sol"; import {Status as ProposalStatus} from "../../contracts/libraries/ExecutableProposals.sol"; +import {Proposal} from "../../contracts/libraries/EnumerableProposals.sol"; // This is to make a type available for a NONDET summary import {IExternalExecutor} from "../../contracts/interfaces/IExternalExecutor.sol"; import {State, DualGovernanceStateMachine} from "../../contracts/libraries/DualGovernanceStateMachine.sol"; @@ -49,6 +50,10 @@ contract DualGovernanceHarness is DualGovernance { return TIMELOCK.getProposalInfo(proposalId); } + function getProposalHarnessed(uint256 proposalId) external view returns (ITimelock.Proposal memory proposal) { + return TIMELOCK.getProposal(proposalId); + } + function getVetoSignallingActivatedAt() external view returns (Timestamp) { return _stateMachine.vetoSignallingActivatedAt; } diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 8c352495..39156bcf 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -45,18 +45,17 @@ methods { // to be a safer alternative to directly using call, according to its // comments. function Address.functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) => CVLFunctionCallWithValue(target, data, value); - // This function belongs to ISealble which we do not have an implementation + // This function belongs to ISealable which we do not have an implementation // of and it causes a havoc of all contracts. It is reached by // ResealManager.reseal/resume. This is a view function so it must be safe. function _.getResumeSinceTimestamp() external => CONSTANT; - // This function belongs to IOnable which we do not have an implementation + // This function belongs to IOwnable which we do not have an implementation // of and it causes a havoc of all contracts. It is reached by EPT. - // transferExecutorOwnership. It is not a view functionion, + // transferExecutorOwnership. It is not a view function, // but from the description it likely only affects its own state. function _.transferOwnership(address newOwner) external => NONDET; - // This is in is reached by 2 calls in EPT and reaches a call to - // functionCallWithValue. (It may be subsumed by the summary to - // Address.functionCallWithValue) + // This is reached by 2 calls in EPT and reaches a call to + // functionCallWithValue. function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; // This NONDET is meant to address a timeout of dg_kp_2 @@ -66,9 +65,6 @@ methods { DualGovernanceHarness.ExternalCall[] calls) external returns (uint256) => NONDET; } -// Ideally we would return a ghost but then we run into the tool bug -// where a ghost declared bytes is actually given type "hashblob" -// and this bug won't be fixed :) function CVLFunctionCallWithValue(address target, bytes data, uint256 value) returns bytes { bytes ret; return ret; @@ -94,24 +90,13 @@ function rageQuitThresholdAssumptions() returns bool { // “for each entry in the struct in the array, show that the index inside is // the same as the real array index” // NOTE: this has not yet been addressed by Lido, so this should fail now. -rule w2_1a_indexes_match (method f) { - env e; - calldataarg args; - Proposers.Proposer[] proposers = getProposers(e); - require proposers.length <= 5; // loop unrolling - uint256 idx; - require idx <= proposers.length; - mathint get_proposers_length = proposers.length; - address proposer_addr = proposers[idx].account; - require getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; - require getProposerIndexFromExecutor(proposer_addr) - 1 == idx; - - f(e, args); - // check proposerIndex is <= proposers array length - assert getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length; - // check proposerIndex == real array index - assert getProposerIndexFromExecutor(proposer_addr) - 1 == idx; -} +invariant w2_1a_indexes_match (address proposer_addr, uint256 idx, + Proposers.Proposer[] proposers) + proposers.length <= 5 && // loop unrolling + proposer_addr == proposers[idx].account && + (getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length) && + (getProposerIndexFromExecutor(proposer_addr) - 1 == idx) { + } // Proposals cannot be executed in the Veto Signaling (both parent state and // Deactivation sub-state) and Rage Quit states. @@ -120,7 +105,8 @@ rule dg_kp_1_proposal_execution { uint256 proposal_id; scheduleProposal(e, proposal_id); DualGovernanceHarness.DGHarnessState state = getState(); - assert !isVetoSignalling(state) && !isRageQuit(state); + assert !isVetoSignalling(state) && !isRageQuit(state) && + !isVetoSignallingDeactivation(state); } // Proposals cannot be submitted in the Veto Signaling Deactivation sub-state @@ -157,9 +143,20 @@ rule dg_kp_3_cooldown_execution (method f) { assert submittedAt <= vetoSignallingActivatedAt; } +// One rage quit cannot start until the previous rage quit has finalized. In +// other words, there can only be at most one active rage quit escrow at a time. // One rage quit cannot start until the previous rage quit has finalized. In // other words, there can only be at most one active rage quit escrow at a time. rule dg_kp_4_single_ragequit (method f) { + env e; + calldataarg args; + require getRageQuitEscrow() != 0 => escrowAddressIsRageQuit(getRageQuitEscrow()); + require EscrowA == EscrowB || !(EscrowA.isRageQuitState() && EscrowB.isRageQuitState()); + f(e, args); + assert EscrowA == EscrowB || !(EscrowA.isRageQuitState() && EscrowB.isRageQuitState()); +} + +rule dg_kp_4_single_ragequit_adendum (method f) { env e; calldataarg args; require !escrowAddressIsRageQuit(getVetoSignallingEscrow()); @@ -172,7 +169,6 @@ rule dg_kp_4_single_ragequit (method f) { // support before the ProposalExecutionMinTimelock expires, they can extend // the timelock for a proportional time, according to the dynamic timelock // calculation. -// expected complexity: low rule pp_kp_1_ragequit_extends { env e; // Assume not initially in VetoCooldown as we stay in this state @@ -200,7 +196,6 @@ rule pp_kp_1_ragequit_extends { // PP-2: It's not possible to prevent a proposal from being executed // indefinitely without triggering a rage quit. -// expected complexity: extra high rule pp_kp_2_ragequit_trigger { env e; calldataarg args; @@ -237,7 +232,6 @@ rule pp_kp_2_ragequit_trigger { } // PP-3: It's not possible to block proposal submission indefinitely. -// expected complexity: high rule pp_kp_3_no_indefinite_proposal_submission_block { env e; From 60135c99d3e537c020ea5fca42e549e79c7452de Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Tue, 3 Sep 2024 11:47:49 +0100 Subject: [PATCH 56/67] try putting space instead of tabs --- certora/specs/DualGovernance.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 39156bcf..d8ad4a14 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -2,8 +2,8 @@ using EscrowA as EscrowA; using EscrowB as EscrowB; methods { - // envfrees - function getProposer(address account) external returns (Proposers.Proposer memory) envfree; + // envfrees + function getProposer(address account) external returns (Proposers.Proposer memory) envfree; function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; function getState() external returns (DualGovernanceHarness.DGHarnessState) envfree; function isUnset(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; From edc8239cb6be95f1b30691b467f3cf6d5643067a Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Tue, 3 Sep 2024 14:51:26 +0100 Subject: [PATCH 57/67] methods block whitespace --- certora/specs/DualGovernance.spec | 116 +++++++++++++++--------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index d8ad4a14..a0e6eb71 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -5,64 +5,64 @@ methods { // envfrees function getProposer(address account) external returns (Proposers.Proposer memory) envfree; function getProposerIndexFromExecutor(address proposer) external returns (uint32) envfree; - function getState() external returns (DualGovernanceHarness.DGHarnessState) envfree; - function isUnset(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isNormal(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isVetoSignalling(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isVetoSignallingDeactivation(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isVetoCooldown(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; - function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; - function getRageQuitEscrow() external returns (address) envfree; - function getVetoSignallingEscrow() external returns (address) envfree; - function getFirstSeal() external returns (uint256) envfree; - function getSecondSeal() external returns (uint256) envfree; - - // envfrees escrow - function EscrowA.isRageQuitState() external returns (bool) envfree; - function EscrowB.isRageQuitState() external returns (bool) envfree; - - // route escrow functions to implementations while - // still allowing escrow addresses to vary - function _.startRageQuit(DualGovernanceHarness.Duration, DualGovernance.Duration) external => DISPATCHER(true); - function _.initialize(DualGovernanceHarness.Duration) external => DISPATCHER(true); - function _.setMinAssetsLockDuration(DualGovernanceHarness.Duration newMinAssetsLockDuration) external => DISPATCHER(true); - function _.getRageQuitSupport() external => DISPATCHER(true); - function _.isRageQuitFinalized() external => DISPATCHER(true); - - - // The NONDETs and summaries here essentially introduce an assumption - // that the summarized/NONDETed function does not influence the - // state of the contracts explicitly added to the scene. - // This is reached by Escrow.withdrawETH() and makes a lowlevel - // call on recipient causing a HAVOC. The call in Address passes - // an empty payload, so it will call the `receive` function of - // the recipient if there is one. - function Address.sendValue(address recipient, uint256 amount) internal => NONDET; - // This is reached by ResealManager.reseal and makes a low-level call - // on target which havocs all contracts. (And we can't NONDET functions - // that return bytes). The implementation of Address is meant - // to be a safer alternative to directly using call, according to its - // comments. - function Address.functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) => CVLFunctionCallWithValue(target, data, value); - // This function belongs to ISealable which we do not have an implementation - // of and it causes a havoc of all contracts. It is reached by - // ResealManager.reseal/resume. This is a view function so it must be safe. - function _.getResumeSinceTimestamp() external => CONSTANT; - // This function belongs to IOwnable which we do not have an implementation - // of and it causes a havoc of all contracts. It is reached by EPT. - // transferExecutorOwnership. It is not a view function, - // but from the description it likely only affects its own state. - function _.transferOwnership(address newOwner) external => NONDET; - // This is reached by 2 calls in EPT and reaches a call to - // functionCallWithValue. - function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; - - // This NONDET is meant to address a timeout of dg_kp_2 - // for which EPT is a significant bottleneck but not - // really needed for verifying DG. We also have separate rules for EPT. - function EmergencyProtectedTimelock.submit(address executor, - DualGovernanceHarness.ExternalCall[] calls) external returns (uint256) => NONDET; + function getState() external returns (DualGovernanceHarness.DGHarnessState) envfree; + function isUnset(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isNormal(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoSignalling(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoSignallingDeactivation(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isVetoCooldown(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function isRageQuit(DualGovernanceHarness.DGHarnessState state) external returns (bool) envfree; + function getVetoSignallingActivatedAt() external returns (DualGovernanceHarness.Timestamp) envfree; + function getRageQuitEscrow() external returns (address) envfree; + function getVetoSignallingEscrow() external returns (address) envfree; + function getFirstSeal() external returns (uint256) envfree; + function getSecondSeal() external returns (uint256) envfree; + + // envfrees escrow + function EscrowA.isRageQuitState() external returns (bool) envfree; + function EscrowB.isRageQuitState() external returns (bool) envfree; + + // route escrow functions to implementations while + // still allowing escrow addresses to vary + function _.startRageQuit(DualGovernanceHarness.Duration, DualGovernance.Duration) external => DISPATCHER(true); + function _.initialize(DualGovernanceHarness.Duration) external => DISPATCHER(true); + function _.setMinAssetsLockDuration(DualGovernanceHarness.Duration newMinAssetsLockDuration) external => DISPATCHER(true); + function _.getRageQuitSupport() external => DISPATCHER(true); + function _.isRageQuitFinalized() external => DISPATCHER(true); + + + // The NONDETs and summaries here essentially introduce an assumption + // that the summarized/NONDETed function does not influence the + // state of the contracts explicitly added to the scene. + // This is reached by Escrow.withdrawETH() and makes a lowlevel + // call on recipient causing a HAVOC. The call in Address passes + // an empty payload, so it will call the `receive` function of + // the recipient if there is one. + function Address.sendValue(address recipient, uint256 amount) internal => NONDET; + // This is reached by ResealManager.reseal and makes a low-level call + // on target which havocs all contracts. (And we can't NONDET functions + // that return bytes). The implementation of Address is meant + // to be a safer alternative to directly using call, according to its + // comments. + function Address.functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) => CVLFunctionCallWithValue(target, data, value); + // This function belongs to ISealable which we do not have an implementation + // of and it causes a havoc of all contracts. It is reached by + // ResealManager.reseal/resume. This is a view function so it must be safe. + function _.getResumeSinceTimestamp() external => CONSTANT; + // This function belongs to IOwnable which we do not have an implementation + // of and it causes a havoc of all contracts. It is reached by EPT. + // transferExecutorOwnership. It is not a view function, + // but from the description it likely only affects its own state. + function _.transferOwnership(address newOwner) external => NONDET; + // This is reached by 2 calls in EPT and reaches a call to + // functionCallWithValue. + function Executor.execute(address, uint256, bytes) external returns (bytes) => NONDET; + + // This NONDET is meant to address a timeout of dg_kp_2 + // for which EPT is a significant bottleneck but not + // really needed for verifying DG. We also have separate rules for EPT. + function EmergencyProtectedTimelock.submit(address executor, + DualGovernanceHarness.ExternalCall[] calls) external returns (uint256) => NONDET; } function CVLFunctionCallWithValue(address target, bytes data, uint256 value) returns bytes { From 1a7f141a5ce8b1bb4f9fdf42ee55e6b113bf34b4 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Tue, 3 Sep 2024 14:53:01 +0100 Subject: [PATCH 58/67] delete extraneous comment --- certora/specs/DualGovernance.spec | 2 -- 1 file changed, 2 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index a0e6eb71..34c1b2ea 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -143,8 +143,6 @@ rule dg_kp_3_cooldown_execution (method f) { assert submittedAt <= vetoSignallingActivatedAt; } -// One rage quit cannot start until the previous rage quit has finalized. In -// other words, there can only be at most one active rage quit escrow at a time. // One rage quit cannot start until the previous rage quit has finalized. In // other words, there can only be at most one active rage quit escrow at a time. rule dg_kp_4_single_ragequit (method f) { From 0fdb20d40b94a837d5b2408bf34bcc81ed878713 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Tue, 3 Sep 2024 18:55:29 +0200 Subject: [PATCH 59/67] comments on EPT_5 and helper functions --- certora/specs/Timelock.spec | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/certora/specs/Timelock.spec b/certora/specs/Timelock.spec index ac205008..ae3798c8 100644 --- a/certora/specs/Timelock.spec +++ b/certora/specs/Timelock.spec @@ -71,6 +71,9 @@ rule EPT_KP_2_SchedulingToExecutionDelay { assert getProposal(proposalId).scheduledAt + getAfterScheduleDelay() <= e.block.timestamp; } +// These functions model the deactivation of the committees after the emergency protection elapses, +// and are used in place of directly getting the committee addresses in our rules +// to make sure that anything we prove about the committee special actions is also guarded by the time function effectiveEmergencyExecutionCommittee(env e) returns address { if (e.block.timestamp <= getEmergencyProtectionContext().emergencyProtectionEndsAfter || isEmergencyModeActive()) { return getEmergencyProtectionContext().emergencyExecutionCommittee; @@ -173,8 +176,11 @@ rule EPT_3_EmergencyModeExecutionRestriction(method f) filtered { f -> f.selecto /** @title Emergency Protection deactivation without emergency - @notice The usefullness of this rule depends on us using the effectiveXXXCommittee functions also in all other rules - where we check that something is guarded by the committee. + @notice This rule checks that our effectiveXXXCommittee functions correctly model the expected behaviour upon deactivating + emergency mode by the timelock elapsing. It cannot directly catch any bugs in the solidity code, + but checks our helper functions which in turn make sure that other rules take the elapsing of the emergency protection into account. + The usefullness of this rule depends on us using the effectiveXXXCommittee functions also in all other rules + where we check that something is guarded by a committee. */ rule EPT_5_EmergencyProtectionElapsed() { EmergencyProtection.Context context = getEmergencyProtectionContext(); From 856442ee16b4c39f7fb80b98b9dcf92d77a6381f Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Thu, 5 Sep 2024 13:33:31 +0200 Subject: [PATCH 60/67] AssetsAccounting mutants --- certora/mutation/conf/Escrow.conf | 4 + .../AssetsAccountingDoubleStETHShares.sol | 425 +++++++++++++++++ ...ountingStETHSharesWithdrawDoubleShares.sol | 425 +++++++++++++++++ ...tsAccountingStEthSharesUnlockUnchecked.sol | 424 +++++++++++++++++ ...nstETHFinalizedOneInsteadOfTotalAmount.sol | 425 +++++++++++++++++ ...sAccountingUnstETHLockDoubleAccounting.sol | 426 ++++++++++++++++++ ...etsAccountingUnstETHUnlockOnlyOneShare.sol | 425 +++++++++++++++++ 7 files changed, 2554 insertions(+) create mode 100644 certora/mutation/mutants/AssetsAccounting/AssetsAccountingDoubleStETHShares.sol create mode 100644 certora/mutation/mutants/AssetsAccounting/AssetsAccountingStETHSharesWithdrawDoubleShares.sol create mode 100644 certora/mutation/mutants/AssetsAccounting/AssetsAccountingStEthSharesUnlockUnchecked.sol create mode 100644 certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHFinalizedOneInsteadOfTotalAmount.sol create mode 100644 certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHLockDoubleAccounting.sol create mode 100644 certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHUnlockOnlyOneShare.sol diff --git a/certora/mutation/conf/Escrow.conf b/certora/mutation/conf/Escrow.conf index f97c7556..d8d41373 100644 --- a/certora/mutation/conf/Escrow.conf +++ b/certora/mutation/conf/Escrow.conf @@ -39,6 +39,10 @@ "file_to_mutate": "contracts/Escrow.sol", "mutants_location": "certora/mutation/mutants/Escrow" }, + { + "file_to_mutate": "contracts/libraries/AssetsAccounting.sol", + "mutants_location": "certora/mutation/mutants/AssetsAccounting" + }, ] } } \ No newline at end of file diff --git a/certora/mutation/mutants/AssetsAccounting/AssetsAccountingDoubleStETHShares.sol b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingDoubleStETHShares.sol new file mode 100644 index 00000000..0e703137 --- /dev/null +++ b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingDoubleStETHShares.sol @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; + +import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; + +/// @notice Tracks the stETH and unstETH tokens associated with users +/// @param stETHLockedShares Total number of stETH shares held by the user +/// @param unstETHLockedShares Total number of shares contained in the unstETH NFTs held by the user +/// @param lastAssetsLockTimestamp Timestamp of the most recent lock of stETH shares or unstETH NFTs +/// @param unstETHIds List of unstETH ids locked by the user +struct HolderAssets { + /// @dev slot0: [0..39] + Timestamp lastAssetsLockTimestamp; + /// @dev slot0: [40..167] + SharesValue stETHLockedShares; + /// @dev slot1: [0..127] + SharesValue unstETHLockedShares; + /// @dev slot2: [0..255] - the length of the array + each item occupies 1 slot + uint256[] unstETHIds; +} + +/// @notice Tracks the unfinalized shares and finalized ETH amount of unstETH NFTs +/// @param unfinalizedShares Total number of unfinalized unstETH shares +/// @param finalizedETH Total amount of ETH claimable from finalized unstETH +struct UnstETHAccounting { + /// @dev slot0: [0..127] + SharesValue unfinalizedShares; + /// @dev slot1: [128..255] + ETHValue finalizedETH; +} + +/// @notice Tracks the locked shares and claimed ETH amounts +/// @param lockedShares Total number of accounted stETH shares +/// @param claimedETH Total amount of ETH received from claiming the locked stETH shares +struct StETHAccounting { + /// @dev slot0: [0..127] + SharesValue lockedShares; + /// @dev slot0: [128..255] + ETHValue claimedETH; +} + +/// @notice Represents the state of an accounted WithdrawalRequest +/// @param NotLocked Indicates the default value of the unstETH record, meaning it was not accounted as locked or +/// was unlocked by the account that previously locked it +/// @param Locked Indicates the unstETH record was accounted as locked +/// @param Finalized Indicates the unstETH record was marked as finalized +/// @param Claimed Indicates the unstETH record was claimed +/// @param Withdrawn Indicates the unstETH record was withdrawn after a successful claim +enum UnstETHRecordStatus { + NotLocked, + Locked, + Finalized, + Claimed, + Withdrawn +} + +/// @notice Stores information about an accounted unstETH NFT +/// @param state The current state of the unstETH record. Refer to `UnstETHRecordStatus` for details. +/// @param index The one-based index of the unstETH record in the `UnstETHAccounting.unstETHIds` array +/// @param lockedBy The address of the account that locked the unstETH +/// @param shares The amount of shares contained in the unstETH +/// @param claimableAmount The amount of claimable ETH contained in the unstETH. This value is 0 +/// until the NFT is marked as finalized or claimed. +struct UnstETHRecord { + /// @dev slot 0: [0..7] + UnstETHRecordStatus status; + /// @dev slot 0: [8..39] + IndexOneBased index; + /// @dev slot 0: [40..199] + address lockedBy; + /// @dev slot 1: [0..127] + SharesValue shares; + /// @dev slot 1: [128..255] + ETHValue claimableAmount; +} + +/// @notice Provides functionality for accounting user stETH and unstETH tokens +/// locked in the Escrow contract +library AssetsAccounting { + /// @notice The context of the AssetsAccounting library + /// @param stETHTotals The total number of shares and the amount of stETH locked by users + /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users + /// @param assets Mapping to store information about the assets locked by each user + /// @param unstETHRecords Mapping to track the state of the locked unstETH ids + struct Context { + /// @dev slot0: [0..255] + StETHAccounting stETHTotals; + /// @dev slot1: [0..255] + UnstETHAccounting unstETHTotals; + /// @dev slot2: [0..255] empty slot for mapping track in the storage + mapping(address account => HolderAssets) assets; + /// @dev slot3: [0..255] empty slot for mapping track in the storage + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } + + // --- + // Events + // --- + + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); + event UnstETHUnlocked( + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement + ); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); + + event ETHClaimed(ETHValue amount); + + // --- + // Errors + // --- + + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); + + // --- + // stETH shares operations accounting + // --- + + function accountStETHSharesLock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + // mutated + assets.stETHLockedShares = assets.stETHLockedShares + shares + shares; + // assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = Timestamps.now(); + emit StETHSharesLocked(holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + + HolderAssets storage assets = self.assets[holder]; + if (assets.stETHLockedShares < shares) { + revert InvalidSharesValue(shares); + } + + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); + } + + function accountStETHSharesWithdraw( + Context storage self, + address holder + ) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; + + _checkNonZeroShares(stETHSharesToWithdraw); + + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); + } + + function accountClaimedStETH(Context storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); + } + + // --- + // unstETH operations accounting + // --- + + function accountUnstETHLock( + Context storage self, + address holder, + uint256[] memory unstETHIds, + WithdrawalRequestStatus[] memory statuses + ) internal { + assert(unstETHIds.length == statuses.length); + + SharesValue totalUnstETHLocked; + uint256 unstETHcount = unstETHIds.length; + for (uint256 i = 0; i < unstETHcount; ++i) { + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); + } + self.assets[holder].lastAssetsLockTimestamp = Timestamps.now(); + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; + + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } + + function accountUnstETHUnlock(Context storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; + } + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); + + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); + } + + function accountUnstETHFinalized( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal { + assert(claimableAmounts.length == unstETHIds.length); + + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; + } + + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; + emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); + } + + function accountUnstETHClaimed( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal returns (ETHValue totalAmountClaimed) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); + } + emit UnstETHClaimed(unstETHIds, totalAmountClaimed); + } + + function accountUnstETHWithdraw( + Context storage self, + address holder, + uint256[] memory unstETHIds + ) internal returns (ETHValue amountWithdrawn) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); + } + emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals( + Context storage self + ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { + finalizedETH = self.unstETHTotals.finalizedETH; + unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + } + + function checkMinAssetsLockDurationPassed( + Context storage self, + address holder, + Duration minAssetsLockDuration + ) internal view { + Timestamp assetsUnlockAllowedAfter = minAssetsLockDuration.addTo(self.assets[holder].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert MinAssetsLockDurationNotPassed(assetsUnlockAllowedAfter); + } + } + + // --- + // Helper methods + // --- + + function _addUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId, + WithdrawalRequestStatus memory status + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); + + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } + + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); + + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); + } + + function _removeUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; + } + + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length); + + if (lastUnstETHIdIndex != unstETHIdIndex) { + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.toZeroBasedValue()]; + assets.unstETHIds[unstETHIdIndex.toZeroBasedValue()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; + } + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; + } + + function _finalizeUnstETHRecord( + Context storage self, + uint256 unstETHId, + uint256 claimableAmount + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); + } + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); + + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; + + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } + } else { + unstETHRecord.claimableAmount = claimableAmount; + } + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _withdrawUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; + } + + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); + } + } +} diff --git a/certora/mutation/mutants/AssetsAccounting/AssetsAccountingStETHSharesWithdrawDoubleShares.sol b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingStETHSharesWithdrawDoubleShares.sol new file mode 100644 index 00000000..0aeb8749 --- /dev/null +++ b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingStETHSharesWithdrawDoubleShares.sol @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; + +import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; + +/// @notice Tracks the stETH and unstETH tokens associated with users +/// @param stETHLockedShares Total number of stETH shares held by the user +/// @param unstETHLockedShares Total number of shares contained in the unstETH NFTs held by the user +/// @param lastAssetsLockTimestamp Timestamp of the most recent lock of stETH shares or unstETH NFTs +/// @param unstETHIds List of unstETH ids locked by the user +struct HolderAssets { + /// @dev slot0: [0..39] + Timestamp lastAssetsLockTimestamp; + /// @dev slot0: [40..167] + SharesValue stETHLockedShares; + /// @dev slot1: [0..127] + SharesValue unstETHLockedShares; + /// @dev slot2: [0..255] - the length of the array + each item occupies 1 slot + uint256[] unstETHIds; +} + +/// @notice Tracks the unfinalized shares and finalized ETH amount of unstETH NFTs +/// @param unfinalizedShares Total number of unfinalized unstETH shares +/// @param finalizedETH Total amount of ETH claimable from finalized unstETH +struct UnstETHAccounting { + /// @dev slot0: [0..127] + SharesValue unfinalizedShares; + /// @dev slot1: [128..255] + ETHValue finalizedETH; +} + +/// @notice Tracks the locked shares and claimed ETH amounts +/// @param lockedShares Total number of accounted stETH shares +/// @param claimedETH Total amount of ETH received from claiming the locked stETH shares +struct StETHAccounting { + /// @dev slot0: [0..127] + SharesValue lockedShares; + /// @dev slot0: [128..255] + ETHValue claimedETH; +} + +/// @notice Represents the state of an accounted WithdrawalRequest +/// @param NotLocked Indicates the default value of the unstETH record, meaning it was not accounted as locked or +/// was unlocked by the account that previously locked it +/// @param Locked Indicates the unstETH record was accounted as locked +/// @param Finalized Indicates the unstETH record was marked as finalized +/// @param Claimed Indicates the unstETH record was claimed +/// @param Withdrawn Indicates the unstETH record was withdrawn after a successful claim +enum UnstETHRecordStatus { + NotLocked, + Locked, + Finalized, + Claimed, + Withdrawn +} + +/// @notice Stores information about an accounted unstETH NFT +/// @param state The current state of the unstETH record. Refer to `UnstETHRecordStatus` for details. +/// @param index The one-based index of the unstETH record in the `UnstETHAccounting.unstETHIds` array +/// @param lockedBy The address of the account that locked the unstETH +/// @param shares The amount of shares contained in the unstETH +/// @param claimableAmount The amount of claimable ETH contained in the unstETH. This value is 0 +/// until the NFT is marked as finalized or claimed. +struct UnstETHRecord { + /// @dev slot 0: [0..7] + UnstETHRecordStatus status; + /// @dev slot 0: [8..39] + IndexOneBased index; + /// @dev slot 0: [40..199] + address lockedBy; + /// @dev slot 1: [0..127] + SharesValue shares; + /// @dev slot 1: [128..255] + ETHValue claimableAmount; +} + +/// @notice Provides functionality for accounting user stETH and unstETH tokens +/// locked in the Escrow contract +library AssetsAccounting { + /// @notice The context of the AssetsAccounting library + /// @param stETHTotals The total number of shares and the amount of stETH locked by users + /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users + /// @param assets Mapping to store information about the assets locked by each user + /// @param unstETHRecords Mapping to track the state of the locked unstETH ids + struct Context { + /// @dev slot0: [0..255] + StETHAccounting stETHTotals; + /// @dev slot1: [0..255] + UnstETHAccounting unstETHTotals; + /// @dev slot2: [0..255] empty slot for mapping track in the storage + mapping(address account => HolderAssets) assets; + /// @dev slot3: [0..255] empty slot for mapping track in the storage + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } + + // --- + // Events + // --- + + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); + event UnstETHUnlocked( + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement + ); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); + + event ETHClaimed(ETHValue amount); + + // --- + // Errors + // --- + + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); + + // --- + // stETH shares operations accounting + // --- + + function accountStETHSharesLock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = Timestamps.now(); + emit StETHSharesLocked(holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + + HolderAssets storage assets = self.assets[holder]; + if (assets.stETHLockedShares < shares) { + revert InvalidSharesValue(shares); + } + + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); + } + + function accountStETHSharesWithdraw( + Context storage self, + address holder + ) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + // mutated + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares + assets.stETHLockedShares; + // SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; + + _checkNonZeroShares(stETHSharesToWithdraw); + + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); + } + + function accountClaimedStETH(Context storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); + } + + // --- + // unstETH operations accounting + // --- + + function accountUnstETHLock( + Context storage self, + address holder, + uint256[] memory unstETHIds, + WithdrawalRequestStatus[] memory statuses + ) internal { + assert(unstETHIds.length == statuses.length); + + SharesValue totalUnstETHLocked; + uint256 unstETHcount = unstETHIds.length; + for (uint256 i = 0; i < unstETHcount; ++i) { + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); + } + self.assets[holder].lastAssetsLockTimestamp = Timestamps.now(); + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; + + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } + + function accountUnstETHUnlock(Context storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; + } + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); + + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); + } + + function accountUnstETHFinalized( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal { + assert(claimableAmounts.length == unstETHIds.length); + + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; + } + + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; + emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); + } + + function accountUnstETHClaimed( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal returns (ETHValue totalAmountClaimed) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); + } + emit UnstETHClaimed(unstETHIds, totalAmountClaimed); + } + + function accountUnstETHWithdraw( + Context storage self, + address holder, + uint256[] memory unstETHIds + ) internal returns (ETHValue amountWithdrawn) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); + } + emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals( + Context storage self + ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { + finalizedETH = self.unstETHTotals.finalizedETH; + unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + } + + function checkMinAssetsLockDurationPassed( + Context storage self, + address holder, + Duration minAssetsLockDuration + ) internal view { + Timestamp assetsUnlockAllowedAfter = minAssetsLockDuration.addTo(self.assets[holder].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert MinAssetsLockDurationNotPassed(assetsUnlockAllowedAfter); + } + } + + // --- + // Helper methods + // --- + + function _addUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId, + WithdrawalRequestStatus memory status + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); + + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } + + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); + + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); + } + + function _removeUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; + } + + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length); + + if (lastUnstETHIdIndex != unstETHIdIndex) { + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.toZeroBasedValue()]; + assets.unstETHIds[unstETHIdIndex.toZeroBasedValue()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; + } + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; + } + + function _finalizeUnstETHRecord( + Context storage self, + uint256 unstETHId, + uint256 claimableAmount + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); + } + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); + + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; + + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } + } else { + unstETHRecord.claimableAmount = claimableAmount; + } + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _withdrawUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; + } + + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); + } + } +} diff --git a/certora/mutation/mutants/AssetsAccounting/AssetsAccountingStEthSharesUnlockUnchecked.sol b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingStEthSharesUnlockUnchecked.sol new file mode 100644 index 00000000..848b81b5 --- /dev/null +++ b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingStEthSharesUnlockUnchecked.sol @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; + +import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; + +/// @notice Tracks the stETH and unstETH tokens associated with users +/// @param stETHLockedShares Total number of stETH shares held by the user +/// @param unstETHLockedShares Total number of shares contained in the unstETH NFTs held by the user +/// @param lastAssetsLockTimestamp Timestamp of the most recent lock of stETH shares or unstETH NFTs +/// @param unstETHIds List of unstETH ids locked by the user +struct HolderAssets { + /// @dev slot0: [0..39] + Timestamp lastAssetsLockTimestamp; + /// @dev slot0: [40..167] + SharesValue stETHLockedShares; + /// @dev slot1: [0..127] + SharesValue unstETHLockedShares; + /// @dev slot2: [0..255] - the length of the array + each item occupies 1 slot + uint256[] unstETHIds; +} + +/// @notice Tracks the unfinalized shares and finalized ETH amount of unstETH NFTs +/// @param unfinalizedShares Total number of unfinalized unstETH shares +/// @param finalizedETH Total amount of ETH claimable from finalized unstETH +struct UnstETHAccounting { + /// @dev slot0: [0..127] + SharesValue unfinalizedShares; + /// @dev slot1: [128..255] + ETHValue finalizedETH; +} + +/// @notice Tracks the locked shares and claimed ETH amounts +/// @param lockedShares Total number of accounted stETH shares +/// @param claimedETH Total amount of ETH received from claiming the locked stETH shares +struct StETHAccounting { + /// @dev slot0: [0..127] + SharesValue lockedShares; + /// @dev slot0: [128..255] + ETHValue claimedETH; +} + +/// @notice Represents the state of an accounted WithdrawalRequest +/// @param NotLocked Indicates the default value of the unstETH record, meaning it was not accounted as locked or +/// was unlocked by the account that previously locked it +/// @param Locked Indicates the unstETH record was accounted as locked +/// @param Finalized Indicates the unstETH record was marked as finalized +/// @param Claimed Indicates the unstETH record was claimed +/// @param Withdrawn Indicates the unstETH record was withdrawn after a successful claim +enum UnstETHRecordStatus { + NotLocked, + Locked, + Finalized, + Claimed, + Withdrawn +} + +/// @notice Stores information about an accounted unstETH NFT +/// @param state The current state of the unstETH record. Refer to `UnstETHRecordStatus` for details. +/// @param index The one-based index of the unstETH record in the `UnstETHAccounting.unstETHIds` array +/// @param lockedBy The address of the account that locked the unstETH +/// @param shares The amount of shares contained in the unstETH +/// @param claimableAmount The amount of claimable ETH contained in the unstETH. This value is 0 +/// until the NFT is marked as finalized or claimed. +struct UnstETHRecord { + /// @dev slot 0: [0..7] + UnstETHRecordStatus status; + /// @dev slot 0: [8..39] + IndexOneBased index; + /// @dev slot 0: [40..199] + address lockedBy; + /// @dev slot 1: [0..127] + SharesValue shares; + /// @dev slot 1: [128..255] + ETHValue claimableAmount; +} + +/// @notice Provides functionality for accounting user stETH and unstETH tokens +/// locked in the Escrow contract +library AssetsAccounting { + /// @notice The context of the AssetsAccounting library + /// @param stETHTotals The total number of shares and the amount of stETH locked by users + /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users + /// @param assets Mapping to store information about the assets locked by each user + /// @param unstETHRecords Mapping to track the state of the locked unstETH ids + struct Context { + /// @dev slot0: [0..255] + StETHAccounting stETHTotals; + /// @dev slot1: [0..255] + UnstETHAccounting unstETHTotals; + /// @dev slot2: [0..255] empty slot for mapping track in the storage + mapping(address account => HolderAssets) assets; + /// @dev slot3: [0..255] empty slot for mapping track in the storage + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } + + // --- + // Events + // --- + + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); + event UnstETHUnlocked( + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement + ); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); + + event ETHClaimed(ETHValue amount); + + // --- + // Errors + // --- + + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); + + // --- + // stETH shares operations accounting + // --- + + function accountStETHSharesLock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = Timestamps.now(); + emit StETHSharesLocked(holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + + HolderAssets storage assets = self.assets[holder]; + // mutated + // if (assets.stETHLockedShares < shares) { + // revert InvalidSharesValue(shares); + // } + + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); + } + + function accountStETHSharesWithdraw( + Context storage self, + address holder + ) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; + + _checkNonZeroShares(stETHSharesToWithdraw); + + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); + } + + function accountClaimedStETH(Context storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); + } + + // --- + // unstETH operations accounting + // --- + + function accountUnstETHLock( + Context storage self, + address holder, + uint256[] memory unstETHIds, + WithdrawalRequestStatus[] memory statuses + ) internal { + assert(unstETHIds.length == statuses.length); + + SharesValue totalUnstETHLocked; + uint256 unstETHcount = unstETHIds.length; + for (uint256 i = 0; i < unstETHcount; ++i) { + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); + } + self.assets[holder].lastAssetsLockTimestamp = Timestamps.now(); + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; + + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } + + function accountUnstETHUnlock(Context storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; + } + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); + + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); + } + + function accountUnstETHFinalized( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal { + assert(claimableAmounts.length == unstETHIds.length); + + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; + } + + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; + emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); + } + + function accountUnstETHClaimed( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal returns (ETHValue totalAmountClaimed) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); + } + emit UnstETHClaimed(unstETHIds, totalAmountClaimed); + } + + function accountUnstETHWithdraw( + Context storage self, + address holder, + uint256[] memory unstETHIds + ) internal returns (ETHValue amountWithdrawn) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); + } + emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals( + Context storage self + ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { + finalizedETH = self.unstETHTotals.finalizedETH; + unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + } + + function checkMinAssetsLockDurationPassed( + Context storage self, + address holder, + Duration minAssetsLockDuration + ) internal view { + Timestamp assetsUnlockAllowedAfter = minAssetsLockDuration.addTo(self.assets[holder].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert MinAssetsLockDurationNotPassed(assetsUnlockAllowedAfter); + } + } + + // --- + // Helper methods + // --- + + function _addUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId, + WithdrawalRequestStatus memory status + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); + + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } + + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); + + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); + } + + function _removeUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; + } + + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length); + + if (lastUnstETHIdIndex != unstETHIdIndex) { + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.toZeroBasedValue()]; + assets.unstETHIds[unstETHIdIndex.toZeroBasedValue()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; + } + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; + } + + function _finalizeUnstETHRecord( + Context storage self, + uint256 unstETHId, + uint256 claimableAmount + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); + } + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); + + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; + + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } + } else { + unstETHRecord.claimableAmount = claimableAmount; + } + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _withdrawUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; + } + + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); + } + } +} diff --git a/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHFinalizedOneInsteadOfTotalAmount.sol b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHFinalizedOneInsteadOfTotalAmount.sol new file mode 100644 index 00000000..c17c9d43 --- /dev/null +++ b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHFinalizedOneInsteadOfTotalAmount.sol @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; + +import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; + +/// @notice Tracks the stETH and unstETH tokens associated with users +/// @param stETHLockedShares Total number of stETH shares held by the user +/// @param unstETHLockedShares Total number of shares contained in the unstETH NFTs held by the user +/// @param lastAssetsLockTimestamp Timestamp of the most recent lock of stETH shares or unstETH NFTs +/// @param unstETHIds List of unstETH ids locked by the user +struct HolderAssets { + /// @dev slot0: [0..39] + Timestamp lastAssetsLockTimestamp; + /// @dev slot0: [40..167] + SharesValue stETHLockedShares; + /// @dev slot1: [0..127] + SharesValue unstETHLockedShares; + /// @dev slot2: [0..255] - the length of the array + each item occupies 1 slot + uint256[] unstETHIds; +} + +/// @notice Tracks the unfinalized shares and finalized ETH amount of unstETH NFTs +/// @param unfinalizedShares Total number of unfinalized unstETH shares +/// @param finalizedETH Total amount of ETH claimable from finalized unstETH +struct UnstETHAccounting { + /// @dev slot0: [0..127] + SharesValue unfinalizedShares; + /// @dev slot1: [128..255] + ETHValue finalizedETH; +} + +/// @notice Tracks the locked shares and claimed ETH amounts +/// @param lockedShares Total number of accounted stETH shares +/// @param claimedETH Total amount of ETH received from claiming the locked stETH shares +struct StETHAccounting { + /// @dev slot0: [0..127] + SharesValue lockedShares; + /// @dev slot0: [128..255] + ETHValue claimedETH; +} + +/// @notice Represents the state of an accounted WithdrawalRequest +/// @param NotLocked Indicates the default value of the unstETH record, meaning it was not accounted as locked or +/// was unlocked by the account that previously locked it +/// @param Locked Indicates the unstETH record was accounted as locked +/// @param Finalized Indicates the unstETH record was marked as finalized +/// @param Claimed Indicates the unstETH record was claimed +/// @param Withdrawn Indicates the unstETH record was withdrawn after a successful claim +enum UnstETHRecordStatus { + NotLocked, + Locked, + Finalized, + Claimed, + Withdrawn +} + +/// @notice Stores information about an accounted unstETH NFT +/// @param state The current state of the unstETH record. Refer to `UnstETHRecordStatus` for details. +/// @param index The one-based index of the unstETH record in the `UnstETHAccounting.unstETHIds` array +/// @param lockedBy The address of the account that locked the unstETH +/// @param shares The amount of shares contained in the unstETH +/// @param claimableAmount The amount of claimable ETH contained in the unstETH. This value is 0 +/// until the NFT is marked as finalized or claimed. +struct UnstETHRecord { + /// @dev slot 0: [0..7] + UnstETHRecordStatus status; + /// @dev slot 0: [8..39] + IndexOneBased index; + /// @dev slot 0: [40..199] + address lockedBy; + /// @dev slot 1: [0..127] + SharesValue shares; + /// @dev slot 1: [128..255] + ETHValue claimableAmount; +} + +/// @notice Provides functionality for accounting user stETH and unstETH tokens +/// locked in the Escrow contract +library AssetsAccounting { + /// @notice The context of the AssetsAccounting library + /// @param stETHTotals The total number of shares and the amount of stETH locked by users + /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users + /// @param assets Mapping to store information about the assets locked by each user + /// @param unstETHRecords Mapping to track the state of the locked unstETH ids + struct Context { + /// @dev slot0: [0..255] + StETHAccounting stETHTotals; + /// @dev slot1: [0..255] + UnstETHAccounting unstETHTotals; + /// @dev slot2: [0..255] empty slot for mapping track in the storage + mapping(address account => HolderAssets) assets; + /// @dev slot3: [0..255] empty slot for mapping track in the storage + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } + + // --- + // Events + // --- + + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); + event UnstETHUnlocked( + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement + ); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); + + event ETHClaimed(ETHValue amount); + + // --- + // Errors + // --- + + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); + + // --- + // stETH shares operations accounting + // --- + + function accountStETHSharesLock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = Timestamps.now(); + emit StETHSharesLocked(holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + + HolderAssets storage assets = self.assets[holder]; + if (assets.stETHLockedShares < shares) { + revert InvalidSharesValue(shares); + } + + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); + } + + function accountStETHSharesWithdraw( + Context storage self, + address holder + ) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; + + _checkNonZeroShares(stETHSharesToWithdraw); + + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); + } + + function accountClaimedStETH(Context storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); + } + + // --- + // unstETH operations accounting + // --- + + function accountUnstETHLock( + Context storage self, + address holder, + uint256[] memory unstETHIds, + WithdrawalRequestStatus[] memory statuses + ) internal { + assert(unstETHIds.length == statuses.length); + + SharesValue totalUnstETHLocked; + uint256 unstETHcount = unstETHIds.length; + for (uint256 i = 0; i < unstETHcount; ++i) { + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); + } + self.assets[holder].lastAssetsLockTimestamp = Timestamps.now(); + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; + + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } + + function accountUnstETHUnlock(Context storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; + } + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); + + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); + } + + function accountUnstETHFinalized( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal { + assert(claimableAmounts.length == unstETHIds.length); + + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; + } + + // mutated + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + ETHValues.from(1); + // self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; + emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); + } + + function accountUnstETHClaimed( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal returns (ETHValue totalAmountClaimed) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); + } + emit UnstETHClaimed(unstETHIds, totalAmountClaimed); + } + + function accountUnstETHWithdraw( + Context storage self, + address holder, + uint256[] memory unstETHIds + ) internal returns (ETHValue amountWithdrawn) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); + } + emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals( + Context storage self + ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { + finalizedETH = self.unstETHTotals.finalizedETH; + unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + } + + function checkMinAssetsLockDurationPassed( + Context storage self, + address holder, + Duration minAssetsLockDuration + ) internal view { + Timestamp assetsUnlockAllowedAfter = minAssetsLockDuration.addTo(self.assets[holder].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert MinAssetsLockDurationNotPassed(assetsUnlockAllowedAfter); + } + } + + // --- + // Helper methods + // --- + + function _addUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId, + WithdrawalRequestStatus memory status + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); + + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } + + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); + + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); + } + + function _removeUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; + } + + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length); + + if (lastUnstETHIdIndex != unstETHIdIndex) { + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.toZeroBasedValue()]; + assets.unstETHIds[unstETHIdIndex.toZeroBasedValue()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; + } + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; + } + + function _finalizeUnstETHRecord( + Context storage self, + uint256 unstETHId, + uint256 claimableAmount + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); + } + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); + + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; + + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } + } else { + unstETHRecord.claimableAmount = claimableAmount; + } + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _withdrawUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; + } + + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); + } + } +} diff --git a/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHLockDoubleAccounting.sol b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHLockDoubleAccounting.sol new file mode 100644 index 00000000..74fe6e82 --- /dev/null +++ b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHLockDoubleAccounting.sol @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; + +import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; + +/// @notice Tracks the stETH and unstETH tokens associated with users +/// @param stETHLockedShares Total number of stETH shares held by the user +/// @param unstETHLockedShares Total number of shares contained in the unstETH NFTs held by the user +/// @param lastAssetsLockTimestamp Timestamp of the most recent lock of stETH shares or unstETH NFTs +/// @param unstETHIds List of unstETH ids locked by the user +struct HolderAssets { + /// @dev slot0: [0..39] + Timestamp lastAssetsLockTimestamp; + /// @dev slot0: [40..167] + SharesValue stETHLockedShares; + /// @dev slot1: [0..127] + SharesValue unstETHLockedShares; + /// @dev slot2: [0..255] - the length of the array + each item occupies 1 slot + uint256[] unstETHIds; +} + +/// @notice Tracks the unfinalized shares and finalized ETH amount of unstETH NFTs +/// @param unfinalizedShares Total number of unfinalized unstETH shares +/// @param finalizedETH Total amount of ETH claimable from finalized unstETH +struct UnstETHAccounting { + /// @dev slot0: [0..127] + SharesValue unfinalizedShares; + /// @dev slot1: [128..255] + ETHValue finalizedETH; +} + +/// @notice Tracks the locked shares and claimed ETH amounts +/// @param lockedShares Total number of accounted stETH shares +/// @param claimedETH Total amount of ETH received from claiming the locked stETH shares +struct StETHAccounting { + /// @dev slot0: [0..127] + SharesValue lockedShares; + /// @dev slot0: [128..255] + ETHValue claimedETH; +} + +/// @notice Represents the state of an accounted WithdrawalRequest +/// @param NotLocked Indicates the default value of the unstETH record, meaning it was not accounted as locked or +/// was unlocked by the account that previously locked it +/// @param Locked Indicates the unstETH record was accounted as locked +/// @param Finalized Indicates the unstETH record was marked as finalized +/// @param Claimed Indicates the unstETH record was claimed +/// @param Withdrawn Indicates the unstETH record was withdrawn after a successful claim +enum UnstETHRecordStatus { + NotLocked, + Locked, + Finalized, + Claimed, + Withdrawn +} + +/// @notice Stores information about an accounted unstETH NFT +/// @param state The current state of the unstETH record. Refer to `UnstETHRecordStatus` for details. +/// @param index The one-based index of the unstETH record in the `UnstETHAccounting.unstETHIds` array +/// @param lockedBy The address of the account that locked the unstETH +/// @param shares The amount of shares contained in the unstETH +/// @param claimableAmount The amount of claimable ETH contained in the unstETH. This value is 0 +/// until the NFT is marked as finalized or claimed. +struct UnstETHRecord { + /// @dev slot 0: [0..7] + UnstETHRecordStatus status; + /// @dev slot 0: [8..39] + IndexOneBased index; + /// @dev slot 0: [40..199] + address lockedBy; + /// @dev slot 1: [0..127] + SharesValue shares; + /// @dev slot 1: [128..255] + ETHValue claimableAmount; +} + +/// @notice Provides functionality for accounting user stETH and unstETH tokens +/// locked in the Escrow contract +library AssetsAccounting { + /// @notice The context of the AssetsAccounting library + /// @param stETHTotals The total number of shares and the amount of stETH locked by users + /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users + /// @param assets Mapping to store information about the assets locked by each user + /// @param unstETHRecords Mapping to track the state of the locked unstETH ids + struct Context { + /// @dev slot0: [0..255] + StETHAccounting stETHTotals; + /// @dev slot1: [0..255] + UnstETHAccounting unstETHTotals; + /// @dev slot2: [0..255] empty slot for mapping track in the storage + mapping(address account => HolderAssets) assets; + /// @dev slot3: [0..255] empty slot for mapping track in the storage + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } + + // --- + // Events + // --- + + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); + event UnstETHUnlocked( + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement + ); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); + + event ETHClaimed(ETHValue amount); + + // --- + // Errors + // --- + + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); + + // --- + // stETH shares operations accounting + // --- + + function accountStETHSharesLock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = Timestamps.now(); + emit StETHSharesLocked(holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + + HolderAssets storage assets = self.assets[holder]; + if (assets.stETHLockedShares < shares) { + revert InvalidSharesValue(shares); + } + + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); + } + + function accountStETHSharesWithdraw( + Context storage self, + address holder + ) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; + + _checkNonZeroShares(stETHSharesToWithdraw); + + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); + } + + function accountClaimedStETH(Context storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); + } + + // --- + // unstETH operations accounting + // --- + + function accountUnstETHLock( + Context storage self, + address holder, + uint256[] memory unstETHIds, + WithdrawalRequestStatus[] memory statuses + ) internal { + assert(unstETHIds.length == statuses.length); + + SharesValue totalUnstETHLocked; + uint256 unstETHcount = unstETHIds.length; + for (uint256 i = 0; i < unstETHcount; ++i) { + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); + } + self.assets[holder].lastAssetsLockTimestamp = Timestamps.now(); + // mutated + self.assets[holder].unstETHLockedShares = + self.assets[holder].unstETHLockedShares + totalUnstETHLocked + totalUnstETHLocked; + // self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; + + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } + + function accountUnstETHUnlock(Context storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; + } + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); + + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); + } + + function accountUnstETHFinalized( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal { + assert(claimableAmounts.length == unstETHIds.length); + + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; + } + + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; + emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); + } + + function accountUnstETHClaimed( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal returns (ETHValue totalAmountClaimed) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); + } + emit UnstETHClaimed(unstETHIds, totalAmountClaimed); + } + + function accountUnstETHWithdraw( + Context storage self, + address holder, + uint256[] memory unstETHIds + ) internal returns (ETHValue amountWithdrawn) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); + } + emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals( + Context storage self + ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { + finalizedETH = self.unstETHTotals.finalizedETH; + unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + } + + function checkMinAssetsLockDurationPassed( + Context storage self, + address holder, + Duration minAssetsLockDuration + ) internal view { + Timestamp assetsUnlockAllowedAfter = minAssetsLockDuration.addTo(self.assets[holder].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert MinAssetsLockDurationNotPassed(assetsUnlockAllowedAfter); + } + } + + // --- + // Helper methods + // --- + + function _addUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId, + WithdrawalRequestStatus memory status + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); + + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } + + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); + + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); + } + + function _removeUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; + } + + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length); + + if (lastUnstETHIdIndex != unstETHIdIndex) { + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.toZeroBasedValue()]; + assets.unstETHIds[unstETHIdIndex.toZeroBasedValue()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; + } + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; + } + + function _finalizeUnstETHRecord( + Context storage self, + uint256 unstETHId, + uint256 claimableAmount + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); + } + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); + + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; + + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } + } else { + unstETHRecord.claimableAmount = claimableAmount; + } + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _withdrawUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; + } + + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); + } + } +} diff --git a/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHUnlockOnlyOneShare.sol b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHUnlockOnlyOneShare.sol new file mode 100644 index 00000000..b32498d6 --- /dev/null +++ b/certora/mutation/mutants/AssetsAccounting/AssetsAccountingUnstETHUnlockOnlyOneShare.sol @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; + +import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; + +/// @notice Tracks the stETH and unstETH tokens associated with users +/// @param stETHLockedShares Total number of stETH shares held by the user +/// @param unstETHLockedShares Total number of shares contained in the unstETH NFTs held by the user +/// @param lastAssetsLockTimestamp Timestamp of the most recent lock of stETH shares or unstETH NFTs +/// @param unstETHIds List of unstETH ids locked by the user +struct HolderAssets { + /// @dev slot0: [0..39] + Timestamp lastAssetsLockTimestamp; + /// @dev slot0: [40..167] + SharesValue stETHLockedShares; + /// @dev slot1: [0..127] + SharesValue unstETHLockedShares; + /// @dev slot2: [0..255] - the length of the array + each item occupies 1 slot + uint256[] unstETHIds; +} + +/// @notice Tracks the unfinalized shares and finalized ETH amount of unstETH NFTs +/// @param unfinalizedShares Total number of unfinalized unstETH shares +/// @param finalizedETH Total amount of ETH claimable from finalized unstETH +struct UnstETHAccounting { + /// @dev slot0: [0..127] + SharesValue unfinalizedShares; + /// @dev slot1: [128..255] + ETHValue finalizedETH; +} + +/// @notice Tracks the locked shares and claimed ETH amounts +/// @param lockedShares Total number of accounted stETH shares +/// @param claimedETH Total amount of ETH received from claiming the locked stETH shares +struct StETHAccounting { + /// @dev slot0: [0..127] + SharesValue lockedShares; + /// @dev slot0: [128..255] + ETHValue claimedETH; +} + +/// @notice Represents the state of an accounted WithdrawalRequest +/// @param NotLocked Indicates the default value of the unstETH record, meaning it was not accounted as locked or +/// was unlocked by the account that previously locked it +/// @param Locked Indicates the unstETH record was accounted as locked +/// @param Finalized Indicates the unstETH record was marked as finalized +/// @param Claimed Indicates the unstETH record was claimed +/// @param Withdrawn Indicates the unstETH record was withdrawn after a successful claim +enum UnstETHRecordStatus { + NotLocked, + Locked, + Finalized, + Claimed, + Withdrawn +} + +/// @notice Stores information about an accounted unstETH NFT +/// @param state The current state of the unstETH record. Refer to `UnstETHRecordStatus` for details. +/// @param index The one-based index of the unstETH record in the `UnstETHAccounting.unstETHIds` array +/// @param lockedBy The address of the account that locked the unstETH +/// @param shares The amount of shares contained in the unstETH +/// @param claimableAmount The amount of claimable ETH contained in the unstETH. This value is 0 +/// until the NFT is marked as finalized or claimed. +struct UnstETHRecord { + /// @dev slot 0: [0..7] + UnstETHRecordStatus status; + /// @dev slot 0: [8..39] + IndexOneBased index; + /// @dev slot 0: [40..199] + address lockedBy; + /// @dev slot 1: [0..127] + SharesValue shares; + /// @dev slot 1: [128..255] + ETHValue claimableAmount; +} + +/// @notice Provides functionality for accounting user stETH and unstETH tokens +/// locked in the Escrow contract +library AssetsAccounting { + /// @notice The context of the AssetsAccounting library + /// @param stETHTotals The total number of shares and the amount of stETH locked by users + /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users + /// @param assets Mapping to store information about the assets locked by each user + /// @param unstETHRecords Mapping to track the state of the locked unstETH ids + struct Context { + /// @dev slot0: [0..255] + StETHAccounting stETHTotals; + /// @dev slot1: [0..255] + UnstETHAccounting unstETHTotals; + /// @dev slot2: [0..255] empty slot for mapping track in the storage + mapping(address account => HolderAssets) assets; + /// @dev slot3: [0..255] empty slot for mapping track in the storage + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } + + // --- + // Events + // --- + + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); + event UnstETHUnlocked( + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement + ); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); + + event ETHClaimed(ETHValue amount); + + // --- + // Errors + // --- + + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); + + // --- + // stETH shares operations accounting + // --- + + function accountStETHSharesLock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = Timestamps.now(); + emit StETHSharesLocked(holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); + } + + function accountStETHSharesUnlock(Context storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + + HolderAssets storage assets = self.assets[holder]; + if (assets.stETHLockedShares < shares) { + revert InvalidSharesValue(shares); + } + + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); + } + + function accountStETHSharesWithdraw( + Context storage self, + address holder + ) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; + + _checkNonZeroShares(stETHSharesToWithdraw); + + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); + } + + function accountClaimedStETH(Context storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); + } + + // --- + // unstETH operations accounting + // --- + + function accountUnstETHLock( + Context storage self, + address holder, + uint256[] memory unstETHIds, + WithdrawalRequestStatus[] memory statuses + ) internal { + assert(unstETHIds.length == statuses.length); + + SharesValue totalUnstETHLocked; + uint256 unstETHcount = unstETHIds.length; + for (uint256 i = 0; i < unstETHcount; ++i) { + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); + } + self.assets[holder].lastAssetsLockTimestamp = Timestamps.now(); + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; + + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } + + function accountUnstETHUnlock(Context storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; + } + // mutated + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - SharesValues.from(1); + // self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); + + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); + } + + function accountUnstETHFinalized( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal { + assert(claimableAmounts.length == unstETHIds.length); + + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; + + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; + } + + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; + emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); + } + + function accountUnstETHClaimed( + Context storage self, + uint256[] memory unstETHIds, + uint256[] memory claimableAmounts + ) internal returns (ETHValue totalAmountClaimed) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); + } + emit UnstETHClaimed(unstETHIds, totalAmountClaimed); + } + + function accountUnstETHWithdraw( + Context storage self, + address holder, + uint256[] memory unstETHIds + ) internal returns (ETHValue amountWithdrawn) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); + } + emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals( + Context storage self + ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { + finalizedETH = self.unstETHTotals.finalizedETH; + unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + } + + function checkMinAssetsLockDurationPassed( + Context storage self, + address holder, + Duration minAssetsLockDuration + ) internal view { + Timestamp assetsUnlockAllowedAfter = minAssetsLockDuration.addTo(self.assets[holder].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert MinAssetsLockDurationNotPassed(assetsUnlockAllowedAfter); + } + } + + // --- + // Helper methods + // --- + + function _addUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId, + WithdrawalRequestStatus memory status + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); + + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } + + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); + + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); + } + + function _removeUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; + } + + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.fromOneBasedValue(assets.unstETHIds.length); + + if (lastUnstETHIdIndex != unstETHIdIndex) { + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.toZeroBasedValue()]; + assets.unstETHIds[unstETHIdIndex.toZeroBasedValue()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; + } + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; + } + + function _finalizeUnstETHRecord( + Context storage self, + uint256 unstETHId, + uint256 claimableAmount + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); + } + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); + + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; + + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } + } else { + unstETHRecord.claimableAmount = claimableAmount; + } + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; + } + + function _withdrawUnstETHRecord( + Context storage self, + address holder, + uint256 unstETHId + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); + } + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; + } + + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); + } + } +} From 56c5c864ae8c161063c7bc6ab857d3badf647148 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Fri, 6 Sep 2024 15:55:50 +0200 Subject: [PATCH 61/67] new version of w2_1 --- certora/specs/DualGovernance.spec | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/certora/specs/DualGovernance.spec b/certora/specs/DualGovernance.spec index 34c1b2ea..3a6bdda6 100644 --- a/certora/specs/DualGovernance.spec +++ b/certora/specs/DualGovernance.spec @@ -90,14 +90,26 @@ function rageQuitThresholdAssumptions() returns bool { // “for each entry in the struct in the array, show that the index inside is // the same as the real array index” // NOTE: this has not yet been addressed by Lido, so this should fail now. -invariant w2_1a_indexes_match (address proposer_addr, uint256 idx, - Proposers.Proposer[] proposers) - proposers.length <= 5 && // loop unrolling - proposer_addr == proposers[idx].account && - (getProposerIndexFromExecutor(proposer_addr) - 1 < proposers.length) && - (getProposerIndexFromExecutor(proposer_addr) - 1 == idx) { +invariant w2_1_indexes_match(uint idx, address proposer_addr) + proposer_addr != 0 && idx > 0 && getProposerIndexFromExecutor(proposer_addr) == idx => + idx <= currentContract._proposers.proposers.length && + currentContract._proposers.proposers[require_uint256(idx - 1)] == proposer_addr + && getProposerIndexFromExecutor(currentContract._proposers.proposers[require_uint256(idx - 1)]) == idx { + preserved unregisterProposer(address a) with (env e) { + requireInvariant w2_1_indexes_match(getProposerIndexFromExecutor(a), a); + requireInvariant zero_address_is_not_valid_proposer(); + } + preserved { + // loop unrolling + require currentContract._proposers.proposers.length <= 5; + requireInvariant zero_address_is_not_valid_proposer(); + } } +invariant zero_address_is_not_valid_proposer() + currentContract._proposers.executors[0].proposerIndex == 0 && + (forall uint idx. (idx >= 0 && idx < currentContract._proposers.proposers.length => currentContract._proposers.proposers[idx] != 0)); + // Proposals cannot be executed in the Veto Signaling (both parent state and // Deactivation sub-state) and Rage Quit states. rule dg_kp_1_proposal_execution { @@ -121,7 +133,7 @@ rule dg_kp_2_proposal_submission { // If a proposal was submitted after the last time the Veto Signaling state was // activated, then it cannot be executed in the Veto Cooldown state. -rule dg_kp_3_cooldown_execution (method f) { +rule dg_kp_3_cooldown_execution { calldataarg args; env e; uint256 proposalId; From df8f29f5c0f4dbf3e45358a0d94f17f106da20bc Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Mon, 9 Sep 2024 11:35:24 +0200 Subject: [PATCH 62/67] W2_2 with frontrunning formulation plus mutant preserving the buggy state --- .../Escrow/EscrowWithW2_2BugStillPresent.sol | 472 ++++++++++++++++++ certora/specs/Escrow.spec | 40 ++ 2 files changed, 512 insertions(+) create mode 100644 certora/mutation/mutants/Escrow/EscrowWithW2_2BugStillPresent.sol diff --git a/certora/mutation/mutants/Escrow/EscrowWithW2_2BugStillPresent.sol b/certora/mutation/mutants/Escrow/EscrowWithW2_2BugStillPresent.sol new file mode 100644 index 00000000..e604017c --- /dev/null +++ b/certora/mutation/mutants/Escrow/EscrowWithW2_2BugStillPresent.sol @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; +import {ETHValue, ETHValues} from "./types/ETHValue.sol"; +import {SharesValue, SharesValues} from "./types/SharesValue.sol"; +import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; + +import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; + +import {EscrowState} from "./libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; + +/// @notice Summary of the total locked assets in the Escrow +/// @param stETHLockedShares Total number of stETH shares locked in the Escrow +/// @param stETHClaimedETH Total amount of ETH claimed from the stETH locked in the Escrow +/// @param unstETHUnfinalizedShares Total number of shares from unstETH NFTs that have not yet been +/// marked as finalized +/// @param unstETHFinalizedETH Total claimable amount of ETH from unstETH NFTs that have been marked +/// as finalized +struct LockedAssetsTotals { + uint256 stETHLockedShares; + uint256 stETHClaimedETH; + uint256 unstETHUnfinalizedShares; + uint256 unstETHFinalizedETH; +} + +struct VetoerState { + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; +} + +contract Escrow is IEscrow { + using EscrowState for EscrowState.Context; + using AssetsAccounting for AssetsAccounting.Context; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; + + // --- + // Errors + // --- + + error UnclaimedBatches(); + error UnexpectedUnstETHId(); + error UnfinalizedUnstETHIds(); + error NonProxyCallsForbidden(); + error BatchesQueueIsNotClosed(); + error InvalidBatchSize(uint256 size); + error CallerIsNotDualGovernance(address caller); + error InvalidHintsLength(uint256 actual, uint256 expected); + error InvalidETHSender(address actual, address expected); + + // --- + // Events + // --- + + event ConfigProviderSet(address newConfigProvider); + + // --- + // Sanity check params immutables + // --- + + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + + // --- + // Dependencies immutables + // --- + + IStETH public immutable ST_ETH; + IWstETH public immutable WST_ETH; + IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; + + // --- + // Implementation immutables + + address private immutable _SELF; + IDualGovernance public immutable DUAL_GOVERNANCE; + + // --- + // Aspects + // --- + + EscrowState.Context internal _escrowState; + AssetsAccounting.Context private _accounting; + WithdrawalsBatchesQueue.Context private _batchesQueue; + + // --- + // Construction & initializing + // --- + + constructor( + IStETH stETH, + IWstETH wstETH, + IWithdrawalQueue withdrawalQueue, + IDualGovernance dualGovernance, + uint256 minWithdrawalsBatchSize + ) { + _SELF = address(this); + DUAL_GOVERNANCE = dualGovernance; + + ST_ETH = stETH; + WST_ETH = wstETH; + WITHDRAWAL_QUEUE = withdrawalQueue; + + MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; + } + + function initialize(Duration minAssetsLockDuration) external { + if (address(this) == _SELF) { + revert NonProxyCallsForbidden(); + } + _checkCallerIsDualGovernance(); + + _escrowState.initialize(minAssetsLockDuration); + + ST_ETH.approve(address(WST_ETH), type(uint256).max); + ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); + } + + // --- + // Lock & unlock stETH + // --- + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockStETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + + DUAL_GOVERNANCE.activateNextState(); + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); + ST_ETH.transferShares(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock wstETH + // --- + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WST_ETH.transferFrom(msg.sender, address(this), amount); + lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockWstETH() external returns (uint256 unlockedStETHShares) { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); + WST_ETH.transfer(msg.sender, unlockedStETHShares); + + DUAL_GOVERNANCE.activateNextState(); + } + + // --- + // Lock & unlock unstETH + // --- + function lockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function unlockUnstETH(uint256[] memory unstETHIds) external { + _escrowState.checkSignallingEscrow(); + DUAL_GOVERNANCE.activateNextState(); + + _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); + } + + DUAL_GOVERNANCE.activateNextState(); + } + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { + _escrowState.checkSignallingEscrow(); + + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); + } + + // --- + // Convert to NFT + // --- + + function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + _escrowState.checkSignallingEscrow(); + + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + + // --- + // Start rage quit + // --- + + function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { + _checkCallerIsDualGovernance(); + _escrowState.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); + } + + // --- + // Request withdrawal batches + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { + _escrowState.checkRageQuitEscrow(); + + if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { + revert InvalidBatchSize(batchSize); + } + + uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); + uint256 minStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + + if (stETHRemaining < minStETHWithdrawalRequestAmount) { + return _batchesQueue.close(); + } + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: minStETHWithdrawalRequestAmount, + maxRequestAmount: maxStETHWithdrawalRequestAmount, + remainingAmount: Math.min(stETHRemaining, maxStETHWithdrawalRequestAmount * batchSize) + }); + + _batchesQueue.addUnstETHIds(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); + } + + // --- + // Claim requested withdrawal batches + // --- + + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + _claimNextWithdrawalsBatch( + unstETHIds[0], + unstETHIds, + WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) + ); + } + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); + + _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); + } + + // --- + // Start rage quit extension delay + // --- + + function startRageQuitExtensionDelay() external { + if (!_batchesQueue.isClosed()) { + revert BatchesQueueIsNotClosed(); + } + + /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionDelay can only begin + /// when the last locked unstETH id is finalized in the WithdrawalQueue. + /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: + /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created + /// during the request for withdrawal batches. + /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. + /// - The finalization of unstETH NFTs occurs in FIFO order. + if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { + revert UnfinalizedUnstETHIds(); + } + + if (!_batchesQueue.isAllBatchesClaimed()) { + revert UnclaimedBatches(); + } + + _escrowState.startRageQuitExtensionDelay(); + } + + // --- + // Claim locked unstETH NFTs + // --- + + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); + assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); + } + + // --- + // Escrow management + // --- + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // Withdraw logic + // --- + + function withdrawETH() external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + function withdrawETH(uint256[] calldata unstETHIds) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkWithdrawalsTimelockPassed(); + ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); + ethToWithdraw.sendTo(payable(msg.sender)); + } + + // --- + // Getters + // --- + + function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); + totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); + totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); + } + + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + state.unstETHIdsCount = assets.unstETHIds.length; + state.stETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); + } + + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + return _batchesQueue.getNextWithdrawalsBatches(limit); + } + + function isWithdrawalsBatchesFinalized() external view returns (bool) { + return _batchesQueue.isClosed(); + } + + function isRageQuitExtensionDelayStarted() external view returns (bool) { + return _escrowState.isRageQuitExtensionDelayStarted(); + } + + function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp) { + return _escrowState.rageQuitExtensionDelayStartedAt; + } + + function getRageQuitSupport() external view returns (PercentD16) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + function isRageQuitFinalized() external view returns (bool) { + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionDelayPassed(); + } + + // --- + // Receive ETH + // --- + + receive() external payable { + if (msg.sender != address(WITHDRAWAL_QUEUE)) { + revert InvalidETHSender(msg.sender, address(WITHDRAWAL_QUEUE)); + } + } + + // --- + // Internal methods + // --- + + function _claimNextWithdrawalsBatch( + uint256 fromUnstETHId, + uint256[] memory unstETHIds, + uint256[] memory hints + ) internal { + if (fromUnstETHId != unstETHIds[0]) { + revert UnexpectedUnstETHId(); + } + + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); + } + + ETHValue ethBalanceBefore = ETHValues.fromAddressBalance(address(this)); + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); + + _accounting.accountClaimedStETH(ethBalanceAfter - ethBalanceBefore); + } + + function _checkCallerIsDualGovernance() internal view { + if (msg.sender != address(DUAL_GOVERNANCE)) { + revert CallerIsNotDualGovernance(msg.sender); + } + } +} diff --git a/certora/specs/Escrow.spec b/certora/specs/Escrow.spec index ef007b78..133e9f7b 100644 --- a/certora/specs/Escrow.spec +++ b/certora/specs/Escrow.spec @@ -3,6 +3,7 @@ using DummyWstETH as wst_eth; using Escrow as escrow; using DualGovernance as dualGovernance; using ImmutableDualGovernanceConfigProvider as config; +using DummyWithdrawalQueue as withdrawalQueue; methods { // calls to Escrow from dualGovernance @@ -15,6 +16,7 @@ methods { //envfree function isWithdrawalsBatchesFinalized() external returns (bool) envfree; function getRageQuitSupport() external returns (Escrow.PercentD16) envfree; + function withdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT() external returns (uint) envfree; //calls to stEth and wst_eth from spec function DummyStETH.getTotalShares() external returns(uint256) envfree; @@ -175,6 +177,44 @@ rule W2_2_batchesQueueCloseNoChange(method f){ beforeSecond == currentContract._batchesQueue.batches[any].lastUnstETHId; } +/** +@title W2-2 In a situation where requestNextWithdrawalsBatch should close the queue, + there is no way to prevent it from being closed by first calling another function. +@notice We are filtering out some functions that are not interesting since they cannot + successfully be called in a situation where requestNextWithdrawalsBatch makes sense to call. +*/ +rule W2_2_front_running(method f) filtered { + f -> f.selector != sig:initialize(Durations.Duration).selector + // no longer possible in rage quit state, which we need to be in for requestNextWithdrawalsBatch + && f.selector != sig:unlockUnstETH(uint256[]).selector + && f.selector != sig:lockUnstETH(uint256[]).selector + && f.selector != sig:markUnstETHFinalized(uint256[],uint256[]).selector + && f.selector != sig:unlockStETH().selector + && f.selector != sig:unlockWstETH().selector + && f.selector != sig:lockStETH(uint256).selector + && f.selector != sig:lockWstETH(uint256).selector + && f.selector != sig:requestWithdrawals(uint256[]).selector + && f.selector != sig:startRageQuit(Durations.Duration,Durations.Duration).selector + // only possible after batches queue is already closed + && f.selector != sig:startRageQuitExtensionDelay().selector +} { + storage initial_storage = lastStorage; + + // set up one run in which requestNextWithdrawalsBatch closes the queue + require !isWithdrawalsBatchesFinalized(); + env e; + uint batchsize; + requestNextWithdrawalsBatch(e, batchsize); + require isWithdrawalsBatchesFinalized(); + + // if we frontrun something else, at the end it should still be closed + calldataarg args; + f(e, args) at initial_storage; + requestNextWithdrawalsBatch(e, batchsize); + uint stETHRemaining = stEth.balanceOf(currentContract); + uint minStETHWithdrawalRequestAmount = withdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT(); + assert stETHRemaining < minStETHWithdrawalRequestAmount => isWithdrawalsBatchesFinalized(); +} /** W1-1 Evading Ragequit second seal: From e282bc98d5dbc60f0b674d0969c5934d7098fd96 Mon Sep 17 00:00:00 2001 From: Nurit Dor <57101353+nd-certora@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:18:45 +0300 Subject: [PATCH 63/67] escrow valid state and solvency rules --- certora/confs/Escrow.conf | 1 + certora/confs/Escrow_solvency.conf | 34 ++ certora/confs/Escrow_validState.conf | 32 ++ certora/helpers/DummyWithdrawalQueue.sol | 45 +- certora/specs/Escrow.spec | 192 +++---- certora/specs/Escrow_solvency.spec | 88 +++ certora/specs/Escrow_validState.spec | 647 +++++++++++++++++++++++ 7 files changed, 921 insertions(+), 118 deletions(-) create mode 100644 certora/confs/Escrow_solvency.conf create mode 100644 certora/confs/Escrow_validState.conf create mode 100644 certora/specs/Escrow_solvency.spec create mode 100644 certora/specs/Escrow_validState.spec diff --git a/certora/confs/Escrow.conf b/certora/confs/Escrow.conf index 06cc5834..588a28e9 100644 --- a/certora/confs/Escrow.conf +++ b/certora/confs/Escrow.conf @@ -21,6 +21,7 @@ "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], + "solc": "solc8.26", "optimistic_loop": true, "optimistic_fallback": true, diff --git a/certora/confs/Escrow_solvency.conf b/certora/confs/Escrow_solvency.conf new file mode 100644 index 00000000..24c405f4 --- /dev/null +++ b/certora/confs/Escrow_solvency.conf @@ -0,0 +1,34 @@ +{ + "files": [ + "contracts/Escrow.sol", + "contracts/DualGovernance.sol", + "contracts/DualGovernanceConfigProvider.sol:ImmutableDualGovernanceConfigProvider", + "certora/helpers/DummyWithdrawalQueue.sol", + "certora/harnesses/ERC20Like/DummyStETH.sol", + "certora/harnesses/ERC20Like/DummyWstETH.sol", + ], + "link": [ + "Escrow:DUAL_GOVERNANCE=DualGovernance", + "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", + "Escrow:ST_ETH=DummyStETH", + "Escrow:WST_ETH=DummyWstETH", + "DummyWstETH:stETH=DummyStETH", + "DummyWithdrawalQueue:stETH=DummyStETH", + "DualGovernance:_configProvider=ImmutableDualGovernanceConfigProvider", + ], + + "msg": "sanity", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], +// "prover_args": [ +// " -smt_groundQuantifiers false" +// ], + "solc": "solc8.26", + "optimistic_loop": true, + "optimistic_fallback": true, + "loop_iter": "3", + "build_cache" : true, + "rule_sanity" : "basic", + "verify": "Escrow:certora/specs/Escrow_solvency.spec" +} \ No newline at end of file diff --git a/certora/confs/Escrow_validState.conf b/certora/confs/Escrow_validState.conf new file mode 100644 index 00000000..2132cfeb --- /dev/null +++ b/certora/confs/Escrow_validState.conf @@ -0,0 +1,32 @@ +{ + "files": [ + "contracts/Escrow.sol", + "contracts/DualGovernance.sol", + "contracts/DualGovernanceConfigProvider.sol:ImmutableDualGovernanceConfigProvider", + "certora/helpers/DummyWithdrawalQueue.sol", + "certora/harnesses/ERC20Like/DummyStETH.sol", + "certora/harnesses/ERC20Like/DummyWstETH.sol", + ], + "link": [ + "Escrow:DUAL_GOVERNANCE=DualGovernance", + "Escrow:WITHDRAWAL_QUEUE=DummyWithdrawalQueue", + "Escrow:ST_ETH=DummyStETH", + "Escrow:WST_ETH=DummyWstETH", + "DummyWstETH:stETH=DummyStETH", + "DummyWithdrawalQueue:stETH=DummyStETH", + "DualGovernance:_configProvider=ImmutableDualGovernanceConfigProvider", + ], + + "msg": "Escrow_validState", + "packages": [ + "@openzeppelin=lib/openzeppelin-contracts" + ], + + "solc": "solc8.26", + "optimistic_loop": true, + "optimistic_fallback": true, + "loop_iter": "3", + "build_cache" : true, + "rule_sanity" : "basic", + "verify": "Escrow:certora/specs/Escrow_validState.spec" +} \ No newline at end of file diff --git a/certora/helpers/DummyWithdrawalQueue.sol b/certora/helpers/DummyWithdrawalQueue.sol index a6d7bb90..35438ae1 100644 --- a/certora/helpers/DummyWithdrawalQueue.sol +++ b/certora/helpers/DummyWithdrawalQueue.sol @@ -19,8 +19,9 @@ contract DummyWithdrawalQueue { IStETH public stETH; - struct WithdrawalRequestStatus { - uint256 amountOfStETH; // + struct WithdrawalRequestInfo { + uint256 amountOfStETH; + uint256 claimableAmount; uint256 amountOfShares; address owner; uint256 timestamp; @@ -28,9 +29,21 @@ contract DummyWithdrawalQueue { bool isClaimed; } - mapping(uint256 => WithdrawalRequestStatus) requests; + struct WithdrawalRequestStatus { + uint256 amountOfStETH; + uint256 amountOfShares; + address owner; + uint256 timestamp; + bool isFinalized; + bool isClaimed; + } + mapping(uint256 => WithdrawalRequestInfo) public requests; + // get the last (exsisting) requestId + function getLastRequestId() public view returns (uint256) { + return lastRequestId; + } function getLastFinalizedRequestId() public view returns (uint256) { return lastFinalizedRequestId; } @@ -38,11 +51,17 @@ contract DummyWithdrawalQueue { uint256 randomNumOfFinalzied; // if reduction true we simulate reduce by half function finalize(uint256 upToRequestId, bool reduction) external { + if (lastFinalizedRequestId == 0 ) + lastFinalizedRequestId++; + require (upToRequestId > lastFinalizedRequestId && upToRequestId <= lastRequestId); for(uint256 i = lastFinalizedRequestId; i <= upToRequestId ; i++) { require(!requests[i].isFinalized); requests[i].isFinalized = true; if (reduction) { - requests[i].amountOfStETH = requests[i].amountOfStETH / 2; + requests[i].claimableAmount = requests[i].amountOfStETH / 2; + } + else { + requests[i].claimableAmount = requests[i].amountOfStETH; } } lastFinalizedRequestId = upToRequestId; @@ -56,7 +75,14 @@ contract DummyWithdrawalQueue { statuses = new WithdrawalRequestStatus[](_requestIds.length); for (uint256 i = 0; i < _requestIds.length; ++i) { require(_requestIds[i] <= lastRequestId); - statuses[i] = requests[_requestIds[i]]; + WithdrawalRequestInfo memory r = requests[_requestIds[i]]; + statuses[i] = WithdrawalRequestStatus( + r.amountOfStETH, + r.amountOfShares, + r.owner, + r.timestamp, + r.isFinalized, + r.isClaimed); } } @@ -85,7 +111,7 @@ contract DummyWithdrawalQueue { claimableEthValues[i] = 0; } else { - claimableEthValues[i] = requests[_requestIds[i]].amountOfStETH; + claimableEthValues[i] = requests[_requestIds[i]].claimableAmount; } } } @@ -98,11 +124,13 @@ contract DummyWithdrawalQueue { for (uint256 i = 0; i < _amounts.length; ++i) { stETH.transferFrom(msg.sender, address(this), _amounts[i]); uint256 amountOfShares = stETH.getSharesByPooledEth(_amounts[i]); + require (amountOfShares > 0 ); lastRequestId += 1; requestIds[i] = lastRequestId; requests[lastRequestId] = - WithdrawalRequestStatus( + WithdrawalRequestInfo( _amounts[i], + 0, amountOfShares, _owner, block.timestamp, @@ -114,8 +142,9 @@ contract DummyWithdrawalQueue { function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { for (uint256 i = 0; i < requestIds.length; ++i) { require( ! requests[requestIds[i]].isClaimed && requests[requestIds[i]].isFinalized); + require (requests[requestIds[i]].owner == msg.sender); requests[requestIds[i]].isClaimed = true; - (bool success,) = msg.sender.call{value: requests[requestIds[i]].amountOfStETH }(""); + (bool success,) = msg.sender.call{value: requests[requestIds[i]].claimableAmount }(""); require(success); } } diff --git a/certora/specs/Escrow.spec b/certora/specs/Escrow.spec index ef007b78..33461466 100644 --- a/certora/specs/Escrow.spec +++ b/certora/specs/Escrow.spec @@ -3,6 +3,7 @@ using DummyWstETH as wst_eth; using Escrow as escrow; using DualGovernance as dualGovernance; using ImmutableDualGovernanceConfigProvider as config; +using DummyWithdrawalQueue as withdrawalQueue; methods { // calls to Escrow from dualGovernance @@ -15,6 +16,8 @@ methods { //envfree function isWithdrawalsBatchesFinalized() external returns (bool) envfree; function getRageQuitSupport() external returns (Escrow.PercentD16) envfree; + function getRageQuitExtensionDelayStartedAt() external returns (Escrow.Timestamp) envfree; + //calls to stEth and wst_eth from spec function DummyStETH.getTotalShares() external returns(uint256) envfree; @@ -22,6 +25,7 @@ methods { function DummyStETH.balanceOf(address) external returns(uint256) envfree; function DummyWstETH.balanceOf(address) external returns(uint256) envfree; function DummyStETH.getPooledEthByShares(uint256) external returns (uint256) envfree; + function DummyWithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT() external returns (uint256) envfree; //calls to resealManager are from dualGov are unrelated function _.resume(address sealable) external => NONDET; @@ -42,16 +46,38 @@ methods { } -use builtin rule sanity; +/** +Helper functions +**/ +function isNotInitializedState() returns bool { + return require_uint8(currentContract._escrowState.state) == 0 /*EscrowState.State.NotInitialized*/; +} -/** -@title Ragequit is a final state of the contract, i.e can not change the state +function isSignallingState() returns bool { + return require_uint8(currentContract._escrowState.state) ==1 /*EscrowState.State.SignallingEscrow*/; +} -**/ function isRageQuitState() returns bool { return require_uint8(currentContract._escrowState.state) == 2 /*EscrowState.State.RageQuitEscrow*/; } + +function isBatchQueueStateAbset() returns bool { + return require_uint8(currentContract._batchesQueue.info. state) == 0; +} + +function isBatchQueueStateOpened() returns bool { + return require_uint8(currentContract._batchesQueue.info. state) == 1; +} + +function isBatchQueueStateClosed() returns bool { + return require_uint8(currentContract._batchesQueue.info. state) == 2; +} + +function isAllStEthNFTClaimed() returns bool { + return currentContract._batchesQueue.info.totalUnstETHIdsClaimed == currentContract._batchesQueue.info.totalUnstETHIdsCount ; +} + /** @title If the state of an escrow is RageQuitEscrow, we can execute any method and it will still be in the same state afterwards **/ @@ -69,6 +95,9 @@ rule E_State_1_rageQuitFinalState(method f) } +/** +@title only dual governance can start a rage quit +**/ rule E_KP_5_rageQuitStarter(method f) { bool rageQuitStateBefore = isRageQuitState(); @@ -81,15 +110,12 @@ rule E_KP_5_rageQuitStarter(method f) assert !rageQuitStateBefore && rageQuitStateAfter => e.msg.sender == dualGovernance; - // && - // enought support && time=> rageQuitStarted } /** @title It's not possible to lock funds in or unlock funds from an escrow that is already in the rage quit state. -locking/unlocking implies chaning the stETHLockedShares or unstETHLockedShares of an account - -this can happen only on withdrawEth +locking/unlocking implies changing the stETHLockedShares or unstETHLockedShares of an account. +WithdrawEth (after rage quit) is the other option to change account's asset entry. **/ rule E_KP_3_rageQuitNolockUnlock(method f, address holder) @@ -110,10 +136,10 @@ rule E_KP_3_rageQuitNolockUnlock(method f, address holder) } /** -@title An agent cannot unlock their funds until SignallingEscrowMinLockTime has passed since this user last locked funds. +@title Before rage quit An agent cannot unlock their funds until SignallingEscrowMinLockTime has passed since this user last locked funds. +funds can move between stEthLocked and unstETHLockedShares **/ -//TODO - there is a violation : https://prover.certora.com/output/40726/de13f7a8cc0a43ea9d0a2626098cb465/?anonymousKey=cd50592304ac7f689618e4afa778f954271896cd -// need to acknowledge it is ok + rule E_KP_4_unlockMinTime(method f, address holder) { bool rageQuitStateBefore = isRageQuitState(); @@ -130,8 +156,7 @@ rule E_KP_4_unlockMinTime(method f, address holder) assert (!rageQuitStateBefore && e.block.timestamp < lastTimestamp + min_time) => - beforeStShares <= currentContract._accounting.assets[holder].stETHLockedShares && - beforeUnStShares <= currentContract._accounting.assets[holder].unstETHLockedShares; + beforeStShares + beforeUnStShares <= currentContract._accounting.assets[holder].stETHLockedShares + currentContract._accounting.assets[holder].unstETHLockedShares; } /** @@ -179,7 +204,7 @@ rule W2_2_batchesQueueCloseNoChange(method f){ /** W1-1 Evading Ragequit second seal: -@title when is ragequit the ragequit support is at least SECOND_SEAL_RAGE_QUIT_SUPPORT +@title when in ragequit, the ragequit support is at least SECOND_SEAL_RAGE_QUIT_SUPPORT **/ @@ -191,19 +216,17 @@ invariant W1_1_rageQuitSupportMinValue() /** - @title Reage quit support value - -ignoring imprecisions due to fixed-point arithmetic, the rage quit support of an escrow is equal to -(S+W+U+F) / (T+F) - where -S - is the ETH amount locked in the escrow in the form of stET + -W - is the ETH amount locked in the escrow in the form of wstETH: _accounting.stETHTotals.lockedShares -U - is the ETH amount locked in the escrow in the form of unfinalized Withdrawal NFTs: _accounting.unstETHTotals.unfinalizedShares (sum of all nft deposited) -F - is the ETH amount locked in the escrow in the form of finalized Withdrawal NFTs: _accounting.unstETHTotals.unstETHFinalizedETH (out of unstETHUnfinalizedShares ) -T - is the total supply of stETH. + @title Rage quit support value + The rage quit support of an escrow is equal to: + (S+W+U+F) / (T+F) + where: + S - is the ETH amount locked in the escrow in the form of stET + W - is the ETH amount locked in the escrow in the form of wstETH: _accounting.stETHTotals.lockedShares + U - is the ETH amount locked in the escrow in the form of unfinalized Withdrawal NFTs: _accounting.unstETHTotals.unfinalizedShares (sum of all nft deposited) + F - is the ETH amount locked in the escrow in the form of finalized Withdrawal NFTs: _accounting.unstETHTotals.unstETHFinalizedETH (out of unstETHUnfinalizedShares ) + T - is the total supply of stETH. **/ - rule E_KP_1_rageQuitSupportValue() { // this mostly checks for overflow/underflow mathint actual = getRageQuitSupport(); @@ -217,98 +240,47 @@ T - is the total supply of stETH. assert actual == expected; } -/************* Solvency Rules **********/ -/************* E-KP-2 : total holding of each token ***********/ -/** -@title total holding of wst_eth is zero as all wst_eth are converted to st_eth -**/ -invariant zeroWstEthBalance() - wst_eth.balanceOf(currentContract) == 0 - filtered { f -> f.contract != stEth && f.contract != wst_eth} { - preserved with (env e) { - require e.msg.sender != currentContract; - } -} -ghost mathint sumStETHLockedShares{ - // assuming value zero at the initial state before constructor - init_state axiom sumStETHLockedShares == 0; -} -/* updated sumStETHLockedShares according to the change of a single account */ -hook Sstore currentContract._accounting.assets[KEY address a].stETHLockedShares Escrow.SharesValue new_balance -// the old value that balances[a] holds before the store - (Escrow.SharesValue old_balance) { - sumStETHLockedShares = sumStETHLockedShares + new_balance - old_balance; -} - -hook Sload Escrow.SharesValue value currentContract._accounting.assets[KEY address a].stETHLockedShares { - require value <= sumStETHLockedShares; -} - -invariant totalLockedShares() - sumStETHLockedShares <= currentContract._accounting.stETHTotals.lockedShares; - - /** @title total holding of stEth before rageQuit start - **/ -invariant solvency_stETH_before_ragequit() - !isRageQuitState() => stEth.getPooledEthByShares(currentContract._accounting.stETHTotals.lockedShares - ) <= stEth.balanceOf(currentContract) - - filtered { f -> f.contract != stEth && f.contract != wst_eth} { - preserved with (env e) { - require e.msg.sender != currentContract; - } -} -////// Nurit : From here work in progress +// make rule of state changed to UnstETHRecordStatus /* -invariant solvency_stETH_before_ragequit() - !isRageQuitState() => - currentContract._accounting.unstETHTotals.unfinalizedShares - - filtered { f -> f.contract != stEth && f.contract != wst_eth} { - preserved with (env e) { - require e.msg.sender != currentContract; - } -} -*/ - - -invariant solvency_claimedETH() - isRageQuitState() => currentContract._accounting.stETHTotals.claimedETH * sumStETHLockedShares /*/ currentContract._accounting.stETHTotals.lockedShares*/ <= nativeBalances[currentContract] * currentContract._accounting.stETHTotals.lockedShares - - filtered { f -> f.contract != stEth && f.contract != wst_eth} { - preserved with (env e) { - require e.msg.sender != currentContract; - } +enum UnstETHRecordStatus { + NotLocked = 0 + Locked = 1 + Finalized = 2 + Claimed = 3 + Withdrawn = 4 } - - - - -//todo - StETHAccounting.claimedETH <= nativeBalances[currentContract] -// need to prove sum of balance <= self.stETHTotals.lockedShares -/* -rule change_eth(method f) -{ - uint256 before = nativeBalances[currentContract]; +Valid transitions are: +0 -> 1 +1 -> 0 +1 -> 2 +1 -> 3 +2 -> 3 +2 -> 0 +3 -> 4 +4 final state +*/ + +rule stateTransition_unstethRecord(uint256 unstETHId, method f) { + uint8 before = require_uint8(currentContract._accounting.unstETHRecords[unstETHId].status); + require before == 3 => isRageQuitState(); env e; calldataarg args; f(e,args); - uint256 after = nativeBalances[currentContract]; - assert after == before; -} -rule change_st_eth(method f) -{ - uint256 before = stEth.balanceOf(currentContract); - env e; - calldataarg args; - f(e,args); - uint256 after = stEth.balanceOf(currentContract); - assert after == before; + uint8 after = require_uint8(currentContract._accounting.unstETHRecords[unstETHId].status); + assert before != after => + ( ( before == 0 <=> after == 1) + && ( before == 1 => after <= 3) + && ( before == 2 => ( after == 0 || after == 3) ) + && ( before == 3 <=> after == 4 ) + && ( after == 2 => before == 1 ) + && ( after == 3 => (before == 1 || before == 2) ) + ); + assert after == 3 => isRageQuitState(); + } -*/ -//todo - count of all unstETHIds <= withdrawalQueue.balanaceOf(currentContract) + diff --git a/certora/specs/Escrow_solvency.spec b/certora/specs/Escrow_solvency.spec new file mode 100644 index 00000000..6b2e5193 --- /dev/null +++ b/certora/specs/Escrow_solvency.spec @@ -0,0 +1,88 @@ + +import "./escrow_validState.spec"; +/** + Verification of asset holding + +**/ +/** +@title Total holding of wst_eth is zero as all wst_eth are converted to st_eth +**/ +invariant solvency_zeroWstEthBalance() + wst_eth.balanceOf(currentContract) == 0 + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + } +} + + /** @title Total holding of stEth before rageQuit start + **/ +invariant solvency_stETH_before_rageQuit() + !isRageQuitState() => stEth.getPooledEthByShares(currentContract._accounting.stETHTotals.lockedShares + ) <= stEth.balanceOf(currentContract) + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + } +} + + +/** @title Before rage quit eth value of escrow can not be reduced +**/ +rule solvency_ETH_before_rageQuit(method f) +{ + bool rageQuitStateBefore = isRageQuitState(); + uint256 before = nativeBalances[currentContract]; + env e; + calldataarg args; + // Escrow is the starting point, it can never call directly an arbitrary function + require (e.msg.sender != currentContract); + f(e,args); + uint256 after = nativeBalances[currentContract]; + assert !rageQuitStateBefore => after >= before; +} + + +/** @title Total holding of eth by the escrow: + 1. For the locked shares: + calimedETH * sumStETHLockedShares / stETHTotals.lockedShares + + where sumStETHLockedShares is the current holding of shares + 2. For the unstEth holding: + sumClaimedUnSTEth - sumWithdrawnUnSTEth + where: + sumClaimedUnSTEth - total amount of all claimed unstEth + sumWithdrawnUnSTEth - total amount already withdrawn + +**/ +invariant solvency_ETH() + // pool of batch queue + (( (currentContract._batchesQueue.info.totalUnstETHIdsCount!= 0 && currentContract._accounting.stETHTotals.lockedShares!=0) ? (currentContract._accounting.stETHTotals.claimedETH * sumStETHLockedShares /currentContract._accounting.stETHTotals.lockedShares ) : 0 ) + // unstEth pool + + + (sumClaimedUnSTEth - sumWithdrawnUnSTEth) + <= nativeBalances[currentContract] + ) + && + (currentContract._batchesQueue.info.totalUnstETHIdsClaimed== 0 => currentContract._accounting.stETHTotals.claimedETH == 0) + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + requireInvariant solvency_stETH_before_rageQuit(); + validState(); + } + preserved withdrawETH(uint256[] unstETHIds) with (env e) { + require unstETHIds.length ==1; + require sumClaimedUnSTEth >= unstETHIds[0]; + require e.msg.sender != currentContract; + requireInvariant solvency_stETH_before_rageQuit(); + validState(); + + } +} + + + + diff --git a/certora/specs/Escrow_validState.spec b/certora/specs/Escrow_validState.spec new file mode 100644 index 00000000..b0ababbb --- /dev/null +++ b/certora/specs/Escrow_validState.spec @@ -0,0 +1,647 @@ +import "./Escrow.spec"; + + + +/****** Ghost declaration *****/ + + +/** @title Ghost sumStETHLockedShares is: + sum of _accounting.assets[a].stETHLockedShares Escrow.SharesValue + for all addresses a +**/ +ghost mathint sumStETHLockedShares { + // assuming value zero at the initial state before constructor + init_state axiom sumStETHLockedShares == 0; +} + + +/** @title Count how many IDs in each bathcIndex: + numOfIdsInBatch[batchIndex] = _batchesQueue.batches[batchIndex].lastUnstETHId - batchesQueue.batches[batchIndex].firstUnstETHId +1 +**/ +ghost mapping(mathint => mathint) numOfIdsInBatch { + init_state axiom forall mathint x. numOfIdsInBatch[x] == 0; +} + +/** @title Accumulated count of batch id in all previous index + countOFBatchIds[0] = 0; there is one that is just a start + countOFBatchIds[x] = \count_{i=1}^{x-1} batch[x].last - batch[x].first + 1 ; +**/ +ghost mapping(mathint => mathint) countOFBatchIds { + init_state axiom forall mathint x. countOFBatchIds[x] == 0; +} +///@title a mirror for batchesQueue.batches[batchIndex].length +ghost uint256 ghostLengthMirror { + init_state axiom ghostLengthMirror == 0; +} + +///@title mirror claimableETH +ghost mapping(mathint => mathint) claimableETH { + init_state axiom forall mathint x. claimableETH[x] == 0; +} + +/** @title the partial sum of all unstethRecord that is claimed status >= 3 (claimed or withdrawn) + this can increase when moving to state 3 + i.e., partialSumOfClaimedUnstETH[id] == sum of _accounting.unstETHRecords[id'].claimableAmount + where: id' <= id and id' is in state >= 3 +**/ +ghost mapping(mathint => mathint) partialSumOfClaimedUnstETH { + init_state axiom forall mathint x. partialSumOfClaimedUnstETH[x] == 0; +} + +/// @title the sum of all unstethRecord.claimableAmount in status >= 3 /* claimed or withdrawn */ +ghost mathint sumClaimedUnSTEth { + init_state axiom sumClaimedUnSTEth == 0; +} +/// @title the sum of all unstethRecord.claimableAmount that was claimed and then withdrawn form escrow, status == 4 /* withdrawn */ +ghost mathint sumWithdrawnUnSTEth { + init_state axiom sumWithdrawnUnSTEth == 0; +} +/** @title the partial sum of all unstethRecord that is claimed status >= 4 (withdrawn) + this can increase when moving to state 4 + i.e., partialSumOfWithdrawnUnstETH[id] == sum of _accounting.unstETHRecords[id'].claimableAmount + where: id' <= id and id' is in state >= 4 +**/ +ghost mapping(mathint => mathint) partialSumOfWithdrawnUnstETH + { + init_state axiom forall mathint x. partialSumOfWithdrawnUnstETH[x] == 0; +} + +/****** Hooks for ghost updates *****/ + +hook Sload uint256 length currentContract._batchesQueue.batches.length { + require length == ghostLengthMirror; +} + +hook Sstore currentContract._batchesQueue.batches.length uint256 newLength { + ghostLengthMirror = newLength; +} + +/* updated sumStETHLockedShares according to the change of a single account */ +hook Sstore currentContract._accounting.assets[KEY address a].stETHLockedShares Escrow.SharesValue new_balance +// the old value that balances[a] holds before the store + (Escrow.SharesValue old_balance) { + sumStETHLockedShares = sumStETHLockedShares + new_balance - old_balance; +} + +/* assume a sum is ge it's element */ +hook Sload Escrow.SharesValue value currentContract._accounting.assets[KEY address a].stETHLockedShares { + require value <= sumStETHLockedShares; +} + +hook Sstore currentContract._batchesQueue.batches[INDEX uint256 batchIndex].firstUnstETHId uint256 newStart (uint256 oldStart) { + // update numOFIdsInBatch for batchIndex + mathint end = currentContract._batchesQueue.batches[batchIndex].lastUnstETHId; + numOfIdsInBatch[batchIndex] = (batchIndex == 0 ? 0: end - newStart + 1); + // update partial sums for x > to_mathint(batchIndex) + countOFBatchIds[batchIndex+1] = countOFBatchIds[batchIndex] + numOfIdsInBatch[batchIndex] ; +} + +hook Sstore currentContract._batchesQueue.batches[INDEX uint256 batchIndex].lastUnstETHId uint256 newLastId (uint256 oldLastId) { + mathint start = currentContract._batchesQueue.batches[batchIndex].firstUnstETHId; + //require(numOfIdsInBatch[batchIndex] == (batchIndex == 0 ? 0: oldLastId - start + 1)); + numOfIdsInBatch[batchIndex] = (batchIndex == 0 ? 0: newLastId - start + 1); + // update partial sums for x > to_mathint(batchIndex) + countOFBatchIds[batchIndex+1] = countOFBatchIds[batchIndex] + numOfIdsInBatch[batchIndex] ; +} + +hook Sload uint256 end currentContract._batchesQueue.batches[INDEX uint256 batchIndex].lastUnstETHId { + mathint start = currentContract._batchesQueue.batches[batchIndex].firstUnstETHId; + require numOfIdsInBatch[batchIndex] == ((batchIndex == 0)? 0 : + end - start +1 ); +} + +hook Sload uint256 start currentContract._batchesQueue.batches[INDEX uint256 batchIndex].firstUnstETHId { + mathint end = currentContract._batchesQueue.batches[batchIndex].lastUnstETHId; + require numOfIdsInBatch[batchIndex] == ((batchIndex == 0)? 0 : + end - start +1 ); +} + + +hook Sstore currentContract._accounting.unstETHRecords[KEY uint256 unstETHId].status Escrow.UnstETHRecordStatus new_status (Escrow.UnstETHRecordStatus old_status ) +{ + // to claimed add to sumClaimedUnSTEth, note that it is also updated in the hook on claimableAmount as order is not known + sumClaimedUnSTEth = sumClaimedUnSTEth + + ( (to_mathint(old_status) != 3 && to_mathint(new_status) == 3 ) ? currentContract._accounting.unstETHRecords[unstETHId].claimableAmount : 0 ); + + // when chaning to state withrawn (state 4), claimableAmount must be already set, just update sumWithdrawnUnSTEth + sumWithdrawnUnSTEth = sumWithdrawnUnSTEth + + ( (to_mathint(old_status) != 4 && to_mathint(new_status) == 4 ) ? currentContract._accounting.unstETHRecords[unstETHId].claimableAmount : 0 ); + // update also partial sum of WithdrawanETH + if (to_mathint(old_status) == 3 && to_mathint(new_status) == 4) { + havoc partialSumOfWithdrawnUnstETH assuming forall uint256 id. + (partialSumOfWithdrawnUnstETH@new[id] == partialSumOfWithdrawnUnstETH@old[id] + ( id > unstETHId ? claimableETH[unstETHId] : 0)); + } + + // when claiming (state 3) update partialSumOfClaimedUnstETH for all indexes gt then unstETHId + if (to_mathint(old_status) != 3 && to_mathint(new_status) == 3 ) { + havoc partialSumOfClaimedUnstETH assuming forall uint256 id. + (partialSumOfClaimedUnstETH@new[id] == partialSumOfClaimedUnstETH@old[id] + ( id > unstETHId ? claimableETH[unstETHId] : 0 )); + } + + +} + + +hook Sstore currentContract._accounting.unstETHRecords[KEY uint256 unstETHId].claimableAmount Escrow.ETHValue new_claimableAmount (Escrow.ETHValue old_claimableAmount ) +{ + if ( to_mathint(currentContract._accounting.unstETHRecords[unstETHId].status) == 3) { + havoc partialSumOfClaimedUnstETH assuming forall uint256 id. + (partialSumOfClaimedUnstETH@new[id] == partialSumOfClaimedUnstETH@old[id] + ( id > unstETHId ? new_claimableAmount - old_claimableAmount : 0)); + sumClaimedUnSTEth = sumClaimedUnSTEth + new_claimableAmount - old_claimableAmount ; + } + claimableETH[unstETHId] = new_claimableAmount; +} + + +hook Sload Escrow.ETHValue claimableAmount currentContract._accounting.unstETHRecords[KEY uint256 unstETHId].claimableAmount +{ + + require claimableETH[unstETHId] == claimableAmount; + if ( to_mathint(currentContract._accounting.unstETHRecords[unstETHId].status) >= 3) { + require forall uint256 id. (id > unstETHId) => partialSumOfClaimedUnstETH[id] >= claimableAmount; + require claimableAmount <= sumClaimedUnSTEth; + } +} + +/** + @title CVL function to gather all valid state rules and a few other assumptions: + 1. the size of the batch queue is not close to max_uint + 2. lastRequestId zero is not valid +**/ +function validState() { + // push is an unchecked operation, safely assume array will not overflow + require currentContract._batchesQueue.batches.length < 1000; + require currentContract._batchesQueue.batches[0].lastUnstETHId < 1000000; + require withdrawalQueue.lastRequestId < 100000000 && withdrawalQueue.lastRequestId > 0 ; + requireInvariant validState_nonInitialized(); + requireInvariant validState_signalling(); + requireInvariant validState_rageQuit(); + uint256 any; + requireInvariant validState_batchesQueue_monotonicity(); + requireInvariant validState_batchesQueue_withdrawalQueue(); + requireInvariant validState_totalLockedShares(); + requireInvariant validState_batchesQueue_distinct_unstETHRecords(); + requireInvariant validState_batchesQueue_ordering(); + validState_batchesQueue_claimed_vs_actual(); + requireInvariant validState_withdrawalQueue(); + requireInvariant validState_batchQueuesSum(); + requireInvariant validState_totalETHIds() ; + requireInvariant validState_partialSumOfClaimedUnstETH(); + validState_partialSumMonotonicity(); + requireInvariant validState_claimedUnstEth(); + requireInvariant validState_withdrawnEth(); + requireInvariant valid_batchIndex(); + + + + } + + +/** @title Current sum of all locked shares is le the total lockedShares + @notice lockedShares is the total and is not reduced on withdraw **/ +invariant validState_totalLockedShares() + sumStETHLockedShares <= currentContract._accounting.stETHTotals.lockedShares && sumStETHLockedShares >= 0 ; + + +/****** Escrow State Invariants *****/ + + +/// @title Before initialization everything is zero +invariant validState_nonInitialized() + ( isNotInitializedState() => ( + isBatchQueueStateAbset() && + currentContract._accounting.stETHTotals.claimedETH == 0 && + getRageQuitExtensionDelayStartedAt() ==0 && + currentContract._batchesQueue.info.totalUnstETHIdsClaimed == 0 && + currentContract._accounting.stETHTotals.lockedShares == 0 && + sumClaimedUnSTEth == 0 && + currentContract._batchesQueue.batches.length == 0 + ) + ) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + // push is an unchecked opetation, safely assume array will not overflow + require currentContract._batchesQueue.batches.length < max_uint256; + } + } + +/// @title while in signaling no claims and no batch queues +invariant validState_signalling() + // While in signaling state, no batch queues are open and no claim + ( isSignallingState() => ( isBatchQueueStateAbset() && + currentContract._accounting.stETHTotals.claimedETH == 0 && + getRageQuitExtensionDelayStartedAt() ==0 && + currentContract._batchesQueue.info.totalUnstETHIdsClaimed == 0 && + sumClaimedUnSTEth == 0 && + currentContract._batchesQueue.batches.length == 0 + ) + ) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + +/// @title Once rageQuit start, batch queues are either open or closed +invariant validState_rageQuit() + (isRageQuitState() <=> ( !isBatchQueueStateAbset() ) ) + && (isRageQuitState() <=> (currentContract._batchesQueue.batches.length >= 1 && currentContract._batchesQueue.batches[0].lastUnstETHId != 0)) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + + +/****** Batch Queue Invariants *****/ + + +/** @title Monotonicity of batch queues: + 1. In each entry, first id is le than the last id + 2. Entry zero (if exist) has only one element +**/ +invariant validState_batchesQueue_monotonicity( ) + // each batch entry is monotonic, first <= last + (forall uint256 h2. (h2 >=0 && h2 < currentContract._batchesQueue.batches.length) => (currentContract._batchesQueue.batches[h2].firstUnstETHId <= currentContract._batchesQueue.batches[h2].lastUnstETHId) + ) && + ((currentContract._batchesQueue.batches.length > 0 ) => currentContract._batchesQueue.batches[0].firstUnstETHId == currentContract._batchesQueue.batches[0].lastUnstETHId) + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + +/** @title Ordering of batch queues: + The first id in each entry is greater than the last in the previous entry + @dev To help verification of other rules, we prove for next index and also for all indexes gt than the current one +**/ +invariant validState_batchesQueue_ordering() + // monotonic, each batch is starting at a higher requestID than the previos one + (forall uint256 index. forall uint256 indexNext. (currentContract._batchesQueue.batches.length >= 1 && currentContract._batchesQueue.batches.length-1 >= indexNext && to_mathint(indexNext) == index+1 ) => + (currentContract._batchesQueue.batches[indexNext].firstUnstETHId > currentContract._batchesQueue.batches[index].lastUnstETHId ) + ) + && + (forall uint256 index. forall uint256 indexNext. (currentContract._batchesQueue.batches.length >= 1 && currentContract._batchesQueue.batches.length-1 >= indexNext && indexNext > index ) => + (currentContract._batchesQueue.batches[indexNext].firstUnstETHId > currentContract._batchesQueue.batches[index].lastUnstETHId ) + ) + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + +/** @title Validity of batch queue ids: + The last id in the last entry is le than the lastRequestId in withdrawal queue + @notice Given the proof that all indexes are ordered, this implies that all ids are le lastRequestId +**/ +invariant validState_batchesQueue_withdrawalQueue() + // last element is le withdrawalQueue.lastRequestId + (currentContract._batchesQueue.batches.length >= 1 => currentContract._batchesQueue.batches[require_uint256(currentContract._batchesQueue.batches.length - 1)].lastUnstETHId <= withdrawalQueue.lastRequestId) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + +/// @title all unstEth are less than the lastRequestId and first batch if exists +invariant validState_batchesQueue_distinct_unstETHRecords( ) + forall uint256 id. ( + // if id is an unsetEth + ( to_mathint(currentContract._accounting.unstETHRecords[id].status) != 0 => + // then id is a valid one in withdrawalQueue + id <= withdrawalQueue.lastRequestId ) + && + // and id is an unsetEth and there are batch queues + ( ( to_mathint(currentContract._accounting.unstETHRecords[id].status) != 0 && + currentContract._batchesQueue.batches.length > 0 ) => + // then id has to be le than the batch queues ids (we check the first as the rest are monotonic increasing) + id <= currentContract._batchesQueue.batches[0].firstUnstETHId )) + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + +/** @title Valid state of withdrawalQueue: + 1. an id is claimed only if it a valid requestId and was finalized + 2. an id is finalized iff it is le lastFinalizedRequestId + @dev This is only concerning withdrawlQueue but these properties are needed to prove properties of Escrow +**/ +invariant validState_withdrawalQueue() + (forall uint256 id. + ( (withdrawalQueue.requests[id].isClaimed) => ( + withdrawalQueue.requests[id].isFinalized && + id <= withdrawalQueue.lastRequestId + ) + ) + && (id!=0 => (withdrawalQueue.requests[id].isFinalized <=> id <= withdrawalQueue.lastFinalizedRequestId)) + ) + && (withdrawalQueue.lastRequestId >= withdrawalQueue.lastFinalizedRequestId) + && (!withdrawalQueue.requests[0].isClaimed && !withdrawalQueue.requests[0].isFinalized) + filtered { f -> f.contract == withdrawalQueue} + +/// @title countOFBatchIds is as expected + invariant validState_batchQueuesSum() + // batch index 0 is not relevant + countOFBatchIds[0] == 0 && numOfIdsInBatch[0] == 0 && countOFBatchIds[1] == 0 && + (forall uint256 index. (index > 0 && index < currentContract._batchesQueue.batches.length) => + (( numOfIdsInBatch[index] == currentContract._batchesQueue.batches[index].lastUnstETHId - currentContract._batchesQueue.batches[index].firstUnstETHId +1 ) && (numOfIdsInBatch[index] <= countOFBatchIds[index + 1])) + ) + && + (forall uint256 index. (index > 0 && index <= currentContract._batchesQueue.batches.length) => + countOFBatchIds[index] == countOFBatchIds[index-1] + numOfIdsInBatch[index-1] + ) + filtered { f -> f.contract == currentContract} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + + } + } + +/** @title integrity of totalEthids: + 1. if lastClaimedBatchIndex is not zero, then totalUnstETHIdsClaimed is the count of all batch indexs calimed plus the number of ids claimed in the current batch + 2. if lastClaimedBatchIndex is zero, then claimed index is also zero and so it the total claimed ids + 3. totalUnstETHIdsCount is the total ids in all batch queues + 4. total claimed le total ids +*/ +invariant validState_totalETHIds() + (( currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex + countOFBatchIds[currentContract._batchesQueue.info.lastClaimedBatchIndex] + 1 == currentContract._batchesQueue.info.totalUnstETHIdsClaimed ) || + (currentContract._batchesQueue.info.lastClaimedBatchIndex == 0 )) + && + (currentContract._batchesQueue.info.lastClaimedBatchIndex == 0 => + ( currentContract._batchesQueue.info.totalUnstETHIdsClaimed == 0 && + currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex == 0)) + && + currentContract._batchesQueue.info.totalUnstETHIdsCount == countOFBatchIds[currentContract._batchesQueue.batches.length] + && + (currentContract._batchesQueue.info.totalUnstETHIdsClaimed== 0 => currentContract._accounting.stETHTotals.claimedETH == 0) + && (currentContract._batchesQueue.info.totalUnstETHIdsClaimed <= currentContract._batchesQueue.info.totalUnstETHIdsCount) + + filtered { f -> f.contract == currentContract} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + + +/// @title Last claimed batch index is lt the length of batch queue, if exists +invariant valid_batchIndex() + ( currentContract._batchesQueue.info.lastClaimedBatchIndex < currentContract._batchesQueue.batches.length || + (currentContract._batchesQueue.info.lastClaimedBatchIndex==0 && currentContract._batchesQueue.batches.length==0 ) + ) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + +/** @title If an id is within the claimed indexes than it is marked as claimed in the withdrawal queue +**/ +invariant validState_batchesQueue_claimed_vs_actual_1(uint256 index, uint256 id) + ( // if id is in one of the batch indexes + ( index < ghostLengthMirror && index != 0 && + currentContract._batchesQueue.batches[index].firstUnstETHId <= id && currentContract._batchesQueue.batches[index].lastUnstETHId >= id ) => + // then it is claimed iff the lastClaimedBatchIndex , lastClaimedUnstETHIdIndex say so + ( + // index is less than lastClaimedBatchIndex + ( index < currentContract._batchesQueue.info.lastClaimedBatchIndex || + ( index == currentContract._batchesQueue.info.lastClaimedBatchIndex && id <= currentContract._batchesQueue.batches[index].firstUnstETHId + currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex) + ) + => withdrawalQueue.requests[id].isClaimed + ) + ) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + } + } + +/** @dev since validState_batchesQueue_claimed_vs_actual_1 is proved for all index and for all id +we have a function that assume it for all values */ +function validState_batchesQueue_claimed_vs_actual() { + require ( forall uint256 index. forall uint256 id. + ( index < ghostLengthMirror && index != 0 && + currentContract._batchesQueue.batches[index].firstUnstETHId <= id && currentContract._batchesQueue.batches[index].lastUnstETHId >= id && withdrawalQueue.requests[id].isClaimed) + => + ( index < currentContract._batchesQueue.info.lastClaimedBatchIndex || + ( index == currentContract._batchesQueue.info.lastClaimedBatchIndex && id <= (currentContract._batchesQueue.batches[index].firstUnstETHId + currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex) ) ) + ) + && + ( forall uint256 index. forall uint256 id. + ( ( index < ghostLengthMirror && index != 0 && + currentContract._batchesQueue.batches[index].firstUnstETHId <= id && currentContract._batchesQueue.batches[index].lastUnstETHId >= id && + ( index < currentContract._batchesQueue.info.lastClaimedBatchIndex || + ( index == currentContract._batchesQueue.info.lastClaimedBatchIndex && id <= currentContract._batchesQueue.batches[index].firstUnstETHId + currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex)) + ) + => withdrawalQueue.requests[id].isClaimed + ) + ); +} + +/** @title claimed unstETHRecords properties: + 1. if an unstETHRecord is finalized (status 2) then it is marked as finalized and not claimed in the withdrawal queue + 2. if an unstETHRecord is claimed or withdrawn (status 3 or 4) then it is marked as finalized and claimed in the withdrawal queue +**/ +invariant validState_partialSumOfClaimedUnstETH() + // batch index 0 is not relevant + partialSumOfClaimedUnstETH[0] == 0 && claimableETH[0] == 0 && partialSumOfClaimedUnstETH[1] == 0 && + ( + forall uint256 id. (id > 0 && (to_mathint(currentContract._accounting.unstETHRecords[id].status) == 2)) => (withdrawalQueue.requests[id].isFinalized && !withdrawalQueue.requests[id].isClaimed) + ) + && + ( + forall uint256 id. (id > 0 && (to_mathint(currentContract._accounting.unstETHRecords[id].status) == 3 || to_mathint(currentContract._accounting.unstETHRecords[id].status) == 4)) => (withdrawalQueue.requests[id].isClaimed && withdrawalQueue.requests[id].isFinalized) + ) + filtered { f -> f.contract == currentContract} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + + } + } + +/** @title partial sum of withdrawn is le partial sum of claimed, by at least the element that is claimed but not withdrawn + @notice proving on a single element +**/ +invariant validState_partialSumMonotonicity_1(uint256 id) + partialSumOfWithdrawnUnstETH[require_uint256(id+1)] + ( to_mathint(currentContract._accounting.unstETHRecords[id].status) == 3 ? currentContract._accounting.unstETHRecords[id].claimableAmount: 0 ) <= partialSumOfClaimedUnstETH[require_uint256(id+1)] + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + validState(); + } + preserved withdrawETH(uint256[] unstETHIds) with (env e) { + require unstETHIds.length == 1; + require unstETHIds[0] == id || unstETHIds[0] == require_uint256(id +1); + require e.msg.sender != currentContract; + validState(); + } + +} + +/** @title partial sum of two ids is as expected +**/ +invariant validState_partialSumMonotonicity_2(uint256 id, uint256 id2) + id2 > id => (partialSumOfWithdrawnUnstETH[require_uint256(id2)] +( to_mathint(currentContract._accounting.unstETHRecords[id].status) == 3 ? currentContract._accounting.unstETHRecords[id].claimableAmount: 0 ) <= partialSumOfClaimedUnstETH[require_uint256(id2)]) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + validState(); + } + preserved withdrawETH(uint256[] unstETHIds) with (env e) { + require unstETHIds.length == 1; + require unstETHIds[0] == id ; + require e.msg.sender != currentContract; + validState(); + } +} + +/// @title a function for partialSumMonotonicity on all elements +function validState_partialSumMonotonicity() { + require ( forall uint256 id. forall uint256 idNext. ((idNext > id) => (partialSumOfWithdrawnUnstETH[idNext] +( to_mathint(currentContract._accounting.unstETHRecords[id].status) == 3 ? currentContract._accounting.unstETHRecords[id].claimableAmount: 0 ) ) <= partialSumOfClaimedUnstETH[idNext])); +} + +/// @title Total withdrawn unstEth is the partial sum of withdrawn of the lastFinalizedRequestId+ 1 +invariant validState_withdrawnEth() + (forall uint256 id. (id > withdrawalQueue.lastFinalizedRequestId) => + partialSumOfWithdrawnUnstETH[id] == sumWithdrawnUnSTEth ) + && + ( sumWithdrawnUnSTEth >= 0 && sumWithdrawnUnSTEth <= sumClaimedUnSTEth ) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + // check requireInvariant solvency_stETH_before_ragequit(); + validState(); + } + preserved withdrawETH(uint256[] unstETHIds) with (env e) { + uint256 claimedId; + require unstETHIds.length == 1; + require unstETHIds[0] == claimedId ; + // help grounding: + requireInvariant validState_partialSumMonotonicity_1(claimedId); + requireInvariant validState_partialSumMonotonicity_1(withdrawalQueue.lastFinalizedRequestId); + + require partialSumOfWithdrawnUnstETH[require_uint256(withdrawalQueue.lastFinalizedRequestId+1)] + + ( to_mathint(currentContract._accounting.unstETHRecords[claimedId].status) == 3 ? currentContract._accounting.unstETHRecords[claimedId].claimableAmount: 0 ) <= partialSumOfClaimedUnstETH[require_uint256(withdrawalQueue.lastFinalizedRequestId+1)]; + + require withdrawalQueue.requests[claimedId].isFinalized <=> claimedId <= withdrawalQueue.lastFinalizedRequestId; + + require (partialSumOfClaimedUnstETH[claimedId] <= sumClaimedUnSTEth); + require (partialSumOfClaimedUnstETH[claimedId+1] <= sumClaimedUnSTEth); + require e.msg.sender != currentContract; + validState(); + +}} + +/// @title Total claimed unstEth is the partial sum of claimed of the lastFinalizedRequestId+ 1 +invariant validState_claimedUnstEth() + forall uint256 id. (id > withdrawalQueue.lastFinalizedRequestId) => + partialSumOfClaimedUnstETH[id] == sumClaimedUnSTEth + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + require e.msg.sender != currentContract; + validState(); + } +} + + +/****** Helper Functions *****/ + +/** @dev Use this function to get smaller counter examples, not needed for verification **/ +function assumingThreeOnly() { + require currentContract._batchesQueue.batches.length <= 3; + if (currentContract._batchesQueue.batches.length > 0) { + require currentContract._batchesQueue.batches[0].firstUnstETHId == currentContract._batchesQueue.batches[0].lastUnstETHId && + currentContract._batchesQueue.batches[0].firstUnstETHId > 0; + } + if (currentContract._batchesQueue.batches.length > 1) { + require currentContract._batchesQueue.batches[1].firstUnstETHId > currentContract._batchesQueue.batches[0].lastUnstETHId && + currentContract._batchesQueue.batches[1].firstUnstETHId <= currentContract._batchesQueue.batches[1].lastUnstETHId; + } + if (currentContract._batchesQueue.batches.length > 2) { + require currentContract._batchesQueue.batches[2].firstUnstETHId > currentContract._batchesQueue.batches[1].lastUnstETHId && + currentContract._batchesQueue.batches[2].firstUnstETHId <= currentContract._batchesQueue.batches[2].lastUnstETHId; + } + // allow up to three unstEthIds + uint256 i; + uint256 j; + uint256 k; + require ( i > 0 && i < j && j < k && k <= withdrawalQueue.lastRequestId); + require (currentContract._batchesQueue.batches.length > 0) => k <= currentContract._batchesQueue.batches[0].firstUnstETHId; + require ( forall uint256 any. (any != i && any != j && any != k ) => + to_mathint(currentContract._accounting.unstETHRecords[any].status) == 0 ); +} + +/** Extra rules **/ +/* +invariant validState_batchesQueue_solvent_leftToClaim(uint256 index, uint256 id) + (( index > 0 && index < currentContract._batchesQueue.batches.length && id >= currentContract._batchesQueue.batches[index].firstUnstETHId && + id <= currentContract._batchesQueue.batches[index].lastUnstETHId && (!withdrawalQueue.requests[id].isClaimed)) => + ( currentContract._batchesQueue.info.totalUnstETHIdsCount - currentContract._batchesQueue.info.totalUnstETHIdsClaimed >= (countOFBatchIds[currentContract._batchesQueue.batches.length] - countOFBatchIds[index]) - ( id - (currentContract._batchesQueue.batches[index].firstUnstETHId + 1)) ) + ) +filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + requireInvariant validState_batchesQueue_solvent_allClaimed(index, id); + } + } + + +invariant validState_batchesQueue_solvent_allClaimed(uint256 index, uint256 id) + + // isAllBatchesClaimed and there are batches queues + (currentContract._batchesQueue.batches.length > 1 => + (isAllStEthNFTClaimed() => withdrawalQueue.requests[currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId].isClaimed + )) + && + ( currentContract._batchesQueue.info.lastClaimedBatchIndex < currentContract._batchesQueue.batches.length || + (currentContract._batchesQueue.info.lastClaimedBatchIndex==0 && currentContract._batchesQueue.batches.length==0 ) + ) + && + ( getRageQuitExtensionDelayStartedAt() > 0 => + (( + withdrawalQueue.requests[currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId].isFinalized ) + && + ( currentContract._batchesQueue.info.totalUnstETHIdsClaimed == currentContract._batchesQueue.info.totalUnstETHIdsCount && + isBatchQueueStateClosed() ) + ) + ) + + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + requireInvariant validState_batchesQueue_solvent_leftToClaim(index, id); + } + } +**/ From 6e2e658eff4d82eb9d855087cd2a97595c0e9a04 Mon Sep 17 00:00:00 2001 From: Christiane Goltz Date: Tue, 10 Sep 2024 13:42:41 +0200 Subject: [PATCH 64/67] revert check instead of filter --- certora/specs/Escrow.spec | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/certora/specs/Escrow.spec b/certora/specs/Escrow.spec index 133e9f7b..43381d03 100644 --- a/certora/specs/Escrow.spec +++ b/certora/specs/Escrow.spec @@ -183,21 +183,7 @@ rule W2_2_batchesQueueCloseNoChange(method f){ @notice We are filtering out some functions that are not interesting since they cannot successfully be called in a situation where requestNextWithdrawalsBatch makes sense to call. */ -rule W2_2_front_running(method f) filtered { - f -> f.selector != sig:initialize(Durations.Duration).selector - // no longer possible in rage quit state, which we need to be in for requestNextWithdrawalsBatch - && f.selector != sig:unlockUnstETH(uint256[]).selector - && f.selector != sig:lockUnstETH(uint256[]).selector - && f.selector != sig:markUnstETHFinalized(uint256[],uint256[]).selector - && f.selector != sig:unlockStETH().selector - && f.selector != sig:unlockWstETH().selector - && f.selector != sig:lockStETH(uint256).selector - && f.selector != sig:lockWstETH(uint256).selector - && f.selector != sig:requestWithdrawals(uint256[]).selector - && f.selector != sig:startRageQuit(Durations.Duration,Durations.Duration).selector - // only possible after batches queue is already closed - && f.selector != sig:startRageQuitExtensionDelay().selector -} { +rule W2_2_front_running(method f) { storage initial_storage = lastStorage; // set up one run in which requestNextWithdrawalsBatch closes the queue @@ -209,11 +195,12 @@ rule W2_2_front_running(method f) filtered { // if we frontrun something else, at the end it should still be closed calldataarg args; - f(e, args) at initial_storage; + f@withrevert(e, args) at initial_storage; + bool fReverted = lastReverted; requestNextWithdrawalsBatch(e, batchsize); uint stETHRemaining = stEth.balanceOf(currentContract); uint minStETHWithdrawalRequestAmount = withdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT(); - assert stETHRemaining < minStETHWithdrawalRequestAmount => isWithdrawalsBatchesFinalized(); + assert fReverted || stETHRemaining < minStETHWithdrawalRequestAmount => isWithdrawalsBatchesFinalized(); } /** From 6a7f4c767e78a401f85a46043b7dbf42915d0f72 Mon Sep 17 00:00:00 2001 From: Nurit Dor <57101353+nd-certora@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:33:41 +0300 Subject: [PATCH 65/67] review fixes and 2 more solvency rules --- certora/confs/Escrow_solvency.conf | 4 +- certora/helpers/DummyWithdrawalQueue.sol | 6 ++- certora/specs/Escrow.spec | 14 ------ certora/specs/Escrow_solvency.spec | 59 +++++++++++++++++++++- certora/specs/Escrow_validState.spec | 64 +++++------------------- 5 files changed, 76 insertions(+), 71 deletions(-) diff --git a/certora/confs/Escrow_solvency.conf b/certora/confs/Escrow_solvency.conf index 24c405f4..f9acfa34 100644 --- a/certora/confs/Escrow_solvency.conf +++ b/certora/confs/Escrow_solvency.conf @@ -21,9 +21,7 @@ "packages": [ "@openzeppelin=lib/openzeppelin-contracts" ], -// "prover_args": [ -// " -smt_groundQuantifiers false" -// ], + "solc": "solc8.26", "optimistic_loop": true, "optimistic_fallback": true, diff --git a/certora/helpers/DummyWithdrawalQueue.sol b/certora/helpers/DummyWithdrawalQueue.sol index 35438ae1..2eec13a3 100644 --- a/certora/helpers/DummyWithdrawalQueue.sol +++ b/certora/helpers/DummyWithdrawalQueue.sol @@ -15,6 +15,7 @@ contract DummyWithdrawalQueue { uint256 internal lastFinalizedRequestId; mapping(address => uint256) balances; + mapping(address => mapping(address => bool)) public allowance; IStETH public stETH; @@ -38,6 +39,8 @@ contract DummyWithdrawalQueue { bool isClaimed; } + + mapping(uint256 => WithdrawalRequestInfo) public requests; // get the last (exsisting) requestId @@ -87,7 +90,8 @@ contract DummyWithdrawalQueue { } function transferFrom(address from, address to, uint256 requestId) external { - require (requests[requestId].owner == from && balances[from] >= 1); + require (requests[requestId].owner == from && balances[from] >= 1 ); + require (allowance[from][msg.sender] || msg.sender == from); requests[requestId].owner = to; balances[from] = balances[from] -1; balances[to] = balances[to] -1; diff --git a/certora/specs/Escrow.spec b/certora/specs/Escrow.spec index 33461466..8cdee710 100644 --- a/certora/specs/Escrow.spec +++ b/certora/specs/Escrow.spec @@ -201,20 +201,6 @@ rule W2_2_batchesQueueCloseNoChange(method f){ } -/** -W1-1 Evading Ragequit second seal: - -@title when in ragequit, the ragequit support is at least SECOND_SEAL_RAGE_QUIT_SUPPORT - -**/ - -invariant W1_1_rageQuitSupportMinValue() - isRageQuitState() => getRageQuitSupport() <= config.SECOND_SEAL_RAGE_QUIT_SUPPORT - // startRageQuit is only called from DualGoverance (rule ragequitStarter) - // and those functions are checked through dual governance - filtered { f-> f.selector != sig:startRageQuit(Durations.Duration, Durations.Duration).selector} - - /** @title Rage quit support value The rage quit support of an escrow is equal to: diff --git a/certora/specs/Escrow_solvency.spec b/certora/specs/Escrow_solvency.spec index 6b2e5193..930b766c 100644 --- a/certora/specs/Escrow_solvency.spec +++ b/certora/specs/Escrow_solvency.spec @@ -46,7 +46,7 @@ rule solvency_ETH_before_rageQuit(method f) /** @title Total holding of eth by the escrow: 1. For the locked shares: - calimedETH * sumStETHLockedShares / stETHTotals.lockedShares + claimedETH * sumStETHLockedShares / stETHTotals.lockedShares where sumStETHLockedShares is the current holding of shares 2. For the unstEth holding: @@ -58,8 +58,9 @@ rule solvency_ETH_before_rageQuit(method f) **/ invariant solvency_ETH() // pool of batch queue + // if lockedShares is zero than this pool is zero (to avoid divide by zero) (( (currentContract._batchesQueue.info.totalUnstETHIdsCount!= 0 && currentContract._accounting.stETHTotals.lockedShares!=0) ? (currentContract._accounting.stETHTotals.claimedETH * sumStETHLockedShares /currentContract._accounting.stETHTotals.lockedShares ) : 0 ) - // unstEth pool + // unstEth pool: the all claimed unsteth records minus those withdrawn already + (sumClaimedUnSTEth - sumWithdrawnUnSTEth) <= nativeBalances[currentContract] @@ -83,6 +84,60 @@ invariant solvency_ETH() } } +/** @title Those request id left to claim are indeed not claimed +**/ +invariant solvency_batchesQueue_solvent_leftToClaim(uint256 index, uint256 id) + (( index > 0 && index < currentContract._batchesQueue.batches.length && id >= currentContract._batchesQueue.batches[index].firstUnstETHId && + id <= currentContract._batchesQueue.batches[index].lastUnstETHId && (!withdrawalQueue.requests[id].isClaimed)) => + ( currentContract._batchesQueue.info.totalUnstETHIdsCount - currentContract._batchesQueue.info.totalUnstETHIdsClaimed >= + // all indexes unclaimed completely (at least one element to claim) + (countOFBatchIds[currentContract._batchesQueue.batches.length] - countOFBatchIds[index]) - + // claimed in this index + ( id - (currentContract._batchesQueue.batches[index].firstUnstETHId)) ) + ) + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + requireInvariant solvency_batchesQueue_allClaimed(index, id); + assumingThreeOnly(); + } + } + +/** @title when all nft are claimed, the last one has been claimed **/ +invariant solvency_batchesQueue_allClaimed(uint256 index, uint256 id) + + // isAllBatchesClaimed and there are batches queues + (currentContract._batchesQueue.batches.length > 1 => + (isAllStEthNFTClaimed() => withdrawalQueue.requests[currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId].isClaimed + )) + && + ( currentContract._batchesQueue.info.lastClaimedBatchIndex < currentContract._batchesQueue.batches.length || + (currentContract._batchesQueue.info.lastClaimedBatchIndex==0 && currentContract._batchesQueue.batches.length==0 ) + ) + && ( currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex <= currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId -currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].firstUnstETHId) + && + ( getRageQuitExtensionDelayStartedAt() > 0 => + (( + withdrawalQueue.requests[currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId].isFinalized ) + && + ( currentContract._batchesQueue.info.totalUnstETHIdsClaimed == currentContract._batchesQueue.info.totalUnstETHIdsCount && + isBatchQueueStateClosed() ) + ) + ) + + + filtered { f -> f.contract != stEth && f.contract != wst_eth} { + preserved with (env e) { + // no dynamic call so Escrow + require e.msg.sender != currentContract; + validState(); + requireInvariant solvency_batchesQueue_solvent_leftToClaim(index, id); + assumingThreeOnly(); + } + } + diff --git a/certora/specs/Escrow_validState.spec b/certora/specs/Escrow_validState.spec index b0ababbb..24a5ae4b 100644 --- a/certora/specs/Escrow_validState.spec +++ b/certora/specs/Escrow_validState.spec @@ -303,12 +303,17 @@ invariant validState_batchesQueue_ordering() } /** @title Validity of batch queue ids: - The last id in the last entry is le than the lastRequestId in withdrawal queue + 1. The last id in the last entry is le than the lastRequestId in withdrawal queue + 2. Escrow is the owner of the listed ids @notice Given the proof that all indexes are ordered, this implies that all ids are le lastRequestId **/ invariant validState_batchesQueue_withdrawalQueue() // last element is le withdrawalQueue.lastRequestId (currentContract._batchesQueue.batches.length >= 1 => currentContract._batchesQueue.batches[require_uint256(currentContract._batchesQueue.batches.length - 1)].lastUnstETHId <= withdrawalQueue.lastRequestId) + && ( forall uint256 index. forall uint256 id. + ( ( index < ghostLengthMirror && index != 0 && + currentContract._batchesQueue.batches[index].firstUnstETHId <= id && currentContract._batchesQueue.batches[index].lastUnstETHId >= id )) => withdrawalQueue.requests[id].owner == currentContract) + && (forall address any. (!withdrawalQueue.allowance[currentContract][any])) filtered { f -> f.contract != stEth && f.contract != wst_eth} { preserved with (env e) { // no dynamic call so Escrow @@ -323,13 +328,13 @@ invariant validState_batchesQueue_distinct_unstETHRecords( ) // if id is an unsetEth ( to_mathint(currentContract._accounting.unstETHRecords[id].status) != 0 => // then id is a valid one in withdrawalQueue - id <= withdrawalQueue.lastRequestId ) + (id <= withdrawalQueue.lastRequestId && withdrawalQueue.requests[id].owner == currentContract ) && // and id is an unsetEth and there are batch queues ( ( to_mathint(currentContract._accounting.unstETHRecords[id].status) != 0 && currentContract._batchesQueue.batches.length > 0 ) => // then id has to be le than the batch queues ids (we check the first as the rest are monotonic increasing) - id <= currentContract._batchesQueue.batches[0].firstUnstETHId )) + id <= currentContract._batchesQueue.batches[0].firstUnstETHId ))) filtered { f -> f.contract != stEth && f.contract != wst_eth} { preserved with (env e) { @@ -410,6 +415,10 @@ invariant valid_batchIndex() ( currentContract._batchesQueue.info.lastClaimedBatchIndex < currentContract._batchesQueue.batches.length || (currentContract._batchesQueue.info.lastClaimedBatchIndex==0 && currentContract._batchesQueue.batches.length==0 ) ) + && + ( currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex <= currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId - currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].firstUnstETHId + ) + && ghostLengthMirror == currentContract._batchesQueue.batches.length filtered { f -> f.contract != stEth && f.contract != wst_eth} { preserved with (env e) { // no dynamic call so Escrow @@ -430,7 +439,7 @@ invariant validState_batchesQueue_claimed_vs_actual_1(uint256 index, uint256 id) ( index < currentContract._batchesQueue.info.lastClaimedBatchIndex || ( index == currentContract._batchesQueue.info.lastClaimedBatchIndex && id <= currentContract._batchesQueue.batches[index].firstUnstETHId + currentContract._batchesQueue.info.lastClaimedUnstETHIdIndex) ) - => withdrawalQueue.requests[id].isClaimed + <=> withdrawalQueue.requests[id].isClaimed ) ) filtered { f -> f.contract != stEth && f.contract != wst_eth} { @@ -598,50 +607,3 @@ function assumingThreeOnly() { to_mathint(currentContract._accounting.unstETHRecords[any].status) == 0 ); } -/** Extra rules **/ -/* -invariant validState_batchesQueue_solvent_leftToClaim(uint256 index, uint256 id) - (( index > 0 && index < currentContract._batchesQueue.batches.length && id >= currentContract._batchesQueue.batches[index].firstUnstETHId && - id <= currentContract._batchesQueue.batches[index].lastUnstETHId && (!withdrawalQueue.requests[id].isClaimed)) => - ( currentContract._batchesQueue.info.totalUnstETHIdsCount - currentContract._batchesQueue.info.totalUnstETHIdsClaimed >= (countOFBatchIds[currentContract._batchesQueue.batches.length] - countOFBatchIds[index]) - ( id - (currentContract._batchesQueue.batches[index].firstUnstETHId + 1)) ) - ) -filtered { f -> f.contract != stEth && f.contract != wst_eth} { - preserved with (env e) { - // no dynamic call so Escrow - require e.msg.sender != currentContract; - validState(); - requireInvariant validState_batchesQueue_solvent_allClaimed(index, id); - } - } - - -invariant validState_batchesQueue_solvent_allClaimed(uint256 index, uint256 id) - - // isAllBatchesClaimed and there are batches queues - (currentContract._batchesQueue.batches.length > 1 => - (isAllStEthNFTClaimed() => withdrawalQueue.requests[currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId].isClaimed - )) - && - ( currentContract._batchesQueue.info.lastClaimedBatchIndex < currentContract._batchesQueue.batches.length || - (currentContract._batchesQueue.info.lastClaimedBatchIndex==0 && currentContract._batchesQueue.batches.length==0 ) - ) - && - ( getRageQuitExtensionDelayStartedAt() > 0 => - (( - withdrawalQueue.requests[currentContract._batchesQueue.batches[currentContract._batchesQueue.info.lastClaimedBatchIndex].lastUnstETHId].isFinalized ) - && - ( currentContract._batchesQueue.info.totalUnstETHIdsClaimed == currentContract._batchesQueue.info.totalUnstETHIdsCount && - isBatchQueueStateClosed() ) - ) - ) - - - filtered { f -> f.contract != stEth && f.contract != wst_eth} { - preserved with (env e) { - // no dynamic call so Escrow - require e.msg.sender != currentContract; - validState(); - requireInvariant validState_batchesQueue_solvent_leftToClaim(index, id); - } - } -**/ From 7730d7a4c4f9f54da5aa8b2eecee39b0ffbdc6f3 Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Thu, 10 Oct 2024 10:27:37 +0100 Subject: [PATCH 66/67] Add README.md, delete outdated run scripts --- certora/README.md | 25 +++++++++++++++++++++++++ certora/scripts/runAllSetupConfs.py | 27 --------------------------- 2 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 certora/README.md delete mode 100644 certora/scripts/runAllSetupConfs.py diff --git a/certora/README.md b/certora/README.md new file mode 100644 index 00000000..f414829c --- /dev/null +++ b/certora/README.md @@ -0,0 +1,25 @@ +# Overview and Directory Structure +This directory contains formal verification specifications for the Certora +Prover and written in the Certora Verification Language (CVL). The subdirectory +contents are as follows: + * confs -- contains configuration files to run the verification jobs + * harness -- contains test harnesses to help with verification, and mock + versions of ERC20 contracts that are relevant to but not part of this solidity project + * mutation -- contains mutation tests that we used to gain further assurance + about our specifications + * helpers -- contains a mock WithdrawalQueue and two simple contracts that inherit from Escrow + to alow us to model multiple distinct Escrow addresses +* specs -- contains our formal verification specifications + +# Run instructions +Ensure you have installed the Certora Prover. These specifications were tested with +`certora-cli 7.17.2`. Launch each of the verification jobs from the root directory of the project with +`certoraRun certora/confs/DualGovernance.conf` +`certoraRun certora/confs/EmergencyProtectedTimelock.conf` +`certoraRun certora/confs/Escrow.conf` +`certoraRun certora/confs/Escrow_solvency.conf` +`certoraRun certora/confs/Escrow_validState.conf` + +Note that sum of the specifictations in `Escrow.spec` are difficult for the tool and will hit timeouts +unless they are run separately. As a result some of the rules launched by `Escrow.conf` will timeout, +but these will pass when they are run separately with `Escrow_solvency.conf` and `Escrow_validState.conf`. \ No newline at end of file diff --git a/certora/scripts/runAllSetupConfs.py b/certora/scripts/runAllSetupConfs.py deleted file mode 100644 index 5ef359ca..00000000 --- a/certora/scripts/runAllSetupConfs.py +++ /dev/null @@ -1,27 +0,0 @@ -import argparse -import subprocess - -parser = argparse.ArgumentParser() -parser.add_argument('-M', '--batchMsg', metavar='M', type=str, nargs='?', - default='', - help='a message for all the jobs') - -setup_confs = { - "DualGovernance", - "EmergencyActivationCommittee", - "EmergencyExecutionCommittee", - "EmergencyProtectedTimelock", - "Escrow", - "Executor", - "ResealManager", - "TiebreakerCore", - "TiebreakerSubCommittee" -} - -for name in setup_confs: - args = parser.parse_args() - script = f"certora/confs/{name}_sanity.conf" - command = f"certoraRun {script} --msg \"{name} : {args.batchMsg}\"" - print(f"runing {command}") - subprocess.run(command, shell=True) - From c00145f324e4c52c9f6008d546f533bc88f1a0cb Mon Sep 17 00:00:00 2001 From: Andrew Ferraiuolo Date: Thu, 10 Oct 2024 11:51:28 +0100 Subject: [PATCH 67/67] Update README --- certora/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/certora/README.md b/certora/README.md index f414829c..400c0d71 100644 --- a/certora/README.md +++ b/certora/README.md @@ -20,6 +20,6 @@ Ensure you have installed the Certora Prover. These specifications were tested w `certoraRun certora/confs/Escrow_solvency.conf` `certoraRun certora/confs/Escrow_validState.conf` -Note that sum of the specifictations in `Escrow.spec` are difficult for the tool and will hit timeouts -unless they are run separately. As a result some of the rules launched by `Escrow.conf` will timeout, -but these will pass when they are run separately with `Escrow_solvency.conf` and `Escrow_validState.conf`. \ No newline at end of file +One of the rules in Escrow_solvency.conf `solvency_ETH` can have performance issues resulting in +a timeout. As a workaround, it can be run separately by running +`certoraRun certora/confs/Escrow_solvency.conf --rule solvency_ETH` in which case it should pass. \ No newline at end of file