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);
+ }
+}