diff --git a/.github/workflows/certora-review-execution-chain.yml b/.github/workflows/certora-review-execution-chain.yml new file mode 100644 index 0000000..4099c93 --- /dev/null +++ b/.github/workflows/certora-review-execution-chain.yml @@ -0,0 +1,66 @@ +name: certora-review-execution-chain + +on: + push: + branches: + - main + pull_request: + branches: + - main + + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Install python + uses: actions/setup-python@v2 + with: { python-version: 3.9 } + + - name: Install java + uses: actions/setup-java@v1 + with: { java-version: "11", java-package: jre } + + - name: Install certora cli + run: pip3 install certora-cli + + - name: Install solc + run: | + wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux + chmod +x solc-static-linux + sudo mv solc-static-linux /usr/local/bin/solc8.19 + + - name: Verify rule ${{ matrix.rule }} + run: | + certoraRun security/certora/confs/payloads/${{ matrix.rule }} + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} + + strategy: + fail-fast: false + max-parallel: 16 + matrix: + rule: + - verifyPayloadsController.conf --rule payload_maximal_access_level_gt_action_access_level no_late_cancel state_cant_decrease no_transition_beyond_state_gt_3 no_transition_beyond_state_variable_gt_3 no_queue_after_expiration decode2encode_sanity_check_message_leq_96_pass decode2encode_sanity_check_message_eq_96_satisfy empty_actions_if_out_of_bound_payload expirationTime_equal_to_createAt_plus_EXPIRATION_DELAY empty_actions_iff_uninitialized null_access_level_if_out_of_bound_payload null_creator_and_zero_expiration_time_if_out_of_bound_payload empty_actions_only_if_uninitialized_payload executor_access_level_within_range consecutiveIDs empty_actions_if_uninitialized_payload queued_before_expiration_delay payload_grace_period_eq_global_grace_period null_access_level_only_if_out_of_bound_payload null_state_variable_if_out_of_bound_payload created_in_the_past executedAt_is_zero_before_executed queued_after_created executed_after_queue queuedAt_is_zero_before_queued no_early_cancellation guardian_can_cancel executed_when_in_queued_state execute_before_delay__maximumAccessLevelRequired action_immutable_fixed_size_fields initialized_payload_fields_are_immutable payload_fields_immutable_after_createPayload + - verifyPayloadsController.conf --rule executor_exists + - verifyPayloadsController.conf --rule executor_exists_if_action_not_null + - verifyPayloadsController.conf --rule executor_exists_only_if_action_not_null + - verifyPayloadsController.conf --rule payload_delay_within_range + - verifyPayloadsController.conf --rule delay_of_executor_of_max_access_level_within_range + - verifyPayloadsController.conf --rule nonempty_actions + - verifyPayloadsController.conf --rule executor_exists_iff_action_not_null + - verifyPayloadsController.conf --rule null_access_level_iff_state_is_none + - verifyPayloadsController.conf --rule executor_of_maximumAccessLevelRequired_exists + - verifyPayloadsController.conf --rule executor_of_maximumAccessLevelRequired_exists_after_createPayload + - verifyPayloadsController.conf --rule action_access_level_isnt_null_after_createPayload + - verifyPayloadsController.conf --rule executor_exists_after_createPayload + - verifyPayloadsController.conf --rule action_callData_immutable + - verifyPayloadsController.conf --rule action_signature_immutable + - verifyPayloadsController.conf --rule action_immutable_check_only_fixed_size_fields + \ No newline at end of file diff --git a/.github/workflows/certora-review-mainnet.yml b/.github/workflows/certora-review-mainnet.yml new file mode 100644 index 0000000..405b0be --- /dev/null +++ b/.github/workflows/certora-review-mainnet.yml @@ -0,0 +1,66 @@ +# Github action for verifying the contracts under src/contracts/voting +name: certora-review-mainnet + +on: + push: + branches: + - main + pull_request: + branches: + - main + + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Install python + uses: actions/setup-python@v2 + with: { python-version: 3.9 } + + - name: Install java + uses: actions/setup-java@v1 + with: { java-version: "11", java-package: jre } + + - name: Install certora cli + run: pip3 install certora-cli + + - name: Install solc + run: | + wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux + chmod +x solc-static-linux + sudo mv solc-static-linux /usr/local/bin/solc8.19 + + - name: Verify rule ${{ matrix.rule }} + run: | + certoraRun security/certora/confs/${{ matrix.rule }} + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} + + strategy: + fail-fast: false + max-parallel: 16 + matrix: + rule: + - verifyVotingStrategy_unittests.conf + - verifyGovernancePowerStrategy.conf + - verifyGovernance.conf --rule cancellationFeeZeroForFutureProposals null_state_variable_iff_null_access_level zero_voting_portal_iff_uninitialized_proposal + - verifyGovernance.conf --rule no_self_representative no_representative_is_zero consecutiveIDs totalCancellationFeeEqualETHBalance zero_address_is_not_a_valid_voting_portal + - verifyGovernance.conf --rule no_representative_is_zero_2 no_representative_of_zero empty_payloads_if_uninitialized_proposal null_state_variable_only_if_uninitialized_proposal + - verifyGovernance.conf --rule post_state state_changing_function_self_check state_variable_changing_function_self_check initialize_sanity sanity userFeeDidntChangeImplyNativeBalanceDidntDecrease + - verifyGovernance.conf --rule check_new_representative_set_size_after_updateRepresentatives check_old_representative_set_size_after_updateRepresentatives set_size_leq_max_uint160 + - verifyGovernance.conf --rule at_least_single_payload_active at_least_single_payload_active_variable creator_is_not_zero creator_is_not_zero_2 empty_payloads_iff_uninitialized_proposal + - verifyGovernance.conf --rule null_state_iff_uninitialized_proposal null_state_variable_iff_uninitialized_proposal null_state_if_uninitialized_proposal null_state_variable_if_uninitialized_proposal + - verifyGovernance.conf --rule null_state_only_if_uninitialized_proposal pre_state terminal_state_cannot_change state_changing_function_cannot_be_called_while_in_terminal_state proposal_executes_after_cooldown_period + - verifyGovernance.conf --rule only_valid_voting_portal_can_queue_proposal immutable_after_activation immutable_payload_after_creation immutable_after_creation only_guardian_can_cancel guardian_can_cancel + - verifyGovernance.conf --rule cannot_queue_when_voting_portal_unapproved only_owner_can_set_voting_config_witness only_owner_can_set_voting_config single_state_transition_per_block_non_creator_witness + - verifyGovernance.conf --rule single_state_transition_per_block_non_creator_non_guardian state_cant_decrease no_state_transitions_beyond_3 immutable_voting_portal insufficient_proposition_power_time_elapsed_tight_witness + - verifyGovernance.conf --rule insufficient_proposition_power_allow_time_elapse insufficient_proposition_power proposal_after_voting_portal_invalidate + \ No newline at end of file diff --git a/.github/workflows/certora-review-voting-chain.yml b/.github/workflows/certora-review-voting-chain.yml new file mode 100644 index 0000000..c600064 --- /dev/null +++ b/.github/workflows/certora-review-voting-chain.yml @@ -0,0 +1,57 @@ +# Github action for verifying the contracts under src/contracts/voting +name: certora-review-voting-chain + +on: + push: + branches: + - main + pull_request: + branches: + - main + + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Install python + uses: actions/setup-python@v2 + with: { python-version: 3.9 } + + - name: Install java + uses: actions/setup-java@v1 + with: { java-version: "11", java-package: jre } + + - name: Install certora cli + run: pip3 install certora-cli + + - name: Install solc + run: | + wget https://github.com/ethereum/solidity/releases/download/v0.8.19/solc-static-linux + chmod +x solc-static-linux + sudo mv solc-static-linux /usr/local/bin/solc8.19 + + - name: Verify rule ${{ matrix.rule }} + run: | + certoraRun security/certora/confs/voting/${{ matrix.rule }} + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} + + strategy: + fail-fast: false + max-parallel: 16 + matrix: + rule: + - verifyLegality.conf + - verifyMisc.conf + - verifyPower_summary.conf + - verifyProposal_config.conf + - verifyProposal_states.conf + - verifyVoting_and_tally.conf diff --git a/security/certora/confs/payloads/verifyPayloadsController.conf b/security/certora/confs/payloads/verifyPayloadsController.conf new file mode 100644 index 0000000..6da1a24 --- /dev/null +++ b/security/certora/confs/payloads/verifyPayloadsController.conf @@ -0,0 +1,25 @@ +{ + "files": [ + "security/certora/harness/PayloadsControllerHarness.sol", + "src/contracts/payloads/Executor.sol", + "src/contracts/payloads/PayloadsControllerUtils.sol", + "security/certora/harness/DummyERC20Impl.sol" + ], + "packages": [ + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "solidity-utils=lib/solidity-utils/src" + ], + "loop_iter": "3", + "msg": "All payloadControllers rules", + "optimistic_hashing": true, + "optimistic_loop": true, + "prover_args": [ + " -smt_LIASolvers [z3:def,z3:lia1,z3:lia2] -smt_NIASolvers [z3:def]" + ], + "smt_timeout": "6000", + "solc": "solc8.19", + "struct_link": [ + "PayloadsControllerHarness:executor=Executor" + ], + "verify": "PayloadsControllerHarness:security/certora/specs/payloads/PayloadsController.spec" +} \ No newline at end of file diff --git a/security/certora/confs/verifyGovernance.conf b/security/certora/confs/verifyGovernance.conf new file mode 100644 index 0000000..66e8a92 --- /dev/null +++ b/security/certora/confs/verifyGovernance.conf @@ -0,0 +1,31 @@ +{ + "files": [ + "security/certora/harness/GovernanceHarness.sol", + "src/contracts/VotingPortal.sol", + "src/contracts/voting/VotingStrategy.sol", + "lib/aave-token-v3/src/AaveTokenV3.sol", + "src/contracts/GovernancePowerStrategy.sol", + "src/contracts/payloads/PayloadsControllerUtils.sol" + ], + "link": [ + "GovernanceHarness:_powerStrategy=GovernancePowerStrategy" + ], + "packages": [ + "aave-address-book=lib/aave-address-book/src", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v3=lib/aave-token-v3/src", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "GovernanceHarness:security/certora/specs/Governance.spec", + "struct_link": [ + "GovernanceHarness:votingPortal=VotingPortal" + ], + "loop_iter": "3", + "optimistic_loop": true, + "prover_args": [ + " -copyLoopUnroll 8" + ], + "solc": "solc8.19", + "msg": "All Governance rules", +} \ No newline at end of file diff --git a/security/certora/confs/verifyGovernancePowerStrategy.conf b/security/certora/confs/verifyGovernancePowerStrategy.conf new file mode 100644 index 0000000..28e37d5 --- /dev/null +++ b/security/certora/confs/verifyGovernancePowerStrategy.conf @@ -0,0 +1,23 @@ +{ + "files": [ + "src/contracts/GovernancePowerStrategy.sol", + "security/certora/harness/tokens/AaveTokenV3_DummyA.sol", + "security/certora/harness/tokens/AaveTokenV3_DummyB.sol", + "security/certora/harness/tokens/AaveTokenV3_DummyC.sol" + ], + "link": [ + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "GovernancePowerStrategy:security/certora/specs/GovernancePowerStrategy.spec", + "optimistic_loop": true, + "loop_iter": "3", // Needs 3 for the 3 tokens + "solc": "solc8.19", + "msg": "GovernancePowerStrategy tests" +} diff --git a/security/certora/confs/verifyVotingStrategy_unittests.conf b/security/certora/confs/verifyVotingStrategy_unittests.conf new file mode 100644 index 0000000..cdd50af --- /dev/null +++ b/security/certora/confs/verifyVotingStrategy_unittests.conf @@ -0,0 +1,27 @@ +{ + "files": [ + "src/contracts/voting/VotingStrategy.sol", + "src/contracts/voting/DataWarehouse.sol", + "security/certora/harness/voting/DelegationModeHarness.sol" + ], + "link": [ + "VotingStrategy:DATA_WAREHOUSE=DataWarehouse" + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v2=lib/aave-token-v3/lib/aave-token-v2/contracts", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "hyperlane-monorepo=lib/aave-delivery-infrastructure/lib/hyperlane-monorepo/solidity", + "openzeppelin-contracts=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "solidity-examples=lib/aave-delivery-infrastructure/lib/solidity-examples/contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "VotingStrategy:security/certora/specs/VotingStrategy_unittests.spec", + "optimistic_loop": true, + "loop_iter": "2", // Needs 2 for isTokenSlotAccepted (A_AAVE uses 2 slots) + "solc": "solc8.19", + "msg": "VotingStrategy tests" +} + \ No newline at end of file diff --git a/security/certora/confs/voting/verifyLegality.conf b/security/certora/confs/voting/verifyLegality.conf new file mode 100644 index 0000000..ac5dbe6 --- /dev/null +++ b/security/certora/confs/voting/verifyLegality.conf @@ -0,0 +1,34 @@ +// conf file for VotingMachine - legality.spec +{ + "files": [ + "security/certora/harness/voting/VotingMachineHarness.sol", + "security/certora/harness/voting/VotingStrategyHarness.sol", + "src/contracts/voting/DataWarehouse.sol", + "src/contracts/voting/libs/StateProofVerifier.sol", + "src/contracts/libraries/SlotUtils.sol", + "lib/aave-delivery-infrastructure/src/contracts/CrossChainController.sol" + ], + "link": [ + "VotingMachineHarness:VOTING_STRATEGY=VotingStrategyHarness", + "VotingMachineHarness:CROSS_CHAIN_CONTROLLER=CrossChainController", + "VotingMachineHarness:DATA_WAREHOUSE=DataWarehouse", // NOTE: same as in VotingStrategy + "VotingStrategyHarness:DATA_WAREHOUSE=DataWarehouse" + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v2=lib/aave-token-v3/lib/aave-token-v2/contracts", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "hyperlane-monorepo=lib/aave-delivery-infrastructure/lib/hyperlane-monorepo/solidity", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-examples=lib/aave-delivery-infrastructure/lib/solidity-examples/contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "VotingMachineHarness:security/certora/specs/voting/legality.spec", + "optimistic_loop": true, + "loop_iter": "1", + "optimistic_hashing": true, + "solc": "solc8.19", + "msg": "VotingMachine - legality rules" +} diff --git a/security/certora/confs/voting/verifyMisc.conf b/security/certora/confs/voting/verifyMisc.conf new file mode 100644 index 0000000..f6e895d --- /dev/null +++ b/security/certora/confs/voting/verifyMisc.conf @@ -0,0 +1,34 @@ +// conf file for VotingMachine - misc.spec +{ + "files": [ + "security/certora/harness/voting/VotingMachineHarnessTriple.sol", + "security/certora/harness/voting/VotingStrategyHarness.sol", + "src/contracts/voting/DataWarehouse.sol", + "src/contracts/voting/libs/StateProofVerifier.sol", + "src/contracts/libraries/SlotUtils.sol", + "lib/aave-delivery-infrastructure/src/contracts/CrossChainController.sol" + ], + "link": [ + "VotingMachineHarnessTriple:VOTING_STRATEGY=VotingStrategyHarness", + "VotingMachineHarnessTriple:CROSS_CHAIN_CONTROLLER=CrossChainController", + "VotingMachineHarnessTriple:DATA_WAREHOUSE=DataWarehouse", // NOTE: same as in VotingStrategy + "VotingStrategyHarness:DATA_WAREHOUSE=DataWarehouse" + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v2=lib/aave-token-v3/lib/aave-token-v2/contracts", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "hyperlane-monorepo=lib/aave-delivery-infrastructure/lib/hyperlane-monorepo/solidity", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-examples=lib/aave-delivery-infrastructure/lib/solidity-examples/contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "VotingMachineHarnessTriple:security/certora/specs/voting/misc.spec", + "optimistic_loop": true, + "loop_iter": "3", // Needs 3 for `submitVoteTripleProof` + "optimistic_hashing": true, + "solc": "solc8.19", + "msg": "VotingMachine - miscellaneous rules" +} diff --git a/security/certora/confs/voting/verifyPower_summary.conf b/security/certora/confs/voting/verifyPower_summary.conf new file mode 100644 index 0000000..0c9f911 --- /dev/null +++ b/security/certora/confs/voting/verifyPower_summary.conf @@ -0,0 +1,34 @@ +// conf file for VotingMachine setup +{ + "files": [ + "security/certora/harness/voting/VotingMachineHarnessTriple.sol", + "security/certora/harness/voting/VotingStrategyHarness.sol", + "src/contracts/voting/DataWarehouse.sol", + "src/contracts/voting/libs/StateProofVerifier.sol", + "src/contracts/libraries/SlotUtils.sol", + "lib/aave-delivery-infrastructure/src/contracts/CrossChainController.sol" + ], + "link": [ + "VotingMachineHarnessTriple:VOTING_STRATEGY=VotingStrategyHarness", + "VotingMachineHarnessTriple:CROSS_CHAIN_CONTROLLER=CrossChainController", + "VotingMachineHarnessTriple:DATA_WAREHOUSE=DataWarehouse", // NOTE: same as in VotingStrategy + "VotingStrategyHarness:DATA_WAREHOUSE=DataWarehouse" + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v2=lib/aave-token-v3/lib/aave-token-v2/contracts", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "hyperlane-monorepo=lib/aave-delivery-infrastructure/lib/hyperlane-monorepo/solidity", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-examples=lib/aave-delivery-infrastructure/lib/solidity-examples/contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "VotingMachineHarnessTriple:security/certora/specs/voting/power_summary.spec", + "optimistic_loop": true, + "loop_iter": "3", // 3 is needed for `submitVoteTripleProof`; 2 for `isTokenSlotAccepted` (A_AAVE uses two slots) + "optimistic_hashing": true, + "solc": "solc8.19", + "msg": "VotingMachine setup - basic tests" +} diff --git a/security/certora/confs/voting/verifyProposal_config.conf b/security/certora/confs/voting/verifyProposal_config.conf new file mode 100644 index 0000000..286dc64 --- /dev/null +++ b/security/certora/confs/voting/verifyProposal_config.conf @@ -0,0 +1,34 @@ +// conf file for VotingMachine - proposal_config.spec +{ + "files": [ + "security/certora/harness/voting/VotingMachineHarness.sol", + "security/certora/harness/voting/VotingStrategyHarness.sol", + "src/contracts/voting/DataWarehouse.sol", + "src/contracts/voting/libs/StateProofVerifier.sol", + "src/contracts/libraries/SlotUtils.sol", + "lib/aave-delivery-infrastructure/src/contracts/CrossChainController.sol" + ], + "link": [ + "VotingMachineHarness:VOTING_STRATEGY=VotingStrategyHarness", + "VotingMachineHarness:CROSS_CHAIN_CONTROLLER=CrossChainController", + "VotingMachineHarness:DATA_WAREHOUSE=DataWarehouse", // NOTE: same as in VotingStrategy + "VotingStrategyHarness:DATA_WAREHOUSE=DataWarehouse" + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v2=lib/aave-token-v3/lib/aave-token-v2/contracts", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "hyperlane-monorepo=lib/aave-delivery-infrastructure/lib/hyperlane-monorepo/solidity", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-examples=lib/aave-delivery-infrastructure/lib/solidity-examples/contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "VotingMachineHarness:security/certora/specs/voting/proposal_config.spec", + "optimistic_loop": true, + "loop_iter": "1", + "optimistic_hashing": true, + "solc": "solc8.19", + "msg": "VotingMachine - proposal configuration" +} diff --git a/security/certora/confs/voting/verifyProposal_states.conf b/security/certora/confs/voting/verifyProposal_states.conf new file mode 100644 index 0000000..cfc4693 --- /dev/null +++ b/security/certora/confs/voting/verifyProposal_states.conf @@ -0,0 +1,34 @@ +// conf file for VotingMachine - proposal_states.spec +{ + "files": [ + "security/certora/harness/voting/VotingMachineHarness.sol", + "security/certora/harness/voting/VotingStrategyHarness.sol", + "src/contracts/voting/DataWarehouse.sol", + "src/contracts/voting/libs/StateProofVerifier.sol", + "src/contracts/libraries/SlotUtils.sol", + "lib/aave-delivery-infrastructure/src/contracts/CrossChainController.sol" + ], + "link": [ + "VotingMachineHarness:VOTING_STRATEGY=VotingStrategyHarness", + "VotingMachineHarness:CROSS_CHAIN_CONTROLLER=CrossChainController", + "VotingMachineHarness:DATA_WAREHOUSE=DataWarehouse", // NOTE: same as in VotingStrategy + "VotingStrategyHarness:DATA_WAREHOUSE=DataWarehouse" + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v2=lib/aave-token-v3/lib/aave-token-v2/contracts", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "hyperlane-monorepo=lib/aave-delivery-infrastructure/lib/hyperlane-monorepo/solidity", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-examples=lib/aave-delivery-infrastructure/lib/solidity-examples/contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "VotingMachineHarness:security/certora/specs/voting/proposal_states.spec", + "optimistic_loop": true, + "loop_iter": "1", + "optimistic_hashing": true, + "solc": "solc8.19", + "msg": "VotingMachine - proposal states" +} diff --git a/security/certora/confs/voting/verifyVoting_and_tally.conf b/security/certora/confs/voting/verifyVoting_and_tally.conf new file mode 100644 index 0000000..91a6814 --- /dev/null +++ b/security/certora/confs/voting/verifyVoting_and_tally.conf @@ -0,0 +1,34 @@ +// conf file for VotingMachine - voting_and_tally.spec +{ + "files": [ + "security/certora/harness/voting/VotingMachineHarness.sol", + "security/certora/harness/voting/VotingStrategyHarness.sol", + "src/contracts/voting/DataWarehouse.sol", + "src/contracts/voting/libs/StateProofVerifier.sol", + "src/contracts/libraries/SlotUtils.sol", + "lib/aave-delivery-infrastructure/src/contracts/CrossChainController.sol" + ], + "link": [ + "VotingMachineHarness:VOTING_STRATEGY=VotingStrategyHarness", + "VotingMachineHarness:CROSS_CHAIN_CONTROLLER=CrossChainController", + "VotingMachineHarness:DATA_WAREHOUSE=DataWarehouse", // NOTE: same as in VotingStrategy + "VotingStrategyHarness:DATA_WAREHOUSE=DataWarehouse" + ], + "packages": [ + "@openzeppelin=lib/aave-delivery-infrastructure/lib/openzeppelin-contracts", + "aave-delivery-infrastructure=lib/aave-delivery-infrastructure/src", + "aave-token-v2=lib/aave-token-v3/lib/aave-token-v2/contracts", + "aave-token-v3=lib/aave-token-v3/src", + "forge-std=lib/forge-std/src", + "hyperlane-monorepo=lib/aave-delivery-infrastructure/lib/hyperlane-monorepo/solidity", + "openzeppelin-contracts=lib/openzeppelin-contracts", + "solidity-examples=lib/aave-delivery-infrastructure/lib/solidity-examples/contracts", + "solidity-utils=lib/solidity-utils/src" + ], + "verify": "VotingMachineHarness:security/certora/specs/voting/voting_and_tally.spec", + "optimistic_loop": true, + "loop_iter": "1", + "optimistic_hashing": true, + "solc": "solc8.19", + "msg": "VotingMachine - voting and tally rules" +} diff --git a/security/certora/harness/DummyERC20Impl.sol b/security/certora/harness/DummyERC20Impl.sol new file mode 100644 index 0000000..0ff4d92 --- /dev/null +++ b/security/certora/harness/DummyERC20Impl.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8; + +// with mint +contract DummyERC20Impl { + uint256 t; + mapping (address => uint256) b; + mapping (address => mapping (address => uint256)) a; + + string public name; + string public symbol; + uint public decimals; + + function myAddress() public returns (address) { + return address(this); + } + + function add(uint a, uint b) internal pure returns (uint256) { + uint c = a +b; + require (c >= a); + return c; + } + function sub(uint a, uint b) internal pure returns (uint256) { + require (a>=b); + return a-b; + } + + 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] = sub(b[msg.sender], amount); + b[recipient] = add(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] = sub(b[sender], amount); + b[recipient] = add(b[recipient], amount); + a[sender][msg.sender] = sub(a[sender][msg.sender], amount); + return true; + } +} \ No newline at end of file diff --git a/security/certora/harness/GovernanceHarness.sol b/security/certora/harness/GovernanceHarness.sol new file mode 100644 index 0000000..742391c --- /dev/null +++ b/security/certora/harness/GovernanceHarness.sol @@ -0,0 +1,132 @@ + +pragma solidity ^0.8.8; + +import {Governance} from '../../../src/contracts/Governance.sol'; +import {PayloadsControllerUtils} from '../../../src/contracts/payloads/PayloadsControllerUtils.sol'; +import {IGovernanceCore, EnumerableSet} from '../../../src/interfaces/IGovernanceCore.sol'; + + +contract GovernanceHarness is Governance { + using EnumerableSet for EnumerableSet.AddressSet; + + + constructor( + address crossChainController, + uint256 coolDownPeriod, + address cancellationFeeCollector + ) + Governance(crossChainController, coolDownPeriod, cancellationFeeCollector){} + + function getPayloadLength(uint256 proposalId) external view returns (uint256) { + return _proposals[proposalId].payloads.length; + } + + function getProposalStateVariable(uint256 proposalId) external view returns (State) { + return _proposals[proposalId].state; + } + function getProposalVotingPortal(uint256 proposalId) external view returns (address) { + return _proposals[proposalId].votingPortal; + } + + function getProposalAccessLevel(uint256 proposalId) external view returns (PayloadsControllerUtils.AccessControl) { + return _proposals[proposalId].accessLevel; + } + + function getProposalCreator(uint256 proposalId) external view returns (address) { + return _proposals[proposalId].creator;} + + function getProposalVotingDuration(uint256 proposalId) external view returns (uint24) { + return _proposals[proposalId].votingDuration;} + + function getProposalCreationTime(uint256 proposalId) external view returns (uint40) { + return _proposals[proposalId].creationTime;} + + function getProposalIpfsHash(uint256 proposalId) external view returns (bytes32) { + return _proposals[proposalId].ipfsHash;} + + function getProposalVotingActivationTime(uint256 proposalId) external view returns (uint40) { + return _proposals[proposalId].votingActivationTime;} + + function getProposalSnapshotBlockHash(uint256 proposalId) external view returns (bytes32) { + return _proposals[proposalId].snapshotBlockHash;} + + function getProposalCancellationFee(uint256 proposalId) external view returns (uint256) { + return _proposals[proposalId].cancellationFee;} + + function getPayloadChain(uint256 proposalId, uint256 payloadID) external view returns (uint256) { + return _proposals[proposalId].payloads[payloadID].chain; + } + + function getPayloadAccessLevel(uint256 proposalId, uint256 payloadID) external view returns (PayloadsControllerUtils.AccessControl) { + return _proposals[proposalId].payloads[payloadID].accessLevel; + } + function getPayloadPayloadsController(uint256 proposalId, uint256 payloadID) external view returns (address) { + return _proposals[proposalId].payloads[payloadID].payloadsController; + } + + function getPayloadPayloadId(uint256 proposalId, uint256 payloadID) external view returns (uint40) { + return _proposals[proposalId].payloads[payloadID].payloadId; + } + + function getProposalCount() external view returns (uint256) { + return _proposalsCount; + } + + function isRepresentativeOfVoter( + address voter, + address representative, + uint256 chainId + ) external view returns (bool) { + return _votersRepresented[representative][chainId].contains(voter); + } + + function updateSingleRepresentativeForChain( + address representative, uint256 chainId0) external { + + RepresentativeInput memory representativeInput; + representativeInput.representative = representative; + representativeInput.chainId = chainId0; + + RepresentativeInput[] memory representatives = new RepresentativeInput[](1); + representatives[0] = representativeInput; + + + //todo: call as external + //updateRepresentativesForChain(representatives); + + uint256 i = 0; + uint256 chainId = representatives[i].chainId; + address newRepresentative = representatives[i].representative != + msg.sender + ? representatives[i].representative + : address(0); + address oldRepresentative = _representatives[msg.sender][chainId]; + + if (oldRepresentative != address(0)) { + _votersRepresented[oldRepresentative][chainId].remove(msg.sender); + } + + if (newRepresentative != address(0)) { + _votersRepresented[newRepresentative][chainId].add(msg.sender); + } + + _representatives[msg.sender][chainId] = newRepresentative; + + emit RepresentativeUpdated(msg.sender, newRepresentative, chainId); + + } + + + /** + * @notice Returns the size of the voters set of a given representative + */ + function getRepresentedVotersSize( + address representative, + uint256 chainId + ) external view returns (uint256) { + return _votersRepresented[representative][chainId].values().length; + } + + + +} diff --git a/security/certora/harness/PayloadsControllerHarness.sol b/security/certora/harness/PayloadsControllerHarness.sol new file mode 100644 index 0000000..441e617 --- /dev/null +++ b/security/certora/harness/PayloadsControllerHarness.sol @@ -0,0 +1,132 @@ + +pragma solidity ^0.8.8; + +import {PayloadsControllerUtils} from '../../../src/contracts/payloads/PayloadsControllerUtils.sol'; +import {IPayloadsControllerCore, PayloadsControllerUtils} from '../../../src/contracts/payloads/interfaces/IPayloadsControllerCore.sol'; + + +import {PayloadsController} from '../../../src/contracts/payloads/PayloadsController.sol'; + +contract PayloadsControllerHarness is PayloadsController { + +constructor( + address crossChainController, + address messageOriginator, + uint256 originChainId + ) PayloadsController (crossChainController,messageOriginator,originChainId) {} + + + + function getPayloadFieldsById(uint40 payloadId) external view + returns (address,PayloadsControllerUtils.AccessControl,PayloadState,uint40,uint40,uint40,uint40,uint40,uint40,uint40) + { + Payload memory payload = _payloads[payloadId]; + + return (payload.creator, payload.maximumAccessLevelRequired, payload.state, + payload.createdAt, payload.queuedAt, payload.executedAt, payload.cancelledAt, + payload.expirationTime, payload.delay, payload.gracePeriod); + } + + function getCreator(uint40 payloadId) external view returns (address){ + return _payloads[payloadId].creator; + } + + function getExpirationTime(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].expirationTime; + } + + function getPayloadQueuedAtById(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].queuedAt; + } + + function getPayloadExpirationTimeById(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].expirationTime; + } + + function getPayloadGracePeriod(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].gracePeriod; + } + + function getPayloadDelay(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].delay; + } + + function getPayloadCreatedAt(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].createdAt; + } + + function getPayloadQueuedAt(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].queuedAt; + } + + function getPayloadExecutedAt(uint40 payloadId) external view returns (uint40){ + return _payloads[payloadId].executedAt; + } + +function getActionsLength(uint40 payloadId) external view returns (uint256 length) { + return _payloads[payloadId].actions.length; + } + +function getAction(uint40 payloadId, uint256 actionIndex) external view + returns (ExecutionAction memory action) { + return (_payloads[payloadId].actions[actionIndex]); + } + + +function getActionFixedSizeFields(uint40 payloadId, uint256 actionIndex) external view + returns (address, bool, PayloadsControllerUtils.AccessControl, uint256) { + return (_payloads[payloadId].actions[actionIndex].target, + _payloads[payloadId].actions[actionIndex].withDelegateCall, + _payloads[payloadId].actions[actionIndex].accessLevel, + _payloads[payloadId].actions[actionIndex].value); + } + +// function getActionAccessLevel(uint40 payloadId, uint256 actionIndex) external view +// returns (PayloadsControllerUtils.AccessControl) { +// return _payloads[payloadId].actions[actionIndex].accessLevel; +// } + +function getActionAccessLevel(uint40 payloadId, uint256 actionIndex) external view returns (PayloadsControllerUtils.AccessControl) { + return (_payloads[payloadId].actions[actionIndex].accessLevel); + } + +function getActionSignature(uint40 payloadId, uint256 actionIndex) external view returns (string memory) { + return (_payloads[payloadId].actions[actionIndex].signature); + } +function getActionCallData(uint40 payloadId, uint256 actionIndex) external view returns (bytes memory) { + return (_payloads[payloadId].actions[actionIndex].callData); + } + +function getMaximumAccessLevelRequired(uint40 payloadId) external view returns (PayloadsControllerUtils.AccessControl level) { + return _payloads[payloadId].maximumAccessLevelRequired; + } + + +function compare(string memory str1, string memory str2) external pure returns (bool) { + return keccak256(abi.encodePacked(str1)) == keccak256(abi.encodePacked(str2)); + } + +function compare(bytes memory b1, bytes memory b2) external pure returns (bool) { + return keccak256(abi.encodePacked(b1)) == keccak256(abi.encodePacked(b2)); + } + + function getPayloadStateVariable(uint40 payloadId) external view returns (PayloadState) { + return _payloads[payloadId].state; + } + + /** + * @notice method to encode a message, so it could be sent to the governance chain + * @param payloadId field 1 + * @param accessLevel field 2 + * @param proposalVoteActivationTimestamp field 3 + * @return message encoded message + */ +function encodeMessage(uint40 payloadId, PayloadsControllerUtils.AccessControl accessLevel, uint40 proposalVoteActivationTimestamp) + external pure returns (bytes memory) + { + bytes memory message = abi.encode(payloadId, accessLevel, proposalVoteActivationTimestamp); + return message; + } + +} + diff --git a/security/certora/harness/tokens/AaveTokenV3_DummyA.sol b/security/certora/harness/tokens/AaveTokenV3_DummyA.sol new file mode 100644 index 0000000..682aab6 --- /dev/null +++ b/security/certora/harness/tokens/AaveTokenV3_DummyA.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.8.0; + +import {AaveTokenV3} from 'aave-token-v3/AaveTokenV3.sol'; + +contract AaveTokenV3_DummyA is AaveTokenV3 { + + function getDelegatedPowerByTypeHarness( + address user, + GovernancePowerType delegationType + ) public returns (uint256) { + DelegationState memory userState = _getDelegationState(user); + return _getDelegatedPowerByType(userState, delegationType); + } +} diff --git a/security/certora/harness/tokens/AaveTokenV3_DummyB.sol b/security/certora/harness/tokens/AaveTokenV3_DummyB.sol new file mode 100644 index 0000000..4c6a4f1 --- /dev/null +++ b/security/certora/harness/tokens/AaveTokenV3_DummyB.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.8.0; + +import {AaveTokenV3} from 'aave-token-v3/AaveTokenV3.sol'; + +contract AaveTokenV3_DummyB is AaveTokenV3 { + + function getDelegatedPowerByTypeHarness( + address user, + GovernancePowerType delegationType + ) public returns (uint256) { + DelegationState memory userState = _getDelegationState(user); + return _getDelegatedPowerByType(userState, delegationType); + } +} diff --git a/security/certora/harness/tokens/AaveTokenV3_DummyC.sol b/security/certora/harness/tokens/AaveTokenV3_DummyC.sol new file mode 100644 index 0000000..b9f6188 --- /dev/null +++ b/security/certora/harness/tokens/AaveTokenV3_DummyC.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.8.0; + +import {AaveTokenV3} from 'aave-token-v3/AaveTokenV3.sol'; + +contract AaveTokenV3_DummyC is AaveTokenV3 { + + function getDelegatedPowerByTypeHarness( + address user, + GovernancePowerType delegationType + ) public returns (uint256) { + DelegationState memory userState = _getDelegationState(user); + return _getDelegatedPowerByType(userState, delegationType); + } +} diff --git a/security/certora/harness/voting/DelegationModeHarness.sol b/security/certora/harness/voting/DelegationModeHarness.sol new file mode 100644 index 0000000..e041b8b --- /dev/null +++ b/security/certora/harness/voting/DelegationModeHarness.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {DelegationMode} from 'aave-token-v3/DelegationAwareBalance.sol'; + +/** + * @title Hack to use DelegationMode in spec + * `DelegationMode` is not part of any contract, and so cannot be used in spec, + * this hack solves the problem by providing enum `Mode` which is equal. + */ +contract DelegationModeHarness { + + enum Mode { + NO_DELEGATION, + VOTING_DELEGATED, + PROPOSITION_DELEGATED, + FULL_POWER_DELEGATED + } + + function is_equal_to_original() public view returns (bool) { + return ( + uint8(type(Mode).min) == uint8(type(DelegationMode).min) && + uint8(type(Mode).max) == uint8(type(DelegationMode).max) && + uint8(Mode.NO_DELEGATION) == uint8(DelegationMode.NO_DELEGATION) && + uint8(Mode.VOTING_DELEGATED) == uint8(DelegationMode.VOTING_DELEGATED) && + uint8(Mode.PROPOSITION_DELEGATED) == uint8(DelegationMode.PROPOSITION_DELEGATED) && + uint8(Mode.FULL_POWER_DELEGATED) == uint8(DelegationMode.FULL_POWER_DELEGATED) + ); + } + + function mode_to_int(Mode mode) public view returns (uint8) { + return uint8(mode); + } +} diff --git a/security/certora/harness/voting/VotingMachineHarness.sol b/security/certora/harness/voting/VotingMachineHarness.sol new file mode 100644 index 0000000..db95222 --- /dev/null +++ b/security/certora/harness/voting/VotingMachineHarness.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {VotingMachine} from '../../../../src/contracts/voting/VotingMachine.sol'; +import {IDataWarehouse, IVotingStrategy} from '../../../../src/contracts/voting/interfaces/IVotingMachineWithProofs.sol'; + + +/** + * @title VotingMachineHarness + * Harnessing VotingMachine to handle arrays of VotingBalanceProof. + */ +contract VotingMachineHarness is VotingMachine { + + constructor( + address crossChainController, + uint256 gasLimit, + uint256 l1VotingPortalChainId, + IVotingStrategy votingStrategy, + address l1VotingPortal, + address governance + ) VotingMachine( + crossChainController, + gasLimit, + l1VotingPortalChainId, + votingStrategy, + l1VotingPortal, + governance + ) { + } + + /** + * @notice A variant of submitVote that accepts a single VotingBalanceProof. + */ + function submitVoteSingleProof( + uint256 proposalId, + bool support, + VotingBalanceProof calldata proof + ) external { + VotingBalanceProof[] memory proofs = new VotingBalanceProof[](1); + + // Copy proof + proofs[0].underlyingAsset = proof.underlyingAsset; + proofs[0].slot = proof.slot; + proofs[0].proof = proof.proof; + + // To convert `proofs` into `calldata` we make an external call using `this`. + // Unfortunately this makes the msg.sender into the contract, so we call submitVoteFromVoter. + this.submitVoteFromVoter(msg.sender, proposalId, support, proofs); + } + + // Hack - see submitVoteSingleProof + function submitVoteFromVoter( + address voter, + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + require(msg.sender == address(this)); // Safety measure + _submitVote(voter, proposalId, support, votingBalanceProofs); + } + + /** + * @notice For testing `_createBridgedProposalVote`, e.g. in `newProposalUnusedId`. + */ + function createProposalVoteHarness( + uint256 proposalId, + bytes32 blockHash, + uint24 votingDuration + ) external { + _createBridgedProposalVote(proposalId, blockHash, votingDuration); + } + + // Needed for proposalIdIsImmutable rule + function getIdOfProposal( + uint256 proposalId + ) external view returns (uint256) { + return _proposals[proposalId].id; + } +} diff --git a/security/certora/harness/voting/VotingMachineHarnessTriple.sol b/security/certora/harness/voting/VotingMachineHarnessTriple.sol new file mode 100644 index 0000000..e295777 --- /dev/null +++ b/security/certora/harness/voting/VotingMachineHarnessTriple.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {VotingMachineHarness} from './VotingMachineHarness.sol'; +import {IDataWarehouse, IVotingStrategy} from '../../../../src/contracts/voting/interfaces/IVotingMachineWithProofs.sol'; + + +/** + * @title VotingMachineHarnessTriple + * Adds the submitVoteTripleProof method, this is separated to avoid using + * loop_iter=3 throughout. + */ +contract VotingMachineHarnessTriple is VotingMachineHarness { + + constructor( + address crossChainController, + uint256 gasLimit, + uint256 l1VotingPortalChainId, + IVotingStrategy votingStrategy, + address l1VotingPortal, + address governance + ) VotingMachineHarness( + crossChainController, + gasLimit, + l1VotingPortalChainId, + votingStrategy, + l1VotingPortal, + governance + ) { + } + + /** + * @notice A variant of submitVote that accepts three proofs. + */ + function submitVoteTripleProof( + uint256 proposalId, + bool support, + VotingBalanceProof calldata proof1, + VotingBalanceProof calldata proof2, + VotingBalanceProof calldata proof3 + ) external { + VotingBalanceProof[] memory proofs = new VotingBalanceProof[](3); + + // Copy proofs + proofs[0].underlyingAsset = proof1.underlyingAsset; + proofs[0].slot = proof1.slot; + proofs[0].proof = proof1.proof; + + proofs[1].underlyingAsset = proof2.underlyingAsset; + proofs[1].slot = proof2.slot; + proofs[1].proof = proof2.proof; + + proofs[2].underlyingAsset = proof3.underlyingAsset; + proofs[2].slot = proof3.slot; + proofs[2].proof = proof3.proof; + + // To convert `proofs` into `calldata` we make an external call using `this`. + // Unfortunately this makes the msg.sender into the contract, so we call submitVoteFromVoter. + this.submitVoteFromVoter(msg.sender, proposalId, support, proofs); + } +} diff --git a/security/certora/harness/voting/VotingStrategyHarness.sol b/security/certora/harness/voting/VotingStrategyHarness.sol new file mode 100644 index 0000000..da4079b --- /dev/null +++ b/security/certora/harness/voting/VotingStrategyHarness.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.8.0; + +import {VotingStrategy} from '../../../../src/contracts/voting/VotingStrategy.sol'; + + +/** + * @title VotingStrategyHarness + * Needed for `hasRequiredRoots`. + */ +contract VotingStrategyHarness is VotingStrategy { + constructor(address dataWarehouse) VotingStrategy (dataWarehouse) { + } + + // Converts `hasRequiredRoots` to a function that returns boolean + function is_hasRequiredRoots(bytes32 blockHash) external view returns (bool) { + bool is_ok; + try this.hasRequiredRoots(blockHash) { + is_ok = true; + } catch (bytes memory) {} + return is_ok; + } + + // Just the length of the array + function getVotingAssetListLength() external view returns (uint256) { + return this.getVotingAssetList().length; + } +} diff --git a/security/certora/reports/Formal Verification Report Of Aave Governance V3.pdf b/security/certora/reports/Formal Verification Report Of Aave Governance V3.pdf new file mode 100644 index 0000000..2cc5da4 Binary files /dev/null and b/security/certora/reports/Formal Verification Report Of Aave Governance V3.pdf differ diff --git a/security/certora/Formal_Verification_Report_Aave_Governance_V3.md b/security/certora/reports/Formal_Verification_Report_Aave_Governance_V3.md similarity index 100% rename from security/certora/Formal_Verification_Report_Aave_Governance_V3.md rename to security/certora/reports/Formal_Verification_Report_Aave_Governance_V3.md diff --git a/security/certora/specs/Governance.spec b/security/certora/specs/Governance.spec new file mode 100644 index 0000000..5b6c1f2 --- /dev/null +++ b/security/certora/specs/Governance.spec @@ -0,0 +1,1089 @@ +import "set.spec"; + + +using GovernancePowerStrategy as _GovernancePowerStrategy; +using VotingPortal as _VotingPortal; + +methods { + + //call by modifier initializer, allow reachability of _initializeCore + function _.isContract(address) internal => NONDET; + +// function _.forwardVoteMessage(uint256,address,bool,IVotingMachineWithProofs.VotingAssetWithSlot[]) external => NONDET; +//todo: investigate why voteViaPortal() is unreachable without summary of forwardVoteMessage or _sendMessage + function _VotingPortal._sendMessage(address,IVotingPortal.MessageType,uint256,bytes memory) internal => NONDET; + // TODO: consider implementing a dummy CrossChainForwarder.sol + + + //caller: src/contracts/BaseGovernancePowerStrategy.sol + //implemented: lib/aave-token-v3/src/BaseDelegation.sol + //GovernancePowerStrategy.getFullPropositionPower() calls IGovernancePowerDelegationToken.getPowerCurrent() +// function _.getPowerCurrent(address user, IGovernancePowerDelegationToken.GovernancePowerType ) external => get_fixed_power(user) expect uint256 ALL; + //function _.getPowerCurrent(address , IGovernancePowerDelegationToken.GovernancePowerType ) external => NONDET; +// function _._getFullPowerByType(address user,IGovernancePowerDelegationToken.GovernancePowerType) internal => get_fixed_power(user) expect uint256 ALL; + + +//function _GovernancePowerStrategy._getFullPowerByType(address user,IGovernancePowerDelegationToken.GovernancePowerType) internal returns (uint256)=> get_fixed_power(user) ; + +//todo: allow user power to change over time. Should be fixed per, user, type, and block.timestamp +function _GovernancePowerStrategy._getFullPowerByType(address user,IGovernancePowerDelegationToken.GovernancePowerType type) + internal returns (uint256) => get_fixed_user_and_type_power(user, type); +//function _._getFullPowerByType(address user,IGovernancePowerDelegationToken.GovernancePowerType type) +// internal with (env e) => get_fixed_user_and_type_power(user, type); +//function _GovernancePowerStrategy._getFullPowerByType(address user,IGovernancePowerDelegationToken.GovernancePowerType type) +// internal returns (uint256)=> NONDET ; + + //called by executeProposal() + function _.forwardMessage(uint256,address,uint256,bytes) external => NONDET; + + //function _GovernancePowerStrategy.getFullPropositionPower(address) external returns (uint256) => CONSTANT; +// function _.forwardStartVotingMessage(uint256,bytes32,uint24 ) external => NONDET; + + + //todo: replace with a link +// function _.getVotingAssetList() external => DISPATCHER(true); + + // function _.isTokenSlotAccepted(address,uint128) external => DISPATCHER(true); + + + function createProposal(PayloadsControllerUtils.Payload[],address,bytes32) external returns (uint256) ; + + //Governance + function PRECISION_DIVIDER() external returns (uint256) envfree; + function ACHIEVABLE_VOTING_PARTICIPATION() external returns (uint256) envfree; + function COOLDOWN_PERIOD() external returns (uint256) envfree; + function getProposalsCount() external returns (uint256) envfree; + function getVotingPortalsCount() external returns (uint256) envfree; + function isVotingPortalApproved(address) external returns (bool) envfree; + function getVotingConfig(PayloadsControllerUtils.AccessControl) external returns (IGovernanceCore.VotingConfig) envfree; + function getRepresentativeByChain(address,uint256) external returns (address) envfree; + function getRepresentedVotersByChain(address,uint256) external returns (address[] memory) envfree; + function guardian() external returns (address) envfree; + function owner() external returns (address) envfree; + + + + //function removeVotingPortals(address[]) external envfree; + + //GovernanceHarness + function getPayloadLength(uint256 proposalId) external returns (uint256) envfree; + function getProposalStateVariable(uint256 proposalId) external returns (IGovernanceCore.State) envfree; + + function getProposalCreator(uint256) external returns (address) envfree; + function getProposalVotingPortal(uint256) external returns (address) envfree; + function getProposalAccessLevel(uint256) external returns (PayloadsControllerUtils.AccessControl) envfree; + + function getProposalVotingDuration(uint256) external returns (uint24) envfree; + function getProposalCreationTime(uint256) external returns (uint40) envfree; + function getProposalIpfsHash(uint256) external returns (bytes32) envfree; + function getProposalVotingActivationTime(uint256) external returns (uint40) envfree; + function getProposalSnapshotBlockHash(uint256) external returns (bytes32) envfree; + function getProposalCancellationFee(uint256) external returns (uint256) envfree; + function getProposalCount() external returns (uint256) envfree; + + + function getPayloadChain(uint256, uint256) external returns (uint256) envfree; + function getPayloadAccessLevel(uint256,uint256) external returns (PayloadsControllerUtils.AccessControl) envfree; + function getPayloadPayloadsController(uint256,uint256) external returns (address) envfree; + function getPayloadPayloadId(uint256,uint256) external returns (uint40) envfree; + function isRepresentativeOfVoter(address,address,uint256) external returns (bool) envfree; + + + // GovernancePowerStrategy + //todo: investigate why it passes envfreeFuncsStaticCheck + //function _GovernancePowerStrategy.getFullPropositionPower(address) external returns (uint256) envfree; + +} + +ghost mathint totalCancellationFee{ + init_state axiom totalCancellationFee == 0; +} +ghost bool isCancellationChanged; + +hook Sstore _proposals[KEY uint256 proposalId].cancellationFee uint256 newFee + (uint256 oldFee) STORAGE +{ + if (newFee != oldFee){ + isCancellationChanged = true; + } + totalCancellationFee = totalCancellationFee + newFee - oldFee; +} + + +// invariants of AddressSet +use invariant set_size_leq_max_uint160; + + +// State changing methods +definition state_advancing_function(method f) returns bool = + f.selector == sig:createProposal(PayloadsControllerUtils.Payload[],address,bytes32).selector || + f.selector == sig:activateVoting(uint256).selector || + f.selector == sig:queueProposal(uint256,uint128,uint128).selector || + f.selector == sig:executeProposal(uint256).selector; + +definition state_changing_function(method f) returns bool = + state_advancing_function(f) || f.selector == sig:cancelProposal(uint256).selector; + +definition initializeSig(method f) returns bool = + f.selector == sig:initialize(address,address,address, IGovernanceCore.SetVotingConfigInput[],address[],uint256,uint256).selector; + +definition terminalState(uint256 proposalId) returns bool = + getProposalStateVariable(proposalId) == IGovernanceCore.State.Executed || + getProposalStateVariable(proposalId) == IGovernanceCore.State.Failed || + getProposalStateVariable(proposalId) == IGovernanceCore.State.Cancelled || + getProposalStateVariable(proposalId) == IGovernanceCore.State.Expired; + + +function getMinPropositionPower(IGovernanceCore.VotingConfig votingConfig) returns uint56{ + return votingConfig.minPropositionPower; +} + +// ghost mapping(address => uint256) user_power; +// ghost mapping(IGovernancePowerDelegationToken.GovernancePowerType => uint256) type_power; +ghost mapping(address => mapping(IGovernancePowerDelegationToken.GovernancePowerType => uint256)) user_type_power; + + +function get_fixed_user_and_type_power(address user, IGovernancePowerDelegationToken.GovernancePowerType type) returns uint256{ + return user_type_power[user][type]; +} + + +// +// from properties.md +// + +// @title Property #1: Proposal IDs are consecutive and incremental. +// Proposal ID increments by 1 iff createProposal was called +rule consecutiveIDs(method f) filtered +{ f -> f.selector != sig:createProposal(PayloadsControllerUtils.Payload[],address,bytes32).selector } +{ + + env e1; env e2; env e3; + calldataarg args1; calldataarg args2; calldataarg args3; + + mathint id_first = createProposal(e1, args1); + f(e2, args2); + mathint id_second = createProposal(e3, args3); + assert id_second == id_first + 1; +} + + +// @title Property #2: Every proposal should contain at least one payload. +// For initialized property, the proposal list is not empty +invariant at_least_single_payload_active (env e, uint256 proposalId) + getProposalState(e, proposalId) != IGovernanceCore.State.Null + => getPayloadLength(proposalId) > 0 + { + preserved{ + requireInvariant empty_payloads_if_uninitialized_proposal(proposalId); + } + } +// Same property just referring directly to the storage +invariant at_least_single_payload_active_variable (uint256 proposalId) + getProposalStateVariable(proposalId) != IGovernanceCore.State.Null => getPayloadLength(proposalId) > 0 + { + preserved{ + requireInvariant empty_payloads_if_uninitialized_proposal(proposalId); + } + } + +// Address zero cannot be a creator of a proposal +invariant creator_is_not_zero(uint256 proposalId) + getProposalStateVariable(proposalId) != IGovernanceCore.State.Null => getProposalCreator(proposalId) != 0 + { + preserved with (env e) + {require e.msg.sender != 0;} + } + +invariant creator_is_not_zero_2(uint256 proposalId) + proposalId < getProposalsCount() => getProposalCreator(proposalId) != 0 + { + preserved with (env e) + {require e.msg.sender != 0;} + } + +// Uninitialized proposals has no payloads +invariant empty_payloads_iff_uninitialized_proposal(uint256 proposalId) + proposalId >= getProposalsCount() <=> getPayloadLength(proposalId) == 0; + +invariant empty_payloads_if_uninitialized_proposal(uint256 proposalId) + proposalId >= getProposalsCount() => getPayloadLength(proposalId) == 0; + +// A proposal is uninitialized iff its state is Null +invariant null_state_iff_uninitialized_proposal(env e, uint256 proposalId) + proposalId >= getProposalsCount() <=> getProposalState(e, proposalId) == IGovernanceCore.State.Null; + + +invariant null_state_variable_iff_uninitialized_proposal(uint256 proposalId) + proposalId >= getProposalsCount() <=> getProposalStateVariable(proposalId) == IGovernanceCore.State.Null; + +invariant null_state_if_uninitialized_proposal(env e, uint256 proposalId) + proposalId >= getProposalsCount() => getProposalState(e, proposalId) == IGovernanceCore.State.Null; + +invariant null_state_variable_if_uninitialized_proposal(uint256 proposalId) + proposalId >= getProposalsCount() => getProposalStateVariable(proposalId) == IGovernanceCore.State.Null; + +invariant null_state_only_if_uninitialized_proposal(env e, uint256 proposalId) + getProposalState(e, proposalId) == IGovernanceCore.State.Null => proposalId >= getProposalsCount(); + +invariant null_state_variable_only_if_uninitialized_proposal(uint256 proposalId) + getProposalStateVariable(proposalId) == IGovernanceCore.State.Null => proposalId >= getProposalsCount(); + + + +// @title Property #3: If a voting portal gets invalidated during the proposal life cycle, +// the proposal should not transition to any state apart from Cancelled, Expired, and Failed. + +// If a proposal lost its voting power its state can transition to Executed, Cancelled, Expired, and Failed only. +// Note: if voting power decreases after queuing the proposal can still be executed. +// +// Issue reported on June 27, 2023 - violation of property #5: No further state transitions are possible if proposal.state > 3. +//todo: verify fix +// second fail, reported on June 29 + +rule proposal_after_voting_portal_invalidate{ + + env e1; env e2; env e3; + calldataarg args; + method f; + uint256 proposalId; + + require e1.block.timestamp <= e3.block.timestamp; + + IGovernanceCore.State state1 = getProposalState(e1,proposalId); + f(e2, args); + require !isVotingPortalApproved(getProposalVotingPortal(proposalId)); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert state1 != state2 => + state2 == IGovernanceCore.State.Executed || + state2 == IGovernanceCore.State.Cancelled || + state2 == IGovernanceCore.State.Expired || state2 == IGovernanceCore.State.Failed; + +} + + + +// @title Property #4: If the proposer's proposition power goes below the minimum required threshold, the proposal +// should not go to any state apart from Failed or Canceled. + +// In case of insufficient proposition power state can change to +rule insufficient_proposition_power(method f) filtered { f -> !f.isView}{ + env e1; env e2; + calldataarg args; + uint256 proposalId; + + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e1, args); + IGovernanceCore.State state2 = getProposalState(e1, proposalId); + + mathint creator_power = _GovernancePowerStrategy.getFullPropositionPower(e2,getProposalCreator(proposalId)); + mathint voting_config_min_power = getMinPropositionPower(getVotingConfig(getProposalAccessLevel(proposalId))) * PRECISION_DIVIDER(); //uint56 + + require state1 != state2; + require creator_power <= voting_config_min_power; + assert state2 == IGovernanceCore.State.Cancelled || state2 == IGovernanceCore.State.Failed; + +} + +//pass +rule insufficient_proposition_power_allow_time_elapse(method f) filtered { f -> !f.isView} +{ + env e1; env e2; env e3; env e4; + calldataarg args; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp && e2.block.timestamp <= e3.block.timestamp; + + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + mathint creator_power = _GovernancePowerStrategy.getFullPropositionPower(e4,getProposalCreator(proposalId)); + mathint voting_config_min_power = getMinPropositionPower(getVotingConfig(getProposalAccessLevel(proposalId))) * PRECISION_DIVIDER(); //uint56 + + assert (state1 != state2 && (creator_power <= voting_config_min_power)) => + state2 == IGovernanceCore.State.Cancelled || state2 == IGovernanceCore.State.Failed || state2 == IGovernanceCore.State.Expired; + +} + +rule insufficient_proposition_power_time_elapsed_tight_witness(method f) filtered { f -> state_advancing_function(f)}{ + env e1; env e2; env e3; env e4; + calldataarg args; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp && e2.block.timestamp <= e3.block.timestamp; + + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + mathint creator_power = _GovernancePowerStrategy.getFullPropositionPower(e4,getProposalCreator(proposalId)); + mathint voting_config_min_power = getMinPropositionPower(getVotingConfig(getProposalAccessLevel(proposalId))) * PRECISION_DIVIDER(); //uint56 + + require state1 != state2; + require creator_power <= (voting_config_min_power+1); + satisfy ! (state2 == IGovernanceCore.State.Cancelled || state2 == IGovernanceCore.State.Failed); +} + + +//helper a proposal state is Null iff its required access level is null +invariant null_state_variable_iff_null_access_level(uint256 proposalId) + getProposalStateVariable(proposalId) == IGovernanceCore.State.Null <=> + getProposalAccessLevel(proposalId) == PayloadsControllerUtils.AccessControl.Level_null; + + + +// Once assign the voting portal is immutable +rule immutable_voting_portal(){ + + env e; + calldataarg args; + method f; + uint256 proposalId; + + requireInvariant zero_voting_portal_iff_uninitialized_proposal(proposalId); + + address votingPortal_before = getProposalVotingPortal(proposalId); + f(e, args); + address votingPortal_after = getProposalVotingPortal(proposalId); + + assert votingPortal_before != 0 => votingPortal_before == votingPortal_after; +} + +// helper: A proposal is uninitialized iff its voting portal is address zero. +invariant zero_voting_portal_iff_uninitialized_proposal(uint256 proposalId) + proposalId >= getProposalsCount() <=> getProposalVotingPortal(proposalId) == 0 + { + preserved { + requireInvariant zero_address_is_not_a_valid_voting_portal(); + } + + } + +//helper: Zero address is never an approved voting portal +invariant zero_address_is_not_a_valid_voting_portal() + !isVotingPortalApproved(0); + + + +// @title Property #5: No further state transitions are possible if proposal.state > 3. +// All state that are greater than 3 are terminal +rule no_state_transitions_beyond_3{ + env e1; env e2; env e3; + calldataarg args; + method f; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp && e2.block.timestamp <= e3.block.timestamp; + requireInvariant null_state_iff_uninitialized_proposal(e1, proposalId); + + + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert + assert_uint256(state1) > 3=> state1 == state2; +} + + +// @title Property #6 proposal.state can't decrease. +// Forward progress of the proposal state-machine: the state cannot decrease. +rule state_cant_decrease{ + env e1; env e2; env e3; + calldataarg args; + method f; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp && e2.block.timestamp <= e3.block.timestamp; + requireInvariant null_state_iff_uninitialized_proposal(e1, proposalId); + + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert assert_uint256(state1) <= assert_uint256(state2); +} + + +// @title Property #7 +// It should be impossible to do more than 1 state transition per proposal per block, except: +// Cancellation because of the proposition power change. +// Cancellation after proposal creation by creator. +// Proposal execution after proposal queuing if COOLDOWN_PERIOD is 0. + +// No 2 state transitions happens in a single block timestamp, except cancellation by the owner or by a guardian. +rule single_state_transition_per_block_non_creator_non_guardian(method f, method g, method h) +filtered { f -> state_changing_function(f), + g -> !g.isView && !initializeSig(g), + h -> state_changing_function(h)} +{ + env e1; + env e2; + env e3; + env e4; + env e5; + env e6; + calldataarg args1; + calldataarg args2; + calldataarg args3; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp; + require e2.block.timestamp == e3.block.timestamp; + require e3.block.timestamp == e4.block.timestamp; + require e4.block.timestamp == e5.block.timestamp; + require e5.block.timestamp == e6.block.timestamp; + require e6.block.timestamp < 2^40; + + requireInvariant null_state_iff_uninitialized_proposal(e2, proposalId); + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args1); + g(e3, args2); + IGovernanceCore.State state2 = getProposalState(e4, proposalId); + h(e5, args3); + IGovernanceCore.State state3 = getProposalState(e6, proposalId); + + assert getProposalCreator(proposalId) != e5.msg.sender && // creator can cancel + guardian() != e5.msg.sender && + owner() != e3.msg.sender && //owner can call setVotingConfigs, removeVotingPortals. TODO: add the assumption to the final report + COOLDOWN_PERIOD() != 0 && + state1 != state2 => state2 == state3; +} + + +//todo: add witnesses of double transition +//todo: investigate: should there be more witnesses in addition to queueProposal-executeProposal +rule single_state_transition_per_block_non_creator_witness +{ + env e1; + env e2; + env e3; + env e4; + env e5; + calldataarg args1; + calldataarg args2; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp; + require e2.block.timestamp == e3.block.timestamp; + require e3.block.timestamp == e4.block.timestamp; + require e4.block.timestamp == e5.block.timestamp; + require e5.block.timestamp < 2^40; + requireInvariant null_state_iff_uninitialized_proposal(e2, proposalId); + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + queueProposal(e2, args1); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + executeProposal(e4, args2); + IGovernanceCore.State state3 = getProposalState(e5, proposalId); + + require getProposalCreator(proposalId) != e4.msg.sender; // creator can cancel + require currentContract != e2.msg.sender; + require currentContract != e4.msg.sender; + require guardian() != e4.msg.sender; + + require state1 != state2; + satisfy !(state2 == state3); +} + + + +/// Property #8: Only the owner can set the voting power strategy and voting config. +// fails on initialize + +// A unauthorized user (not an owner) cannot change voting parameters +rule only_owner_can_set_voting_config(method f) filtered { + f -> !f.isView && + !initializeSig(f) } +{ + env e; + calldataarg args; + PayloadsControllerUtils.AccessControl accessLevel; + + IGovernanceCore.VotingConfig voting_config_before = getVotingConfig(accessLevel); + f(e, args); + IGovernanceCore.VotingConfig voting_config_after = getVotingConfig(accessLevel); + + assert e.msg.sender != owner() => voting_config_before.coolDownBeforeVotingStart == voting_config_after.coolDownBeforeVotingStart; + assert e.msg.sender != owner() => voting_config_before.votingDuration == voting_config_after.votingDuration; + assert e.msg.sender != owner() => voting_config_before.yesThreshold == voting_config_after.yesThreshold; + assert e.msg.sender != owner() => voting_config_before.yesNoDifferential == voting_config_after.yesNoDifferential; + assert e.msg.sender != owner() => voting_config_before.minPropositionPower == voting_config_after.minPropositionPower; + +} +//todo add witness - owner changes voting config + +rule only_owner_can_set_voting_config_witness(method f) filtered { f -> !f.isView} +{ + env e; + calldataarg args; + PayloadsControllerUtils.AccessControl accessLevel; + + IGovernanceCore.VotingConfig voting_config_before = getVotingConfig(accessLevel); + f(e, args); + IGovernanceCore.VotingConfig voting_config_after = getVotingConfig(accessLevel); + + satisfy voting_config_before.coolDownBeforeVotingStart == voting_config_after.coolDownBeforeVotingStart; + satisfy voting_config_before.votingDuration == voting_config_after.votingDuration; + satisfy voting_config_before.yesThreshold == voting_config_after.yesThreshold; + satisfy voting_config_before.yesNoDifferential == voting_config_after.yesNoDifferential; + satisfy voting_config_before.minPropositionPower == voting_config_after.minPropositionPower; + +} + +//Property #9: When invalidating voting config, proposal can not be queued + +// One can not queue a proposal if its voting portal is unapproved +rule cannot_queue_when_voting_portal_unapproved{ + + env e1; env e2; env e3; + calldataarg args; + method f; + uint256 proposalId; + uint128 forVotes; + uint128 againstVotes; + + bool is_voting_portal_approved = isVotingPortalApproved(getProposalVotingPortal(proposalId)); + queueProposal(e1, proposalId, forVotes, againstVotes); + assert is_voting_portal_approved; +} + + + + +//Property #10: Guardian can cancel proposals with proposal.state < 4 + +// A guardian can cancel a proposla whose state < 4 but it cannot cancel if state is >= 5 +rule guardian_can_cancel() +{ + env e1; + env e2; + env e3; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp; + + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + cancelProposal(e2, proposalId); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert state2 == IGovernanceCore.State.Cancelled; + assert assert_uint256(state1) < 4; +} + +// Only a guardian, an owner can cancel any proposal, a creator can cancel his own proposal +rule only_guardian_can_cancel(method f)filtered +{ f -> !f.isView && + !initializeSig(f) + } +{ + env e1; + env e2; + env e3; + + calldataarg args1; + calldataarg args2; + +// require e1.block.timestamp <= e2.block.timestamp; + + uint256 proposalId; + + + require createProposal(e1, args1) == proposalId; + //mathint creator_power_before = _GovernancePowerStrategy.getFullPropositionPower(e2,getProposalCreator(proposalId)); + + f(e2, args2); +// IGovernanceCore.State state1 = getProposalState(e1, proposalId); + +// mathint creator_power_after = _GovernancePowerStrategy.getFullPropositionPower(e2,getProposalCreator(proposalId)); + cancelProposal(e3, proposalId); + + assert guardian() == e2.msg.sender || + owner() == e2.msg.sender || //todo: review + guardian() == e3.msg.sender || + getProposalCreator(proposalId) == e3.msg.sender + // || creator_power_after < creator_power_before + ; +} + + +//helper parametric function +function call_state_changing_function(env e, uint256 proposalId) { +uint128 forVotes; + calldataarg args; + uint128 againstVotes; + uint256 sel; + if (sel == 1) {require createProposal(e, args) == proposalId;} + else if (sel ==2) {activateVoting(e, proposalId);} + else if (sel ==3) {queueProposal(e, proposalId, forVotes, againstVotes);} + else if (sel ==4) {executeProposal(e, proposalId);} + else if (sel ==5) {cancelProposal(e, proposalId);} + else {require false;} + } + + +//Property #11: The following proposal parameters can only be set once, at proposal creation: +// creator, accessLevel, votingPortal, votingDuration, creationTime, ipfsHash, payloads. + +// Once a proposal is initialized its creator, accessLevel, votingPortal, votingDuration, creationTime, ipfsHash, payloads length cannot change. +rule immutable_after_creation(method f){ + + env e1; + env e2; + calldataarg args; + uint256 proposalId; + + + requireInvariant null_state_iff_uninitialized_proposal(e2, proposalId); + + IGovernanceCore.State state_before = getProposalState(e1, proposalId); + + address creator_before = getProposalCreator(proposalId); + address voting_portal_before = getProposalVotingPortal(proposalId); + PayloadsControllerUtils.AccessControl access_level_before = getProposalAccessLevel(proposalId); + uint24 voting_duration_before = getProposalVotingDuration(proposalId); + uint40 creation_time_before = getProposalCreationTime(proposalId); + bytes32 ipfs_hash_before = getProposalIpfsHash(proposalId); + uint256 payloads_length_before = getPayloadLength(proposalId); + + f(e2, args); + address creator_after = getProposalCreator(proposalId); + address voting_portal_after = getProposalVotingPortal(proposalId); + PayloadsControllerUtils.AccessControl access_level_after = getProposalAccessLevel(proposalId); + uint24 voting_duration_after = getProposalVotingDuration(proposalId); + uint40 creation_time_after = getProposalCreationTime(proposalId); + bytes32 ipfs_hash_after = getProposalIpfsHash(proposalId); + uint256 payloads_length_after = getPayloadLength(proposalId); + + + assert state_before != IGovernanceCore.State.Null => creator_before == creator_after; + assert state_before != IGovernanceCore.State.Null => access_level_before == access_level_after; + assert state_before != IGovernanceCore.State.Null => voting_portal_before == voting_portal_after; + assert state_before != IGovernanceCore.State.Null => voting_duration_before == voting_duration_before; + assert state_before != IGovernanceCore.State.Null => creation_time_before == creation_time_before; + assert state_before != IGovernanceCore.State.Null => ipfs_hash_before == ipfs_hash_after; + assert state_before != IGovernanceCore.State.Null => payloads_length_before == payloads_length_after; +} + + + +// Proposal payloads cannot change. +rule immutable_payload_after_creation(method f){ + + env e1; + env e2; + calldataarg args; + uint256 proposalId; + uint256 payloadId; + + + requireInvariant null_state_iff_uninitialized_proposal(e2, proposalId); + requireInvariant empty_payloads_iff_uninitialized_proposal(proposalId); + + IGovernanceCore.State state_before = getProposalState(e1, proposalId); + + uint256 payload_chain_before = getPayloadChain(proposalId, payloadId); + PayloadsControllerUtils.AccessControl payload_access_level_before = getPayloadAccessLevel(proposalId, payloadId); + address payloads_controller_before = getPayloadPayloadsController(proposalId, payloadId); + uint40 payloads_id_before = getPayloadPayloadId(proposalId, payloadId); + + + f(e2, args); + uint256 payload_chain_after = getPayloadChain(proposalId, payloadId); + PayloadsControllerUtils.AccessControl payload_access_level_after = getPayloadAccessLevel(proposalId, payloadId); + address payloads_controller_after = getPayloadPayloadsController(proposalId, payloadId); + uint40 payloads_id_after = getPayloadPayloadId(proposalId, payloadId); + + assert payload_chain_before == payload_chain_after; + assert payload_access_level_before == payload_access_level_after; + assert payloads_controller_before == payloads_controller_after; + assert payloads_id_before == payloads_id_after; + +} + +// Property #12: The following proposal parameters can only be set once, during voting activation: +// votingActivationTime, snapshotBlockHash, snapshotBlockNumber. + +// Proposal's votingActivationTime and snapshotBlockHash are immutable + +rule immutable_after_activation(method f) +filtered {f -> !f.isView} +{ + env e1; + env e2; + calldataarg args; + uint256 proposalId; + + activateVoting(e1, proposalId); + + uint40 voting_activation_time_before = getProposalVotingActivationTime(proposalId); + bytes32 snapshot_blockhash_before = getProposalSnapshotBlockHash(proposalId); + f(e2, args); + uint40 voting_activation_time_after = getProposalVotingActivationTime(proposalId); + bytes32 snapshot_blockhash_after = getProposalSnapshotBlockHash(proposalId); + + assert voting_activation_time_before == voting_activation_time_after; + assert snapshot_blockhash_before == snapshot_blockhash_after; +} + + + +//Property #14: Only a valid voting portal can queue a proposal and only if this is in Active state. + +// Only by an approved voting protal can call queue(), only if state is Active +rule only_valid_voting_portal_can_queue_proposal(method f){ + + env e1; + env e2; + env e3; + calldataarg args; + uint256 proposalId; + require e1.block.timestamp <= e3.block.timestamp; + + IGovernanceCore.State state_before = getProposalState(e1, proposalId); + f(e2, args); + IGovernanceCore.State state_after = getProposalState(e3, proposalId); + + assert state_before != state_after && state_after == IGovernanceCore.State.Queued => isVotingPortalApproved(e2.msg.sender); + assert state_before != state_after && state_after == IGovernanceCore.State.Queued => state_before == IGovernanceCore.State.Active; +} +//Property #15: A proposal can be executed only in Queued state, after passing the cooldown period. +//todo: consider checking that _forwardPayloadForExecution() or ICrossChainForwarder.forwardMessage() is called rather than executeProposal() + +// A proposal can be executed only after the cooldown period has elapsed since it was queued +rule proposal_executes_after_cooldown_period(){ + + env e1; + env e2; + env e3; + uint256 proposalId; + uint128 forVotes; + uint128 againstVotes; + require e2.block.timestamp <= e3.block.timestamp; + require e1.block.timestamp < 2^40; + require e3.block.timestamp < 2^40; + + queueProposal(e1, proposalId, forVotes, againstVotes); + IGovernanceCore.State state_before = getProposalState(e2, proposalId); + executeProposal(e3, proposalId); + + assert state_before == IGovernanceCore.State.Queued; + assert e3.block.timestamp - e1.block.timestamp >= to_mathint(COOLDOWN_PERIOD()); +} + + +//Property #16: The Governance Core system shouldn’t know anything about the voting procedure. +// It only expects a whitelisted entity to submit voting results about a specific proposal id. +//Property #17: The Governance Core system shouldn’t know anything about final execution. +// From its perspective, execution is sent to a Portal. +//Property #18: VOTING_TOKENS_CAP in GovernanceCore should be big enough to account for tokens that need to pass multiple slots, +// and big enough for at least mig to long term + + +//property #20 (old version): When if in a terminal state, no state changing function can be called. + +// Terminal states >= 4 are terminal, a proposal in a terminal state cannot change its state +rule state_changing_function_cannot_be_called_while_in_terminal_state() +{ + env e1; + env e2; + env e3; + uint256 proposalId; + require e1.block.timestamp <= e2.block.timestamp; + + requireInvariant null_state_iff_uninitialized_proposal(e2, proposalId); + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + call_state_changing_function(e2, proposalId); + + assert assert_uint256(state1) < 4; +} + + +// Terminal states >= 4 are terminal, a proposal in a terminal state cannot change its state +rule terminal_state_cannot_change(method f) +{ + env e1; + env e2; + env e3; + calldataarg args; + uint256 proposalId; + + require e1.block.timestamp <= e2.block.timestamp; + require e2.block.timestamp <= e3.block.timestamp; + + requireInvariant null_state_iff_uninitialized_proposal(e2, proposalId); + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert assert_uint256(state1) >= 4 => state1 == state2; +} + + +// Only the relevant state-changing function actually change the state +// Check by the states before a transtion occurs +rule pre_state(method f) +{ + env e1; + env e2; + env e3; + calldataarg args1; + uint256 proposalId; + + require e1.block.timestamp <= e3.block.timestamp; + requireInvariant null_state_iff_uninitialized_proposal(e1, proposalId); + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args1); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert state1 != state2 && state1 == IGovernanceCore.State.Null && state2 != IGovernanceCore.State.Expired + => f.selector == sig:createProposal(PayloadsControllerUtils.Payload[],address,bytes32).selector; + assert state1 != state2 && state1 == IGovernanceCore.State.Created && state2 != IGovernanceCore.State.Expired + => f.selector == sig:activateVoting(uint256).selector || f.selector == sig:cancelProposal(uint256).selector; + assert state1 != state2 && state1 == IGovernanceCore.State.Active && state2 != IGovernanceCore.State.Expired + => f.selector == sig:queueProposal(uint256,uint128,uint128).selector || f.selector == sig:cancelProposal(uint256).selector; + assert state1 != state2 && state1 == IGovernanceCore.State.Queued && state2 != IGovernanceCore.State.Expired + => (f.selector == sig:executeProposal(uint256).selector || f.selector == sig:cancelProposal(uint256).selector); + //todo: relevant assertion for Failed, Expired? + +} + + +// Only the relevant state-changing function actually change the state +// Check by the states after a transtion occurs +rule post_state(method f) +{ + env e1; + env e2; + env e3; + calldataarg args1; + uint256 proposalId; + + require e1.block.timestamp <= e3.block.timestamp; + + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args1); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert state1 != state2 && state2 == IGovernanceCore.State.Created => + f.selector == sig:createProposal(PayloadsControllerUtils.Payload[],address,bytes32).selector; + assert state1 != state2 && state2 == IGovernanceCore.State.Active => f.selector == sig:activateVoting(uint256).selector; + assert state1 != state2 && state2 == IGovernanceCore.State.Queued => f.selector == sig:queueProposal(uint256,uint128,uint128).selector; + assert state1 != state2 && state2 == IGovernanceCore.State.Executed => f.selector == sig:executeProposal(uint256).selector; + assert state1 != state2 && state2 == IGovernanceCore.State.Cancelled => f.selector == sig:cancelProposal(uint256).selector; + //todo: relevant assertion for Failed, Expired? + +} + +//helper: only method of state_changing_function can change a proposal state +rule state_changing_function_self_check(method f) +filtered { f -> !state_changing_function(f)} +{ + env e1; + env e2; + env e3; + calldataarg args1; + uint256 proposalId; + + require e1.block.timestamp <= e3.block.timestamp; + IGovernanceCore.State state1 = getProposalState(e1, proposalId); + f(e2, args1); + IGovernanceCore.State state2 = getProposalState(e3, proposalId); + + assert state2 != IGovernanceCore.State.Expired => state1 == state2; + assert e1.block.timestamp == e3.block.timestamp => state1 == state2; +} + +rule state_variable_changing_function_self_check(method f) +filtered { f -> !state_changing_function(f)} +{ + env e1; + env e2; + env e3; + calldataarg args1; + uint256 proposalId; + + IGovernanceCore.State state1 = getProposalStateVariable(e1, proposalId); + f(e2, args1); + IGovernanceCore.State state2 = getProposalStateVariable(e3, proposalId); + + assert state1 == state2; +} + + + +// self check - reachability +rule initialize_sanity{ + env e; + calldataarg arg; + initialize(e, arg); + satisfy true; +} + +rule sanity { + env e; + calldataarg arg; + method f; + + require COOLDOWN_PERIOD() == 0; + f(e, arg); + satisfy true; +} + +// Property: For any proposal id that wasn't yet created, the cancellation fee must be 0 +invariant cancellationFeeZeroForFutureProposals(uint256 proposalId) + proposalId >= getProposalCount() => getProposalCancellationFee(proposalId) == 0; + +// Property: In any case that proposal.CancellationFee does change, eth balance can cover the total cancellation fee of users +invariant totalCancellationFeeEqualETHBalance() + to_mathint(nativeBalances[currentContract]) >= totalCancellationFee + { + preserved with (env e2) + { + requireInvariant cancellationFeeZeroForFutureProposals(require_uint256(getProposalCount())); + require e2.msg.sender != currentContract; + } + } + +// Property: In any case that proposal.CancellationFee doesn't change, eth balance cannot decrease +rule userFeeDidntChangeImplyNativeBalanceDidntDecrease(){ + require(!isCancellationChanged); + uint256 _ethBal = nativeBalances[currentContract]; + method f; env e; calldataarg args; + f(e, args); + uint256 ethBal_ = nativeBalances[currentContract]; + assert(!isCancellationChanged => _ethBal <= ethBal_); +} + +// Representative +// Additional properties for voting by representative + +// A voter cannot represent himself +invariant no_self_representative(address voter, uint256 chainId) + voter == 0 <=> getRepresentativeByChain(voter, chainId) == voter + { + preserved with (env e){ + require e.msg.sender != 0; + } + } + +// Address zero can be a representative +// No voter is contained in the voters' set of address zero +invariant no_representative_is_zero(address voter, uint256 chainId) + !isRepresentativeOfVoter(voter, 0, chainId); + +// The size of the voter's set of address zero is zero +invariant no_representative_is_zero_2(uint256 chainId) + getRepresentedVotersSize(0, chainId) == 0; + +// Address zero has no representatives +invariant no_representative_of_zero(uint256 chainId) + getRepresentativeByChain(0, chainId) == 0 + { + preserved with (env e){ + require e.msg.sender != 0; + } + } + +// The size of the new representative set is correct after updateRepresentatives() +rule check_new_representative_set_size_after_updateRepresentatives{ + + env e; + address new_representative; + uint256 chainId; + + requireInvariant no_self_representative(e.msg.sender, chainId); + requireInvariant in_representatives_iff_in_votersRepresented(e.msg.sender, new_representative, chainId); + + + address[] new_voters_before = getRepresentedVotersByChain(new_representative, chainId); + mathint new_voters_size_before = new_voters_before.length; + address representative_before = getRepresentativeByChain(e.msg.sender, chainId); + + updateSingleRepresentativeForChain(e, new_representative, chainId); + address[] new_voters_after = getRepresentedVotersByChain(new_representative, chainId); + mathint new_voters_size_after = new_voters_after.length; + address representative_after = getRepresentativeByChain(e.msg.sender, chainId); + + + assert new_representative != 0 && new_representative !=e.msg.sender && new_representative != representative_before => + new_voters_size_after == new_voters_size_before + 1; + + assert new_representative != 0 && new_representative !=e.msg.sender && new_representative == representative_before => + new_voters_size_after == new_voters_size_before; + + assert (new_representative == e.msg.sender ) => + new_voters_size_after == new_voters_size_before; + +} + +// The size of the old representative set is correct after updateRepresentatives() +rule check_old_representative_set_size_after_updateRepresentatives{ + + env e; + address new_representative; + uint256 chainId; + + address representative_before = getRepresentativeByChain(e.msg.sender, chainId); + + requireInvariant in_representatives_iff_in_votersRepresented(e.msg.sender, representative_before, chainId); + requireInvariant no_representative_is_zero_2(chainId); + + address[] old_voters_before = getRepresentedVotersByChain(representative_before, chainId); + mathint old_voters_size_before = old_voters_before.length; + + updateSingleRepresentativeForChain(e, new_representative, chainId); + address[] old_voters_after = getRepresentedVotersByChain(representative_before, chainId); + mathint old_voters_size_after = old_voters_after.length; + address representative_after = getRepresentativeByChain(e.msg.sender, chainId); + + + assert new_representative != 0 && new_representative !=e.msg.sender + && new_representative != representative_before && old_voters_size_before > 0 => + old_voters_size_after + 1 == old_voters_size_before; + + assert new_representative != 0 && new_representative !=e.msg.sender && new_representative == representative_before => + old_voters_size_after == old_voters_size_before; + + assert (new_representative == e.msg.sender || new_representative == 0) && old_voters_size_before > 0 => + old_voters_size_after + 1 == old_voters_size_before; + + + +} + + +// +// Failing rules - begin +// + +//TODO: rerun once CERT-3618 is resolved +//Ignore fail in CI +use invariant addressSetInvariant; +//Ignore fail in CI +use invariant setInvariant; + +// +// Failing rules - end +// + + +invariant no_representative_of_zero_in_set(address representative, uint256 chainId) + !isRepresentativeOfVoter(0, representative, chainId) + { + preserved with (env e){ + require e.msg.sender != 0; + requireInvariant addressSetInvariant(representative, chainId); + } + } + + +invariant in_representatives_iff_in_votersRepresented(address voter, address representative, uint256 chainId) + (representative != 0) => + (isRepresentativeOfVoter(voter, representative, chainId) <=> + getRepresentativeByChain(voter, chainId) == representative) //parentheses are required here! + { + preserved with (env e){ + requireInvariant addressSetInvariant(representative, chainId); + } + } + diff --git a/security/certora/specs/GovernancePowerStrategy.spec b/security/certora/specs/GovernancePowerStrategy.spec new file mode 100644 index 0000000..7252843 --- /dev/null +++ b/security/certora/specs/GovernancePowerStrategy.spec @@ -0,0 +1,207 @@ +// Verification of `GovernancePowerStrategy` contract ========================== +using AaveTokenV3_DummyA as _DummyTokenA; +using AaveTokenV3_DummyB as _DummyTokenB; +using AaveTokenV3_DummyC as _DummyTokenC; + + +methods +{ + // IBaseVotingStrategy ===================================================== + function AAVE() external returns (address) envfree; + function A_AAVE() external returns (address) envfree; + function STK_AAVE() external returns (address) envfree; + function BASE_BALANCE_SLOT() external returns (uint128) envfree; + function A_AAVE_BASE_BALANCE_SLOT() external returns (uint128) envfree; + function A_AAVE_DELEGATED_STATE_SLOT() external returns (uint128) envfree; + + function isTokenSlotAccepted(address, uint128) external returns (bool) envfree; + function getVotingAssetList() external returns (address[]) envfree; + function getVotingAssetConfig(address) external returns ( + IBaseVotingStrategy.VotingAssetConfig + ) envfree; + function getFullVotingPower(address) external returns (uint256) envfree; + function getFullPropositionPower(address) external returns (uint256) envfree; + + // AaveTokenV3 ============================================================= + function AaveTokenV3_DummyA.getPowerCurrent( + address, + IGovernancePowerDelegationToken.GovernancePowerType + ) external returns (uint256) envfree; + function AaveTokenV3_DummyA.getDelegateeByType( + address, + IGovernancePowerDelegationToken.GovernancePowerType + ) external returns (address) envfree; + + function AaveTokenV3_DummyB.getPowerCurrent( + address, + IGovernancePowerDelegationToken.GovernancePowerType + ) external returns (uint256) envfree; + + function AaveTokenV3_DummyC.getPowerCurrent( + address, + IGovernancePowerDelegationToken.GovernancePowerType + ) external returns (uint256) envfree; + + function _.getPowerCurrent( + address, + IGovernancePowerDelegationToken.GovernancePowerType + ) external => DISPATCHER(true); +} + + +// Utils ======================================================================= + +/// @title Each dummy token is a unique and accepted token +function eachDummyIsUniqueToken() { + // Tokens are unique + require ( + _DummyTokenA != _DummyTokenB && + _DummyTokenA != _DummyTokenC && + _DummyTokenB != _DummyTokenC + ); + + // Tokens are accepted + uint128 slotA; + uint128 slotB; + uint128 slotC; + require ( + isTokenSlotAccepted(_DummyTokenA, slotA) && + isTokenSlotAccepted(_DummyTokenB, slotB) && + isTokenSlotAccepted(_DummyTokenC, slotC) + ); +} + + +/// @title Return the value of the correct power type +function _getPower( + address voter, + IGovernancePowerDelegationToken.GovernancePowerType govType +) returns uint256 { + return ( + govType == IGovernancePowerDelegationToken.GovernancePowerType.VOTING ? + getFullVotingPower(voter) : + getFullPropositionPower(voter) + ); +} + + +// Rules ======================================================================= + +/// @title Invalid token or slot is refused - a unittest +rule invalidTokenRefused(address token, uint128 slot) { + require ( + (token != AAVE() || slot != BASE_BALANCE_SLOT()) && + (token != STK_AAVE() || slot != BASE_BALANCE_SLOT()) && + ( + token != A_AAVE() || + (slot != A_AAVE_BASE_BALANCE_SLOT() && slot != A_AAVE_DELEGATED_STATE_SLOT()) + ) + ); + assert !isTokenSlotAccepted(token, slot); +} + + +/// @title No power in each token implies no power at all +rule powerlessCompliance( + address voter, + IGovernancePowerDelegationToken.GovernancePowerType govType +) { + eachDummyIsUniqueToken(); + require ( + _DummyTokenA.getPowerCurrent(voter, govType) == 0 && + _DummyTokenB.getPowerCurrent(voter, govType) == 0 && + _DummyTokenC.getPowerCurrent(voter, govType) == 0 + ); + + assert _getPower(voter, govType) == 0; +} + + +/** @title Transferring does not raise the power of the sender nor lowers the power of + * the receiver, except in specific cases + */ +rule transferPowerCompliance( + address voter, + address another, + uint256 amount, + IGovernancePowerDelegationToken.GovernancePowerType govType +) { + require ( + voter != _DummyTokenA && voter != another && another != _DummyTokenA + ); + eachDummyIsUniqueToken(); + + // `voter`'s power can increase if `another` delegated its power to `voter` + address delegatee = _DummyTokenA.getDelegateeByType(another, govType); + + uint256 prePowerVoter = _getPower(voter, govType); + uint256 prePowerAnother = _getPower(another, govType); + + env e; + require e.msg.sender == voter; + _DummyTokenA.transfer(e, another, amount); + + uint256 postPowerVoter = _getPower(voter, govType); + uint256 postPowerAnother = _getPower(another, govType); + + assert (amount == 0) => ( + postPowerVoter == prePowerVoter && postPowerAnother == prePowerAnother + ); + assert (amount > 0) => ( + (postPowerVoter > prePowerVoter) => ( + delegatee == voter && + postPowerAnother <= prePowerAnother + ) && + (delegatee == 0) => (postPowerAnother > prePowerAnother) + ); +} + + +/** @title Delegating does not increase the power of the delegator, nor reduce the + * power of the new delegatee. + * + * @notice This rule cannot include a strict inequality. The reason is that + * the delegated power in `AaveTokenV3` is saved as rounded down balance, + * see `BaseDelegation._governancePowerTransferByType` (dividing by `POWER_SCALE_FACTOR` + * when setting the delegated power field and multiplying by it when extracting). + * This causes violations when trying to assert propositions such as: + * (postPowerNewDelegatee > prePowerNewDelegatee) <=> + * (postPowerCurDelegatee < prePowerCurDelegatee) + */ +rule delegatePowerCompliance( + address voter, + address newDelegatee, + IGovernancePowerDelegationToken.GovernancePowerType govType +) { + require ( + voter != 0 && + voter != _DummyTokenA && + voter != newDelegatee && + newDelegatee != 0 && + newDelegatee != _DummyTokenA + ); + eachDummyIsUniqueToken(); + + uint256 prePowerVoter = _getPower(voter, govType); + uint256 prePowerNewDelegatee = _getPower(newDelegatee, govType); + + // The current delegatee is zero if there is none + address getDelegRes = _DummyTokenA.getDelegateeByType(voter, govType); + address curDelegatee = getDelegRes == 0 ? voter : getDelegRes; + uint256 prePowerCurDelegatee = _getPower(curDelegatee, govType); + require newDelegatee != curDelegatee; + + env e; + require e.msg.sender == voter; + _DummyTokenA.delegateByType(e, newDelegatee, govType); + + uint256 postPowerVoter = _getPower(voter, govType); + uint256 postPowerNewDelegatee = _getPower(newDelegatee, govType); + uint256 postPowerCurDelegatee = _getPower(curDelegatee, govType); + + assert ( + postPowerVoter <= prePowerVoter && + postPowerNewDelegatee >= prePowerNewDelegatee && + postPowerCurDelegatee <= prePowerCurDelegatee + ); +} diff --git a/security/certora/specs/VotingStrategy_unittests.spec b/security/certora/specs/VotingStrategy_unittests.spec new file mode 100644 index 0000000..06c6589 --- /dev/null +++ b/security/certora/specs/VotingStrategy_unittests.spec @@ -0,0 +1,190 @@ +/* Setup for `VotingStrategy` contract ========================================= + * Since we can't really verify `VotingStrategy.getWeightedPower` (which is the main + * method), we opted for some "unit tests". + * + * TODO: + * - Still missing unit tests for delegated power! + * - General rules, e.g. voting power is monotonic increasing with balance ... + */ +using DelegationModeHarness as _Delegation; + + +methods +{ + // VotingStrategy ========================================================== + function AAVE() external returns (address) envfree; + function A_AAVE() external returns (address) envfree; + function STK_AAVE() external returns (address) envfree; + function STK_AAVE_SLASHING_EXCHANGE_RATE_PRECISION() external returns (uint256) envfree; + function POWER_SCALE_FACTOR() external returns (uint256) envfree; + function BASE_BALANCE_SLOT() external returns (uint128) envfree; + function A_AAVE_BASE_BALANCE_SLOT() external returns (uint128) envfree; + function A_AAVE_DELEGATED_STATE_SLOT() external returns (uint128) envfree; + + function isTokenSlotAccepted(address, uint128) external returns (bool) envfree; + + // DataWarehouse =========================================================== + function DataWarehouse.getRegisteredSlot( + bytes32 blockHash, + address account, + bytes32 slot + ) external returns (uint256) => _getRegisteredSlot(blockHash, account, slot); + + // DelegationModeHarness =================================================== + function DelegationModeHarness.is_equal_to_original() external returns (bool) envfree; + function DelegationModeHarness.mode_to_int( + DelegationModeHarness.Mode + ) external returns (uint8) envfree; +} + + +// Summarize `getRegisteredSlot` =============================================== +ghost mapping(address => uint256) _exchangeRateSlotValue; + +/** + * @title Summarize `getRegisteredSlot` + * The summary is intended to be used for calculating `exchangeRateSlotValue`, as + * constant per asset (=account). + */ +function _getRegisteredSlot( + bytes32 blockHash, + address account, + bytes32 slot +) returns uint256 { + return _exchangeRateSlotValue[account]; +} + + +// Utilities =================================================================== +function constructPower( + address asset, + uint120 balance, + uint72 delegated, + DelegationModeHarness.Mode delegation, + uint128 slot +) returns uint256 { + // Only `A_AAVE` uses `uint120` as balance, the others use `uint104` + require asset != A_AAVE() => balance <= max_uint104; + + // The delegation mode is the highest 8 bits of `power` + uint8 delegationMode = _Delegation.mode_to_int(delegation); + mathint power = balance + (2^(256 - 8)) * delegationMode; + return isTokenSlotAccepted(asset, slot) ? assert_uint256(power) : 0; +} + + +// Test `DelegationMode` hack ================================================== + +/** @title Verify that `DelegationModeHarness.Mode` equals `DelegationMode` + * + * @notice The reason for using `DelegationModeHarness.Mode` in the first place is + * that `DelegationMode` is an enum that is not part of any contract. So it cannot be + * used inside the spec. Therefore I created `DelegationModeHarness` that has + * an enum `DelegationModeHarness.Mode` that is supposed to be equal to the original + * `DelegationMode`. This spec uses `DelegationModeHarness.Mode`, and this rule + * verifies that it is equal to `DelegationMode`. + */ +rule delegationModeHackTest() { + assert _Delegation.is_equal_to_original(), "DelegationMode changed"; +} + + +// Unit-tests for `getVotingPower` ============================================= + +/// @title Zero power implies zero voting power +rule zeroPowerIsZeroVotingPower( + address asset, + uint128 baseStorageSlot, + bytes32 blockHash +) { + env e; + uint256 votingPower = getVotingPower(e, asset, baseStorageSlot, 0, blockHash); + assert votingPower == 0, "Non-zero voting power despite power being zero"; +} + + +/// @title Undelegated balance is roughly voting power (for `AAVE` and `A_AAVE`) +rule UnDelegatedBalanceIsPower_AAVE_A_AAVE( + address asset, + bytes32 blockHash, + uint120 balance, + DelegationModeHarness.Mode delegation +) { + require asset == AAVE() || asset == A_AAVE(); + require ( + delegation != DelegationModeHarness.Mode.VOTING_DELEGATED && + delegation != DelegationModeHarness.Mode.FULL_POWER_DELEGATED + ); // Voter's balance is not delegated for voting + + uint128 storageSlot = ( + (asset == AAVE()) ? BASE_BALANCE_SLOT() : A_AAVE_BASE_BALANCE_SLOT() + ); + uint256 raw_power = constructPower(asset, balance, 0, delegation, storageSlot); + + env e; + uint256 votingPower = getVotingPower(e, asset, storageSlot, raw_power, blockHash); + + uint256 calc_power = require_uint256(balance); + assert votingPower == calc_power, "Undelegated balance != power in AAVE/A_AAVE"; +} + + +/// @title Formula for voting power given only undelegated balance in `STK_AAVE` +rule UnDelegatedBalancePower_STK_AAVE( + bytes32 blockHash, + uint104 balance, + DelegationModeHarness.Mode delegation +) { + address asset = STK_AAVE(); + require ( + delegation != DelegationModeHarness.Mode.VOTING_DELEGATED && + delegation != DelegationModeHarness.Mode.FULL_POWER_DELEGATED + ); // Voter's balance is not delegated for voting + uint256 raw_power = constructPower( + asset, balance, 0, delegation, BASE_BALANCE_SLOT() + ); + // The code uses only the `uint216` part of the slot, the require below + // allows us to use the same value. + uint256 exchangeRate = require_uint216(_exchangeRateSlotValue[asset]); + + env e; + uint256 votingPower = getVotingPower( + e, asset, BASE_BALANCE_SLOT(), raw_power, blockHash + ); + + mathint calculated = ( + (balance * STK_AAVE_SLASHING_EXCHANGE_RATE_PRECISION()) / exchangeRate + ); + uint256 calc_power = assert_uint256(calculated); + assert votingPower == calc_power, "Undelegated balance != power in STK_AAVE"; +} + + +/// @title Wrong slot yields zero power +rule wrongSlotYieldsZeroPower( + address asset, + uint128 baseStorageSlot, + uint256 power, + bytes32 blockHash +) { + require !isTokenSlotAccepted(asset, baseStorageSlot); + + env e; + uint256 votingPower = getVotingPower(e, asset, baseStorageSlot, power, blockHash); + assert votingPower == 0, "Non-zero voting power despite wrong baseStorageSlot"; +} + + +/// @title Wrong asset yields zero power +rule wrongAssetYieldsZeroPower( + address asset, + uint128 baseStorageSlot, + uint256 power, + bytes32 blockHash +) { + require asset != AAVE() && asset != A_AAVE() && asset != STK_AAVE(); + + env e; + uint256 votingPower = getVotingPower(e, asset, baseStorageSlot, power, blockHash); + assert votingPower == 0, "Non-zero voting power despite wrong asset"; +} diff --git a/security/certora/specs/payloads/PayloadsController.spec b/security/certora/specs/payloads/PayloadsController.spec new file mode 100644 index 0000000..e81118d --- /dev/null +++ b/security/certora/specs/payloads/PayloadsController.spec @@ -0,0 +1,851 @@ + + +methods { + + //Summarization + function Executor.executeTransaction(address,uint256,string,bytes,bool) external returns (bytes) => NONDET; + // todo: summarize low-level ETH transfer in emergencyEtherTransfer() + function _.transfer(address,uint256) external => DISPATCHER(true); + + //Envfree methods + function getActionsLength(uint40) external returns (uint256) envfree; + function getPayloadsCount() external returns (uint40) envfree; + function getMaximumAccessLevelRequired(uint40) external returns (PayloadsControllerUtils.AccessControl) envfree; + function getActionFixedSizeFields(uint40,uint256) external returns (address,bool,PayloadsControllerUtils.AccessControl,uint256) envfree; + function getAction(uint40,uint256) external returns (IPayloadsControllerCore.ExecutionAction) envfree; + function getActionAccessLevel(uint40, uint256) external returns (PayloadsControllerUtils.AccessControl) envfree; + function getActionSignature(uint40,uint256) external returns (string) envfree; + function getActionCallData(uint40,uint256) external returns (bytes) envfree; + function compare(string,string) external returns (bool) envfree; + function compare(bytes,bytes) external returns (bool) envfree; + function getExecutorSettingsByAccessControl(PayloadsControllerUtils.AccessControl) external returns (IPayloadsControllerCore.ExecutorConfig) envfree; + function getPayloadById(uint40) external returns (IPayloadsControllerCore.Payload); + function getPayloadFieldsById(uint40 payloadId) external + returns (address,PayloadsControllerUtils.AccessControl,IPayloadsControllerCore.PayloadState,uint40,uint40,uint40,uint40,uint40,uint40,uint40) envfree; + function getPayloadQueuedAtById(uint40 payloadId) external returns (uint40) envfree; + function getPayloadExpirationTimeById(uint40 payloadId) external returns (uint40) envfree; + function getPayloadGracePeriod(uint40 payloadId) external returns (uint40) envfree; + function getPayloadDelay(uint40 payloadId) external returns (uint40) envfree; + function getPayloadCreatedAt(uint40 payloadId) external returns (uint40) envfree; + function getPayloadQueuedAt(uint40 payloadId) external returns (uint40) envfree; + function getPayloadExecutedAt(uint40 payloadId) external returns (uint40) envfree; + + + function getPayloadStateVariable(uint40) external returns (IPayloadsControllerCore.PayloadState) envfree; + function getCreator(uint40) external returns (address) envfree; + function getExpirationTime(uint40) external returns (uint40) envfree; + function MIN_EXECUTION_DELAY() external returns (uint40) envfree; + function decodeMessage(bytes) external returns (uint40, PayloadsControllerUtils.AccessControl, uint40) envfree; + function encodeMessage(uint40,PayloadsControllerUtils.AccessControl,uint40) external returns (bytes) envfree; + + function GRACE_PERIOD() external returns (uint40) envfree; + function MIN_EXECUTION_DELAY() external returns (uint40) envfree; + function MAX_EXECUTION_DELAY() external returns (uint40) envfree; + function EXPIRATION_DELAY() external returns (uint40) envfree; +} + +// +// CVL Functions extracting struct fields +// +function get_action_executor(uint40 payloadID, uint256 actionID) returns address{ + IPayloadsControllerCore.ExecutorConfig executorCfg = getExecutorSettingsByAccessControl(getActionAccessLevel(payloadID, actionID)); + return executorCfg.executor; +} +function get_executor_of_maximumAccessLevelRequired(uint40 id) returns address{ + IPayloadsControllerCore.ExecutorConfig executorCfg = getExecutorSettingsByAccessControl(getMaximumAccessLevelRequired(id)); + return executorCfg.executor; +} + +function get_delay_of_maximumAccessLevelRequired(uint40 id) returns uint40{ + PayloadsControllerUtils.AccessControl maximumAccessLevelRequired = getMaximumAccessLevelRequired(id); + IPayloadsControllerCore.ExecutorConfig executorCfg = getExecutorSettingsByAccessControl(maximumAccessLevelRequired); + return executorCfg.delay; +} + +function get_executor(PayloadsControllerUtils.AccessControl access_level) returns address{ + IPayloadsControllerCore.ExecutorConfig executorCfg = getExecutorSettingsByAccessControl(access_level); + return executorCfg.executor; +} +function get_delay(PayloadsControllerUtils.AccessControl access_level) returns uint40{ + IPayloadsControllerCore.ExecutorConfig executorCfg = getExecutorSettingsByAccessControl(access_level); + return executorCfg.delay; +} + +// +// Helpers +// + +// +// Payloads +// +// General: a payload is uninitialized and has no actions unless it was added to _payloads mapping + +/// @title A payload has no actions if it's beyond _payloadsCount +invariant empty_actions_if_out_of_bound_payload(uint40 id) + id >= getPayloadsCount() => getActionsLength(id) == 0; + + +/// @title Payload state is None if the payload is beyond _payloadsCount +invariant null_state_variable_if_out_of_bound_payload(uint40 id) + id >= getPayloadsCount() => getPayloadStateVariable(id) == IPayloadsControllerCore.PayloadState.None; + +/// @title Payload maximal access level is null (not valid) if the payload is beyond _payloadsCount +invariant null_access_level_if_out_of_bound_payload(uint40 id) + id >= getPayloadsCount() => getMaximumAccessLevelRequired(id) == PayloadsControllerUtils.AccessControl.Level_null; + +/// @title Payload creator is address(0) and it expiration time is zero if the payload is beyond _payloadsCount +invariant null_creator_and_zero_expiration_time_if_out_of_bound_payload(uint40 id) + id >= getPayloadsCount() => + getCreator(id) == 0 && getExpirationTime(id) == 0 + && getPayloadGracePeriod(id) == 0 && getPayloadDelay(id) == 0; + +/// @title Payload's maximal access level is null if and only if state is none +invariant null_access_level_iff_state_is_none(uint40 id) + getMaximumAccessLevelRequired(id) == PayloadsControllerUtils.AccessControl.Level_null <=> + getPayloadStateVariable(id) == IPayloadsControllerCore.PayloadState.None; + + + +// +// Actions +// + +/// @title accessLevel of a valid action is not null +/// @dev a helper invariant +invariant nonempty_actions(uint40 payloadID, uint256 actionID) + getActionsLength(payloadID) != 0 && actionID < getActionsLength(payloadID) + => getActionAccessLevel(payloadID, actionID) != PayloadsControllerUtils.AccessControl.Level_null; + + +/// @title executor of a valid action is not address(0) +/// @dev a helper invariant +//Todo: improve runtime, replace logical condition +invariant executor_exists(uint40 payloadID, uint256 actionID) + getActionsLength(payloadID) != 0 && actionID < getActionsLength(payloadID) + => get_action_executor(payloadID, actionID) != 0; + +/// @title Action's accessLevel is not null iff it's executor is not address(0) +invariant executor_exists_iff_action_not_null(uint40 payloadID, uint256 actionID) + getActionAccessLevel(payloadID, actionID) == PayloadsControllerUtils.AccessControl.Level_null + <=> get_action_executor(payloadID, actionID) == 0; + + +/// @title A payload maximal access level is greater than or equal to the access level of its action +invariant payload_maximal_access_level_gt_action_access_level(uint40 payloadID, uint256 actionID) + getActionAccessLevel(payloadID, actionID) == PayloadsControllerUtils.AccessControl.Level_2 => + getMaximumAccessLevelRequired(payloadID) == PayloadsControllerUtils.AccessControl.Level_2 + { + preserved{ + requireInvariant empty_actions_if_out_of_bound_payload(payloadID); + } + } + + +/// @title Action's accessLevel is not null if it's executor is not address(0) +//runtime error: CERT-2868 +invariant executor_exists_if_action_not_null(uint40 payloadID, uint256 actionID) + getActionAccessLevel(payloadID, actionID) != PayloadsControllerUtils.AccessControl.Level_null + => get_action_executor(payloadID, actionID) != 0; + + +/// @title Action's accessLevel is not null only if it's executor is not address(0) +//not required +invariant executor_exists_only_if_action_not_null(uint40 payloadID, uint256 actionID) + getActionAccessLevel(payloadID, actionID) == PayloadsControllerUtils.AccessControl.Level_null + => get_action_executor(payloadID, actionID) == 0; + + +// +// Additional properties +// + + +//todo: check 2.1.2. If timelock > gracePeriod then no proposal could be executed + +// Reported a violation; Solidity had been fixed. +/// @title A valid payload must have valid maximal access level +invariant null_access_level_only_if_out_of_bound_payload(uint40 id) + id < getPayloadsCount() => getMaximumAccessLevelRequired(id) != PayloadsControllerUtils.AccessControl.Level_null; + + + + +// +// From properties.md +// + +/// @title Property #1: Payloads IDs are consecutive +rule consecutiveIDs(method f) filtered { f-> !f.isView }{ + + env e1; env e2; env e3; + calldataarg args1; calldataarg args2; calldataarg args3; + + mathint id_first = createPayload(e1, args1); + f(e2, args2); + mathint id_second = createPayload(e3, args3); + assert ((f.selector != sig:createPayload(IPayloadsControllerCore.ExecutionAction[]).selector) => (id_second == id_first + 1)); + assert ((f.selector == sig:createPayload(IPayloadsControllerCore.ExecutionAction[]).selector) => (id_second == id_first + 2)); + +} + +/// @title Property #2: A payload must have at least one action +/// @notice An initialized payload has at least one action +/// @notice A payload is empty if its max access level is Null or the state is None or expiration time is zero. +invariant empty_actions_only_if_uninitialized_payload (uint40 id) + (getMaximumAccessLevelRequired(id) != PayloadsControllerUtils.AccessControl.Level_null + || getPayloadStateVariable(id) != IPayloadsControllerCore.PayloadState.None + || getPayloadExpirationTimeById(id) != 0 ) + => getActionsLength(id) > 0 + { + preserved{ + requireInvariant empty_actions_if_out_of_bound_payload(id); + } + } + +/// @title A payload with no actions is uninitialized +invariant empty_actions_if_uninitialized_payload(uint40 id) + ((getActionsLength(id) > 0) => (getMaximumAccessLevelRequired(id) != PayloadsControllerUtils.AccessControl.Level_null) ); + +/// @title A payload has actions if and only if it's initialized +invariant empty_actions_iff_uninitialized(uint40 id) + ((getMaximumAccessLevelRequired(id) == PayloadsControllerUtils.AccessControl.Level_null) <=> (getActionsLength(id) == 0)) + { + preserved{ + requireInvariant null_access_level_if_out_of_bound_payload(id); + } + } + + + +/// @title Property #3.1 : The following Payload params can only be set once during payload creation: ipfsHash +/// @notice verify that additional params are immutable: accessLevel, maximumAccessLevelRequired, creator, createdAt, expirationTime +rule payload_fields_immutable_after_createPayload(method f, uint40 id)filtered { f-> !f.isView }{ + env e1; env e2; + calldataarg args1; calldataarg args2; + + createPayload(e1, args1); + uint40 payload_count = getPayloadsCount(); + + address creator_before; + PayloadsControllerUtils.AccessControl maximumAccessLevelRequired_before; + IPayloadsControllerCore.PayloadState state_before; + uint40 createdAt_before; + uint40 queuedAt_before; + uint40 executedAt_before; + uint40 cancelledAt_before; + uint40 expirationTime_before; + uint40 delay_before; + uint40 gracePeriod_before; + + creator_before, maximumAccessLevelRequired_before, state_before, createdAt_before, + queuedAt_before, executedAt_before, cancelledAt_before, expirationTime_before, + delay_before, gracePeriod_before = + getPayloadFieldsById(id); + + f(e2, args2); + + address creator_after; + PayloadsControllerUtils.AccessControl maximumAccessLevelRequired_after; + IPayloadsControllerCore.PayloadState state_after; + uint40 createdAt_after; + uint40 queuedAt_after; + uint40 executedAt_after; + uint40 cancelledAt_after; + uint40 expirationTime_after; + uint40 delay_after; + uint40 gracePeriod_after; + + creator_after, maximumAccessLevelRequired_after, state_after, createdAt_after, + queuedAt_after, executedAt_after, cancelledAt_after, expirationTime_after, + delay_after, gracePeriod_after = + getPayloadFieldsById(id); + +//todo: replace if with invariant + assert id < payload_count => creator_before == creator_after; + assert id < payload_count => maximumAccessLevelRequired_before == maximumAccessLevelRequired_after; + assert id < payload_count => createdAt_before == createdAt_after; + assert id < payload_count => expirationTime_before == expirationTime_after; + assert id < payload_count => delay_before == delay_after; + assert id < payload_count => gracePeriod_before == gracePeriod_after; + +} + + + +/// @title Payload fields can be initialized only once: ipfsHash, accessLevel, maximumAccessLevelRequired, creator, createdAt, expirationTime +rule initialized_payload_fields_are_immutable(method f, uint40 id)filtered { f-> !f.isView }{ + env e; + calldataarg args; + + requireInvariant null_access_level_if_out_of_bound_payload(id); + requireInvariant null_creator_and_zero_expiration_time_if_out_of_bound_payload(id); + + address creator_before; + PayloadsControllerUtils.AccessControl maximumAccessLevelRequired_before; + IPayloadsControllerCore.PayloadState state_before; + uint40 createdAt_before; + uint40 queuedAt_before; + uint40 executedAt_before; + uint40 cancelledAt_before; + uint40 expirationTime_before; + uint40 delay_before; + uint40 gracePeriod_before; + + creator_before, maximumAccessLevelRequired_before, state_before, createdAt_before, + queuedAt_before, executedAt_before, cancelledAt_before, expirationTime_before, delay_before, gracePeriod_before = + getPayloadFieldsById(id); + f(e, args); + + address creator_after; + PayloadsControllerUtils.AccessControl maximumAccessLevelRequired_after; + IPayloadsControllerCore.PayloadState state_after; + uint40 createdAt_after; + uint40 queuedAt_after; + uint40 executedAt_after; + uint40 cancelledAt_after; + uint40 expirationTime_after; + uint40 delay_after; + uint40 gracePeriod_after; + + creator_after, maximumAccessLevelRequired_after, state_after, createdAt_after, + queuedAt_after, executedAt_after, cancelledAt_after, expirationTime_after, delay_after, gracePeriod_after = + getPayloadFieldsById(id); + + assert maximumAccessLevelRequired_before != PayloadsControllerUtils.AccessControl.Level_null => creator_before == creator_after; + assert maximumAccessLevelRequired_before != PayloadsControllerUtils.AccessControl.Level_null => + maximumAccessLevelRequired_before == maximumAccessLevelRequired_after; + assert maximumAccessLevelRequired_before != PayloadsControllerUtils.AccessControl.Level_null => createdAt_before == createdAt_after; + assert maximumAccessLevelRequired_before != PayloadsControllerUtils.AccessControl.Level_null => expirationTime_before == expirationTime_after; + assert maximumAccessLevelRequired_before != PayloadsControllerUtils.AccessControl.Level_null => delay_before == delay_after; + assert maximumAccessLevelRequired_before != PayloadsControllerUtils.AccessControl.Level_null => gracePeriod_before == gracePeriod_after; + + assert creator_before != 0 => creator_before == creator_after; + assert expirationTime_before != 0 => expirationTime_before == expirationTime_after; + + assert gracePeriod_before != 0 => delay_before == delay_after; + assert gracePeriod_before != 0 => gracePeriod_before == gracePeriod_after; + +} + + + +/// @title Property #3.2 : The following Payload params can only be set once during payload creation: +/// actions fields: target, withDelegateCall, accessLevel, value, signature, callDAta +//todo: this rule should replace the following 3 rules once CERT-2451 (timeout) is resolved +// rule action_immutable(method f)filtered { f-> !f.isView }{ + +// env e; +// calldataarg args; +// uint40 payloadID; +// uint256 action_index; + +// require getActionsLength(payloadID) < 2^100; + + +// IPayloadsControllerCore.ExecutionAction action_before = getAction(payloadID, action_index); +// f(e, args); +// IPayloadsControllerCore.ExecutionAction action_after = getAction(payloadID, action_index); + +// assert action_before.target == action_after.target; +// assert action_before.withDelegateCall == action_after.withDelegateCall; +// assert action_before.accessLevel == action_after.accessLevel; +// assert action_before.value == action_after.value; +// assert compare(action_before.signature, action_after.signature); +// assert compare(action_before.callData, action_after.callData); +// } + +rule action_immutable_check_only_fixed_size_fields(method f)filtered { f-> !f.isView }{ + + env e; + calldataarg args; + uint40 payloadID; + uint256 action_index; + + require getActionsLength(payloadID) < 2^100; + + + IPayloadsControllerCore.ExecutionAction action_before = getAction(payloadID, action_index); + f(e, args); + IPayloadsControllerCore.ExecutionAction action_after = getAction(payloadID, action_index); + + assert action_before.target == action_after.target; + assert action_before.withDelegateCall == action_after.withDelegateCall; + assert action_before.accessLevel == action_after.accessLevel; + assert action_before.value == action_after.value; +} + +/// @title Property #3.2.2 : The following Payload params can only be set once during payload creation: +/// actions fields: target, withDelegateCall, accessLevel, value +/// @dev check fixed-size fields only +//todo: remove rule once CERT-2451 (timeout) is resolved +rule action_immutable_fixed_size_fields(method f){ + + env e; + calldataarg args; + uint40 payloadID; + uint256 action_index; + + require getActionsLength(payloadID) < 2^100; + + address target_before; + bool withDelegateCall_before; + PayloadsControllerUtils.AccessControl accessLevel_before; + uint256 value_before; + target_before, withDelegateCall_before, accessLevel_before, value_before = getActionFixedSizeFields(payloadID, action_index); + + f(e, args); + address target_after; + bool withDelegateCall_after; + PayloadsControllerUtils.AccessControl accessLevel_after; + uint256 value_after; + target_after, withDelegateCall_after, accessLevel_after, value_after = getActionFixedSizeFields(payloadID, action_index); + + assert target_before == target_after; + assert withDelegateCall_before == withDelegateCall_after; + assert accessLevel_before == accessLevel_after; + assert value_before == value_after; +} + + +/// @title Property #3.2.3 : The following Payload params can only be set once during payload creation: +/// actions fields: signature +/// @dev check signature only to reduce rune time +//todo: remove rule once CERT-2451 (timeout) is resolved +rule action_signature_immutable(method f)filtered { f-> !f.isView }{ + + env e; + calldataarg args; + uint40 payloadID; + uint256 action_index; + + require getActionsLength(payloadID) < 2^100;// todo: remove and add flag --optimistic_storage_array_length once CERT-2577 is resolved + + string signature_before = getActionSignature(payloadID, action_index); + f(e, args); + string signature_after = getActionSignature(payloadID, action_index); + assert compare(signature_before, signature_after); +} + + +/// @title Property #3.2.3 : The following Payload params can only be set once during payload creation: +/// actions fields: callData +/// @dev check callData only to reduce rune time +//todo: remove rule once CERT-2451 (timeout) is resolved +rule action_callData_immutable(method f) filtered { f-> !f.isView } +{ + + env e; + calldataarg args; + uint40 payloadID; + uint256 action_index; + + require getActionsLength(payloadID) < 2^100;// todo: remove and add flag --optimistic_storage_array_length once CERT-2577 is resolved + + bytes callData_before = getActionCallData(payloadID, action_index); + f(e, args); + bytes callData_after = getActionCallData(payloadID, action_index); + assert compare(callData_before, callData_after); +} + + + +// @title Property #4: An Executor must exist of the max level required for the payload actions (action must be able to be executed) + +// Payload executor is not address zero +rule executor_exists_after_createPayload() +{ + env e; + calldataarg args; + uint256 actionID; + uint40 payloadID = createPayload(e,args); + + IPayloadsControllerCore.ExecutionAction action = getAction(payloadID, actionID); + IPayloadsControllerCore.ExecutorConfig executorCfg = getExecutorSettingsByAccessControl(action.accessLevel); + + requireInvariant executor_exists(payloadID, actionID); + assert executorCfg.executor != 0; +} + +// @title action access level is not null after creation +// @dev split rules to reduce run time + +// Payload action access level is not null +rule action_access_level_isnt_null_after_createPayload() +{ + env e; + calldataarg args; + + uint40 payloadID = createPayload(e,args); + uint256 actionID; + IPayloadsControllerCore.ExecutionAction action = getAction(payloadID, actionID); + + requireInvariant nonempty_actions(payloadID, actionID); + assert action.accessLevel != PayloadsControllerUtils.AccessControl.Level_null; +} + +// Payload maximal access level is not null +rule executor_of_maximumAccessLevelRequired_exists_after_createPayload() +{ + env e; + calldataarg args; + uint40 id = createPayload(e,args); + + assert get_executor_of_maximumAccessLevelRequired(id) != 0; + + PayloadsControllerUtils.AccessControl maximumAccessLevelRequired = getMaximumAccessLevelRequired(id); + assert maximumAccessLevelRequired != PayloadsControllerUtils.AccessControl.Level_null; + +} + +// Once set the executor of a payload maximal access level is not address zero +rule executor_of_maximumAccessLevelRequired_exists(method f) filtered { f-> !f.isView } +{ + env e; + calldataarg args; + uint40 id; + require get_executor_of_maximumAccessLevelRequired(id) != 0; + f(e,args); + assert get_executor_of_maximumAccessLevelRequired(id) != 0; +} + + +/// @title Property #5: A Payload can only be executed when in queued state and time lock has finished and before the grace period has passed. +/// @notice executePayload() should not check check gracePeriod of every actions. +/// @notice it checks only the executor of the maximal access level. +// +rule execute_before_delay__maximumAccessLevelRequired{ + env e; + uint40 id; + requireInvariant payload_grace_period_eq_global_grace_period(id); + requireInvariant null_access_level_iff_state_is_none(id); + + executePayload(e, id); + mathint timestamp = e.block.timestamp; + assert timestamp > getPayloadQueuedAtById(id) + getPayloadDelay(id); + assert timestamp < getPayloadQueuedAtById(id) + getPayloadDelay(id) + GRACE_PERIOD(); +} + + +/// @title Payload's maximal level delay is equal to the executor delay the payload's maximumAccessLevelRequired +//fail. reported on June 13, 2023 +//invariant payload_delay_eq_delay_of_executor_of_max_access_level(uint40 id) +// getPayloadDelay(id) == get_delay_of_maximumAccessLevelRequired(id); + + +// @title A Payload can only be executed when in queued state +rule executed_when_in_queued_state{ + env e; + uint40 payloadId; + + IPayloadsControllerCore.PayloadState state_before = getPayloadStateVariable(payloadId); + executePayload(e,payloadId); + assert state_before == IPayloadsControllerCore.PayloadState.Queued; +} + + + +// @title property #7: The Guardian can cancel a Payload if it has not been executed +// A payload cannot execute after a guardian cancelled it +rule guardian_can_cancel{ + + env e1; env e2; env e3; + calldataarg args; + method f; + uint40 payloadId; + cancelPayload(e1, payloadId); + f(e2, args); + executePayload(e3,payloadId); + assert false ; + + +} + +/// @title One can not cancel a payload before its creation +// It's impossible to cancel before creation +rule no_early_cancellation{ + env e1; env e2; + calldataarg args; + uint40 payloadId1; + + requireInvariant null_state_variable_if_out_of_bound_payload(payloadId1); + cancelPayload(e1, payloadId1); + uint40 payloadId2 = createPayload(e2,args); + assert payloadId1 != payloadId2; +} + +/// @title One can not cancel a payload after its execution +//It's impossible to cancel a payload after is was executed +rule no_late_cancel{ + + env e1; env e2; env e3; + calldataarg args; + method f; + uint40 payloadId; + requireInvariant null_state_variable_if_out_of_bound_payload(payloadId); + executePayload(e1,payloadId); + f(e2, args); + cancelPayload(e3, payloadId); + assert false ; +} + + +/// @title Property #8: Payload State can’t decrease +// Forward progress of payload state machine +rule state_cant_decrease +{ +env e; + calldataarg args; + method f; + uint40 payloadId; + + requireInvariant null_state_variable_if_out_of_bound_payload(payloadId); + + IPayloadsControllerCore.PayloadState state_before = getPayloadStateVariable(payloadId); + f(e,args); + IPayloadsControllerCore.PayloadState state_after = getPayloadStateVariable(payloadId); + assert state_before == IPayloadsControllerCore.PayloadState.Queued => state_after != IPayloadsControllerCore.PayloadState.Created; + assert state_before == IPayloadsControllerCore.PayloadState.Queued => state_after != IPayloadsControllerCore.PayloadState.None; + assert state_before == IPayloadsControllerCore.PayloadState.Created => state_after != IPayloadsControllerCore.PayloadState.None; + + +} + +/// @title Property #9: No further state transitions are possible if proposal.state > 3 +// State > 3 are terminal +rule no_transition_beyond_state_gt_3{ + + env e; + calldataarg args; + method f; + uint40 payloadId; + + requireInvariant null_state_variable_if_out_of_bound_payload(payloadId); + + IPayloadsControllerCore.PayloadState state_before = getPayloadState(e,payloadId); + require state_before == IPayloadsControllerCore.PayloadState.Executed + || state_before == IPayloadsControllerCore.PayloadState.Cancelled + || state_before == IPayloadsControllerCore.PayloadState.Expired ; + f(e,args); + IPayloadsControllerCore.PayloadState state_after = getPayloadState(e,payloadId); + assert state_before == state_after; +} + + +/// @title Property #9.1: No further state transitions are possible if proposal.state > 3 +/// @notice checking state storage variable +rule no_transition_beyond_state_variable_gt_3{ + + env e; + calldataarg args; + method f; + uint40 payloadId; + + requireInvariant null_state_variable_if_out_of_bound_payload(payloadId); + IPayloadsControllerCore.PayloadState state_before = getPayloadStateVariable(payloadId); + require state_before == IPayloadsControllerCore.PayloadState.Executed + || state_before == IPayloadsControllerCore.PayloadState.Cancelled ; + f(e,args); + IPayloadsControllerCore.PayloadState state_after = getPayloadStateVariable(payloadId); + assert state_before == state_after; +} + + +// +// Additional rules +// + + +// @title Payload's grace period is equal to the contract grace period +invariant payload_grace_period_eq_global_grace_period(uint40 id) + getMaximumAccessLevelRequired(id) != PayloadsControllerUtils.AccessControl.Level_null => getPayloadGracePeriod(id) == GRACE_PERIOD(); + + + + +// @title Payload's delay is in [MIN_EXECUTION_DELAY, MAX_EXECUTION_DELAY] +invariant payload_delay_within_range(uint40 id) + getMaximumAccessLevelRequired(id) != PayloadsControllerUtils.AccessControl.Level_null => + getPayloadDelay(id) >= MIN_EXECUTION_DELAY() && getPayloadDelay(id) <= MAX_EXECUTION_DELAY() + { + preserved { + + requireInvariant executor_access_level_within_range(PayloadsControllerUtils.AccessControl.Level_1); + requireInvariant executor_access_level_within_range(PayloadsControllerUtils.AccessControl.Level_2); + } + } + + +// @title Executor delay of payload's max access level is in [MIN_EXECUTION_DELAY, MAX_EXECUTION_DELAY] +invariant delay_of_executor_of_max_access_level_within_range(uint40 id) + getMaximumAccessLevelRequired(id) != PayloadsControllerUtils.AccessControl.Level_null => + get_delay_of_maximumAccessLevelRequired(id) >= MIN_EXECUTION_DELAY() && get_delay_of_maximumAccessLevelRequired(id) <= MAX_EXECUTION_DELAY() + { + preserved { + requireInvariant executor_access_level_within_range(PayloadsControllerUtils.AccessControl.Level_1); + requireInvariant executor_access_level_within_range(PayloadsControllerUtils.AccessControl.Level_2); + } + } + +// @title Executor delay is in [MIN_EXECUTION_DELAY, MAX_EXECUTION_DELAY] +invariant executor_access_level_within_range(PayloadsControllerUtils.AccessControl access_level) + get_executor(access_level) != 0 => + get_delay(access_level) >= MIN_EXECUTION_DELAY() && get_delay(access_level) <= MAX_EXECUTION_DELAY(); + + + + + + + +/// old version: A proposal can never be executed if lasting more than the expiration time defined per level of permissions. +/// @title Property #6: A Payload can never be executed if it has not been queued before the EXPIRATION_DELAY defined. +//TODO: add relay for field "delay" + + +invariant expirationTime_equal_to_createAt_plus_EXPIRATION_DELAY(uint40 id) + getPayloadStateVariable(id) != IPayloadsControllerCore.PayloadState.None => + getPayloadExpirationTimeById(id) <= require_uint40(EXPIRATION_DELAY() + getPayloadCreatedAt(id)); + + +//todo: check directly contract-level EXPIRATION_DELAY +/// @title Queue happens before creation time + EXPIRATION_DELAY +invariant queued_before_expiration_delay(uint40 id) + getPayloadQueuedAt(id) <= require_uint40(EXPIRATION_DELAY() + getPayloadCreatedAt(id)) + { + preserved with (env e){ + requireInvariant expirationTime_equal_to_createAt_plus_EXPIRATION_DELAY(id); + // requireInvariant created_in_the_past(e, id); + requireInvariant queuedAt_is_zero_before_queued(id); + // requireInvariant executedAt_is_zero_before_executed(id); + requireInvariant null_state_variable_if_out_of_bound_payload(id); + } + } + +//helper: creation time cannot be in the future +invariant created_in_the_past(env e1, uint40 id) + getPayloadCreatedAt(id) <= require_uint40(e1.block.timestamp) + { + preserved with (env e2){ + require e1.block.timestamp == e2.block.timestamp; + + } + } + +/// @title Queue happens after creation time +// queuing time cannot occur after creation time +invariant queued_after_created(uint40 id) + getPayloadQueuedAt(id) != 0 => getPayloadQueuedAt(id) >= getPayloadCreatedAt(id) + { + preserved with (env e){ + requireInvariant created_in_the_past(e, id); + requireInvariant queuedAt_is_zero_before_queued(id); + requireInvariant null_state_variable_if_out_of_bound_payload(id); + } + } + +/// @title Execution happens after queue +//execution time cannot be after queuing time +invariant executed_after_queue(uint40 id) + getPayloadExecutedAt(id) != 0 => getPayloadExecutedAt(id) >= getPayloadQueuedAt(id) + { + preserved{ + // requireInvariant queuedAt_is_zero_before_queued(id); + // requireInvariant null_state_variable_if_out_of_bound_payload(id); + requireInvariant executedAt_is_zero_before_executed(id); + } + } +//helper: queuing time is nonzero for initialized payloads +invariant queuedAt_is_zero_before_queued(uint40 id) + getPayloadStateVariable(id) == IPayloadsControllerCore.PayloadState.None || + getPayloadStateVariable(id) == IPayloadsControllerCore.PayloadState.Created => getPayloadQueuedAt(id) == 0 + +{ + preserved{ + requireInvariant null_state_variable_if_out_of_bound_payload(id); + } + } + +//helper: ExecutedAt == 0 before execution +invariant executedAt_is_zero_before_executed(uint40 id) + getPayloadStateVariable(id) == IPayloadsControllerCore.PayloadState.None || + getPayloadStateVariable(id) == IPayloadsControllerCore.PayloadState.Created || + getPayloadStateVariable(id) == IPayloadsControllerCore.PayloadState.Queued => getPayloadExecutedAt(id) == 0 + +{ + preserved{ + requireInvariant null_state_variable_if_out_of_bound_payload(id); + } + } + + +//helper: One cannot queue a payload if expiration time have elapsed +rule no_queue_after_expiration{ + env e; + uint40 payloadId; + + mathint expiration_time = getPayloadExpirationTimeById(payloadId); + mathint timestamp = e.block.timestamp; + address originSender; + uint256 originChainId; + PayloadsControllerUtils.AccessControl accessLevel; + uint40 proposalVoteActivationTimestamp; + + bytes message = encodeMessage(payloadId, accessLevel, proposalVoteActivationTimestamp); + receiveCrossChainMessage(e, originSender, originChainId, message); + + assert expiration_time > timestamp; +} + + + + + + +rule sanity{ + env e; + calldataarg arg; + method f; + f(e, arg); +// assert false; + satisfy true; +} + +// +// For Prover dev team +// + + + +//todo: remove this rule once CERT-2508 is closed +//pass +rule decode2encode_sanity_check_message_leq_96_pass{ + + uint40 payloadId; + PayloadsControllerUtils.AccessControl accessLevel; + uint40 proposalVoteActivationTimestamp; + bytes message_before; + require message_before.length <= 96; + payloadId, accessLevel, proposalVoteActivationTimestamp = decodeMessage(message_before); + + bytes message_after = encodeMessage(payloadId, accessLevel, proposalVoteActivationTimestamp); + + assert compare(message_before, message_after) == true; +} + +//todo: remove this rule once CERT-2508 is closed +//pass +rule decode2encode_sanity_check_message_eq_96_satisfy{ + + uint40 payloadId; + PayloadsControllerUtils.AccessControl accessLevel; + uint40 proposalVoteActivationTimestamp; + bytes message_before; + require message_before.length == 96; + payloadId, accessLevel, proposalVoteActivationTimestamp = decodeMessage(message_before); + + bytes message_after = encodeMessage(payloadId, accessLevel, proposalVoteActivationTimestamp); + + satisfy compare(message_before, message_after) == true; +} + + diff --git a/security/certora/specs/set.spec b/security/certora/specs/set.spec new file mode 100644 index 0000000..a72ce78 --- /dev/null +++ b/security/certora/specs/set.spec @@ -0,0 +1,191 @@ + +// +// Under approximation warning: checking a single chain ID at at time. +// +methods{ + // function getFacilitatorsListLen() external returns (uint256) envfree; + // function getSupportedChainsLength() external returns (uint256) envfree; + function getRepresentedVotersSize(address,uint256) external returns (uint256) envfree; + +} + +definition MAX_UINT256() returns uint256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; +definition MAX_UINT256Bytes32() returns bytes32 = to_bytes32(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); //todo: remove once CERT-1060 is resolved + +definition TWO_TO_160() returns uint256 = 0x10000000000000000000000000000000000000000; + + +/** +* Set map entries point to valid array entries +* @notice an essential condition of the set, should hold for evert Set implementation +* @return true if all map entries points to valid indexes of the array. +*/ +definition MAP_POINTS_INSIDE_ARRAY() returns bool = forall address rep. forall bytes32 a. mirrorMap[rep][a] <= mirrorArrayLen[rep]; +/** +* Set map is the inverse function of set array. +* @notice an essential condition of the set, should hold for evert Set implementation +* @notice this condition depends on the other set conditions, but the other conditions do not depend on this condition. +* If this condition is omitted the rest of the conditions still hold, but the other conditions are required to prove this condition. +* @return true if for every valid index of the array it holds that map(array(index)) == index + 1. +*/ +definition MAP_IS_INVERSE_OF_ARRAY() returns bool = forall address rep. forall uint256 i. (i < mirrorArrayLen[rep]) => to_mathint(mirrorMap[rep][mirrorArray[rep][i]]) == i + 1; + +/** +* Set array is the inverse function of set map +* @notice an essential condition of the set, should hold for evert Set implementation +* @return true if for every non-zero bytes32 value stored in in the set map it holds that array(map(value) - 1) == value +*/ +definition ARRAY_IS_INVERSE_OF_MAP() returns bool = forall address rep. forall bytes32 a. forall uint256 b. + ((to_mathint(b) == mirrorMap[rep][a]-1) => (mirrorMap[rep][a] != 0)) => (mirrorArray[rep][b] == a); + + + + +/** +* load array length +* @notice a dummy condition that forces load of array length, using it forces initialization of mirrorArrayLen +* @return always true +*/ +definition CVL_LOAD_ARRAY_LENGTH(address representative, uint256 chainId) returns bool + = (getRepresentedVotersSize(representative, chainId) == getRepresentedVotersSize(representative, chainId)); + +/** +* Set-general condition, encapsulating all conditions of Set +* @notice this condition recaps the general characteristics of Set. It should hold for all set implementations i.e. AddressSet, UintSet, Bytes32Set +* @return conjunction of the Set three essential properties. +*/ +definition SET_INVARIANT(address representative, uint256 chainId) returns bool = + MAP_POINTS_INSIDE_ARRAY() && MAP_IS_INVERSE_OF_ARRAY() && ARRAY_IS_INVERSE_OF_MAP() && CVL_LOAD_ARRAY_LENGTH(representative, chainId); + +/** + * Size of stored value does not exceed the size of an address type. + * @notice must be used for AddressSet, must not be used for Bytes32Set, UintSet + * @return true if all array entries are less than 160 bits. + **/ +definition VALUE_IN_BOUNDS_OF_TYPE_ADDRESS() returns bool = (forall address rep. forall uint256 i. (mirrorArray[rep][i]) & to_bytes32(max_uint160) == mirrorArray[rep][i]); + +/** + * A complete invariant condition for AddressSet + * @notice invariant addressSetInvariant proves that this condition holds + * @return conjunction of the Set-general and AddressSet-specific conditions + **/ +definition ADDRESS_SET_INVARIANT(address representative, uint256 chainId) returns bool = + SET_INVARIANT(representative, chainId) && VALUE_IN_BOUNDS_OF_TYPE_ADDRESS(); + +/** + * A complete invariant condition for UintSet, Bytes32Set + * @notice for UintSet and Bytes2St no type-specific condition is required because the type size is the same as the native type (bytes32) size + * @return the Set-general condition + **/ +definition UINT_SET_INVARIANT(address representative, uint256 chainId) returns bool = SET_INVARIANT(representative, chainId); + +/** + * Out of bound array entries are zero + * @notice A non-essential condition. This condition can be proven as an invariant, but it is not necessary for proving the Set correctness. + * @return true if all entries beyond array length are zero + **/ +definition ARRAY_OUT_OF_BOUND_ZERO() returns bool = forall address rep. forall uint256 i. (i >= mirrorArrayLen[rep]) => (mirrorArray[rep][i] == to_bytes32(0)); + +// For CVL use + +/** + * ghost mirror map, mimics Set map + **/ +ghost mapping(address => mapping(bytes32 => uint256)) mirrorMap{ + init_state axiom forall address rep. forall bytes32 a. mirrorMap[rep][a] == 0; + axiom forall address rep. forall bytes32 a. mirrorMap[rep][a] >= 0 && mirrorMap[rep][a] <= MAX_UINT256(); //todo: remove once https://certora.atlassian.net/browse/CERT-1060 is resolved + +} + +/** + * ghost mirror array, mimics Set array + **/ +ghost mapping(address => mapping(uint256 => bytes32)) mirrorArray{ + init_state axiom forall address rep. forall uint256 i. mirrorArray[rep][i] == to_bytes32(0); + axiom forall address rep. forall uint256 a. mirrorArray[rep][a] & MAX_UINT256Bytes32() == mirrorArray[rep][a]; +// axiom forall uint256 a. to_uint256(mirrorArray[a]) >= 0 && to_uint256(mirrorArray[a]) <= MAX_UINT256(); //todo: remove once CERT-1060 is resolved +//axiom forall uint256 a. to_mathint(mirrorArray[a]) >= 0 && to_mathint(mirrorArray[a]) <= MAX_UINT256(); //todo: use this axiom when cast bytes32 to mathint is supported +} + +/** + * ghost mirror array length, mimics Set array length + * @notice ghost includes an assumption about the array length. + * If the assumption were not written in the ghost function it should be written in every rule and invariant. + * The assumption holds: breaking the assumptions would violate the invariant condition 'map(array(index)) == index + 1'. Set map uses 0 as a sentinel value, so the array cannot contain MAX_INT different values. + * The assumption is necessary: if a value is added when length==MAX_INT then length overflows and becomes zero. + **/ +ghost mapping(address => uint256) mirrorArrayLen{ + init_state axiom forall address rep. mirrorArrayLen[rep] == 0; + axiom forall address rep. to_mathint(mirrorArrayLen[rep]) < TWO_TO_160() - 1; //todo: remove once CERT-1060 is resolved +} + + +/** + * hook for Set array stores + * @dev user of this spec must replace _list with the instance name of the Set. + **/ +hook Sstore _votersRepresented [KEY address rep] [KEY uint256 chain] .(offset 0)[INDEX uint256 index] bytes32 newValue (bytes32 oldValue) STORAGE { + mirrorArray[rep][index] = newValue; +} + +/** + * hook for Set array loads + * @dev user of this spec must replace _list with the instance name of the Set. + **/ +hook Sload bytes32 value _votersRepresented [KEY address rep] [KEY uint256 chain] .(offset 0)[INDEX uint256 index] STORAGE { + require(mirrorArray[rep][index] == value); +} +/** + * hook for Set map stores + * @dev user of this spec must replace _list with the instance name of the Set. + **/ +hook Sstore _votersRepresented [KEY address rep] [KEY uint256 chain] .(offset 32)[KEY bytes32 key] uint256 newIndex (uint256 oldIndex) STORAGE { + mirrorMap[rep][key] = newIndex; +} + +/** + * hook for Set map loads + * @dev user of this spec must replace _list with the instance name of the Set. + **/ +hook Sload uint256 index _votersRepresented [KEY address rep] [KEY uint256 chain] .(offset 32)[KEY bytes32 key] STORAGE { + require(mirrorMap[rep][key] == index); +} + +/** + * hook for Set array length stores + * @dev user of this spec must replace _list with the instance name of the Set. + **/ +hook Sstore _votersRepresented [KEY address rep] [KEY uint256 chain] .(offset 0).(offset 0) uint256 newLen (uint256 oldLen) STORAGE { + mirrorArrayLen[rep] = newLen; +} + +/** + * hook for Set array length load + * @dev user of this spec must replace _votersRepresented with the instance name of the Set. + **/ +hook Sload uint256 len _votersRepresented [KEY address rep] [KEY uint256 chain] .(offset 0).(offset 0) STORAGE { + require mirrorArrayLen[rep] == len; +} + +/** + * main Set general invariant + **/ +invariant setInvariant(address representative, uint256 chainId) + SET_INVARIANT(representative, chainId); + + +/** + * main AddressSet invariant + * @dev user of the spec should add 'requireInvariant addressSetInvariant();' to every rule and invariant that refer to a contract that instantiates AddressSet + **/ +invariant addressSetInvariant(address representative, uint256 chainId) + ADDRESS_SET_INVARIANT(representative, chainId); + +/** +* @title Length of AddressSet is less than 2^160 +* @dev the assumption is safe because there are at most 2^160 unique addresses +* @dev the proof of the assumption is vacuous because length > loop_iter +*/ +invariant set_size_leq_max_uint160(address representative, uint256 chainId) + getRepresentedVotersSize(representative, chainId) < max_uint160; + diff --git a/security/certora/specs/voting/README.md b/security/certora/specs/voting/README.md new file mode 100644 index 0000000..5dfcecd --- /dev/null +++ b/security/certora/specs/voting/README.md @@ -0,0 +1,295 @@ +# Voting Machine spec + +## Open issues and questions + +### Voting issues +* How to verify `_registerBridgedVote`? Harness it and test together with `settleVoteFromPortal`? + +### Proposal configuration issues +* It is possible to call `_createBridgedProposalVote` without reverting, but fail + to start the proposal's vote. Can this lead to undesirable situations? +* It follows that there can be proposal configs while the relevant proposal vote + state is `NotCreated`. E.g. if the needed roots are missing from the `DataWarehouse`, + see `startedProposalHasConfig` invariant. +* Should we verify `getProposalsVoteConfigurationIds`? If so, how? + + +## Basic setup +There are two setups used for rules here: +1. Basic setup in [`setup.sepc` file](./setup.spec) +2. Complicated setup summarizing `getVotingPower` in + [`power_summary.spec`](./power_summary.spec) presented below + +Here we deal with the basic setup in the [`setup.spec` file](./setup.spec). + +### Harness + +* **VotingMachineHarness** + Adds two useful methods: + * `submitVoteSingleProof` - vote without needing an array of proofs + * `createProposalVoteHarness` - A way to call `_createBridgedProposalVote` from spec + +* **VotingStrategyHarness** + Adds: + * `is_hasRequiredRoots` - returns true if `hasRequiredRoots` did not revert + * `getVotingAssetListLength` + +### Summarized + +* **IVotingStrategy.getVotingPower** => NONDET +* **DataWarehouse.getStorage** => NONDET +* **CrossChainController.forwardMessage** => NONDET +* **SlotUtils.getAccountSlotHash** => NONDET + + +## Voting Power Summary setup +This complicated setup is done in the [`power_summary.spec` file](./power_summary.spec). +It is used only in [`misc.spec` rules](./misc.spec). + + +### Harness + +* **VotingMachineHarnessTriple** + In addition to the methods in **VotingMachineHarness** adds: + * `submitVoteTripleProof` - for voting using three proofs without needing an array + +* **VotingStrategyHarness** - same as above + +* **DelegationModeHarness** + Since `DelegationMode` is not part of any contract, it cannot be used in spec. + This harness solves the problem by providing an equivalent enum `Mode`. + +### Summarized + +* **IVotingStrategy.getVotingPower(...)** + Returns the voting power. Summarized as constant per power (balance), asset + and storage-slot. Note a wildcard contract is used in this summary. + +* **DataWarehouse.getStorage(...) -> SlotValue** + Summarized, only requiring `slotval.value > 0` where `slotval` is the return value. + +* **CrossChainController.forwardMessage** => NONDET + +* **SlotUtils.getAccountSlotHash** => NONDET + + +## Voting and tally +Rules from [`voting_and_tally.spec` file](./voting_and_tally.spec). + +### Definitions + +* **Votes tally** + The *votes tally* for a proposal is the pair (2-tuple) of votes in favor and votes + against, i.e. `(forVotes, againstVotes)`. +* **Stored voting power** + The *stored voting power* of a voter `v` for a proposal `i` is the field + `getUserProposalVote(v, i).votingPower`. +* **A vote was cast** + We say that *a vote was cast* for a proposal `i` if the exists a voter `v` + whose stored voting power for `i` changed from zero to positive. + +### Rules summary +This spec proves that in a single method call: +1. The voting tally for proposal $i$ changed if and only if a single voter cast a vote + for proposal $i$ +2. At most one voter can cast a vote on one proposal +3. When a vote is cast on a proposal, the proposal's votes tally changes accordingly +4. The voting tally can be changed only using one of the voting methods + (rule `onlyVoteCanChangeResult`). +5. The voting tally in favor and against can only increase, and their sum equals + the sum of stored voting powers for that proposal + +### Ghosts + +* `is_someoneVoting(uint256) -> bool` + Indicating a vote was cast for the given proposal. +* `number_stores() -> mathint` + The number of times values have been stored in voting map. +* `storedVotingPower(uint256, address) -> uint248` + Ghost function following stored votes mapping. + +### Casting Votes Rules + +* **votingPowerGhostIsVotingPower(uint256 proposalId, address voter)** + Invariant showing the ghost `storedVotingPower` is indeed the stored vote power, + i.e. equals `getUserProposalVote`. From this it follows that if a vote is cast on + proposal `i` then `is_someoneVoting(i)` is true. + +* **sumOfVotes(uint256 proposalId)** + Invariant showing that for each proposal $i$ the sum of votes in favor and against + equals the sum of stored voting powers, i.e. + $\mathtt{prop.forVotes} + \mathtt{prop.againstVotes} = \sum_{\mathtt{address}\ a} \mathtt{storedVotingPower}(i, a)$ + where `prop = getProposalById(i)`. + +* **voteTallyChangedOnlyByVoting(method f, uint256 proposalId)** + If a proposal's votes tally changed then a vote was cast on the proposal. + +* **voteUpdatesTally(method f, uint256 proposalId, address voter)** + If a vote was cast for a proposal, then the proposal's votes tally changed. + Moreover, the change in tally corresponds to the vote that was cast. + See also [votedPowerIsImmutable rule](#votedPowerIsImmutable). + +* **onlyVoteCanChangeResult(method f, uint256 proposalId)** + Vote tally can be changed only by one of the voting methods. + +* **votingTallyCanOnlyIncrease(method f, uint256 proposalId)** + Voting tally can only increase (either votes in favor or votes against increased). + +### Other proposals and voters + +* **strangerVoteUnchanged(method f, uint256 proposalId, address stranger)** + A stranger's stored vote is unchanged when another votes. + +* **otherProposalUnchanged(method f, uint256 proposalId, uint256 otherProposal, address otherVote)** + Only a single proposal's tally and votes may change by a single method call. + +* **otherVoterUntouched(method f, uint256 proposalId, address voter, address strange)** + Only a single voter's stored voting power may change (on a given proposal). + + +## Voting legality +Rules from [`legality.spec` file](./legality.spec). + +### Rules summary + +The section, together with [Voting and tally](#voting_and_tally) and [Proposal states](#proposal_states), +shows that: + +A vote can be rejected only for one of the following reasons (otherwise must be accepted): +* Voting twice on behalf of particular user (rule `votedPowerIsImmutable` together + with results from `voting_and_tally.spec`) +* Voting before vote start (rule `onlyValidProposalCanChangeTally` and `states.spec`) +* Voting after vote end (rule [onlyValidProposalCanChangeTally](#onlyValidProposalCanChangeTally) + and [Proposal states](#proposal_states)) +* Voting with 0 voting power (rule [legalVote](#legalVote)) + +### Rules + +* **votedPowerIsImmutable(method f, address voter, uint256 proposalId)** + + Stored voting power is immutable (once positive). + Proves that stored voting power can change only when the original value is zero, + and that once it is positive it is immutable. This rule, together with the + previous section proves that a voter cannot vote twice. + +* **onlyValidProposalCanChangeTally(method f, uint256 proposalId)** + + Vote tally can change only for active, properly configured, proposals. + +* **legalVote(method f, uint256 proposalId, address voter)** + + Vote tally may change only if voter had zero stored voting power before and positive after. + + +## Proposal configuration +Rules from [`proposal_config.spec` file](./proposal_config.spec). + +### Rules summary + +* A vote can be created for a proposal only if the proposal has a configuration with a non-zero + block hash +* If a vote is created for a proposal, then the required roots exist +* The proposal's configuration is immutable +* A new must have unused ID. + +### Rules + +* **startedProposalHasConfig(uint256 proposalId)** + When starting a proposal vote it already has a configuration (with non-zero block hash). + An invariant. + **Note** that the opposite need not be true. For example, a in `_createBridgedProposalVote` + (which creates the configuration) the call to `createVote(proposalId)` may revert, + and since it is inside a `try` clause the original call will not revert. + +* **createdProposalHasRoots(uint256 proposalId)** + Once a proposal vote is started the required roots (in the `DataWarehouse`) exist. An invariant. + +* **proposalHasNonzeroDuration** + An existing proposal's voting duration is non zero. + +* **newProposalUnusedId(uint256 proposalId, bytes32 blockHash, uint24 votingDuration)** + A new proposal must have an unused ID. + +* **configIsImmutable(method f, uint256 proposalId)** + A proposal's configuration is immutable once set. + + +## Proposal states + +Rules from [`proposal_states.spec` file](./proposal_states.spec). + +### Rules summary + +* Start time of a proposal's vote is before its end time +* A proposal vote's ID is immutable +* The proposal states are as described in the state-machine below + + +```mermaid +flowchart TD; + NC(NotCreated); + AC(Active); + FN(Finished); + SN(SentToGovernance) + + NC -- "createVote(),\n createProposalVoteHarness()" --> AC; + FN -- "closeAndSendVote()" --> SN; + AC -- T > endTime --> FN + AC -- T > endTime --> SN +``` + +### Rules + +* **startsBeforeEnds(uint256 proposalId)** + A proposal's vote start time is before (or equal to) its end time. An invariant. + +* **startsStrictlyBeforeEnds(uint256 proposalId)** + A started proposal's end time is the start time plus voting duration. An invariant. + This fails on `createProposalVoteHarness` since we allow `l1ProposalBlockHash` to be zero. + For example, we can have a created proposal vote with `l1ProposalBlockHash == 0` but + `endTime > 0`, now calling `_createBridgedProposalVote` can modify the `votingDuration`, + making the invariant false. + +* **proposalLegalStates(uint256 proposalId)** + A proposal must be in one of the following states: + + 1. $\mathtt{NotCreated} \Longleftrightarrow \mathtt{endTime} = 0$ + 2. $\mathtt{Active} \Longleftrightarrow (t \leq \mathtt{endTime}) \land (\mathtt{endTime} \neq 0)$ + 3. $\mathtt{Finished} \Longleftrightarrow (t \geq \mathtt{endTime} > 0) \land \neg \mathtt{sentToGovernance}$ + 4. $\mathtt{SentToGovernance} \Longleftrightarrow (t \geq \mathtt{endTime} > 0) \land \mathtt{sentToGovernance}$ + +* **proposalMethodStateTransitionCompliance(method f, uint256 proposalId)** + A proposal's valid state transitions by method call. + +* **proposalTimeStateTransitionCompliance(uint256 proposalId)** + A proposal's valid state transitions by time. + +* **proposalImmutability(method f, uint256 proposalId)** + Proposal immutability. Verifies that certain fields of the proposal are immutable, + once the proposal is created of course). + +* **proposalIdIsImmutable(uint256 proposalId)** + A created proposal vote's ID is never changed. An invariant. + + +## Miscellaneous +Rules from [`misc.spec` file](./misc.spec). Mostly rules dealing with specific methods. + +**Note.** Uses the [`power_summary.spec` setup](./power_summary.spec). + +### Rules + +* **sendOnlyFinishedVote(uint256 proposalId)** + Only proposals in the state `Finished` can be sent to governance + +* **submitSingleProofVerification(...)** + Verifies compliance of + `submitVoteSingleProof(uint256 proposalId, bool support, VotingBalanceProof proof)`, this is + a harness method that sends a single proof. + +* **submitTripleProofVerification(...)** + Verifies compliance of `submitVoteTripleProof`. + +* **rejectEquivalentProofs(...)** + Reject equivalent proofs. If several equivalent proofs are given (same asset and slot) they should + be rejected. diff --git a/security/certora/specs/voting/legality.spec b/security/certora/specs/voting/legality.spec new file mode 100644 index 0000000..0fbcd6d --- /dev/null +++ b/security/certora/specs/voting/legality.spec @@ -0,0 +1,114 @@ +/// ============================================================================ +/// Voting legality +/// ============================================================================ + +/* Summary + * ------- + * The spec, together with `voting_and_tally.spec` and `states.spec`, shows that: + * A vote can be rejected only for one of the following reasons (otherwise must be + * accepted): + * - Voting twice on behalf of particular user (rule `votedPowerIsImmutable` together + * with results from `voting_and_tally.spec`) + * - Voting before vote start (rule `onlyValidProposalCanChangeTally` and `states.spec`) + * - Voting after vote end (rule `onlyValidProposalCanChangeTally` and `states.spec`) + * - Voting with 0 voting power (rule `legalVote`) + */ + +import "setup.spec"; + + +/// @title Is the proposal's block hash non-zero +function is_proposalHashNonZero(uint256 proposalId) returns bool { + IVotingMachineWithProofs.ProposalVoteConfiguration conf = ( + getProposalVoteConfiguration(proposalId) + ); + return conf.l1ProposalBlockHash != to_bytes32(0); +} + + +/** @title Is the proposal Active + * @return False if the proposal's state is `NotCreated`, true otherwise. + * @notice By rule `proposalLegalStates` the state `NotCreated` is equivalent to + * `endTime` being zero. + */ +function is_proposalCreated(uint256 proposalId) returns bool { + IVotingMachineWithProofs.ProposalWithoutVotes proposal = getProposalById(proposalId); + return proposal.endTime != 0; +} + + +invariant createdVoteHasNonZeroHash(uint256 proposalId) + is_proposalCreated(proposalId) => is_proposalHashNonZero(proposalId); + + +/** @title Stored voting power is immutable (once positive) + * Proves that stored voting power can change only when the original value is zero, + * and that once it is positive it is immutable. This rule, together with the + * previous section proves that a voter cannot vote twice. + */ +rule votedPowerIsImmutable(method f, address voter, uint256 proposalId) { + IVotingMachineWithProofs.Vote pre = getUserProposalVote(voter, proposalId); + + env e; + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.Vote post = getUserProposalVote(voter, proposalId); + + assert pre.votingPower > 0 => post.votingPower == pre.votingPower; + assert post.votingPower != pre.votingPower => pre.votingPower == 0; +} + + +/// @title Vote tally can change only for active and properly configured proposals +rule onlyValidProposalCanChangeTally(method f, uint256 proposalId) { + requireInvariant createdVoteHasNonZeroHash(proposalId); + + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + IVotingMachineWithProofs.ProposalVoteConfiguration conf = ( + getProposalVoteConfiguration(proposalId) + ); + + env e; + IVotingMachineWithProofs.ProposalState state = getProposalState(e, proposalId); + + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + + bool is_tallyChanged = ( + (pre.forVotes != post.forVotes) || (pre.againstVotes != post.againstVotes) + ); + assert is_tallyChanged => ( + (state == IVotingMachineWithProofs.ProposalState.Active) && + (conf.l1ProposalBlockHash != to_bytes32(0)) + ); +} + + +/** @title Vote tally may change only if voter had zero stored voting power before + * and positive after. + */ +rule legalVote(method f, uint256 proposalId, address voter) filtered { + f -> !f.isView +} { + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + IVotingMachineWithProofs.Vote preVote = getUserProposalVote(voter, proposalId); + + env e; + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + IVotingMachineWithProofs.Vote postVote = getUserProposalVote(voter, proposalId); + + bool is_tallyChanged = ( + (pre.forVotes != post.forVotes) || (pre.againstVotes != post.againstVotes) + ); + bool is_voterChanged = (preVote.votingPower != postVote.votingPower); + assert ( + (is_tallyChanged && is_voterChanged) => + (preVote.votingPower == 0 && postVote.votingPower > 0) + ); +} diff --git a/security/certora/specs/voting/misc.spec b/security/certora/specs/voting/misc.spec new file mode 100644 index 0000000..b5fe9c9 --- /dev/null +++ b/security/certora/specs/voting/misc.spec @@ -0,0 +1,171 @@ +/// ============================================================================ +/// Miscellaneous Rules +/// ============================================================================ + +import "power_summary.spec"; + + +// Sending results ============================================================= + +/// @title Can only send results for finished votes +rule sendOnlyFinishedVote(uint256 proposalId) { + env e; + IVotingMachineWithProofs.ProposalState state = getProposalState(e, proposalId); + + closeAndSendVote(e, proposalId); + + assert state == IVotingMachineWithProofs.ProposalState.Finished; +} + + +// Particular voting methods =================================================== + +/// @title Utility function for getting raw voting power from proof +function _getRawSlotPower( + IVotingMachineWithProofs.VotingBalanceProof proof +) returns uint256 { + bytes32 blockHash; // Value is unimportant due to summarization of _getStorage + bytes32 slotHash; // Value is unimportant due to summarization of _getStorage + StateProofVerifier.SlotValue slotValue = _getStorage( + proof.underlyingAsset, blockHash, slotHash, proof.proof + ); + return slotValue.value; +} + + +/// @title Utility function for getting voting power from proof +function _getVotingPowerFromProof( + IVotingMachineWithProofs.VotingBalanceProof proof +) returns uint256 { + uint256 raw_power = _getRawSlotPower(proof); + return mockVotingPower(proof.underlyingAsset, proof.slot, raw_power); +} + + +/** @title Single proof verification + * Verifies the following properties for voting using `submitVoteSingleProof`: + * - A vote is rejected if either: + * a. User's registered voted power for the proposal is not zero (voting twice) + * b. User's voting power is zero + * c. Proposal's state is not `Active` + * - After voting, user's registered support and voted power are the same as the vote + * - The total votes tally is updated correctly + */ +rule submitSingleProofVerification( + uint256 proposalId, + bool support, + IVotingMachineWithProofs.VotingBalanceProof proof +) { + env e; + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + + // If `votePre.votingPower` is not zero, it means user already voted + IVotingMachineWithProofs.Vote votePre = getUserProposalVote(e.msg.sender, proposalId); + + // If `votePower` is zero, the user's vote will be rejected + uint256 voterPower = _getVotingPowerFromProof(proof); + + // If the proposal state is not active, the user's vote will be rejected + IVotingMachineWithProofs.ProposalState state = getProposalState(e, proposalId); + + submitVoteSingleProof(e, proposalId, support, proof); + + assert ( + (votePre.votingPower == 0) && + (voterPower > 0) && + (state == IVotingMachineWithProofs.ProposalState.Active) + ); + + IVotingMachineWithProofs.Vote postVote = getUserProposalVote(e.msg.sender, proposalId); + assert postVote.support == support; + + // Since `voterPower` and `postVote.votingPower` have different types, we cast both + // to `mathint`. + mathint mathVoterPower = to_mathint(voterPower); + mathint mathPostPower = to_mathint(postVote.votingPower); + assert mathPostPower == mathVoterPower; + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + + // Votes can only increase + assert post.forVotes >= pre.forVotes; + assert post.againstVotes >= pre.againstVotes; + + uint128 forChange = assert_uint128(post.forVotes - pre.forVotes); + uint128 againstChange = assert_uint128(post.againstVotes - pre.againstVotes); + uint128 castVoterPower = assert_uint128(postVote.votingPower); + assert support => (forChange == castVoterPower) && (againstChange == 0); + assert !support => (forChange == 0) && (againstChange == castVoterPower); +} + + +/// @title Triple proof verification +rule submitTripleProofVerification( + uint256 proposalId, + bool support, + IVotingMachineWithProofs.VotingBalanceProof proof1, + IVotingMachineWithProofs.VotingBalanceProof proof2, + IVotingMachineWithProofs.VotingBalanceProof proof3 +) { + require proof1.underlyingAsset == _VotingStrategy.AAVE(); + require proof1.slot == 0; + require proof1.proof.length < max_uint32; // Avoid calldata pointer overflow + require proof2.underlyingAsset == _VotingStrategy.STK_AAVE(); + require proof2.slot == 0; + require proof2.proof.length < max_uint32; // Avoid calldata pointer overflow + require proof3.underlyingAsset == _VotingStrategy.A_AAVE(); + require proof3.slot == 52; + require proof3.proof.length < max_uint32; // Avoid calldata pointer overflow + + env e; + + uint256 power1 = _getVotingPowerFromProof(proof1); + uint256 power2 = _getVotingPowerFromProof(proof2); + uint256 power3 = _getVotingPowerFromProof(proof3); + mathint power = power1 + power2 + power3; + + submitVoteTripleProof(e, proposalId, support, proof1, proof2, proof3); + + IVotingMachineWithProofs.Vote postVote = getUserProposalVote(e.msg.sender, proposalId); + assert ( + to_mathint(postVote.votingPower) == power && + postVote.support == support + ); +} + + +/// @title Are two proofs equivalent +function isEquivalent( + IVotingMachineWithProofs.VotingBalanceProof proof1, + IVotingMachineWithProofs.VotingBalanceProof proof2 +) returns bool { + return ( + proof1.underlyingAsset == proof2.underlyingAsset && + proof1.slot == proof2.slot + ); +} + +/// @title Reject equivalent proofs +rule rejectEquivalentProofs( + uint256 proposalId, + bool support, + IVotingMachineWithProofs.VotingBalanceProof proof1, + IVotingMachineWithProofs.VotingBalanceProof proof2, + IVotingMachineWithProofs.VotingBalanceProof proof3 +) { + // Prevent calldata pointer overflow + require ( + proof1.proof.length < max_uint32 && + proof2.proof.length < max_uint32 && + proof3.proof.length < max_uint32 + ); + require ( + isEquivalent(proof1, proof2) || + isEquivalent(proof1, proof3) || + isEquivalent(proof2, proof3) + ); + + env e; + submitVoteTripleProof@withrevert(e, proposalId, support, proof1, proof2, proof3); + assert lastReverted; +} diff --git a/security/certora/specs/voting/power_summary.spec b/security/certora/specs/voting/power_summary.spec new file mode 100644 index 0000000..6b1cd79 --- /dev/null +++ b/security/certora/specs/voting/power_summary.spec @@ -0,0 +1,162 @@ +/// ============================================================================ +/// `VotingMachine` contract - setup with `getVotingPower` summary +/// ============================================================================ +using VotingStrategyHarness as _VotingStrategy; + + +methods +{ + // `VotingMachine` ========================================================= + function getUserProposalVote( + address, uint256 + ) external returns (IVotingMachineWithProofs.Vote) envfree; + + function getProposalById( + uint256 + ) external returns (IVotingMachineWithProofs.ProposalWithoutVotes) envfree; + + function getProposalVoteConfiguration( + uint256 + ) external returns (IVotingMachineWithProofs.ProposalVoteConfiguration) envfree; + + // `VotingStrategy` ======================================================== + function VotingStrategyHarness.AAVE() external returns (address) envfree; + function VotingStrategyHarness.A_AAVE() external returns (address) envfree; + function VotingStrategyHarness.STK_AAVE() external returns (address) envfree; + function VotingStrategyHarness.getVotingAssetListLength( + ) external returns (uint256) envfree; + function VotingStrategyHarness.isTokenSlotAccepted( + address, uint128 + ) external returns (bool) envfree; + function VotingStrategyHarness.is_hasRequiredRoots( + bytes32 + ) external returns (bool) envfree; + + // `getVotingPower` is summarized since it uses bitwise operations and retrieves + // data from slots. We use a wildcard since it is called as: + // `IVotingStrategy(address(VOTING_STRATEGY)).getVotingPower` + function _.getVotingPower( + address asset, + uint128 baseStorageSlot, + uint256 power, + bytes32 blockHash + ) external => + _getVotingPower(asset, baseStorageSlot, power, blockHash) expect (uint256); + + // `DataWarehouse` ========================================================= + // Summarized since it retrieves data from slots + function DataWarehouse.getStorage( + address account, + bytes32 blockHash, + bytes32 slot, + bytes storageProof + ) external returns (StateProofVerifier.SlotValue) => + _getStorage(account, blockHash, slot, storageProof); + + // `CrossChainController` ================================================== + // NOTE: Not clear why this call is not resolved, we summarize it as `NONDET` + function CrossChainController.forwardMessage( + uint256, address, uint256, bytes + ) external returns (bytes32,bytes32) => NONDET; + + // `SlotUtils` ============================================================= + // Summarized for speed-up + function SlotUtils.getAccountSlotHash( + address, uint256 + ) internal returns (bytes32) => NONDET; +} + + +/// `getStorage` summary ======================================================= +/** @title Storage mapping + * @param underlyingAsset + * @param storageProof + * @return raw voting power for the given asset + */ +ghost mapping(address => mapping(bytes => uint256)) _slotValues; + + +/// @title Summary of `DataWarehouse.getStorage` - slot always exists +function _getStorage( + address account, // proof.underlyingAsset + bytes32 blockHash, + bytes32 slot, + bytes storageProof +) returns StateProofVerifier.SlotValue { + StateProofVerifier.SlotValue slotval; + require slotval.exists; + require slotval.value == _slotValues[account][storageProof]; + return slotval; +} + + +/// Voting power summary ======================================================= + +/* The method `getVotingPower` is summarized in `_getVotingPower` below. + * To keep fixed values per voter and asset, we use the ghost mapping `_votingAssetPower`. + */ + + +/** @title Voting power mapping + * Note: use `mockVotingPower` to get a voter's voting power, do not use the mapping + * directly. + * @param power: power of voter + * @param asset: address of asset + * @param baseStorageSlot + * @return voter's voting power for the given asset + */ +ghost mapping(uint256 => mapping(address => mapping(uint128 => uint256))) _votingAssetPower; + + +/** @title Mock voting power + * @param asset: address of asset + * @param baseStorageSlot: slot to use for the asset + * @param power: power (balance) of voter + * @return `_votingAssetPower[power][asset][baseStorageSlot]` if asset is one of + * `AAVE`, `STK_AAVE` or `A_AAVE`, and has the appropriate slot, and power is non-zero, + * zero otherwise + */ +function mockVotingPower( + address asset, + uint128 baseStorageSlot, + uint256 power +) returns uint256 { + // Return 0 if asset is not one of `AAVE`, `A_AAVE` or `STK_AAVE` + if ( + asset != _VotingStrategy.AAVE() && + asset != _VotingStrategy.STK_AAVE() && + asset != _VotingStrategy.A_AAVE()) { + return 0; + } + if (power == 0) { + return 0; + } + return ( + _VotingStrategy.isTokenSlotAccepted(asset, baseStorageSlot) ? + _votingAssetPower[power][asset][baseStorageSlot] : 0 + ); +} + + +/// @title Summary of `VotingStrategy.getVotingPower` +function _getVotingPower( + address asset, + uint128 baseStorageSlot, + uint256 power, + bytes32 blockHash +) returns uint256 { + uint256 votingPower = mockVotingPower(asset, baseStorageSlot, power); + return votingPower; +} + + +// Code mock verification rules ================================================ + +/** @title There are exactly three acceptable tokens + * @notice This invariant fails sanity due to due being tautological, i.e.: + * "trivial invariant check FAILED: post-state assertion is trivially true". + * It is kept since many configs and rules are based on this property, and + * therefore should be retained in CI. + */ +invariant onlyThreeTokens() + _VotingStrategy.getVotingAssetListLength() == 3; diff --git a/security/certora/specs/voting/proposal_config.spec b/security/certora/specs/voting/proposal_config.spec new file mode 100644 index 0000000..7dd1b56 --- /dev/null +++ b/security/certora/specs/voting/proposal_config.spec @@ -0,0 +1,123 @@ +/// ============================================================================ +/// Proposal configuration +/// ============================================================================ + +import "setup.spec"; + + +// Utilities =================================================================== + +/// @title Proposal's voting duration +function getProposalVotingDuration(uint256 proposalId) returns uint24 { + IVotingMachineWithProofs.ProposalVoteConfiguration conf = ( + getProposalVoteConfiguration(proposalId) + ); + return conf.votingDuration; +} + + +/// @title Has the proposal's config been created +function is_proposalConfigCreated(uint256 proposalId) returns bool { + IVotingMachineWithProofs.ProposalVoteConfiguration conf = ( + getProposalVoteConfiguration(proposalId) + ); + return conf.l1ProposalBlockHash != to_bytes32(0); +} + + +/** @title Is the proposal created + * @return False if the proposal's state is `NotCreated`, true otherwise. + * @notice By rule `proposalLegalStates` the state `NotCreated` is equivalent to + * `endTime` being zero. + */ +function is_proposalStarted(uint256 proposalId) returns bool { + IVotingMachineWithProofs.ProposalWithoutVotes proposal = getProposalById(proposalId); + return proposal.endTime != 0; +} + + +/** @title Does the proposal have the required roots + * Essentially this verifies that `VotingStrategy.hasRequiredRoots` does not revert. + * @return true if the roots exist + */ +function is_proposalHasRoots(uint256 proposalId) returns bool { + IVotingMachineWithProofs.ProposalVoteConfiguration conf = ( + getProposalVoteConfiguration(proposalId) + ); + return _VotingStrategy.is_hasRequiredRoots(conf.l1ProposalBlockHash); +} + + +// Rules ======================================================================= + +/** @title When starting a proposal vote it already has a config + * @notice The opposite need not be true - in `_createBridgedProposalVote` the call to + * `startProposalVote(proposalId)` may fail. Since this is inside a try-catch (see + * `VotingMachineWithProofs.sol:412`) it will not revert the original call. + */ +invariant startedProposalHasConfig(uint256 proposalId) + is_proposalStarted(proposalId) => is_proposalConfigCreated(proposalId); + + +/// @title Once a proposal vote is started the required roots exist +invariant createdProposalHasRoots(uint256 proposalId) + is_proposalStarted(proposalId) => is_proposalHasRoots(proposalId) + { + preserved { + // Without this one can create a proposal with `l1ProposalBlockHash` zero + requireInvariant startedProposalHasConfig(proposalId); + } + } + + +/// @title Existing proposal config has non-zero duration +invariant proposalHasNonzeroDuration(uint256 proposalId) + is_proposalConfigCreated(proposalId) <=> (getProposalVotingDuration(proposalId) != 0); + + +/// @title New proposal must have unused ID +rule newProposalUnusedId(uint256 proposalId, bytes32 blockHash, uint24 votingDuration) { + + requireInvariant startedProposalHasConfig(proposalId); + + env e; + IVotingMachineWithProofs.ProposalVoteConfiguration preConf = ( + getProposalVoteConfiguration(proposalId) + ); + IVotingMachineWithProofs.ProposalState preState = getProposalState(e, proposalId); + + createProposalVoteHarness(e, proposalId, blockHash, votingDuration); + + IVotingMachineWithProofs.ProposalState postState = getProposalState(e, proposalId); + + // `preConf.l1ProposalBlockHash == to_bytes32(0)` implies the proposal is not created + assert ( + (preConf.l1ProposalBlockHash == to_bytes32(0)) && + ( + (postState != IVotingMachineWithProofs.ProposalState.NotCreated) => + (preState == IVotingMachineWithProofs.ProposalState.NotCreated) + ) + ); +} + + +/// @title A proposal's configuration is immutable once set +rule configIsImmutable(method f, uint256 proposalId) { + IVotingMachineWithProofs.ProposalVoteConfiguration preConf = ( + getProposalVoteConfiguration(proposalId) + ); + + env e; + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalVoteConfiguration postConf = ( + getProposalVoteConfiguration(proposalId) + ); + + assert ( + (preConf.l1ProposalBlockHash != to_bytes32(0)) => + (preConf.l1ProposalBlockHash == postConf.l1ProposalBlockHash) && + (preConf.votingDuration == postConf.votingDuration) + ); +} diff --git a/security/certora/specs/voting/proposal_states.spec b/security/certora/specs/voting/proposal_states.spec new file mode 100644 index 0000000..ef10795 --- /dev/null +++ b/security/certora/specs/voting/proposal_states.spec @@ -0,0 +1,243 @@ +/// ============================================================================ +/// Proposal states +/// ============================================================================ + +import "setup.spec"; +import "proposal_config.spec"; + +use invariant startedProposalHasConfig; +use invariant proposalHasNonzeroDuration; + + +// Utilities =================================================================== + +function proposalStartTime(uint256 proposalId) returns uint40 { + IVotingMachineWithProofs.ProposalWithoutVotes proposal = getProposalById(proposalId); + return proposal.startTime; +} + + +function proposalEndTime(uint256 proposalId) returns uint40 { + IVotingMachineWithProofs.ProposalWithoutVotes proposal = getProposalById(proposalId); + return proposal.endTime; +} + + +function proposalVotingDuration(uint256 proposalId) returns uint24 { + IVotingMachineWithProofs.ProposalVoteConfiguration conf = ( + getProposalVoteConfiguration(proposalId) + ); + return conf.votingDuration; +} + + +/// @notice: ASSUMES `state.NotCreated <=> endTime != 0` +function isProposalStarted(uint256 proposalId) returns bool { + IVotingMachineWithProofs.ProposalWithoutVotes proposal = getProposalById(proposalId); + return proposal.endTime != 0; +} + +// Rules ======================================================================= + +/// @title A proposal's vote start time is before its end time +invariant startsBeforeEnds(uint256 proposalId) + ( + (proposalStartTime(proposalId) <= proposalEndTime(proposalId)) && + (isProposalStarted(proposalId) => ( + proposalStartTime(proposalId) < proposalEndTime(proposalId) + )) + ) + { + preserved { + // Without this one can create a proposal with `l1ProposalBlockHash` zero + requireInvariant startedProposalHasConfig(proposalId); + + // Without this one can start a vote with zero duration + requireInvariant proposalHasNonzeroDuration(proposalId); + } + } + + +/// @title A started proposal's end time is the start time plus voting duration +invariant startsStrictlyBeforeEnds(uint256 proposalId) + isProposalStarted(proposalId) => ( + to_mathint(proposalEndTime(proposalId)) == + proposalStartTime(proposalId) + proposalVotingDuration(proposalId) + ) + { + preserved { + // Without this one can create a proposal with `l1ProposalBlockHash` zero + requireInvariant startedProposalHasConfig(proposalId); + } + } + + +/// @title A proposal's valid states +rule proposalLegalStates(uint256 proposalId) { + env e; + + IVotingMachineWithProofs.ProposalWithoutVotes proposal = getProposalById(proposalId); + IVotingMachineWithProofs.ProposalState state = getProposalState(e, proposalId); + + // The code casts `block.timestamp` to `uint40`, so we do the same + uint40 t = require_uint40(e.block.timestamp); + + // `NotCreated` state is the same as `endTime == 0` + assert ( + (state == IVotingMachineWithProofs.ProposalState.NotCreated) <=> + (proposal.endTime == 0) + ); + + assert ( + (state == IVotingMachineWithProofs.ProposalState.Active) <=> + ((proposal.endTime != 0) && (t <= proposal.endTime)) + ); + + // After `endTime` the state cannot be `Active` + assert (t > proposal.endTime) => (state != IVotingMachineWithProofs.ProposalState.Active); + + assert ( + (state == IVotingMachineWithProofs.ProposalState.Finished) <=> + ((proposal.endTime != 0) && (t > proposal.endTime) && !proposal.sentToGovernance) + ); + + assert ( + (state == IVotingMachineWithProofs.ProposalState.SentToGovernance) <=> + ((proposal.endTime != 0) && (t > proposal.endTime) && proposal.sentToGovernance) + ); + + // Must be in one of four states + assert ( + state == IVotingMachineWithProofs.ProposalState.NotCreated || + state == IVotingMachineWithProofs.ProposalState.Active || + state == IVotingMachineWithProofs.ProposalState.Finished || + state == IVotingMachineWithProofs.ProposalState.SentToGovernance + ); +} + + +/// @title A proposal's valid state transitions by method call +rule proposalMethodStateTransitionCompliance(method f, uint256 proposalId) { + env e; + + IVotingMachineWithProofs.ProposalState before = getProposalState(e, proposalId); + + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalState after = getProposalState(e, proposalId); + + // `NotCreated` state can be changed only by `startProposalVote` + assert ( + (before == IVotingMachineWithProofs.ProposalState.NotCreated) => + ( + after == before || + ( + after == IVotingMachineWithProofs.ProposalState.Active && + ( + f.selector == sig:startProposalVote(uint256).selector || + f.selector == ( + sig:createProposalVoteHarness(uint256, bytes32, uint24).selector + ) + ) + ) + ) + ); + + // `Active` state can be changed only in time, not method call + assert ( + before == IVotingMachineWithProofs.ProposalState.Active => after == before + ); + + // `Finished` state can be changed only using `closeAndSendVote` + assert ( + (before == IVotingMachineWithProofs.ProposalState.Finished) => + ( + after == before || + ( + after == IVotingMachineWithProofs.ProposalState.SentToGovernance && + f.selector == sig:closeAndSendVote(uint256).selector + ) + ) + ); + + // `SentToGovernance` state is final + assert ( + before == IVotingMachineWithProofs.ProposalState.SentToGovernance => after == before + ); +} + + +/// @title A proposal's valid state transitions by time +rule proposalTimeStateTransitionCompliance(uint256 proposalId) { + env e0; + IVotingMachineWithProofs.ProposalState before = getProposalState(e0, proposalId); + + env e1; + + // Ensure `e1` occurs after `e0` + // Note the code casts `block.timestamp` to `uint40`, so we do the same + uint40 t0 = require_uint40(e0.block.timestamp); + uint40 t1 = require_uint40(e1.block.timestamp); + require t1 >= t0; + IVotingMachineWithProofs.ProposalState after = getProposalState(e1, proposalId); + + // `NotCreated` state can be changed only by `startProposalVote` + assert ( + before == IVotingMachineWithProofs.ProposalState.NotCreated => after == before + ); + + // `Active` state can be changed in time + assert ( + before == IVotingMachineWithProofs.ProposalState.Active => + ( + after == before || + ( + t1 > t0 && + ( + after == IVotingMachineWithProofs.ProposalState.Finished || + after == IVotingMachineWithProofs.ProposalState.SentToGovernance + ) + ) + ) + ); + + // `Finished` and `SentToGovernance` states cannot be changed by time alone + assert ( + ( + before == IVotingMachineWithProofs.ProposalState.Finished || + before == IVotingMachineWithProofs.ProposalState.SentToGovernance + ) => after == before + ); +} + + +/** @title Proposal immutability + * Verifies that certain fields of the proposal are immutable (once the proposal is + * created of course). + */ +rule proposalImmutability(method f, uint256 proposalId) { + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + + env e; + IVotingMachineWithProofs.ProposalState initialState = getProposalState(e, proposalId); + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + + assert ( + (initialState != IVotingMachineWithProofs.ProposalState.NotCreated) => + ( + pre.id == post.id && + pre.startTime == post.startTime && + pre.endTime == post.endTime && + pre.creationBlockNumber == post.creationBlockNumber + ) + ); +} + + +/// @title A created proposal vote's ID is never changed +invariant proposalIdIsImmutable(uint256 proposalId) + isProposalStarted(proposalId) => (getIdOfProposal(proposalId) == proposalId); diff --git a/security/certora/specs/voting/setup.spec b/security/certora/specs/voting/setup.spec new file mode 100644 index 0000000..03603f5 --- /dev/null +++ b/security/certora/specs/voting/setup.spec @@ -0,0 +1,59 @@ +/// ============================================================================ +/// `VotingMachine` contract - basic setup +/// ============================================================================ +using VotingStrategyHarness as _VotingStrategy; + + +methods +{ + // `VotingMachine` ========================================================= + function getUserProposalVote( + address, uint256 + ) external returns (IVotingMachineWithProofs.Vote) envfree; + + function getProposalById( + uint256 + ) external returns (IVotingMachineWithProofs.ProposalWithoutVotes) envfree; + + function getProposalVoteConfiguration( + uint256 + ) external returns (IVotingMachineWithProofs.ProposalVoteConfiguration) envfree; + + function getIdOfProposal(uint256) external returns (uint256) envfree; + + // `VotingStrategy` ======================================================== + function VotingStrategyHarness.is_hasRequiredRoots( + bytes32 + ) external returns (bool) envfree; + + // `getVotingPower` is summarized since it uses bitwise operations and retrieves + // data from slots. We use a wildcard since it is called as: + // `IVotingStrategy(address(VOTING_STRATEGY)).getVotingPower` + function _.getVotingPower( + address asset, + uint128 baseStorageSlot, + uint256 power, + bytes32 blockHash + ) external => NONDET; + + // `DataWarehouse` ========================================================= + // Summarized since it retrieves data from slots + function DataWarehouse.getStorage( + address account, + bytes32 blockHash, + bytes32 slot, + bytes storageProof + ) external returns (StateProofVerifier.SlotValue) => NONDET; + + // `CrossChainController` ================================================== + // NOTE: Not clear why this call is not resolved, we summarize it as `NONDET` + function CrossChainController.forwardMessage( + uint256, address, uint256, bytes + ) external returns (bytes32,bytes32) => NONDET; + + // `SlotUtils` ============================================================= + // Summarized for speed-up + function SlotUtils.getAccountSlotHash( + address, uint256 + ) internal returns (bytes32) => NONDET; +} diff --git a/security/certora/specs/voting/voting_and_tally.spec b/security/certora/specs/voting/voting_and_tally.spec new file mode 100644 index 0000000..a50d93e --- /dev/null +++ b/security/certora/specs/voting/voting_and_tally.spec @@ -0,0 +1,644 @@ +/// ============================================================================ +/// Vote Tally and Casting Votes +/// ============================================================================ + +/* Definitions + * ----------- + * - Votes tally: The votes tally for a proposal is the pair (2-tuple) of votes in favor + * and votes against, i.e. `(forVotes, againstVotes)`. + * - Stored voting power: The stored voting power of a voter `v` for a proposal `i` is + * the field `getUserProposalVote(v, i).votingPower`. + * - A vote was cast: We say that "a vote was cast" for a proposal `i` if there exists a + * voter `v` whose stored voting power for `i` changed from zero to positive. + * + * Summary + * ------- + * This spec proves that in a single method call: + * 1. The voting tally for proposal i changed if and only if a single voter cast a vote + * for proposal i + * 2. At most one voter can cast a vote on one proposal + * 3. When a vote is cast on a proposal, the proposal's votes tally changes accordingly + * 4. The voting tally can be changed only using one of the voting methods + * (rule `onlyVoteCanChangeResult`) + * 5. The voting tally in favor and against can only increase, and their sum equals + * the sum of stored voting powers for that proposal + */ + +import "setup.spec"; + + +// Ghosts and hooks ============================================================ + +/** @title Function indicating a vote was cast for the given proposal + * A ghost function showing that `_proposals[proposalId].votes[voter].votingPower` + * changed from zero to positive (see hook below). + * @param proposalId + * @return Whether a vote was cast for a given proposal + */ +ghost is_someoneVoting(uint256) returns bool; + + +/// @title The number of times values have been stored in voting map +ghost number_stores() returns mathint; + + +/** @title Ghost function following votes mapping + * @param proposalID + * @param voter + * @return The registered voting power for the voter, namely the value of + * ` _proposals[proposalId].votes[voter].votingPower`, see invariant + * `votingPowerGhostIsVotingPower` below + */ +ghost storedVotingPower(uint256, address) returns uint248 { + init_state axiom forall uint256 proposalId. forall address voter. + storedVotingPower(proposalId, voter) == 0; +} + + +/// @title Sum of all (increasing) votes +ghost mapping(uint256 => mathint) votesSum { + init_state axiom forall uint256 proposalId. votesSum[proposalId] == 0; +} + + +/** @title Hook updating the ghost functions + * In particular, this hook implies that whenever `storedVotingPower(i, v)` changed from + * zero to positive number then `is_someoneVoting(i)` must be true. + */ +hook Sstore + _proposals[KEY uint256 proposalId].votes[KEY address voter].votingPower + uint248 newPower (uint248 oldPower) STORAGE + { + // Update `is_someoneVoting` - only the new + havoc is_someoneVoting assuming ( + (oldPower == 0 && newPower > 0) => is_someoneVoting@new(proposalId) + ); + + // Update `number_stores` + havoc number_stores assuming number_stores@new() == number_stores@old() + 1; + + // Update `storedVotingPower` - only the new power + havoc storedVotingPower assuming ( + storedVotingPower@new(proposalId, voter) == newPower && + ( + forall uint256 pId. forall address v. + (pId != proposalId || v != voter) => + storedVotingPower@new(pId, v) == storedVotingPower@old(pId, v) + ) + ); + + // Update `votesSum` + votesSum[proposalId] = ( + newPower > oldPower ? + votesSum[proposalId] + newPower - oldPower : + votesSum[proposalId] + ); + } + + +// Ghost voting power equivalence ============================================== + +/// @title Utility function for `getUserProposalVote` invariant below +function getRegisteredVotingPower(uint256 proposalId, address voter) returns uint248 { + IVotingMachineWithProofs.Vote vote = getUserProposalVote(voter, proposalId); + return vote.votingPower; +} + + +/** @title Stored voting power equals `getUserProposalVote` + * This invariant proves that `storedVotingPower == getRegisteredVotingPower`. + * It follows that if a vote is cast on proposal `i` then `is_someoneVoting(i)` is true. + */ +invariant votingPowerGhostIsVotingPower(uint256 proposalId, address voter) + getRegisteredVotingPower(proposalId, voter) == storedVotingPower(proposalId, voter); + + +// Votes tally ================================================================= + +/// @title A utility function for `sumOfVotes` invariant +function getVotesSum(uint256 proposalId) returns mathint { + IVotingMachineWithProofs.ProposalWithoutVotes prop = getProposalById(proposalId); + return prop.forVotes + prop.againstVotes; +} + + +/// @title The sum of votes in favor and against equals the sum of stored voting powers +invariant sumOfVotes(uint256 proposalId) + votesSum[proposalId] == getVotesSum(proposalId); + + +// Casting votes =============================================================== + +/** @title If a proposal's votes tally changed then a vote was cast on the proposal + * To be precise, if the proposal's votes tally changed then there exists a voter `v` + * whose stored voting power on the proposal changed from zero to positive. + */ +rule voteTallyChangedOnlyByVoting(method f, uint256 proposalId) { + assert sig:getProposalById(uint256).isView; + + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + mathint numStoresPre = number_stores(); + + env e; + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + + mathint numStoresPost = number_stores(); + bool is_tallyChanged = ( + (pre.forVotes != post.forVotes) || (pre.againstVotes != post.againstVotes) + ); + assert is_tallyChanged => ( + is_someoneVoting(proposalId) && (numStoresPost == numStoresPre + 1) + ); +} + + +/** @title Casting a vote changes the proposal's votes tally + * If a vote was cast for a proposal, then the proposal's votes tally changed. + * Moreover, the change in tally corresponds to the vote that was cast. + */ +rule voteUpdatesTally(method f, uint256 proposalId, address voter) { + env e; + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + IVotingMachineWithProofs.Vote preVote = getUserProposalVote(voter, proposalId); + IVotingMachineWithProofs.ProposalState state = getProposalState(e, proposalId); + + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + IVotingMachineWithProofs.Vote postVote = getUserProposalVote(voter, proposalId); + + bool is_voteCast = (preVote.votingPower != postVote.votingPower); + + assert is_voteCast => ( + // hasn't voted before (also implies `postVote.votingPower > 0`) + preVote.votingPower == 0 && + // Can't vote in a state other than `Active` + state == IVotingMachineWithProofs.ProposalState.Active + ); + + mathint forChange = post.forVotes - pre.forVotes; + mathint againstChange = post.againstVotes - pre.againstVotes; + mathint votedPower = to_mathint(postVote.votingPower); + assert (is_voteCast && postVote.support) => ( + (forChange == votedPower) && (againstChange == 0) + ); + assert (is_voteCast && !postVote.support) => ( + (forChange == 0) && (againstChange == votedPower) + ); +} + + +/** @title Returns true if `f` is a voting method where sender is the voter + * Used in `dispatchVote` and `onlyVoteCanChangeResult` below. + */ +function isSenderVoterFunction(method f) returns bool { + return ( + f.selector == sig:submitVote( + uint256, bool, IVotingMachineWithProofs.VotingBalanceProof[] + ).selector || f.selector == sig:submitVoteSingleProof( + uint256, bool, IVotingMachineWithProofs.VotingBalanceProof + ).selector + ); +} + + +/** @title Utility function for dispatching voting methods - ensures correct voter + * Used in `onlyVoteCanChangeResult` and `strangerVoteUnchanged` below. + */ +function dispatchVote(method f, env e, address voter) { + // Commonly used args + uint256 aProposalId; + bool support; + IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs; + + if (f.selector == sig:submitVoteAsRepresentative( + uint256,bool,address,bytes,IVotingMachineWithProofs.VotingBalanceProof[] + ).selector + ) { + bytes proofOfRepresentation; + submitVoteAsRepresentative( + e, aProposalId, support, voter, proofOfRepresentation, votingBalanceProofs + ); + } else if (f.selector == sig:submitVoteAsRepresentativeBySignature( + uint256,address,address,bool,bytes, + IVotingMachineWithProofs.VotingBalanceProof[], + IVotingMachineWithProofs.SignatureParams + ).selector + ) { + address representative; + bytes proofOfRepresentation; + IVotingMachineWithProofs.SignatureParams signatureParams; + submitVoteAsRepresentativeBySignature( + e, aProposalId, voter, representative, support, proofOfRepresentation, + votingBalanceProofs, + signatureParams + ); + } else if (f.selector == sig:submitVoteBySignature( + uint256, address, bool, IVotingMachineWithProofs.VotingBalanceProof[], + uint8, bytes32, bytes32 + ).selector + ) { + uint8 v; + bytes32 r; + bytes32 s; + submitVoteBySignature(e, aProposalId, voter, support, votingBalanceProofs, v, r, s); + } else if (f.selector == sig:submitVoteFromVoter( + address, uint256, bool, IVotingMachineWithProofs.VotingBalanceProof[] + ).selector + ) { + submitVoteFromVoter(e, voter, aProposalId, support, votingBalanceProofs); + } else { + if isSenderVoterFunction(f) { + // The sender is the voter + require voter == e.msg.sender; + } + calldataarg args; + f(e, args); + } +} + + +/// @title Vote tally can be changed only by one of the voting methods +rule onlyVoteCanChangeResult(method f, uint256 proposalId, address voter) { + env e; + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + IVotingMachineWithProofs.Vote preVote = getUserProposalVote(voter, proposalId); + + dispatchVote(f, e, voter); + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + IVotingMachineWithProofs.Vote postVote = getUserProposalVote(voter, proposalId); + + bool is_tallyChanged = ( + (pre.forVotes != post.forVotes) || (pre.againstVotes != post.againstVotes) + ); + // Is the `voter` the one who cast the vote + bool is_voterCastVote = preVote.votingPower != postVote.votingPower; + + assert ( + is_tallyChanged => is_voterCastVote && ( + isSenderVoterFunction(f) || + f.selector == sig:submitVoteAsRepresentative( + uint256,bool,address,bytes,IVotingMachineWithProofs.VotingBalanceProof[] + ).selector || + f.selector == sig:submitVoteAsRepresentativeBySignature( + uint256,address,address,bool,bytes, + IVotingMachineWithProofs.VotingBalanceProof[], + IVotingMachineWithProofs.SignatureParams + ).selector || + f.selector == sig:submitVoteBySignature( + uint256, address, bool, IVotingMachineWithProofs.VotingBalanceProof[], + uint8, bytes32, bytes32 + ).selector || + f.selector == sig:submitVoteFromVoter( + address, uint256, bool, IVotingMachineWithProofs.VotingBalanceProof[] + ).selector + ) + ); +} + + +/// @title Voting tally can only increase +rule votingTallyCanOnlyIncrease(method f, uint256 proposalId) { + IVotingMachineWithProofs.ProposalWithoutVotes pre = getProposalById(proposalId); + + env e; + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalWithoutVotes post = getProposalById(proposalId); + + bool is_tallyChanged = ( + (pre.forVotes != post.forVotes) || (pre.againstVotes != post.againstVotes) + ); + assert is_tallyChanged => ( + (post.forVotes > pre.forVotes) || (post.againstVotes > pre.againstVotes) + ); + assert (post.forVotes >= pre.forVotes) && (post.againstVotes >= pre.againstVotes); +} + + +// Other proposals and voters ================================================== + +/// @title A stranger's stored vote is unchanged when another votes +rule strangerVoteUnchanged(method f, uint256 proposalId, address stranger, address voter) +{ + require voter != stranger; + IVotingMachineWithProofs.Vote strangePre = getUserProposalVote(stranger, proposalId); + + env e; + dispatchVote(f, e, voter); + + IVotingMachineWithProofs.Vote strangePost = getUserProposalVote(stranger, proposalId); + + assert strangePre.support == strangePost.support; + assert strangePre.votingPower == strangePost.votingPower; +} + + +/// @title Only a single proposal's tally and votes may change by a single method call +rule otherProposalUnchanged( + method f, uint256 proposalId, uint256 otherProposal, address otherVoter +) { + require proposalId != otherProposal; + + env e; + IVotingMachineWithProofs.ProposalWithoutVotes preOriginal = getProposalById(proposalId); + IVotingMachineWithProofs.ProposalWithoutVotes preOther = getProposalById(otherProposal); + IVotingMachineWithProofs.Vote preOVote = getUserProposalVote(otherVoter, otherProposal); + + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.ProposalWithoutVotes postOriginal = getProposalById(proposalId); + IVotingMachineWithProofs.ProposalWithoutVotes postOther = getProposalById(otherProposal); + IVotingMachineWithProofs.Vote postOVote = getUserProposalVote(otherVoter, otherProposal); + + bool is_tallyChanged = ( + (preOriginal.forVotes != postOriginal.forVotes) || + (preOriginal.againstVotes != postOriginal.againstVotes) + ); + bool is_otherTallyChanged = ( + (preOther.forVotes != postOther.forVotes) || + (preOther.againstVotes != postOther.againstVotes) + ); + bool is_otherVoteChanged = (preOVote.votingPower != postOVote.votingPower); + assert is_tallyChanged => (!is_otherTallyChanged && !is_otherVoteChanged); +} + + +/// @title Only a single voter's stored voting power may change (on a given proposal) +rule otherVoterUntouched( + method f, uint256 proposalId, address voter, address stranger +) { + require voter != stranger; + + env e; + IVotingMachineWithProofs.Vote preVoter = getUserProposalVote(voter, proposalId); + IVotingMachineWithProofs.Vote preStranger = getUserProposalVote(stranger, proposalId); + + calldataarg args; + f(e, args); + + IVotingMachineWithProofs.Vote postVoter = getUserProposalVote(voter, proposalId); + IVotingMachineWithProofs.Vote postStranger = getUserProposalVote(stranger, proposalId); + + bool is_voterChanged = (preVoter.votingPower != postVoter.votingPower); + bool is_strangerChanged = (preStranger.votingPower != postStranger.votingPower); + assert is_voterChanged => !is_strangerChanged; +} + +// rule sanity{ +// env e; +// calldataarg arg; +// method f; +// f(e, arg); +// satisfy true; +// } + +// // Representative +// rule cannot_vote_twice_with_submitVote(method f) filtered { f -> !f.isView}{ + +// env e1; +// env e2; +// uint256 proposalId1; +// uint256 proposalId2; +// bool support1; +// bool support2; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs1; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; +// env e_f; +// calldataarg args; + +// submitVote(e1, proposalId1,support1,votingBalanceProofs1); +// f(e_f, args); +// submitVote(e2, proposalId2,support2,votingBalanceProofs2); +// assert proposalId1 == proposalId2 => e1.msg.sender != e2.msg.sender; + +// } + + +// rule cannot_vote_twice_with_submitVote_witness{ + +// env e1; +// env e2; +// uint256 proposalId1; +// uint256 proposalId2; +// bool support1; +// bool support2; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs1; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; + +// submitVote(e1, proposalId1,support1,votingBalanceProofs1); +// submitVote(e2, proposalId2,support2,votingBalanceProofs2); +// require proposalId1 == proposalId2; +// satisfy e1.msg.sender != e2.msg.sender; +// } + + +//check submitVoteAsRepresentative and submitVote + +rule cannot_vote_twice_with_submitVote_and_submitVoteAsRepresentative(method f) filtered { f -> !f.isView}{ + + env e1; + env e2; + uint256 proposalId1; + uint256 proposalId2; + bool support1; + bool support2; + IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs1; + IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; + address voter; + bytes proofOfRepresentation; + + env e_f; + calldataarg args; + + submitVote(e1, proposalId1,support1,votingBalanceProofs1); + f(e_f, args); + submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); + + assert proposalId1 == proposalId2 => e1.msg.sender != voter; + +} + +// rule cannot_vote_twice_with_submitVote_and_submitVoteAsRepresentative_witness(method f) filtered { f -> !f.isView}{ + +// env e1; +// env e2; +// uint256 proposalId1; +// uint256 proposalId2; +// bool support1; +// bool support2; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs1; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; +// address voter; +// bytes proofOfRepresentation; + +// env e_f; +// calldataarg args; + +// submitVote(e1, proposalId1,support1,votingBalanceProofs1); +// f(e_f, args); +// submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); + +// require proposalId1 == proposalId2; +// satisfy e1.msg.sender != voter; + +// } + + +rule cannot_vote_twice_with_submitVoteAsRepresentative_and_submitVote(method f) filtered { f -> !f.isView}{ + + env e1; + env e2; + uint256 proposalId1; + uint256 proposalId2; + bool support1; + bool support2; + IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs1; + IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; + address voter; + bytes proofOfRepresentation; + + env e_f; + calldataarg args; + + submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); + f(e_f, args); + submitVote(e1, proposalId1,support1,votingBalanceProofs1); + + assert proposalId1 == proposalId2 => e1.msg.sender != voter; + +} + +// rule cannot_vote_twice_with_submitVoteAsRepresentative_and_submitVote_witness(method f) filtered { f -> !f.isView}{ + +// env e1; +// env e2; +// uint256 proposalId1; +// uint256 proposalId2; +// bool support1; +// bool support2; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs1; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; +// address voter; +// bytes proofOfRepresentation; + +// env e_f; +// calldataarg args; + +// submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); +// f(e_f, args); +// submitVote(e1, proposalId1,support1,votingBalanceProofs1); + +// require proposalId1 == proposalId2; +// satisfy e1.msg.sender != voter; + +// } + +//check submitVoteAsRepresentative and submitVoteSingleProof + + +// rule cannot_vote_twice_with_submitVoteSingleProof_and_submitVoteAsRepresentative(method f) filtered { f -> !f.isView}{ + +// env e1; +// env e2; +// uint256 proposalId1; +// uint256 proposalId2; +// bool support1; +// bool support2; +// IVotingMachineWithProofs.VotingBalanceProof votingBalanceProofs1; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; +// address voter; +// bytes proofOfRepresentation; + +// env e_f; +// calldataarg args; + +// submitVoteSingleProof(e1, proposalId1,support1,votingBalanceProofs1); +// f(e_f, args); +// submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); + +// assert proposalId1 == proposalId2 => e1.msg.sender != voter; + +// } + +// rule cannot_vote_twice_with_submitVoteSingleProof_and_submitVoteAsRepresentative_witness(method f) filtered { f -> !f.isView}{ + +// env e1; +// env e2; +// uint256 proposalId1; +// uint256 proposalId2; +// bool support1; +// bool support2; +// IVotingMachineWithProofs.VotingBalanceProof votingBalanceProofs1; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; +// address voter; +// bytes proofOfRepresentation; + +// env e_f; +// calldataarg args; + +// submitVoteSingleProof(e1, proposalId1,support1,votingBalanceProofs1); +// f(e_f, args); +// submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); + +// require proposalId1 == proposalId2; +// satisfy e1.msg.sender != voter; + +// } + + +rule cannot_vote_twice_with_submitVoteSingleProofAsRepresentative_and_submitVote(method f) filtered { f -> !f.isView}{ + + env e1; + env e2; + uint256 proposalId1; + uint256 proposalId2; + bool support1; + bool support2; + IVotingMachineWithProofs.VotingBalanceProof votingBalanceProofs1; + IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; + address voter; + bytes proofOfRepresentation; + + env e_f; + calldataarg args; + + submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); + f(e_f, args); + submitVoteSingleProof(e1, proposalId1,support1,votingBalanceProofs1); + + assert proposalId1 == proposalId2 => e1.msg.sender != voter; + +} + +// rule cannot_vote_twice_with_submitVoteAsRepresentative_and_submitVoteSingleProof_witness(method f) filtered { f -> !f.isView}{ + +// env e1; +// env e2; +// uint256 proposalId1; +// uint256 proposalId2; +// bool support1; +// bool support2; +// IVotingMachineWithProofs.VotingBalanceProof votingBalanceProofs1; +// IVotingMachineWithProofs.VotingBalanceProof[] votingBalanceProofs2; +// address voter; +// bytes proofOfRepresentation; + +// env e_f; +// calldataarg args; + +// submitVoteAsRepresentative(e2, proposalId2, support2, voter, proofOfRepresentation, votingBalanceProofs2); +// f(e_f, args); +// submitVoteSingleProof(e1, proposalId1,support1,votingBalanceProofs1); + +// require proposalId1 == proposalId2; +// satisfy e1.msg.sender != voter; + +// } diff --git a/security/certora/tests/BaseVotingStrategy-1.sol b/security/certora/tests/BaseVotingStrategy-1.sol new file mode 100644 index 0000000..e131ac9 --- /dev/null +++ b/security/certora/tests/BaseVotingStrategy-1.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IBaseVotingStrategy} from '../interfaces/IBaseVotingStrategy.sol'; +import {Errors} from './libraries/Errors.sol'; + +//import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; + +/** + * @title BaseVotingStrategy + * @author BGD Labs + * @notice This contract contains the base logic of a voting strategy, being on governance chain or voting machine chain. + */ +abstract contract BaseVotingStrategy is IBaseVotingStrategy { + function AAVE() public pure virtual returns (address) { + return 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; + } + + function STK_AAVE() public pure virtual returns (address) { + return 0x4da27a545c0c5B758a6BA100e3a049001de870f5; + } + + function A_AAVE() public pure virtual returns (address) { + return 0xA700b4eB416Be35b2911fd5Dee80678ff64fF6C9; + } + + uint128 public constant BASE_BALANCE_SLOT = 0; + uint128 public constant A_AAVE_BASE_BALANCE_SLOT = 52; + uint128 public constant A_AAVE_DELEGATED_STATE_SLOT = 64; + + /// @dev on the constructor we get all the voting assets and emit the different asset configurations + constructor() { + address[] memory votingAssetList = getVotingAssetList(); + + // Check that voting strategy at least has one asset + require(votingAssetList.length != 0, Errors.NO_VOTING_ASSETS); + + for (uint256 i = 0; i < votingAssetList.length; i++) { + for (uint256 j = i + 1; j < votingAssetList.length; j++) { + require( + votingAssetList[i] != votingAssetList[j], + Errors.REPEATED_STRATEGY_ASSET + ); + } + VotingAssetConfig memory votingAssetConfig = getVotingAssetConfig( + votingAssetList[i] + ); + + require( + votingAssetConfig.storageSlots.length > 0, + Errors.EMPTY_ASSET_STORAGE_SLOTS + ); + + for (uint256 k = 0; k < votingAssetConfig.storageSlots.length; k++) { + for ( + uint256 l = k + 1; + l < votingAssetConfig.storageSlots.length; + l++ + ) { + require( + votingAssetConfig.storageSlots[k] != + votingAssetConfig.storageSlots[l], + Errors.REPEATED_STRATEGY_ASSET_SLOT + ); + } + } + + emit VotingAssetAdd(votingAssetList[i], votingAssetConfig.storageSlots); + } + } + + /// @inheritdoc IBaseVotingStrategy + function getVotingAssetList() public pure returns (address[] memory) { + address[] memory votingAssets = new address[](3); + + votingAssets[0] = A_AAVE(); //AAVE(); + votingAssets[1] = STK_AAVE(); + votingAssets[2] = A_AAVE(); + + return votingAssets; + } + + /// @inheritdoc IBaseVotingStrategy + function getVotingAssetConfig( + address asset + ) public pure returns (VotingAssetConfig memory) { + VotingAssetConfig memory votingAssetConfig; + + if (asset == AAVE() || asset == STK_AAVE()) { + votingAssetConfig.storageSlots = new uint128[](1); + votingAssetConfig.storageSlots[0] = BASE_BALANCE_SLOT; + } else if (asset == A_AAVE()) { + votingAssetConfig.storageSlots = new uint128[](2); + votingAssetConfig.storageSlots[0] = A_AAVE_BASE_BALANCE_SLOT; + votingAssetConfig.storageSlots[1] = A_AAVE_DELEGATED_STATE_SLOT; + } else { + return votingAssetConfig; + } + + return votingAssetConfig; + } + + /// @inheritdoc IBaseVotingStrategy + function isTokenSlotAccepted( + address token, + uint128 slot + ) external pure returns (bool) { + VotingAssetConfig memory votingAssetConfig = getVotingAssetConfig(token); + for (uint256 i = 0; i < votingAssetConfig.storageSlots.length; i++) { + if (slot == votingAssetConfig.storageSlots[i]) { + return true; + } + } + return false; + } +} diff --git a/security/certora/tests/BaseVotingStrategy-2.sol b/security/certora/tests/BaseVotingStrategy-2.sol new file mode 100644 index 0000000..804498c --- /dev/null +++ b/security/certora/tests/BaseVotingStrategy-2.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IBaseVotingStrategy} from '../interfaces/IBaseVotingStrategy.sol'; +import {Errors} from './libraries/Errors.sol'; + +//import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; + +/** + * @title BaseVotingStrategy + * @author BGD Labs + * @notice This contract contains the base logic of a voting strategy, being on governance chain or voting machine chain. + */ +abstract contract BaseVotingStrategy is IBaseVotingStrategy { + function AAVE() public pure virtual returns (address) { + return 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; + } + + function STK_AAVE() public pure virtual returns (address) { + return 0x4da27a545c0c5B758a6BA100e3a049001de870f5; + } + + function A_AAVE() public pure virtual returns (address) { + return 0xA700b4eB416Be35b2911fd5Dee80678ff64fF6C9; + } + + uint128 public constant BASE_BALANCE_SLOT = 0; + uint128 public constant A_AAVE_BASE_BALANCE_SLOT = 52; + uint128 public constant A_AAVE_DELEGATED_STATE_SLOT = 64; + + /// @dev on the constructor we get all the voting assets and emit the different asset configurations + constructor() { + address[] memory votingAssetList = getVotingAssetList(); + + // Check that voting strategy at least has one asset + require(votingAssetList.length != 0, Errors.NO_VOTING_ASSETS); + + for (uint256 i = 0; i < votingAssetList.length; i++) { + for (uint256 j = i + 1; j < votingAssetList.length; j++) { + require( + votingAssetList[i] != votingAssetList[j], + Errors.REPEATED_STRATEGY_ASSET + ); + } + VotingAssetConfig memory votingAssetConfig = getVotingAssetConfig( + votingAssetList[i] + ); + + require( + votingAssetConfig.storageSlots.length > 0, + Errors.EMPTY_ASSET_STORAGE_SLOTS + ); + + for (uint256 k = 0; k < votingAssetConfig.storageSlots.length; k++) { + for ( + uint256 l = k + 1; + l < votingAssetConfig.storageSlots.length; + l++ + ) { + require( + votingAssetConfig.storageSlots[k] != + votingAssetConfig.storageSlots[l], + Errors.REPEATED_STRATEGY_ASSET_SLOT + ); + } + } + + emit VotingAssetAdd(votingAssetList[i], votingAssetConfig.storageSlots); + } + } + + /// @inheritdoc IBaseVotingStrategy + function getVotingAssetList() public pure returns (address[] memory) { + address[] memory votingAssets = new address[](3); + + votingAssets[0] = AAVE(); + votingAssets[1] = STK_AAVE(); + votingAssets[2] = A_AAVE(); + + return votingAssets; + } + + /// @inheritdoc IBaseVotingStrategy + function getVotingAssetConfig( + address asset + ) public pure returns (VotingAssetConfig memory) { + VotingAssetConfig memory votingAssetConfig; + + if (asset == AAVE() || asset == STK_AAVE()) { + votingAssetConfig.storageSlots = new uint128[](1); + votingAssetConfig.storageSlots[0] = BASE_BALANCE_SLOT; + } else if (asset == A_AAVE()) { + votingAssetConfig.storageSlots = new uint128[](2); + votingAssetConfig.storageSlots[0] = A_AAVE_BASE_BALANCE_SLOT; + votingAssetConfig.storageSlots[1] = A_AAVE_DELEGATED_STATE_SLOT; + } else { + return votingAssetConfig; + } + + return votingAssetConfig; + } + + /// @inheritdoc IBaseVotingStrategy + function isTokenSlotAccepted( + address token, + uint128 slot + ) external pure returns (bool) { + VotingAssetConfig memory votingAssetConfig = getVotingAssetConfig(token); + for (uint256 i = 0; i < votingAssetConfig.storageSlots.length-1; i++) { + if (slot == votingAssetConfig.storageSlots[i]) { + return true; + } + } + return false; + } +} diff --git a/security/certora/tests/GovernancePowerStrategy-3.sol b/security/certora/tests/GovernancePowerStrategy-3.sol new file mode 100644 index 0000000..7502db8 --- /dev/null +++ b/security/certora/tests/GovernancePowerStrategy-3.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IGovernancePowerDelegationToken} from 'aave-token-v3/interfaces/IGovernancePowerDelegationToken.sol'; +import {IBaseVotingStrategy} from '../interfaces/IBaseVotingStrategy.sol'; +import {IGovernancePowerStrategy} from '../interfaces/IGovernancePowerStrategy.sol'; +import {BaseVotingStrategy} from './BaseVotingStrategy.sol'; + +/** + * @title GovernancePowerStrategy + * @author BGD Labs + * @notice This contracts overrides the base voting strategy to return the power of specific assets used on the strategy. + * @dev These tokens will be used to get the proposition power to check if proposal can be created, and are the ones + needed on the voting machine chain voting strategy. + */ +contract GovernancePowerStrategy is + BaseVotingStrategy, + IGovernancePowerStrategy +{ + /// @inheritdoc IGovernancePowerStrategy + function getFullVotingPower(address user) external view returns (uint256) { + return + _getFullPowerByType( + user, + IGovernancePowerDelegationToken.GovernancePowerType.PROPOSITION + ); + } + + /// @inheritdoc IGovernancePowerStrategy + function getFullPropositionPower( + address user + ) external view returns (uint256) { + return + _getFullPowerByType( + user, + IGovernancePowerDelegationToken.GovernancePowerType.PROPOSITION + ); + } + + /** + * @notice method to get the full user's power by type + * @param user address of the user to get the full power + * @param powerType type of the power to get (voting, proposal) + * @return full power of an user depending on the type (voting, proposal) + */ + function _getFullPowerByType( + address user, + IGovernancePowerDelegationToken.GovernancePowerType powerType + ) internal view returns (uint256) { + uint256 fullGovernancePower; + + address[] memory votingAssetList = getVotingAssetList(); + for (uint256 i = 0; i < votingAssetList.length; i++) { + fullGovernancePower += IGovernancePowerDelegationToken(votingAssetList[i]) + .getPowerCurrent(user, powerType); + } + + return fullGovernancePower; + } +} diff --git a/security/certora/tests/GovernancePowerStrategy-4.sol b/security/certora/tests/GovernancePowerStrategy-4.sol new file mode 100644 index 0000000..4e43d70 --- /dev/null +++ b/security/certora/tests/GovernancePowerStrategy-4.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IGovernancePowerDelegationToken} from 'aave-token-v3/interfaces/IGovernancePowerDelegationToken.sol'; +import {IBaseVotingStrategy} from '../interfaces/IBaseVotingStrategy.sol'; +import {IGovernancePowerStrategy} from '../interfaces/IGovernancePowerStrategy.sol'; +import {BaseVotingStrategy} from './BaseVotingStrategy.sol'; + +/** + * @title GovernancePowerStrategy + * @author BGD Labs + * @notice This contracts overrides the base voting strategy to return the power of specific assets used on the strategy. + * @dev These tokens will be used to get the proposition power to check if proposal can be created, and are the ones + needed on the voting machine chain voting strategy. + */ +contract GovernancePowerStrategy is + BaseVotingStrategy, + IGovernancePowerStrategy +{ + /// @inheritdoc IGovernancePowerStrategy + function getFullVotingPower(address user) external view returns (uint256) { + return + _getFullPowerByType( + user, + IGovernancePowerDelegationToken.GovernancePowerType.VOTING + ); + } + + /// @inheritdoc IGovernancePowerStrategy + function getFullPropositionPower( + address user + ) external view returns (uint256) { + return + _getFullPowerByType( + user, + IGovernancePowerDelegationToken.GovernancePowerType.PROPOSITION + ); + } + + /** + * @notice method to get the full user's power by type + * @param user address of the user to get the full power + * @param powerType type of the power to get (voting, proposal) + * @return full power of an user depending on the type (voting, proposal) + */ + function _getFullPowerByType( + address user, + IGovernancePowerDelegationToken.GovernancePowerType powerType + ) internal view returns (uint256) { + uint256 fullGovernancePower; + + address[] memory votingAssetList = getVotingAssetList(); + for (uint256 i = 0; i < votingAssetList.length-1; i++) { + fullGovernancePower += IGovernancePowerDelegationToken(votingAssetList[i]) + .getPowerCurrent(user, powerType); + } + + return fullGovernancePower; + } +} diff --git a/security/certora/tests/GovernancePowerStrategy-5.sol b/security/certora/tests/GovernancePowerStrategy-5.sol new file mode 100644 index 0000000..42fc0da --- /dev/null +++ b/security/certora/tests/GovernancePowerStrategy-5.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IGovernancePowerDelegationToken} from 'aave-token-v3/interfaces/IGovernancePowerDelegationToken.sol'; +import {IBaseVotingStrategy} from '../interfaces/IBaseVotingStrategy.sol'; +import {IGovernancePowerStrategy} from '../interfaces/IGovernancePowerStrategy.sol'; +import {BaseVotingStrategy} from './BaseVotingStrategy.sol'; + +/** + * @title GovernancePowerStrategy + * @author BGD Labs + * @notice This contracts overrides the base voting strategy to return the power of specific assets used on the strategy. + * @dev These tokens will be used to get the proposition power to check if proposal can be created, and are the ones + needed on the voting machine chain voting strategy. + */ +contract GovernancePowerStrategy is + BaseVotingStrategy, + IGovernancePowerStrategy +{ + /// @inheritdoc IGovernancePowerStrategy + function getFullVotingPower(address user) external view returns (uint256) { + return + _getFullPowerByType( + user, + IGovernancePowerDelegationToken.GovernancePowerType.VOTING + ); + } + + /// @inheritdoc IGovernancePowerStrategy + function getFullPropositionPower( + address user + ) external view returns (uint256) { + return + _getFullPowerByType( + user, + IGovernancePowerDelegationToken.GovernancePowerType.PROPOSITION + ); + } + + /** + * @notice method to get the full user's power by type + * @param user address of the user to get the full power + * @param powerType type of the power to get (voting, proposal) + * @return full power of an user depending on the type (voting, proposal) + */ + function _getFullPowerByType( + address user, + IGovernancePowerDelegationToken.GovernancePowerType powerType + ) internal view returns (uint256) { + uint256 fullGovernancePower; + + address[] memory votingAssetList = getVotingAssetList(); + for (uint256 i = 0; i < votingAssetList.length; i++) { + fullGovernancePower += IGovernancePowerDelegationToken(votingAssetList[0]) + .getPowerCurrent(user, powerType); + } + + return fullGovernancePower; + } +} diff --git a/security/certora/tests/REPORT-power-strategy.txt b/security/certora/tests/REPORT-power-strategy.txt new file mode 100644 index 0000000..1846402 --- /dev/null +++ b/security/certora/tests/REPORT-power-strategy.txt @@ -0,0 +1,83 @@ +GovernancePowerStrategy.spec +============================ + +Questions: +--------- +1. In eachDummyIsUniqueToken(), why do we need all permutations ? +2. In the getVotingAssetConfig(address asset) if the caller supply a non existing asset, + he get some junk in the return value. Should it revert in that case ? + + +Mutations: +--------- +1. UNDETECTED +Changed file: BaseVotingStrategy.sol ==> BaseVotingStrategy-1.sol +The change: BaseVotingStrategy.sol:76: + orig: + "votingAssets[0] = AAVE();" + mutant: + "votingAssets[0] = A_AAVE(); //AAVE();" + +Suggestion for rules that can catch it: +- invariant that all entries of the array returned by getVotingAssetList() are different +- For every voting token T, and every user U that does not delegate, if the balance of U in T is increased then the power of U increased. + + +2. UNDETECTED +Changed file: BaseVotingStrategy.sol ==> BaseVotingStrategy-2.sol +The change: BaseVotingStrategy.sol:109: + orig: + "for (uint256 i = 0; i < votingAssetConfig.storageSlots.length; i++) {" + mutant: + "for (uint256 i = 0; i < votingAssetConfig.storageSlots.length-1; i++) {" + +Suggestion for rule that can catch it: +- We have the rule: invalidTokenRefused(...). We can add its analogue validTokenAccepted(...). + + +3. DETECTED +Changed file: GovernancePowerStrategy.sol ==> GovernancePowerStrategy-3.sol +The change: GovernancePowerStrategy.sol::25: + orig: + "IGovernancePowerDelegationToken.GovernancePowerType.VOTING" + mutant: + "IGovernancePowerDelegationToken.GovernancePowerType.PROPOSITION" + +Found by: powerlessCompliance + + +4. UNDETECTED +Changed file: GovernancePowerStrategy.sol ==> GovernancePowerStrategy-4.sol +The change: GovernancePowerStrategy.sol::53: + orig: + "for (uint256 i = 0; i < votingAssetList.length; i++) {" + mutant: + "for (uint256 i = 0; i < votingAssetList.length-1; i++) {" + +Suggestion for rule that can catch it: +- Like the recommendation in #1: + For every voting token T, and every user U that does not delegate, if the balance of U in T is increased then the power of U increased. + + +5. UNDETECTED +Changed file: GovernancePowerStrategy.sol ==> GovernancePowerStrategy-5.sol +The change: GovernancePowerStrategy.sol::53: + orig: + fullGovernancePower += IGovernancePowerDelegationToken(votingAssetList[i]) + mutant: + fullGovernancePower += IGovernancePowerDelegationToken(votingAssetList[0]) + +Suggestion for rule that can catch it: +- Same as in #4. + + +6. DETECTED by invalidTokenRefused +Changed file: BaseVotingStrategy.sol ==> BaseVotingStrategy-6.sol +The Change: BaseVotingStrategy.sol::94: + orig: + votingAssetConfig.storageSlots[0] = A_AAVE_BASE_BALANCE_SLOT; + mutant: + votingAssetConfig.storageSlots[0] = BASE_BALANCE_SLOT; + + + diff --git a/security/certora/tests/REPORT-voting.txt b/security/certora/tests/REPORT-voting.txt new file mode 100644 index 0000000..bdab6da --- /dev/null +++ b/security/certora/tests/REPORT-voting.txt @@ -0,0 +1,71 @@ +voting_and_tally.spec +===================== + +Questions/Comments: +------------------ +1. In most of the confs we have loop_iter==1. Is it correct ? +2. When running the tests: + - power_summary.conf has several failures (including sanity fails) + - misc.conf has several failures and timeouts + - proposal_states.conf has one sanity failure. +3. I checked the mutants against: + - legality.conf + - proposal_config.conf + - proposal_states.conf + - voting_and_tally.conf + + +Mutations: +--------- +1. UNDETECTED +Changed file: VotingMachineWithProofs.sol ==> VotingMachineWithProofs-1.sol +The change: VotingMachineWithProofs.sol:88: + orig: + return _bridgedVotes[voter][proposalId]; + mutant: + return _bridgedVotes[voter][proposalId+1]; + +Suggestion for rules that can catch it: + + +2. DETECTED (by sanity rule) +Changed file: VotingMachineWithProofs.sol ==> VotingMachineWithProofs-2.sol +The change: VotingMachineWithProofs.sol:197: + orig: + assetFound = true; + mutant: + assetFound = false; + +Suggestion for rules that can catch it: + +3. DETECTED +Changed file: VotingMachineWithProofs.sol ==> VotingMachineWithProofs-3.sol +The change: VotingMachineWithProofs.sol::222 + orig: + return _proposals[proposalId].votes[user]; + mutant: + return _proposals[proposalId+1].votes[user]; + +Suggestion for rules that can catch it: + + +4. DETECTED +Changed file: VotingMachineWithProofs.sol ==> VotingMachineWithProofs-4.sol +The change: VotingMachineWithProofs.sol::250 + orig: + Proposal storage proposal = _proposals[proposalId]; + mutant: + Proposal storage proposal = _proposals[0]; + +Suggestion for rules that can catch it: + +5. UNDETECTED +Changed file: VotingMachineWithProofs.sol ==> VotingMachineWithProofs-5.sol +The change: VotingMachineWithProofs.sol::288 + orig: + proposalListLength - skip - i - 1 + mutant: + proposalListLength - skip - i + +Suggestion for rules that can catch it: + diff --git a/security/certora/tests/VotingMachineWithProofs-1.sol b/security/certora/tests/VotingMachineWithProofs-1.sol new file mode 100644 index 0000000..5733aa4 --- /dev/null +++ b/security/certora/tests/VotingMachineWithProofs-1.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; +import {StateProofVerifier} from './libs/StateProofVerifier.sol'; +import {IVotingMachineWithProofs, IDataWarehouse, IVotingStrategy} from './interfaces/IVotingMachineWithProofs.sol'; +import {IBaseVotingStrategy} from '../../interfaces/IBaseVotingStrategy.sol'; +import {Errors} from '../libraries/Errors.sol'; +import {SlotUtils} from '../libraries/SlotUtils.sol'; +import {EIP712} from 'openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol'; +import {ECDSA} from 'openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol'; + +/** + * @title VotingMachineWithProofs + * @author BGD Labs + * @notice this contract contains the logic to vote on a bridged proposal. It uses registered proofs to calculate the + voting power of the users. Once the voting is finished it will send the results back to the governance chain. + * @dev Abstract contract that is implemented on VotingMachine contract + */ +abstract contract VotingMachineWithProofs is + IVotingMachineWithProofs, + EIP712, + Ownable +{ + using SafeCast for uint256; + + string public constant VOTING_ASSET_WITH_SLOT_RAW = + 'VotingAssetWithSlot(address underlyingAsset,uint128 slot)'; + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTE_SUBMITTED_TYPEHASH = + keccak256( + abi.encodePacked( + 'SubmitVote(uint256 proposalId,address voter,bool support,VotingAssetWithSlot[] votingAssetsWithSlot)', + VOTING_ASSET_WITH_SLOT_RAW + ) + ); + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTING_ASSET_WITH_SLOT_TYPEHASH = + keccak256(abi.encodePacked(VOTING_ASSET_WITH_SLOT_RAW)); + + /// @inheritdoc IVotingMachineWithProofs + string public constant NAME = 'Aave Voting Machine'; + + /// @inheritdoc IVotingMachineWithProofs + IVotingStrategy public immutable VOTING_STRATEGY; + + /// @inheritdoc IVotingMachineWithProofs + IDataWarehouse public immutable DATA_WAREHOUSE; + + // (proposalId => proposal information) stores the information of the proposals + mapping(uint256 => Proposal) internal _proposals; + + // (proposalId => proposal vote configuration) stores the configuration for voting on each proposal + mapping(uint256 => ProposalVoteConfiguration) + internal _proposalsVoteConfiguration; + + // saves the ids of the proposals that have been bridged for a vote. + uint256[] internal _proposalsVoteConfigurationIds; + + // (voter => proposalId => voteInfo) stores the information for the bridged votes + mapping(address => mapping(uint256 => BridgedVote)) internal _bridgedVotes; + + /** + * @param votingStrategy address of the new VotingStrategy contract + */ + constructor(IVotingStrategy votingStrategy) Ownable() EIP712(NAME, 'V1') { + require( + address(votingStrategy) != address(0), + Errors.INVALID_VOTING_STRATEGY + ); + VOTING_STRATEGY = votingStrategy; + DATA_WAREHOUSE = votingStrategy.DATA_WAREHOUSE(); + } + + /// @inheritdoc IVotingMachineWithProofs + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IVotingMachineWithProofs + function getBridgedVoteInfo( + uint256 proposalId, + address voter + ) external view returns (BridgedVote memory) { + return _bridgedVotes[voter][proposalId+1]; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalVoteConfiguration( + uint256 proposalId + ) external view returns (ProposalVoteConfiguration memory) { + return _proposalsVoteConfiguration[proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function startProposalVote(uint256 proposalId) external returns (uint256) { + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + require( + voteConfig.l1ProposalBlockHash != bytes32(0), + Errors.MISSING_PROPOSAL_BLOCK_HASH + ); + Proposal storage newProposal = _proposals[proposalId]; + + require( + _getProposalState(newProposal) == ProposalState.NotCreated, + Errors.PROPOSAL_VOTE_ALREADY_CREATED + ); + + VOTING_STRATEGY.hasRequiredRoots(voteConfig.l1ProposalBlockHash); + + uint40 startTime = _getCurrentTimeRef(); + uint40 endTime = startTime + voteConfig.votingDuration; + + newProposal.id = proposalId; + newProposal.creationBlockNumber = block.number; + newProposal.startTime = startTime; + newProposal.endTime = endTime; + + emit ProposalVoteStarted( + proposalId, + voteConfig.l1ProposalBlockHash, + startTime, + endTime + ); + + return proposalId; + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVoteBySignature( + uint256 proposalId, + address voter, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32[] memory underlyingAssetsWithSlotHashes = new bytes32[]( + votingBalanceProofs.length + ); + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + underlyingAssetsWithSlotHashes[i] = keccak256( + abi.encode( + VOTING_ASSET_WITH_SLOT_TYPEHASH, + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot + ) + ); + } + + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + VOTE_SUBMITTED_TYPEHASH, + proposalId, + voter, + support, + keccak256(abi.encodePacked(underlyingAssetsWithSlotHashes)) + ) + ) + ); + address signer = ECDSA.recover(digest, v, r, s); + + require(signer == voter && signer != address(0), Errors.INVALID_SIGNATURE); + _submitVote(signer, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function settleVoteFromPortal( + uint256 proposalId, + address voter, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + BridgedVote memory bridgedVote = _bridgedVotes[voter][proposalId]; + + require( + bridgedVote.votingAssetsWithSlot.length == votingBalanceProofs.length, + Errors.INVALID_NUMBER_OF_PROOFS_FOR_VOTING_TOKENS + ); + + // check that the proofs are of the voter assets + for (uint256 i = 0; i < bridgedVote.votingAssetsWithSlot.length; i++) { + bool assetFound; + for (uint256 j = 0; j < votingBalanceProofs.length; j++) { + if ( + votingBalanceProofs[j].underlyingAsset == + bridgedVote.votingAssetsWithSlot[i].underlyingAsset && + votingBalanceProofs[j].slot == + bridgedVote.votingAssetsWithSlot[i].slot + ) { + assetFound = true; + break; + } + } + + require(assetFound, Errors.PROOFS_NOT_FOR_VOTING_TOKENS); + } + + _submitVote(voter, proposalId, bridgedVote.support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVote( + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + _submitVote(msg.sender, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function getUserProposalVote( + address user, + uint256 proposalId + ) external view returns (Vote memory) { + return _proposals[proposalId].votes[user]; + } + + /// @inheritdoc IVotingMachineWithProofs + function closeAndSendVote(uint256 proposalId) external { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Finished, + Errors.PROPOSAL_VOTE_NOT_FINISHED + ); + + proposal.votingClosedAndSentBlockNumber = block.number; + proposal.votingClosedAndSentTimestamp = _getCurrentTimeRef(); + + uint256 forVotes = proposal.forVotes; + uint256 againstVotes = proposal.againstVotes; + + proposal.sentToGovernance = true; + + _sendVoteResults(proposalId, forVotes, againstVotes); + + emit ProposalResultsSent(proposalId, forVotes, againstVotes); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalById( + uint256 proposalId + ) external view returns (ProposalWithoutVotes memory) { + Proposal storage proposal = _proposals[proposalId]; + ProposalWithoutVotes memory proposalWithoutVotes = ProposalWithoutVotes({ + id: proposalId, + startTime: proposal.startTime, + endTime: proposal.endTime, + creationBlockNumber: proposal.creationBlockNumber, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + votingClosedAndSentBlockNumber: proposal.votingClosedAndSentBlockNumber, + votingClosedAndSentTimestamp: proposal.votingClosedAndSentTimestamp, + sentToGovernance: proposal.sentToGovernance + }); + + return proposalWithoutVotes; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalState( + uint256 proposalId + ) external view returns (ProposalState) { + return _getProposalState(_proposals[proposalId]); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalsVoteConfigurationIds( + uint256 skip, + uint256 size + ) external view returns (uint256[] memory) { + uint256 proposalListLength = _proposalsVoteConfigurationIds.length; + if (proposalListLength == 0 || proposalListLength <= skip) { + return new uint256[](0); + } else if (proposalListLength < size + skip) { + size = proposalListLength - skip; + } + + uint256[] memory ids = new uint256[](size); + for (uint256 i = 0; i < size; i++) { + ids[i] = _proposalsVoteConfigurationIds[ + proposalListLength - skip - i - 1 + ]; + } + return ids; + } + + /** + * @notice method to cast a vote on a proposal specified by its id + * @param voter address with the voting power + * @param proposalId id of the proposal on which the vote will be cast + * @param support boolean indicating if the vote is in favor or against the proposal + * @param votingBalanceProofs list of objects containing the information necessary to vote using the tokens + allowed on the voting strategy. + * @dev A vote does not need to use all the tokens allowed, can be a subset + */ + function _submitVote( + address voter, + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) internal { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Active, + Errors.PROPOSAL_VOTE_NOT_IN_ACTIVE_STATE + ); + + Vote storage vote = proposal.votes[voter]; + require(vote.votingPower == 0, Errors.PROPOSAL_VOTE_ALREADY_EXISTS); + + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + + uint256 votingPower; + StateProofVerifier.SlotValue memory balanceVotingPower; + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + for (uint256 j = i + 1; j < votingBalanceProofs.length; j++) { + require( + votingBalanceProofs[i].slot != votingBalanceProofs[j].slot || + votingBalanceProofs[i].underlyingAsset != + votingBalanceProofs[j].underlyingAsset, + Errors.VOTE_ONCE_FOR_ASSET + ); + } + + balanceVotingPower = DATA_WAREHOUSE.getStorage( + votingBalanceProofs[i].underlyingAsset, + voteConfig.l1ProposalBlockHash, + SlotUtils.getAccountSlotHash(voter, votingBalanceProofs[i].slot), + votingBalanceProofs[i].proof + ); + + require(balanceVotingPower.exists, Errors.USER_BALANCE_DOES_NOT_EXISTS); + + if (balanceVotingPower.value != 0) { + votingPower += IVotingStrategy(address(VOTING_STRATEGY)).getVotingPower( + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot, + balanceVotingPower.value, + voteConfig.l1ProposalBlockHash + ); + } + } + require(votingPower != 0, Errors.USER_VOTING_BALANCE_IS_ZERO); + + if (support) { + proposal.forVotes += votingPower.toUint128(); + } else { + proposal.againstVotes += votingPower.toUint128(); + } + + vote.support = support; + vote.votingPower = votingPower.toUint248(); + + emit VoteEmitted(proposalId, voter, support, votingPower); + } + + /** + * @notice method to send the voting results on a proposal back to L1 + * @param proposalId id of the proposal to send the voting result to L1 + * @dev This method should be implemented to trigger the bridging flow + */ + function _sendVoteResults( + uint256 proposalId, + uint256 forVotes, + uint256 againstVotes + ) internal virtual; + + /** + * @notice method to get the state of a proposal specified by its id + * @param proposal the proposal to retrieve the state of + * @return the state of the proposal + */ + function _getProposalState( + Proposal storage proposal + ) internal view returns (ProposalState) { + if (proposal.endTime == 0) { + return ProposalState.NotCreated; + } else if (_getCurrentTimeRef() <= proposal.endTime) { + return ProposalState.Active; + } else if (proposal.sentToGovernance) { + return ProposalState.SentToGovernance; + } else { + return ProposalState.Finished; + } + } + + /** + * @notice method to get the timestamp of a block casted to uint40 + * @return uint40 block timestamp + */ + function _getCurrentTimeRef() internal view returns (uint40) { + return uint40(block.timestamp); + } + + /** + * @notice method that registers a proposal configuration and creates the voting if it can. If not it will register the + the configuration for later creation. + * @param proposalId id of the proposal bridged to start the vote on + * @param blockHash hash of the block on L1 when the proposal was activated for voting + * @param votingDuration duration in seconds of the vote + */ + function _createBridgedProposalVote( + uint256 proposalId, + bytes32 blockHash, + uint24 votingDuration + ) internal { + require( + blockHash != bytes32(0), + Errors.INVALID_VOTE_CONFIGURATION_BLOCKHASH + ); + require( + votingDuration > 0, + Errors.INVALID_VOTE_CONFIGURATION_VOTING_DURATION + ); + require( + _proposalsVoteConfiguration[proposalId].l1ProposalBlockHash == bytes32(0), + Errors.PROPOSAL_VOTE_CONFIGURATION_ALREADY_BRIDGED + ); + + _proposalsVoteConfiguration[proposalId] = IVotingMachineWithProofs + .ProposalVoteConfiguration({ + votingDuration: votingDuration, + l1ProposalBlockHash: blockHash + }); + _proposalsVoteConfigurationIds.push(proposalId); + + bool created; + try this.startProposalVote(proposalId) { + created = true; + } catch (bytes memory) {} + + emit ProposalVoteConfigurationBridged( + proposalId, + blockHash, + votingDuration, + created + ); + } + + /** + * @notice method that registers a vote on a proposal from a specific voter, contained in a bridged message + from governance chain + * @param proposalId id of the proposal bridged to start the vote on + * @param voter address that wants to emit the vote + * @param support indicates if vote is in favor or against the proposal + * @param votingAssetsWithSlot list of token addresses with base slots that the voter will use for voting + */ + function _registerBridgedVote( + uint256 proposalId, + address voter, + bool support, + VotingAssetWithSlot[] memory votingAssetsWithSlot + ) internal { + // It also only allows to register the vote when proposal is active. + // To retry to register a vote (after it fails) the message will need to be retried from the cross chain controller + require( + _getProposalState(_proposals[proposalId]) == ProposalState.Active, + Errors.PROPOSAL_VOTE_CAN_NOT_BE_REGISTERED + ); + require(voter != address(0), Errors.INVALID_VOTER); + require(votingAssetsWithSlot.length > 0, Errors.NO_BRIDGED_VOTING_ASSETS); + require( + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.length == 0, + Errors.VOTE_ALREADY_BRIDGED + ); + _bridgedVotes[voter][proposalId].support = support; + for (uint256 i = 0; i < votingAssetsWithSlot.length; i++) { + require( + IBaseVotingStrategy(address(VOTING_STRATEGY)).isTokenSlotAccepted( + votingAssetsWithSlot[i].underlyingAsset, + votingAssetsWithSlot[i].slot + ), + Errors.INVALID_BRIDGED_VOTING_TOKEN + ); + for (uint256 j = i + 1; j < votingAssetsWithSlot.length; j++) { + require( + votingAssetsWithSlot[j].underlyingAsset != + votingAssetsWithSlot[i].underlyingAsset || + votingAssetsWithSlot[j].slot != votingAssetsWithSlot[i].slot, + Errors.BRIDGED_REPEATED_ASSETS + ); + } + + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.push( + votingAssetsWithSlot[i] + ); + } + + emit VoteBridged(proposalId, voter, support, votingAssetsWithSlot); + } +} diff --git a/security/certora/tests/VotingMachineWithProofs-2.sol b/security/certora/tests/VotingMachineWithProofs-2.sol new file mode 100644 index 0000000..e69c395 --- /dev/null +++ b/security/certora/tests/VotingMachineWithProofs-2.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; +import {StateProofVerifier} from './libs/StateProofVerifier.sol'; +import {IVotingMachineWithProofs, IDataWarehouse, IVotingStrategy} from './interfaces/IVotingMachineWithProofs.sol'; +import {IBaseVotingStrategy} from '../../interfaces/IBaseVotingStrategy.sol'; +import {Errors} from '../libraries/Errors.sol'; +import {SlotUtils} from '../libraries/SlotUtils.sol'; +import {EIP712} from 'openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol'; +import {ECDSA} from 'openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol'; + +/** + * @title VotingMachineWithProofs + * @author BGD Labs + * @notice this contract contains the logic to vote on a bridged proposal. It uses registered proofs to calculate the + voting power of the users. Once the voting is finished it will send the results back to the governance chain. + * @dev Abstract contract that is implemented on VotingMachine contract + */ +abstract contract VotingMachineWithProofs is + IVotingMachineWithProofs, + EIP712, + Ownable +{ + using SafeCast for uint256; + + string public constant VOTING_ASSET_WITH_SLOT_RAW = + 'VotingAssetWithSlot(address underlyingAsset,uint128 slot)'; + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTE_SUBMITTED_TYPEHASH = + keccak256( + abi.encodePacked( + 'SubmitVote(uint256 proposalId,address voter,bool support,VotingAssetWithSlot[] votingAssetsWithSlot)', + VOTING_ASSET_WITH_SLOT_RAW + ) + ); + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTING_ASSET_WITH_SLOT_TYPEHASH = + keccak256(abi.encodePacked(VOTING_ASSET_WITH_SLOT_RAW)); + + /// @inheritdoc IVotingMachineWithProofs + string public constant NAME = 'Aave Voting Machine'; + + /// @inheritdoc IVotingMachineWithProofs + IVotingStrategy public immutable VOTING_STRATEGY; + + /// @inheritdoc IVotingMachineWithProofs + IDataWarehouse public immutable DATA_WAREHOUSE; + + // (proposalId => proposal information) stores the information of the proposals + mapping(uint256 => Proposal) internal _proposals; + + // (proposalId => proposal vote configuration) stores the configuration for voting on each proposal + mapping(uint256 => ProposalVoteConfiguration) + internal _proposalsVoteConfiguration; + + // saves the ids of the proposals that have been bridged for a vote. + uint256[] internal _proposalsVoteConfigurationIds; + + // (voter => proposalId => voteInfo) stores the information for the bridged votes + mapping(address => mapping(uint256 => BridgedVote)) internal _bridgedVotes; + + /** + * @param votingStrategy address of the new VotingStrategy contract + */ + constructor(IVotingStrategy votingStrategy) Ownable() EIP712(NAME, 'V1') { + require( + address(votingStrategy) != address(0), + Errors.INVALID_VOTING_STRATEGY + ); + VOTING_STRATEGY = votingStrategy; + DATA_WAREHOUSE = votingStrategy.DATA_WAREHOUSE(); + } + + /// @inheritdoc IVotingMachineWithProofs + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IVotingMachineWithProofs + function getBridgedVoteInfo( + uint256 proposalId, + address voter + ) external view returns (BridgedVote memory) { + return _bridgedVotes[voter][proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalVoteConfiguration( + uint256 proposalId + ) external view returns (ProposalVoteConfiguration memory) { + return _proposalsVoteConfiguration[proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function startProposalVote(uint256 proposalId) external returns (uint256) { + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + require( + voteConfig.l1ProposalBlockHash != bytes32(0), + Errors.MISSING_PROPOSAL_BLOCK_HASH + ); + Proposal storage newProposal = _proposals[proposalId]; + + require( + _getProposalState(newProposal) == ProposalState.NotCreated, + Errors.PROPOSAL_VOTE_ALREADY_CREATED + ); + + VOTING_STRATEGY.hasRequiredRoots(voteConfig.l1ProposalBlockHash); + + uint40 startTime = _getCurrentTimeRef(); + uint40 endTime = startTime + voteConfig.votingDuration; + + newProposal.id = proposalId; + newProposal.creationBlockNumber = block.number; + newProposal.startTime = startTime; + newProposal.endTime = endTime; + + emit ProposalVoteStarted( + proposalId, + voteConfig.l1ProposalBlockHash, + startTime, + endTime + ); + + return proposalId; + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVoteBySignature( + uint256 proposalId, + address voter, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32[] memory underlyingAssetsWithSlotHashes = new bytes32[]( + votingBalanceProofs.length + ); + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + underlyingAssetsWithSlotHashes[i] = keccak256( + abi.encode( + VOTING_ASSET_WITH_SLOT_TYPEHASH, + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot + ) + ); + } + + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + VOTE_SUBMITTED_TYPEHASH, + proposalId, + voter, + support, + keccak256(abi.encodePacked(underlyingAssetsWithSlotHashes)) + ) + ) + ); + address signer = ECDSA.recover(digest, v, r, s); + + require(signer == voter && signer != address(0), Errors.INVALID_SIGNATURE); + _submitVote(signer, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function settleVoteFromPortal( + uint256 proposalId, + address voter, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + BridgedVote memory bridgedVote = _bridgedVotes[voter][proposalId]; + + require( + bridgedVote.votingAssetsWithSlot.length == votingBalanceProofs.length, + Errors.INVALID_NUMBER_OF_PROOFS_FOR_VOTING_TOKENS + ); + + // check that the proofs are of the voter assets + for (uint256 i = 0; i < bridgedVote.votingAssetsWithSlot.length; i++) { + bool assetFound; + for (uint256 j = 0; j < votingBalanceProofs.length; j++) { + if ( + votingBalanceProofs[j].underlyingAsset == + bridgedVote.votingAssetsWithSlot[i].underlyingAsset && + votingBalanceProofs[j].slot == + bridgedVote.votingAssetsWithSlot[i].slot + ) { + assetFound = false; + break; + } + } + + require(assetFound, Errors.PROOFS_NOT_FOR_VOTING_TOKENS); + } + + _submitVote(voter, proposalId, bridgedVote.support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVote( + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + _submitVote(msg.sender, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function getUserProposalVote( + address user, + uint256 proposalId + ) external view returns (Vote memory) { + return _proposals[proposalId].votes[user]; + } + + /// @inheritdoc IVotingMachineWithProofs + function closeAndSendVote(uint256 proposalId) external { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Finished, + Errors.PROPOSAL_VOTE_NOT_FINISHED + ); + + proposal.votingClosedAndSentBlockNumber = block.number; + proposal.votingClosedAndSentTimestamp = _getCurrentTimeRef(); + + uint256 forVotes = proposal.forVotes; + uint256 againstVotes = proposal.againstVotes; + + proposal.sentToGovernance = true; + + _sendVoteResults(proposalId, forVotes, againstVotes); + + emit ProposalResultsSent(proposalId, forVotes, againstVotes); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalById( + uint256 proposalId + ) external view returns (ProposalWithoutVotes memory) { + Proposal storage proposal = _proposals[proposalId]; + ProposalWithoutVotes memory proposalWithoutVotes = ProposalWithoutVotes({ + id: proposalId, + startTime: proposal.startTime, + endTime: proposal.endTime, + creationBlockNumber: proposal.creationBlockNumber, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + votingClosedAndSentBlockNumber: proposal.votingClosedAndSentBlockNumber, + votingClosedAndSentTimestamp: proposal.votingClosedAndSentTimestamp, + sentToGovernance: proposal.sentToGovernance + }); + + return proposalWithoutVotes; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalState( + uint256 proposalId + ) external view returns (ProposalState) { + return _getProposalState(_proposals[proposalId]); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalsVoteConfigurationIds( + uint256 skip, + uint256 size + ) external view returns (uint256[] memory) { + uint256 proposalListLength = _proposalsVoteConfigurationIds.length; + if (proposalListLength == 0 || proposalListLength <= skip) { + return new uint256[](0); + } else if (proposalListLength < size + skip) { + size = proposalListLength - skip; + } + + uint256[] memory ids = new uint256[](size); + for (uint256 i = 0; i < size; i++) { + ids[i] = _proposalsVoteConfigurationIds[ + proposalListLength - skip - i - 1 + ]; + } + return ids; + } + + /** + * @notice method to cast a vote on a proposal specified by its id + * @param voter address with the voting power + * @param proposalId id of the proposal on which the vote will be cast + * @param support boolean indicating if the vote is in favor or against the proposal + * @param votingBalanceProofs list of objects containing the information necessary to vote using the tokens + allowed on the voting strategy. + * @dev A vote does not need to use all the tokens allowed, can be a subset + */ + function _submitVote( + address voter, + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) internal { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Active, + Errors.PROPOSAL_VOTE_NOT_IN_ACTIVE_STATE + ); + + Vote storage vote = proposal.votes[voter]; + require(vote.votingPower == 0, Errors.PROPOSAL_VOTE_ALREADY_EXISTS); + + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + + uint256 votingPower; + StateProofVerifier.SlotValue memory balanceVotingPower; + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + for (uint256 j = i + 1; j < votingBalanceProofs.length; j++) { + require( + votingBalanceProofs[i].slot != votingBalanceProofs[j].slot || + votingBalanceProofs[i].underlyingAsset != + votingBalanceProofs[j].underlyingAsset, + Errors.VOTE_ONCE_FOR_ASSET + ); + } + + balanceVotingPower = DATA_WAREHOUSE.getStorage( + votingBalanceProofs[i].underlyingAsset, + voteConfig.l1ProposalBlockHash, + SlotUtils.getAccountSlotHash(voter, votingBalanceProofs[i].slot), + votingBalanceProofs[i].proof + ); + + require(balanceVotingPower.exists, Errors.USER_BALANCE_DOES_NOT_EXISTS); + + if (balanceVotingPower.value != 0) { + votingPower += IVotingStrategy(address(VOTING_STRATEGY)).getVotingPower( + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot, + balanceVotingPower.value, + voteConfig.l1ProposalBlockHash + ); + } + } + require(votingPower != 0, Errors.USER_VOTING_BALANCE_IS_ZERO); + + if (support) { + proposal.forVotes += votingPower.toUint128(); + } else { + proposal.againstVotes += votingPower.toUint128(); + } + + vote.support = support; + vote.votingPower = votingPower.toUint248(); + + emit VoteEmitted(proposalId, voter, support, votingPower); + } + + /** + * @notice method to send the voting results on a proposal back to L1 + * @param proposalId id of the proposal to send the voting result to L1 + * @dev This method should be implemented to trigger the bridging flow + */ + function _sendVoteResults( + uint256 proposalId, + uint256 forVotes, + uint256 againstVotes + ) internal virtual; + + /** + * @notice method to get the state of a proposal specified by its id + * @param proposal the proposal to retrieve the state of + * @return the state of the proposal + */ + function _getProposalState( + Proposal storage proposal + ) internal view returns (ProposalState) { + if (proposal.endTime == 0) { + return ProposalState.NotCreated; + } else if (_getCurrentTimeRef() <= proposal.endTime) { + return ProposalState.Active; + } else if (proposal.sentToGovernance) { + return ProposalState.SentToGovernance; + } else { + return ProposalState.Finished; + } + } + + /** + * @notice method to get the timestamp of a block casted to uint40 + * @return uint40 block timestamp + */ + function _getCurrentTimeRef() internal view returns (uint40) { + return uint40(block.timestamp); + } + + /** + * @notice method that registers a proposal configuration and creates the voting if it can. If not it will register the + the configuration for later creation. + * @param proposalId id of the proposal bridged to start the vote on + * @param blockHash hash of the block on L1 when the proposal was activated for voting + * @param votingDuration duration in seconds of the vote + */ + function _createBridgedProposalVote( + uint256 proposalId, + bytes32 blockHash, + uint24 votingDuration + ) internal { + require( + blockHash != bytes32(0), + Errors.INVALID_VOTE_CONFIGURATION_BLOCKHASH + ); + require( + votingDuration > 0, + Errors.INVALID_VOTE_CONFIGURATION_VOTING_DURATION + ); + require( + _proposalsVoteConfiguration[proposalId].l1ProposalBlockHash == bytes32(0), + Errors.PROPOSAL_VOTE_CONFIGURATION_ALREADY_BRIDGED + ); + + _proposalsVoteConfiguration[proposalId] = IVotingMachineWithProofs + .ProposalVoteConfiguration({ + votingDuration: votingDuration, + l1ProposalBlockHash: blockHash + }); + _proposalsVoteConfigurationIds.push(proposalId); + + bool created; + try this.startProposalVote(proposalId) { + created = true; + } catch (bytes memory) {} + + emit ProposalVoteConfigurationBridged( + proposalId, + blockHash, + votingDuration, + created + ); + } + + /** + * @notice method that registers a vote on a proposal from a specific voter, contained in a bridged message + from governance chain + * @param proposalId id of the proposal bridged to start the vote on + * @param voter address that wants to emit the vote + * @param support indicates if vote is in favor or against the proposal + * @param votingAssetsWithSlot list of token addresses with base slots that the voter will use for voting + */ + function _registerBridgedVote( + uint256 proposalId, + address voter, + bool support, + VotingAssetWithSlot[] memory votingAssetsWithSlot + ) internal { + // It also only allows to register the vote when proposal is active. + // To retry to register a vote (after it fails) the message will need to be retried from the cross chain controller + require( + _getProposalState(_proposals[proposalId]) == ProposalState.Active, + Errors.PROPOSAL_VOTE_CAN_NOT_BE_REGISTERED + ); + require(voter != address(0), Errors.INVALID_VOTER); + require(votingAssetsWithSlot.length > 0, Errors.NO_BRIDGED_VOTING_ASSETS); + require( + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.length == 0, + Errors.VOTE_ALREADY_BRIDGED + ); + _bridgedVotes[voter][proposalId].support = support; + for (uint256 i = 0; i < votingAssetsWithSlot.length; i++) { + require( + IBaseVotingStrategy(address(VOTING_STRATEGY)).isTokenSlotAccepted( + votingAssetsWithSlot[i].underlyingAsset, + votingAssetsWithSlot[i].slot + ), + Errors.INVALID_BRIDGED_VOTING_TOKEN + ); + for (uint256 j = i + 1; j < votingAssetsWithSlot.length; j++) { + require( + votingAssetsWithSlot[j].underlyingAsset != + votingAssetsWithSlot[i].underlyingAsset || + votingAssetsWithSlot[j].slot != votingAssetsWithSlot[i].slot, + Errors.BRIDGED_REPEATED_ASSETS + ); + } + + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.push( + votingAssetsWithSlot[i] + ); + } + + emit VoteBridged(proposalId, voter, support, votingAssetsWithSlot); + } +} diff --git a/security/certora/tests/VotingMachineWithProofs-3.sol b/security/certora/tests/VotingMachineWithProofs-3.sol new file mode 100644 index 0000000..de7025d --- /dev/null +++ b/security/certora/tests/VotingMachineWithProofs-3.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; +import {StateProofVerifier} from './libs/StateProofVerifier.sol'; +import {IVotingMachineWithProofs, IDataWarehouse, IVotingStrategy} from './interfaces/IVotingMachineWithProofs.sol'; +import {IBaseVotingStrategy} from '../../interfaces/IBaseVotingStrategy.sol'; +import {Errors} from '../libraries/Errors.sol'; +import {SlotUtils} from '../libraries/SlotUtils.sol'; +import {EIP712} from 'openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol'; +import {ECDSA} from 'openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol'; + +/** + * @title VotingMachineWithProofs + * @author BGD Labs + * @notice this contract contains the logic to vote on a bridged proposal. It uses registered proofs to calculate the + voting power of the users. Once the voting is finished it will send the results back to the governance chain. + * @dev Abstract contract that is implemented on VotingMachine contract + */ +abstract contract VotingMachineWithProofs is + IVotingMachineWithProofs, + EIP712, + Ownable +{ + using SafeCast for uint256; + + string public constant VOTING_ASSET_WITH_SLOT_RAW = + 'VotingAssetWithSlot(address underlyingAsset,uint128 slot)'; + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTE_SUBMITTED_TYPEHASH = + keccak256( + abi.encodePacked( + 'SubmitVote(uint256 proposalId,address voter,bool support,VotingAssetWithSlot[] votingAssetsWithSlot)', + VOTING_ASSET_WITH_SLOT_RAW + ) + ); + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTING_ASSET_WITH_SLOT_TYPEHASH = + keccak256(abi.encodePacked(VOTING_ASSET_WITH_SLOT_RAW)); + + /// @inheritdoc IVotingMachineWithProofs + string public constant NAME = 'Aave Voting Machine'; + + /// @inheritdoc IVotingMachineWithProofs + IVotingStrategy public immutable VOTING_STRATEGY; + + /// @inheritdoc IVotingMachineWithProofs + IDataWarehouse public immutable DATA_WAREHOUSE; + + // (proposalId => proposal information) stores the information of the proposals + mapping(uint256 => Proposal) internal _proposals; + + // (proposalId => proposal vote configuration) stores the configuration for voting on each proposal + mapping(uint256 => ProposalVoteConfiguration) + internal _proposalsVoteConfiguration; + + // saves the ids of the proposals that have been bridged for a vote. + uint256[] internal _proposalsVoteConfigurationIds; + + // (voter => proposalId => voteInfo) stores the information for the bridged votes + mapping(address => mapping(uint256 => BridgedVote)) internal _bridgedVotes; + + /** + * @param votingStrategy address of the new VotingStrategy contract + */ + constructor(IVotingStrategy votingStrategy) Ownable() EIP712(NAME, 'V1') { + require( + address(votingStrategy) != address(0), + Errors.INVALID_VOTING_STRATEGY + ); + VOTING_STRATEGY = votingStrategy; + DATA_WAREHOUSE = votingStrategy.DATA_WAREHOUSE(); + } + + /// @inheritdoc IVotingMachineWithProofs + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IVotingMachineWithProofs + function getBridgedVoteInfo( + uint256 proposalId, + address voter + ) external view returns (BridgedVote memory) { + return _bridgedVotes[voter][proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalVoteConfiguration( + uint256 proposalId + ) external view returns (ProposalVoteConfiguration memory) { + return _proposalsVoteConfiguration[proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function startProposalVote(uint256 proposalId) external returns (uint256) { + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + require( + voteConfig.l1ProposalBlockHash != bytes32(0), + Errors.MISSING_PROPOSAL_BLOCK_HASH + ); + Proposal storage newProposal = _proposals[proposalId]; + + require( + _getProposalState(newProposal) == ProposalState.NotCreated, + Errors.PROPOSAL_VOTE_ALREADY_CREATED + ); + + VOTING_STRATEGY.hasRequiredRoots(voteConfig.l1ProposalBlockHash); + + uint40 startTime = _getCurrentTimeRef(); + uint40 endTime = startTime + voteConfig.votingDuration; + + newProposal.id = proposalId; + newProposal.creationBlockNumber = block.number; + newProposal.startTime = startTime; + newProposal.endTime = endTime; + + emit ProposalVoteStarted( + proposalId, + voteConfig.l1ProposalBlockHash, + startTime, + endTime + ); + + return proposalId; + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVoteBySignature( + uint256 proposalId, + address voter, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32[] memory underlyingAssetsWithSlotHashes = new bytes32[]( + votingBalanceProofs.length + ); + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + underlyingAssetsWithSlotHashes[i] = keccak256( + abi.encode( + VOTING_ASSET_WITH_SLOT_TYPEHASH, + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot + ) + ); + } + + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + VOTE_SUBMITTED_TYPEHASH, + proposalId, + voter, + support, + keccak256(abi.encodePacked(underlyingAssetsWithSlotHashes)) + ) + ) + ); + address signer = ECDSA.recover(digest, v, r, s); + + require(signer == voter && signer != address(0), Errors.INVALID_SIGNATURE); + _submitVote(signer, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function settleVoteFromPortal( + uint256 proposalId, + address voter, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + BridgedVote memory bridgedVote = _bridgedVotes[voter][proposalId]; + + require( + bridgedVote.votingAssetsWithSlot.length == votingBalanceProofs.length, + Errors.INVALID_NUMBER_OF_PROOFS_FOR_VOTING_TOKENS + ); + + // check that the proofs are of the voter assets + for (uint256 i = 0; i < bridgedVote.votingAssetsWithSlot.length; i++) { + bool assetFound; + for (uint256 j = 0; j < votingBalanceProofs.length; j++) { + if ( + votingBalanceProofs[j].underlyingAsset == + bridgedVote.votingAssetsWithSlot[i].underlyingAsset && + votingBalanceProofs[j].slot == + bridgedVote.votingAssetsWithSlot[i].slot + ) { + assetFound = true; + break; + } + } + + require(assetFound, Errors.PROOFS_NOT_FOR_VOTING_TOKENS); + } + + _submitVote(voter, proposalId, bridgedVote.support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVote( + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + _submitVote(msg.sender, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function getUserProposalVote( + address user, + uint256 proposalId + ) external view returns (Vote memory) { + return _proposals[proposalId+1].votes[user]; + } + + /// @inheritdoc IVotingMachineWithProofs + function closeAndSendVote(uint256 proposalId) external { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Finished, + Errors.PROPOSAL_VOTE_NOT_FINISHED + ); + + proposal.votingClosedAndSentBlockNumber = block.number; + proposal.votingClosedAndSentTimestamp = _getCurrentTimeRef(); + + uint256 forVotes = proposal.forVotes; + uint256 againstVotes = proposal.againstVotes; + + proposal.sentToGovernance = true; + + _sendVoteResults(proposalId, forVotes, againstVotes); + + emit ProposalResultsSent(proposalId, forVotes, againstVotes); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalById( + uint256 proposalId + ) external view returns (ProposalWithoutVotes memory) { + Proposal storage proposal = _proposals[proposalId]; + ProposalWithoutVotes memory proposalWithoutVotes = ProposalWithoutVotes({ + id: proposalId, + startTime: proposal.startTime, + endTime: proposal.endTime, + creationBlockNumber: proposal.creationBlockNumber, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + votingClosedAndSentBlockNumber: proposal.votingClosedAndSentBlockNumber, + votingClosedAndSentTimestamp: proposal.votingClosedAndSentTimestamp, + sentToGovernance: proposal.sentToGovernance + }); + + return proposalWithoutVotes; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalState( + uint256 proposalId + ) external view returns (ProposalState) { + return _getProposalState(_proposals[proposalId]); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalsVoteConfigurationIds( + uint256 skip, + uint256 size + ) external view returns (uint256[] memory) { + uint256 proposalListLength = _proposalsVoteConfigurationIds.length; + if (proposalListLength == 0 || proposalListLength <= skip) { + return new uint256[](0); + } else if (proposalListLength < size + skip) { + size = proposalListLength - skip; + } + + uint256[] memory ids = new uint256[](size); + for (uint256 i = 0; i < size; i++) { + ids[i] = _proposalsVoteConfigurationIds[ + proposalListLength - skip - i - 1 + ]; + } + return ids; + } + + /** + * @notice method to cast a vote on a proposal specified by its id + * @param voter address with the voting power + * @param proposalId id of the proposal on which the vote will be cast + * @param support boolean indicating if the vote is in favor or against the proposal + * @param votingBalanceProofs list of objects containing the information necessary to vote using the tokens + allowed on the voting strategy. + * @dev A vote does not need to use all the tokens allowed, can be a subset + */ + function _submitVote( + address voter, + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) internal { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Active, + Errors.PROPOSAL_VOTE_NOT_IN_ACTIVE_STATE + ); + + Vote storage vote = proposal.votes[voter]; + require(vote.votingPower == 0, Errors.PROPOSAL_VOTE_ALREADY_EXISTS); + + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + + uint256 votingPower; + StateProofVerifier.SlotValue memory balanceVotingPower; + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + for (uint256 j = i + 1; j < votingBalanceProofs.length; j++) { + require( + votingBalanceProofs[i].slot != votingBalanceProofs[j].slot || + votingBalanceProofs[i].underlyingAsset != + votingBalanceProofs[j].underlyingAsset, + Errors.VOTE_ONCE_FOR_ASSET + ); + } + + balanceVotingPower = DATA_WAREHOUSE.getStorage( + votingBalanceProofs[i].underlyingAsset, + voteConfig.l1ProposalBlockHash, + SlotUtils.getAccountSlotHash(voter, votingBalanceProofs[i].slot), + votingBalanceProofs[i].proof + ); + + require(balanceVotingPower.exists, Errors.USER_BALANCE_DOES_NOT_EXISTS); + + if (balanceVotingPower.value != 0) { + votingPower += IVotingStrategy(address(VOTING_STRATEGY)).getVotingPower( + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot, + balanceVotingPower.value, + voteConfig.l1ProposalBlockHash + ); + } + } + require(votingPower != 0, Errors.USER_VOTING_BALANCE_IS_ZERO); + + if (support) { + proposal.forVotes += votingPower.toUint128(); + } else { + proposal.againstVotes += votingPower.toUint128(); + } + + vote.support = support; + vote.votingPower = votingPower.toUint248(); + + emit VoteEmitted(proposalId, voter, support, votingPower); + } + + /** + * @notice method to send the voting results on a proposal back to L1 + * @param proposalId id of the proposal to send the voting result to L1 + * @dev This method should be implemented to trigger the bridging flow + */ + function _sendVoteResults( + uint256 proposalId, + uint256 forVotes, + uint256 againstVotes + ) internal virtual; + + /** + * @notice method to get the state of a proposal specified by its id + * @param proposal the proposal to retrieve the state of + * @return the state of the proposal + */ + function _getProposalState( + Proposal storage proposal + ) internal view returns (ProposalState) { + if (proposal.endTime == 0) { + return ProposalState.NotCreated; + } else if (_getCurrentTimeRef() <= proposal.endTime) { + return ProposalState.Active; + } else if (proposal.sentToGovernance) { + return ProposalState.SentToGovernance; + } else { + return ProposalState.Finished; + } + } + + /** + * @notice method to get the timestamp of a block casted to uint40 + * @return uint40 block timestamp + */ + function _getCurrentTimeRef() internal view returns (uint40) { + return uint40(block.timestamp); + } + + /** + * @notice method that registers a proposal configuration and creates the voting if it can. If not it will register the + the configuration for later creation. + * @param proposalId id of the proposal bridged to start the vote on + * @param blockHash hash of the block on L1 when the proposal was activated for voting + * @param votingDuration duration in seconds of the vote + */ + function _createBridgedProposalVote( + uint256 proposalId, + bytes32 blockHash, + uint24 votingDuration + ) internal { + require( + blockHash != bytes32(0), + Errors.INVALID_VOTE_CONFIGURATION_BLOCKHASH + ); + require( + votingDuration > 0, + Errors.INVALID_VOTE_CONFIGURATION_VOTING_DURATION + ); + require( + _proposalsVoteConfiguration[proposalId].l1ProposalBlockHash == bytes32(0), + Errors.PROPOSAL_VOTE_CONFIGURATION_ALREADY_BRIDGED + ); + + _proposalsVoteConfiguration[proposalId] = IVotingMachineWithProofs + .ProposalVoteConfiguration({ + votingDuration: votingDuration, + l1ProposalBlockHash: blockHash + }); + _proposalsVoteConfigurationIds.push(proposalId); + + bool created; + try this.startProposalVote(proposalId) { + created = true; + } catch (bytes memory) {} + + emit ProposalVoteConfigurationBridged( + proposalId, + blockHash, + votingDuration, + created + ); + } + + /** + * @notice method that registers a vote on a proposal from a specific voter, contained in a bridged message + from governance chain + * @param proposalId id of the proposal bridged to start the vote on + * @param voter address that wants to emit the vote + * @param support indicates if vote is in favor or against the proposal + * @param votingAssetsWithSlot list of token addresses with base slots that the voter will use for voting + */ + function _registerBridgedVote( + uint256 proposalId, + address voter, + bool support, + VotingAssetWithSlot[] memory votingAssetsWithSlot + ) internal { + // It also only allows to register the vote when proposal is active. + // To retry to register a vote (after it fails) the message will need to be retried from the cross chain controller + require( + _getProposalState(_proposals[proposalId]) == ProposalState.Active, + Errors.PROPOSAL_VOTE_CAN_NOT_BE_REGISTERED + ); + require(voter != address(0), Errors.INVALID_VOTER); + require(votingAssetsWithSlot.length > 0, Errors.NO_BRIDGED_VOTING_ASSETS); + require( + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.length == 0, + Errors.VOTE_ALREADY_BRIDGED + ); + _bridgedVotes[voter][proposalId].support = support; + for (uint256 i = 0; i < votingAssetsWithSlot.length; i++) { + require( + IBaseVotingStrategy(address(VOTING_STRATEGY)).isTokenSlotAccepted( + votingAssetsWithSlot[i].underlyingAsset, + votingAssetsWithSlot[i].slot + ), + Errors.INVALID_BRIDGED_VOTING_TOKEN + ); + for (uint256 j = i + 1; j < votingAssetsWithSlot.length; j++) { + require( + votingAssetsWithSlot[j].underlyingAsset != + votingAssetsWithSlot[i].underlyingAsset || + votingAssetsWithSlot[j].slot != votingAssetsWithSlot[i].slot, + Errors.BRIDGED_REPEATED_ASSETS + ); + } + + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.push( + votingAssetsWithSlot[i] + ); + } + + emit VoteBridged(proposalId, voter, support, votingAssetsWithSlot); + } +} diff --git a/security/certora/tests/VotingMachineWithProofs-4.sol b/security/certora/tests/VotingMachineWithProofs-4.sol new file mode 100644 index 0000000..355fa56 --- /dev/null +++ b/security/certora/tests/VotingMachineWithProofs-4.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; +import {StateProofVerifier} from './libs/StateProofVerifier.sol'; +import {IVotingMachineWithProofs, IDataWarehouse, IVotingStrategy} from './interfaces/IVotingMachineWithProofs.sol'; +import {IBaseVotingStrategy} from '../../interfaces/IBaseVotingStrategy.sol'; +import {Errors} from '../libraries/Errors.sol'; +import {SlotUtils} from '../libraries/SlotUtils.sol'; +import {EIP712} from 'openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol'; +import {ECDSA} from 'openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol'; + +/** + * @title VotingMachineWithProofs + * @author BGD Labs + * @notice this contract contains the logic to vote on a bridged proposal. It uses registered proofs to calculate the + voting power of the users. Once the voting is finished it will send the results back to the governance chain. + * @dev Abstract contract that is implemented on VotingMachine contract + */ +abstract contract VotingMachineWithProofs is + IVotingMachineWithProofs, + EIP712, + Ownable +{ + using SafeCast for uint256; + + string public constant VOTING_ASSET_WITH_SLOT_RAW = + 'VotingAssetWithSlot(address underlyingAsset,uint128 slot)'; + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTE_SUBMITTED_TYPEHASH = + keccak256( + abi.encodePacked( + 'SubmitVote(uint256 proposalId,address voter,bool support,VotingAssetWithSlot[] votingAssetsWithSlot)', + VOTING_ASSET_WITH_SLOT_RAW + ) + ); + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTING_ASSET_WITH_SLOT_TYPEHASH = + keccak256(abi.encodePacked(VOTING_ASSET_WITH_SLOT_RAW)); + + /// @inheritdoc IVotingMachineWithProofs + string public constant NAME = 'Aave Voting Machine'; + + /// @inheritdoc IVotingMachineWithProofs + IVotingStrategy public immutable VOTING_STRATEGY; + + /// @inheritdoc IVotingMachineWithProofs + IDataWarehouse public immutable DATA_WAREHOUSE; + + // (proposalId => proposal information) stores the information of the proposals + mapping(uint256 => Proposal) internal _proposals; + + // (proposalId => proposal vote configuration) stores the configuration for voting on each proposal + mapping(uint256 => ProposalVoteConfiguration) + internal _proposalsVoteConfiguration; + + // saves the ids of the proposals that have been bridged for a vote. + uint256[] internal _proposalsVoteConfigurationIds; + + // (voter => proposalId => voteInfo) stores the information for the bridged votes + mapping(address => mapping(uint256 => BridgedVote)) internal _bridgedVotes; + + /** + * @param votingStrategy address of the new VotingStrategy contract + */ + constructor(IVotingStrategy votingStrategy) Ownable() EIP712(NAME, 'V1') { + require( + address(votingStrategy) != address(0), + Errors.INVALID_VOTING_STRATEGY + ); + VOTING_STRATEGY = votingStrategy; + DATA_WAREHOUSE = votingStrategy.DATA_WAREHOUSE(); + } + + /// @inheritdoc IVotingMachineWithProofs + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IVotingMachineWithProofs + function getBridgedVoteInfo( + uint256 proposalId, + address voter + ) external view returns (BridgedVote memory) { + return _bridgedVotes[voter][proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalVoteConfiguration( + uint256 proposalId + ) external view returns (ProposalVoteConfiguration memory) { + return _proposalsVoteConfiguration[proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function startProposalVote(uint256 proposalId) external returns (uint256) { + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + require( + voteConfig.l1ProposalBlockHash != bytes32(0), + Errors.MISSING_PROPOSAL_BLOCK_HASH + ); + Proposal storage newProposal = _proposals[proposalId]; + + require( + _getProposalState(newProposal) == ProposalState.NotCreated, + Errors.PROPOSAL_VOTE_ALREADY_CREATED + ); + + VOTING_STRATEGY.hasRequiredRoots(voteConfig.l1ProposalBlockHash); + + uint40 startTime = _getCurrentTimeRef(); + uint40 endTime = startTime + voteConfig.votingDuration; + + newProposal.id = proposalId; + newProposal.creationBlockNumber = block.number; + newProposal.startTime = startTime; + newProposal.endTime = endTime; + + emit ProposalVoteStarted( + proposalId, + voteConfig.l1ProposalBlockHash, + startTime, + endTime + ); + + return proposalId; + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVoteBySignature( + uint256 proposalId, + address voter, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32[] memory underlyingAssetsWithSlotHashes = new bytes32[]( + votingBalanceProofs.length + ); + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + underlyingAssetsWithSlotHashes[i] = keccak256( + abi.encode( + VOTING_ASSET_WITH_SLOT_TYPEHASH, + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot + ) + ); + } + + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + VOTE_SUBMITTED_TYPEHASH, + proposalId, + voter, + support, + keccak256(abi.encodePacked(underlyingAssetsWithSlotHashes)) + ) + ) + ); + address signer = ECDSA.recover(digest, v, r, s); + + require(signer == voter && signer != address(0), Errors.INVALID_SIGNATURE); + _submitVote(signer, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function settleVoteFromPortal( + uint256 proposalId, + address voter, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + BridgedVote memory bridgedVote = _bridgedVotes[voter][proposalId]; + + require( + bridgedVote.votingAssetsWithSlot.length == votingBalanceProofs.length, + Errors.INVALID_NUMBER_OF_PROOFS_FOR_VOTING_TOKENS + ); + + // check that the proofs are of the voter assets + for (uint256 i = 0; i < bridgedVote.votingAssetsWithSlot.length; i++) { + bool assetFound; + for (uint256 j = 0; j < votingBalanceProofs.length; j++) { + if ( + votingBalanceProofs[j].underlyingAsset == + bridgedVote.votingAssetsWithSlot[i].underlyingAsset && + votingBalanceProofs[j].slot == + bridgedVote.votingAssetsWithSlot[i].slot + ) { + assetFound = true; + break; + } + } + + require(assetFound, Errors.PROOFS_NOT_FOR_VOTING_TOKENS); + } + + _submitVote(voter, proposalId, bridgedVote.support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVote( + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + _submitVote(msg.sender, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function getUserProposalVote( + address user, + uint256 proposalId + ) external view returns (Vote memory) { + return _proposals[proposalId].votes[user]; + } + + /// @inheritdoc IVotingMachineWithProofs + function closeAndSendVote(uint256 proposalId) external { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Finished, + Errors.PROPOSAL_VOTE_NOT_FINISHED + ); + + proposal.votingClosedAndSentBlockNumber = block.number; + proposal.votingClosedAndSentTimestamp = _getCurrentTimeRef(); + + uint256 forVotes = proposal.forVotes; + uint256 againstVotes = proposal.againstVotes; + + proposal.sentToGovernance = true; + + _sendVoteResults(proposalId, forVotes, againstVotes); + + emit ProposalResultsSent(proposalId, forVotes, againstVotes); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalById( + uint256 proposalId + ) external view returns (ProposalWithoutVotes memory) { + Proposal storage proposal = _proposals[0]; + ProposalWithoutVotes memory proposalWithoutVotes = ProposalWithoutVotes({ + id: proposalId, + startTime: proposal.startTime, + endTime: proposal.endTime, + creationBlockNumber: proposal.creationBlockNumber, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + votingClosedAndSentBlockNumber: proposal.votingClosedAndSentBlockNumber, + votingClosedAndSentTimestamp: proposal.votingClosedAndSentTimestamp, + sentToGovernance: proposal.sentToGovernance + }); + + return proposalWithoutVotes; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalState( + uint256 proposalId + ) external view returns (ProposalState) { + return _getProposalState(_proposals[proposalId]); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalsVoteConfigurationIds( + uint256 skip, + uint256 size + ) external view returns (uint256[] memory) { + uint256 proposalListLength = _proposalsVoteConfigurationIds.length; + if (proposalListLength == 0 || proposalListLength <= skip) { + return new uint256[](0); + } else if (proposalListLength < size + skip) { + size = proposalListLength - skip; + } + + uint256[] memory ids = new uint256[](size); + for (uint256 i = 0; i < size; i++) { + ids[i] = _proposalsVoteConfigurationIds[ + proposalListLength - skip - i - 1 + ]; + } + return ids; + } + + /** + * @notice method to cast a vote on a proposal specified by its id + * @param voter address with the voting power + * @param proposalId id of the proposal on which the vote will be cast + * @param support boolean indicating if the vote is in favor or against the proposal + * @param votingBalanceProofs list of objects containing the information necessary to vote using the tokens + allowed on the voting strategy. + * @dev A vote does not need to use all the tokens allowed, can be a subset + */ + function _submitVote( + address voter, + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) internal { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Active, + Errors.PROPOSAL_VOTE_NOT_IN_ACTIVE_STATE + ); + + Vote storage vote = proposal.votes[voter]; + require(vote.votingPower == 0, Errors.PROPOSAL_VOTE_ALREADY_EXISTS); + + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + + uint256 votingPower; + StateProofVerifier.SlotValue memory balanceVotingPower; + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + for (uint256 j = i + 1; j < votingBalanceProofs.length; j++) { + require( + votingBalanceProofs[i].slot != votingBalanceProofs[j].slot || + votingBalanceProofs[i].underlyingAsset != + votingBalanceProofs[j].underlyingAsset, + Errors.VOTE_ONCE_FOR_ASSET + ); + } + + balanceVotingPower = DATA_WAREHOUSE.getStorage( + votingBalanceProofs[i].underlyingAsset, + voteConfig.l1ProposalBlockHash, + SlotUtils.getAccountSlotHash(voter, votingBalanceProofs[i].slot), + votingBalanceProofs[i].proof + ); + + require(balanceVotingPower.exists, Errors.USER_BALANCE_DOES_NOT_EXISTS); + + if (balanceVotingPower.value != 0) { + votingPower += IVotingStrategy(address(VOTING_STRATEGY)).getVotingPower( + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot, + balanceVotingPower.value, + voteConfig.l1ProposalBlockHash + ); + } + } + require(votingPower != 0, Errors.USER_VOTING_BALANCE_IS_ZERO); + + if (support) { + proposal.forVotes += votingPower.toUint128(); + } else { + proposal.againstVotes += votingPower.toUint128(); + } + + vote.support = support; + vote.votingPower = votingPower.toUint248(); + + emit VoteEmitted(proposalId, voter, support, votingPower); + } + + /** + * @notice method to send the voting results on a proposal back to L1 + * @param proposalId id of the proposal to send the voting result to L1 + * @dev This method should be implemented to trigger the bridging flow + */ + function _sendVoteResults( + uint256 proposalId, + uint256 forVotes, + uint256 againstVotes + ) internal virtual; + + /** + * @notice method to get the state of a proposal specified by its id + * @param proposal the proposal to retrieve the state of + * @return the state of the proposal + */ + function _getProposalState( + Proposal storage proposal + ) internal view returns (ProposalState) { + if (proposal.endTime == 0) { + return ProposalState.NotCreated; + } else if (_getCurrentTimeRef() <= proposal.endTime) { + return ProposalState.Active; + } else if (proposal.sentToGovernance) { + return ProposalState.SentToGovernance; + } else { + return ProposalState.Finished; + } + } + + /** + * @notice method to get the timestamp of a block casted to uint40 + * @return uint40 block timestamp + */ + function _getCurrentTimeRef() internal view returns (uint40) { + return uint40(block.timestamp); + } + + /** + * @notice method that registers a proposal configuration and creates the voting if it can. If not it will register the + the configuration for later creation. + * @param proposalId id of the proposal bridged to start the vote on + * @param blockHash hash of the block on L1 when the proposal was activated for voting + * @param votingDuration duration in seconds of the vote + */ + function _createBridgedProposalVote( + uint256 proposalId, + bytes32 blockHash, + uint24 votingDuration + ) internal { + require( + blockHash != bytes32(0), + Errors.INVALID_VOTE_CONFIGURATION_BLOCKHASH + ); + require( + votingDuration > 0, + Errors.INVALID_VOTE_CONFIGURATION_VOTING_DURATION + ); + require( + _proposalsVoteConfiguration[proposalId].l1ProposalBlockHash == bytes32(0), + Errors.PROPOSAL_VOTE_CONFIGURATION_ALREADY_BRIDGED + ); + + _proposalsVoteConfiguration[proposalId] = IVotingMachineWithProofs + .ProposalVoteConfiguration({ + votingDuration: votingDuration, + l1ProposalBlockHash: blockHash + }); + _proposalsVoteConfigurationIds.push(proposalId); + + bool created; + try this.startProposalVote(proposalId) { + created = true; + } catch (bytes memory) {} + + emit ProposalVoteConfigurationBridged( + proposalId, + blockHash, + votingDuration, + created + ); + } + + /** + * @notice method that registers a vote on a proposal from a specific voter, contained in a bridged message + from governance chain + * @param proposalId id of the proposal bridged to start the vote on + * @param voter address that wants to emit the vote + * @param support indicates if vote is in favor or against the proposal + * @param votingAssetsWithSlot list of token addresses with base slots that the voter will use for voting + */ + function _registerBridgedVote( + uint256 proposalId, + address voter, + bool support, + VotingAssetWithSlot[] memory votingAssetsWithSlot + ) internal { + // It also only allows to register the vote when proposal is active. + // To retry to register a vote (after it fails) the message will need to be retried from the cross chain controller + require( + _getProposalState(_proposals[proposalId]) == ProposalState.Active, + Errors.PROPOSAL_VOTE_CAN_NOT_BE_REGISTERED + ); + require(voter != address(0), Errors.INVALID_VOTER); + require(votingAssetsWithSlot.length > 0, Errors.NO_BRIDGED_VOTING_ASSETS); + require( + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.length == 0, + Errors.VOTE_ALREADY_BRIDGED + ); + _bridgedVotes[voter][proposalId].support = support; + for (uint256 i = 0; i < votingAssetsWithSlot.length; i++) { + require( + IBaseVotingStrategy(address(VOTING_STRATEGY)).isTokenSlotAccepted( + votingAssetsWithSlot[i].underlyingAsset, + votingAssetsWithSlot[i].slot + ), + Errors.INVALID_BRIDGED_VOTING_TOKEN + ); + for (uint256 j = i + 1; j < votingAssetsWithSlot.length; j++) { + require( + votingAssetsWithSlot[j].underlyingAsset != + votingAssetsWithSlot[i].underlyingAsset || + votingAssetsWithSlot[j].slot != votingAssetsWithSlot[i].slot, + Errors.BRIDGED_REPEATED_ASSETS + ); + } + + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.push( + votingAssetsWithSlot[i] + ); + } + + emit VoteBridged(proposalId, voter, support, votingAssetsWithSlot); + } +} diff --git a/security/certora/tests/VotingMachineWithProofs-5.sol b/security/certora/tests/VotingMachineWithProofs-5.sol new file mode 100644 index 0000000..1ca90ca --- /dev/null +++ b/security/certora/tests/VotingMachineWithProofs-5.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable} from 'solidity-utils/contracts/oz-common/Ownable.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; +import {StateProofVerifier} from './libs/StateProofVerifier.sol'; +import {IVotingMachineWithProofs, IDataWarehouse, IVotingStrategy} from './interfaces/IVotingMachineWithProofs.sol'; +import {IBaseVotingStrategy} from '../../interfaces/IBaseVotingStrategy.sol'; +import {Errors} from '../libraries/Errors.sol'; +import {SlotUtils} from '../libraries/SlotUtils.sol'; +import {EIP712} from 'openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol'; +import {ECDSA} from 'openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol'; + +/** + * @title VotingMachineWithProofs + * @author BGD Labs + * @notice this contract contains the logic to vote on a bridged proposal. It uses registered proofs to calculate the + voting power of the users. Once the voting is finished it will send the results back to the governance chain. + * @dev Abstract contract that is implemented on VotingMachine contract + */ +abstract contract VotingMachineWithProofs is + IVotingMachineWithProofs, + EIP712, + Ownable +{ + using SafeCast for uint256; + + string public constant VOTING_ASSET_WITH_SLOT_RAW = + 'VotingAssetWithSlot(address underlyingAsset,uint128 slot)'; + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTE_SUBMITTED_TYPEHASH = + keccak256( + abi.encodePacked( + 'SubmitVote(uint256 proposalId,address voter,bool support,VotingAssetWithSlot[] votingAssetsWithSlot)', + VOTING_ASSET_WITH_SLOT_RAW + ) + ); + + /// @inheritdoc IVotingMachineWithProofs + bytes32 public constant VOTING_ASSET_WITH_SLOT_TYPEHASH = + keccak256(abi.encodePacked(VOTING_ASSET_WITH_SLOT_RAW)); + + /// @inheritdoc IVotingMachineWithProofs + string public constant NAME = 'Aave Voting Machine'; + + /// @inheritdoc IVotingMachineWithProofs + IVotingStrategy public immutable VOTING_STRATEGY; + + /// @inheritdoc IVotingMachineWithProofs + IDataWarehouse public immutable DATA_WAREHOUSE; + + // (proposalId => proposal information) stores the information of the proposals + mapping(uint256 => Proposal) internal _proposals; + + // (proposalId => proposal vote configuration) stores the configuration for voting on each proposal + mapping(uint256 => ProposalVoteConfiguration) + internal _proposalsVoteConfiguration; + + // saves the ids of the proposals that have been bridged for a vote. + uint256[] internal _proposalsVoteConfigurationIds; + + // (voter => proposalId => voteInfo) stores the information for the bridged votes + mapping(address => mapping(uint256 => BridgedVote)) internal _bridgedVotes; + + /** + * @param votingStrategy address of the new VotingStrategy contract + */ + constructor(IVotingStrategy votingStrategy) Ownable() EIP712(NAME, 'V1') { + require( + address(votingStrategy) != address(0), + Errors.INVALID_VOTING_STRATEGY + ); + VOTING_STRATEGY = votingStrategy; + DATA_WAREHOUSE = votingStrategy.DATA_WAREHOUSE(); + } + + /// @inheritdoc IVotingMachineWithProofs + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return _domainSeparatorV4(); + } + + /// @inheritdoc IVotingMachineWithProofs + function getBridgedVoteInfo( + uint256 proposalId, + address voter + ) external view returns (BridgedVote memory) { + return _bridgedVotes[voter][proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalVoteConfiguration( + uint256 proposalId + ) external view returns (ProposalVoteConfiguration memory) { + return _proposalsVoteConfiguration[proposalId]; + } + + /// @inheritdoc IVotingMachineWithProofs + function startProposalVote(uint256 proposalId) external returns (uint256) { + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + require( + voteConfig.l1ProposalBlockHash != bytes32(0), + Errors.MISSING_PROPOSAL_BLOCK_HASH + ); + Proposal storage newProposal = _proposals[proposalId]; + + require( + _getProposalState(newProposal) == ProposalState.NotCreated, + Errors.PROPOSAL_VOTE_ALREADY_CREATED + ); + + VOTING_STRATEGY.hasRequiredRoots(voteConfig.l1ProposalBlockHash); + + uint40 startTime = _getCurrentTimeRef(); + uint40 endTime = startTime + voteConfig.votingDuration; + + newProposal.id = proposalId; + newProposal.creationBlockNumber = block.number; + newProposal.startTime = startTime; + newProposal.endTime = endTime; + + emit ProposalVoteStarted( + proposalId, + voteConfig.l1ProposalBlockHash, + startTime, + endTime + ); + + return proposalId; + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVoteBySignature( + uint256 proposalId, + address voter, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs, + uint8 v, + bytes32 r, + bytes32 s + ) external { + bytes32[] memory underlyingAssetsWithSlotHashes = new bytes32[]( + votingBalanceProofs.length + ); + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + underlyingAssetsWithSlotHashes[i] = keccak256( + abi.encode( + VOTING_ASSET_WITH_SLOT_TYPEHASH, + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot + ) + ); + } + + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + VOTE_SUBMITTED_TYPEHASH, + proposalId, + voter, + support, + keccak256(abi.encodePacked(underlyingAssetsWithSlotHashes)) + ) + ) + ); + address signer = ECDSA.recover(digest, v, r, s); + + require(signer == voter && signer != address(0), Errors.INVALID_SIGNATURE); + _submitVote(signer, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function settleVoteFromPortal( + uint256 proposalId, + address voter, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + BridgedVote memory bridgedVote = _bridgedVotes[voter][proposalId]; + + require( + bridgedVote.votingAssetsWithSlot.length == votingBalanceProofs.length, + Errors.INVALID_NUMBER_OF_PROOFS_FOR_VOTING_TOKENS + ); + + // check that the proofs are of the voter assets + for (uint256 i = 0; i < bridgedVote.votingAssetsWithSlot.length; i++) { + bool assetFound; + for (uint256 j = 0; j < votingBalanceProofs.length; j++) { + if ( + votingBalanceProofs[j].underlyingAsset == + bridgedVote.votingAssetsWithSlot[i].underlyingAsset && + votingBalanceProofs[j].slot == + bridgedVote.votingAssetsWithSlot[i].slot + ) { + assetFound = true; + break; + } + } + + require(assetFound, Errors.PROOFS_NOT_FOR_VOTING_TOKENS); + } + + _submitVote(voter, proposalId, bridgedVote.support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function submitVote( + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) external { + _submitVote(msg.sender, proposalId, support, votingBalanceProofs); + } + + /// @inheritdoc IVotingMachineWithProofs + function getUserProposalVote( + address user, + uint256 proposalId + ) external view returns (Vote memory) { + return _proposals[proposalId].votes[user]; + } + + /// @inheritdoc IVotingMachineWithProofs + function closeAndSendVote(uint256 proposalId) external { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Finished, + Errors.PROPOSAL_VOTE_NOT_FINISHED + ); + + proposal.votingClosedAndSentBlockNumber = block.number; + proposal.votingClosedAndSentTimestamp = _getCurrentTimeRef(); + + uint256 forVotes = proposal.forVotes; + uint256 againstVotes = proposal.againstVotes; + + proposal.sentToGovernance = true; + + _sendVoteResults(proposalId, forVotes, againstVotes); + + emit ProposalResultsSent(proposalId, forVotes, againstVotes); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalById( + uint256 proposalId + ) external view returns (ProposalWithoutVotes memory) { + Proposal storage proposal = _proposals[proposalId]; + ProposalWithoutVotes memory proposalWithoutVotes = ProposalWithoutVotes({ + id: proposalId, + startTime: proposal.startTime, + endTime: proposal.endTime, + creationBlockNumber: proposal.creationBlockNumber, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + votingClosedAndSentBlockNumber: proposal.votingClosedAndSentBlockNumber, + votingClosedAndSentTimestamp: proposal.votingClosedAndSentTimestamp, + sentToGovernance: proposal.sentToGovernance + }); + + return proposalWithoutVotes; + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalState( + uint256 proposalId + ) external view returns (ProposalState) { + return _getProposalState(_proposals[proposalId]); + } + + /// @inheritdoc IVotingMachineWithProofs + function getProposalsVoteConfigurationIds( + uint256 skip, + uint256 size + ) external view returns (uint256[] memory) { + uint256 proposalListLength = _proposalsVoteConfigurationIds.length; + if (proposalListLength == 0 || proposalListLength <= skip) { + return new uint256[](0); + } else if (proposalListLength < size + skip) { + size = proposalListLength - skip; + } + + uint256[] memory ids = new uint256[](size); + for (uint256 i = 0; i < size; i++) { + ids[i] = _proposalsVoteConfigurationIds[ + proposalListLength - skip - i + ]; + } + return ids; + } + + /** + * @notice method to cast a vote on a proposal specified by its id + * @param voter address with the voting power + * @param proposalId id of the proposal on which the vote will be cast + * @param support boolean indicating if the vote is in favor or against the proposal + * @param votingBalanceProofs list of objects containing the information necessary to vote using the tokens + allowed on the voting strategy. + * @dev A vote does not need to use all the tokens allowed, can be a subset + */ + function _submitVote( + address voter, + uint256 proposalId, + bool support, + VotingBalanceProof[] calldata votingBalanceProofs + ) internal { + Proposal storage proposal = _proposals[proposalId]; + require( + _getProposalState(proposal) == ProposalState.Active, + Errors.PROPOSAL_VOTE_NOT_IN_ACTIVE_STATE + ); + + Vote storage vote = proposal.votes[voter]; + require(vote.votingPower == 0, Errors.PROPOSAL_VOTE_ALREADY_EXISTS); + + ProposalVoteConfiguration memory voteConfig = _proposalsVoteConfiguration[ + proposalId + ]; + + uint256 votingPower; + StateProofVerifier.SlotValue memory balanceVotingPower; + for (uint256 i = 0; i < votingBalanceProofs.length; i++) { + for (uint256 j = i + 1; j < votingBalanceProofs.length; j++) { + require( + votingBalanceProofs[i].slot != votingBalanceProofs[j].slot || + votingBalanceProofs[i].underlyingAsset != + votingBalanceProofs[j].underlyingAsset, + Errors.VOTE_ONCE_FOR_ASSET + ); + } + + balanceVotingPower = DATA_WAREHOUSE.getStorage( + votingBalanceProofs[i].underlyingAsset, + voteConfig.l1ProposalBlockHash, + SlotUtils.getAccountSlotHash(voter, votingBalanceProofs[i].slot), + votingBalanceProofs[i].proof + ); + + require(balanceVotingPower.exists, Errors.USER_BALANCE_DOES_NOT_EXISTS); + + if (balanceVotingPower.value != 0) { + votingPower += IVotingStrategy(address(VOTING_STRATEGY)).getVotingPower( + votingBalanceProofs[i].underlyingAsset, + votingBalanceProofs[i].slot, + balanceVotingPower.value, + voteConfig.l1ProposalBlockHash + ); + } + } + require(votingPower != 0, Errors.USER_VOTING_BALANCE_IS_ZERO); + + if (support) { + proposal.forVotes += votingPower.toUint128(); + } else { + proposal.againstVotes += votingPower.toUint128(); + } + + vote.support = support; + vote.votingPower = votingPower.toUint248(); + + emit VoteEmitted(proposalId, voter, support, votingPower); + } + + /** + * @notice method to send the voting results on a proposal back to L1 + * @param proposalId id of the proposal to send the voting result to L1 + * @dev This method should be implemented to trigger the bridging flow + */ + function _sendVoteResults( + uint256 proposalId, + uint256 forVotes, + uint256 againstVotes + ) internal virtual; + + /** + * @notice method to get the state of a proposal specified by its id + * @param proposal the proposal to retrieve the state of + * @return the state of the proposal + */ + function _getProposalState( + Proposal storage proposal + ) internal view returns (ProposalState) { + if (proposal.endTime == 0) { + return ProposalState.NotCreated; + } else if (_getCurrentTimeRef() <= proposal.endTime) { + return ProposalState.Active; + } else if (proposal.sentToGovernance) { + return ProposalState.SentToGovernance; + } else { + return ProposalState.Finished; + } + } + + /** + * @notice method to get the timestamp of a block casted to uint40 + * @return uint40 block timestamp + */ + function _getCurrentTimeRef() internal view returns (uint40) { + return uint40(block.timestamp); + } + + /** + * @notice method that registers a proposal configuration and creates the voting if it can. If not it will register the + the configuration for later creation. + * @param proposalId id of the proposal bridged to start the vote on + * @param blockHash hash of the block on L1 when the proposal was activated for voting + * @param votingDuration duration in seconds of the vote + */ + function _createBridgedProposalVote( + uint256 proposalId, + bytes32 blockHash, + uint24 votingDuration + ) internal { + require( + blockHash != bytes32(0), + Errors.INVALID_VOTE_CONFIGURATION_BLOCKHASH + ); + require( + votingDuration > 0, + Errors.INVALID_VOTE_CONFIGURATION_VOTING_DURATION + ); + require( + _proposalsVoteConfiguration[proposalId].l1ProposalBlockHash == bytes32(0), + Errors.PROPOSAL_VOTE_CONFIGURATION_ALREADY_BRIDGED + ); + + _proposalsVoteConfiguration[proposalId] = IVotingMachineWithProofs + .ProposalVoteConfiguration({ + votingDuration: votingDuration, + l1ProposalBlockHash: blockHash + }); + _proposalsVoteConfigurationIds.push(proposalId); + + bool created; + try this.startProposalVote(proposalId) { + created = true; + } catch (bytes memory) {} + + emit ProposalVoteConfigurationBridged( + proposalId, + blockHash, + votingDuration, + created + ); + } + + /** + * @notice method that registers a vote on a proposal from a specific voter, contained in a bridged message + from governance chain + * @param proposalId id of the proposal bridged to start the vote on + * @param voter address that wants to emit the vote + * @param support indicates if vote is in favor or against the proposal + * @param votingAssetsWithSlot list of token addresses with base slots that the voter will use for voting + */ + function _registerBridgedVote( + uint256 proposalId, + address voter, + bool support, + VotingAssetWithSlot[] memory votingAssetsWithSlot + ) internal { + // It also only allows to register the vote when proposal is active. + // To retry to register a vote (after it fails) the message will need to be retried from the cross chain controller + require( + _getProposalState(_proposals[proposalId]) == ProposalState.Active, + Errors.PROPOSAL_VOTE_CAN_NOT_BE_REGISTERED + ); + require(voter != address(0), Errors.INVALID_VOTER); + require(votingAssetsWithSlot.length > 0, Errors.NO_BRIDGED_VOTING_ASSETS); + require( + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.length == 0, + Errors.VOTE_ALREADY_BRIDGED + ); + _bridgedVotes[voter][proposalId].support = support; + for (uint256 i = 0; i < votingAssetsWithSlot.length; i++) { + require( + IBaseVotingStrategy(address(VOTING_STRATEGY)).isTokenSlotAccepted( + votingAssetsWithSlot[i].underlyingAsset, + votingAssetsWithSlot[i].slot + ), + Errors.INVALID_BRIDGED_VOTING_TOKEN + ); + for (uint256 j = i + 1; j < votingAssetsWithSlot.length; j++) { + require( + votingAssetsWithSlot[j].underlyingAsset != + votingAssetsWithSlot[i].underlyingAsset || + votingAssetsWithSlot[j].slot != votingAssetsWithSlot[i].slot, + Errors.BRIDGED_REPEATED_ASSETS + ); + } + + _bridgedVotes[voter][proposalId].votingAssetsWithSlot.push( + votingAssetsWithSlot[i] + ); + } + + emit VoteBridged(proposalId, voter, support, votingAssetsWithSlot); + } +}