diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml new file mode 100644 index 0000000..d5a9e7f --- /dev/null +++ b/.github/workflows/certora.yml @@ -0,0 +1,48 @@ +name: Certora + +on: [push, pull_request] + +jobs: + certora: + name: Certora + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + op-token-bridge: + - escrow + - l1-governance-relay + - l2-governance-relay + - l1-token-bridge + - l2-token-bridge + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: '11' + java-package: jre + + - name: Set up Python 3.8 + uses: actions/setup-python@v3 + with: + python-version: 3.8 + + - name: Install solc-select + run: pip3 install solc-select + + - name: Solc Select 0.8.21 + run: solc-select install 0.8.21 + + - name: Install Certora + run: pip3 install certora-cli-beta + + - name: Verify ${{ matrix.op-token-bridge }} + run: make certora-${{ matrix.op-token-bridge }} results=1 + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} diff --git a/.gitignore b/.gitignore index 85198aa..8c30259 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ docs/ # Dotenv file .env + +# Certora +.certora_internal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..792cdea --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +PATH := ~/.solc-select/artifacts/:~/.solc-select/artifacts/solc-0.8.21:$(PATH) +certora-escrow :; PATH=${PATH} certoraRun certora/Escrow.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,) +certora-l1-governance-relay :; PATH=${PATH} certoraRun certora/L1GovernanceRelay.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,) +certora-l2-governance-relay :; PATH=${PATH} certoraRun certora/L2GovernanceRelay.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,) +certora-l1-token-bridge :; PATH=${PATH} certoraRun certora/L1TokenBridge.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,) +certora-l2-token-bridge :; PATH=${PATH} certoraRun certora/L2TokenBridge.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,) diff --git a/certora/Escrow.conf b/certora/Escrow.conf new file mode 100644 index 0000000..bfd8c9e --- /dev/null +++ b/certora/Escrow.conf @@ -0,0 +1,14 @@ +{ + "files": [ + "src/Escrow.sol", + "test/mocks/GemMock.sol" + ], + "solc": "solc-0.8.21", + "solc_optimize": "200", + "verify": "Escrow:certora/Escrow.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["Escrow"], + "build_cache": true, + "msg": "Escrow" +} diff --git a/certora/Escrow.spec b/certora/Escrow.spec new file mode 100644 index 0000000..8be9bd3 --- /dev/null +++ b/certora/Escrow.spec @@ -0,0 +1,121 @@ +// Escrow.spec + +using GemMock as gem; + +methods { + // storage variables + function wards(address) external returns (uint256) envfree; + // + function gem.allowance(address,address) external returns (uint256) envfree; + // + function _.approve(address,uint256) external => DISPATCHER(true); +} + +// Verify that each storage layout is only modified in the corresponding functions +rule storageAffected(method f) { + env e; + + address anyAddr; + + mathint wardsBefore = wards(anyAddr); + + calldataarg args; + f(e, args); + + mathint wardsAfter = wards(anyAddr); + + assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector, "Assert 1"; +} + +// Verify correct storage changes for non reverting rely +rule rely(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + rely(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 1, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on rely +rule rely_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + rely@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting deny +rule deny(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + deny(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 0, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on deny +rule deny_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + deny@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting approve +rule approve(address token, address spender, uint256 value) { + env e; + + require token == gem; + + approve(e, token, spender, value); + + mathint allowance = gem.allowance(currentContract, spender); + + assert allowance == to_mathint(value), "Assert 1"; +} + +// Verify revert rules on approve +rule approve_revert(address token, address spender, uint256 value) { + env e; + + require token == gem; + + mathint wardsSender = wards(e.msg.sender); + + approve@withrevert(e, token, spender, value); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/L1GovernanceRelay.conf b/certora/L1GovernanceRelay.conf new file mode 100644 index 0000000..39e7b1f --- /dev/null +++ b/certora/L1GovernanceRelay.conf @@ -0,0 +1,23 @@ +{ + "files": [ + "src/L1GovernanceRelay.sol", + "certora/harness/Auxiliar.sol", + "test/mocks/MessengerMock.sol", + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "L1GovernanceRelay": "200", + "Auxiliar": "0", + "MessengerMock": "0" + }, + "link": [ + "L1GovernanceRelay:messenger=MessengerMock" + ], + "verify": "L1GovernanceRelay:certora/L1GovernanceRelay.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["L1GovernanceRelay"], + "build_cache": true, + "optimistic_hashing": true, + "msg": "L1GovernanceRelay" +} diff --git a/certora/L1GovernanceRelay.spec b/certora/L1GovernanceRelay.spec new file mode 100644 index 0000000..968bb51 --- /dev/null +++ b/certora/L1GovernanceRelay.spec @@ -0,0 +1,132 @@ +// L1GovernanceRelay.spec + +using Auxiliar as aux; +using MessengerMock as l1messenger; + +methods { + // storage variables + function wards(address) external returns (uint256) envfree; + // immutables + function l2GovernanceRelay() external returns (address) envfree; + function messenger() external returns (address) envfree; + // + function aux.getGovMessageHash(address,bytes) external returns (bytes32) envfree; + function l1messenger.lastTarget() external returns (address) envfree; + function l1messenger.lastMessageHash() external returns (bytes32) envfree; + function l1messenger.lastMinGasLimit() external returns (uint32) envfree; + // + function _.sendMessage(address,bytes,uint32) external => DISPATCHER(true); +} + +// Verify that each storage layout is only modified in the corresponding functions +rule storageAffected(method f) { + env e; + + address anyAddr; + + mathint wardsBefore = wards(anyAddr); + + calldataarg args; + f(e, args); + + mathint wardsAfter = wards(anyAddr); + + assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector, "Assert 1"; +} + +// Verify correct storage changes for non reverting rely +rule rely(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + rely(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 1, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on rely +rule rely_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + rely@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting deny +rule deny(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + deny(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 0, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on deny +rule deny_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + deny@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting relay +rule relay(address target, bytes targetData, uint32 minGasLimit) { + env e; + + address l2GovernanceRelay = l2GovernanceRelay(); + + bytes32 message = aux.getGovMessageHash(target, targetData); + + relay(e, target, targetData, minGasLimit); + + address lastTargetAfter = l1messenger.lastTarget(); + bytes32 lastMessageHashAfter = l1messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l1messenger.lastMinGasLimit(); + + assert lastTargetAfter == l2GovernanceRelay, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == minGasLimit, "Assert 3"; +} + +// Verify revert rules on relay +rule relay_revert(address target, bytes targetData, uint32 minGasLimit) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + relay@withrevert(e, target, targetData, minGasLimit); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/L1TokenBridge.conf b/certora/L1TokenBridge.conf new file mode 100644 index 0000000..d0cf7cb --- /dev/null +++ b/certora/L1TokenBridge.conf @@ -0,0 +1,31 @@ +{ + "files": [ + "src/L1TokenBridge.sol", + "certora/harness/Auxiliar.sol", + "test/mocks/MessengerMock.sol", + "test/mocks/GemMock.sol", + "certora/harness/ImplementationMock.sol" + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "L1TokenBridge": "200", + "Auxiliar": "0", + "MessengerMock": "0", + "GemMock": "0", + "ImplementationMock": "0" + }, + "link": [ + "L1TokenBridge:messenger=MessengerMock" + ], + "verify": "L1TokenBridge:certora/L1TokenBridge.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["L1TokenBridge"], + "build_cache": true, + "optimistic_hashing": true, + "hashing_length_bound": "512", + "prover_args": [ + "-enableStorageSplitting false" + ], + "msg": "L1TokenBridge" +} diff --git a/certora/L1TokenBridge.spec b/certora/L1TokenBridge.spec new file mode 100644 index 0000000..a69eb9a --- /dev/null +++ b/certora/L1TokenBridge.spec @@ -0,0 +1,487 @@ +// L1TokenBridge.spec + +using Auxiliar as aux; +using MessengerMock as l1messenger; +using GemMock as gem; + +methods { + // storage variables + function wards(address) external returns (uint256) envfree; + function l1ToL2Token(address) external returns (address) envfree; + function isOpen() external returns (uint256) envfree; + function escrow() external returns (address) envfree; + // immutables + function otherBridge() external returns (address) envfree; + function messenger() external returns (address) envfree; + // getter + function getImplementation() external returns (address) envfree; + // + function gem.allowance(address,address) external returns (uint256) envfree; + function gem.totalSupply() external returns (uint256) envfree; + function gem.balanceOf(address) external returns (uint256) envfree; + function aux.getBridgeMessageHash(address,address,address,address,uint256,bytes) external returns (bytes32) envfree; + function l1messenger.xDomainMessageSender() external returns (address) envfree; + function l1messenger.lastTarget() external returns (address) envfree; + function l1messenger.lastMessageHash() external returns (bytes32) envfree; + function l1messenger.lastMinGasLimit() external returns (uint32) envfree; + // + function _.proxiableUUID() external => DISPATCHER(true); + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); +} + +definition INITIALIZABLE_STORAGE() returns uint256 = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; +definition IMPLEMENTATION_SLOT() returns uint256 = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + +persistent ghost bool firstRead; +persistent ghost mathint initializedBefore; +persistent ghost bool initializingBefore; +persistent ghost mathint initializedAfter; +persistent ghost bool initializingAfter; +hook ALL_SLOAD(uint256 slot) uint256 val { + if (slot == INITIALIZABLE_STORAGE() && firstRead) { + firstRead = false; + initializedBefore = val % (max_uint64 + 1); + initializingBefore = (val / 2^64) % (max_uint8 + 1) != 0; + } else if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} +hook ALL_SSTORE(uint256 slot, uint256 val) { + if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} + +// Verify no more entry points exist +rule entryPoints(method f) filtered { f -> !f.isView } { + env e; + + calldataarg args; + f(e, args); + + assert f.selector == sig:initialize().selector || + f.selector == sig:upgradeToAndCall(address,bytes).selector || + f.selector == sig:rely(address).selector || + f.selector == sig:deny(address).selector || + f.selector == sig:file(bytes32,address).selector || + f.selector == sig:close().selector || + f.selector == sig:registerToken(address,address).selector || + f.selector == sig:bridgeERC20(address,address,uint256,uint32,bytes).selector || + f.selector == sig:bridgeERC20To(address,address,address,uint256,uint32,bytes).selector || + f.selector == sig:finalizeBridgeERC20(address,address,address,address,uint256,bytes).selector; +} + +// Verify that each storage layout is only modified in the corresponding functions +rule storageAffected(method f) filtered { f -> f.selector != sig:upgradeToAndCall(address, bytes).selector } { + env e; + + address anyAddr; + + initializedAfter = initializedBefore; + + mathint wardsBefore = wards(anyAddr); + address l1ToL2TokenBefore = l1ToL2Token(anyAddr); + mathint isOpenBefore = isOpen(); + address escrowBefore = escrow(); + + calldataarg args; + f(e, args); + + mathint wardsAfter = wards(anyAddr); + address l1ToL2TokenAfter = l1ToL2Token(anyAddr); + mathint isOpenAfter = isOpen(); + address escrowAfter = escrow(); + + assert initializedAfter != initializedBefore => f.selector == sig:initialize().selector, "Assert 1"; + assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector || f.selector == sig:initialize().selector, "Assert 2"; + assert l1ToL2TokenAfter != l1ToL2TokenBefore => f.selector == sig:registerToken(address,address).selector, "Assert 3"; + assert isOpenAfter != isOpenBefore => f.selector == sig:close().selector || f.selector == sig:initialize().selector, "Assert 4"; + assert escrowAfter != escrowBefore => f.selector == sig:file(bytes32,address).selector, "Assert 5"; +} + +// Verify correct storage changes for non reverting initialize +rule initialize() { + env e; + + address other; + require other != e.msg.sender; + + mathint wardsOtherBefore = wards(other); + + initialize(e); + + mathint wardsSenderAfter = wards(e.msg.sender); + mathint wardsOtherAfter = wards(other); + mathint isOpenAfter = isOpen(); + + assert initializedAfter == 1, "Assert 1"; + assert !initializingAfter, "Assert 2"; + assert wardsSenderAfter == 1, "Assert 3"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 4"; + assert isOpenAfter == 1, "Assert 5"; +} + +// Verify revert rules on initialize +rule initialize_revert() { + env e; + + firstRead = true; + mathint bridgeCodeSize = nativeCodesize[currentContract]; // This should actually be always > 0 + + initialize@withrevert(e); + + bool initialSetup = initializedBefore == 0 && !initializingBefore; + bool construction = initializedBefore == 1 && bridgeCodeSize == 0; + + bool revert1 = e.msg.value > 0; + bool revert2 = !initialSetup && !construction; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting initialize +rule upgradeToAndCall(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + upgradeToAndCall(e, newImplementation, data); + + address implementationAfter = getImplementation(); + + assert implementationAfter == newImplementation, "Assert 1"; +} + +// Verify revert rules on upgradeToAndCall +rule upgradeToAndCall_revert(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + address self = currentContract.__self; + address implementation = getImplementation(); + mathint wardsSender = wards(e.msg.sender); + bytes32 newImplementationProxiableUUID = newImplementation.proxiableUUID(e); + + upgradeToAndCall@withrevert(e, newImplementation, data); + + bool revert1 = e.msg.value > 0; + bool revert2 = self == currentContract || implementation != self; + bool revert3 = wardsSender != 1; + bool revert4 = newImplementationProxiableUUID != to_bytes32(IMPLEMENTATION_SLOT()); + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting rely +rule rely(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + rely(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 1, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on rely +rule rely_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + rely@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting deny +rule deny(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + deny(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 0, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on deny +rule deny_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + deny@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting file +rule file(bytes32 what, address data) { + env e; + + file(e, what, data); + + address escrowAfter = escrow(); + + assert escrowAfter == data, "Assert 1"; +} + +// Verify revert rules on file +rule file_revert(bytes32 what, address data) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + file@withrevert(e, what, data); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + bool revert3 = what != to_bytes32(0x657363726f770000000000000000000000000000000000000000000000000000); + + assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting close +rule close() { + env e; + + close(e); + + mathint isOpenAfter = isOpen(); + + assert isOpenAfter == 0, "Assert 1"; +} + +// Verify revert rules on close +rule close_revert() { + env e; + + mathint wardsSender = wards(e.msg.sender); + + close@withrevert(e); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting registerToken +rule registerToken(address l1Token, address l2Token) { + env e; + + registerToken(e, l1Token, l2Token); + + address l1ToL2TokenAfter = l1ToL2Token(l1Token); + + assert l1ToL2TokenAfter == l2Token, "Assert 1"; +} + +// Verify revert rules on registerToken +rule registerToken_revert(address l1Token, address l2Token) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + registerToken@withrevert(e, l1Token, l2Token); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20 +rule bridgeERC20(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + address escrow = escrow(); + require e.msg.sender != escrow; + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, e.msg.sender, _amount, _extraData); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowBefore = gem.balanceOf(escrow); + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSenderBefore + localTokenBalanceOfEscrowBefore; + + bridgeERC20(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l1messenger.lastTarget(); + bytes32 lastMessageHashAfter = l1messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l1messenger.lastMinGasLimit(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowAfter = gem.balanceOf(escrow); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 4"; + assert localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore + _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20 +rule bridgeERC20_revert(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenLocalToken = l1ToL2Token(_localToken); + + address escrow = escrow(); + + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrow = gem.balanceOf(escrow); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSender + localTokenBalanceOfEscrow; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20@withrevert(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = nativeCodesize[e.msg.sender] != 0; + bool revert3 = isOpen != 1; + bool revert4 = _remoteToken == 0 || l1ToL2TokenLocalToken != _remoteToken; + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20To +rule bridgeERC20To(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + address escrow = escrow(); + require e.msg.sender != escrow; + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, _to, _amount, _extraData); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowBefore = gem.balanceOf(escrow); + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSenderBefore + localTokenBalanceOfEscrowBefore; + + bridgeERC20To(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l1messenger.lastTarget(); + bytes32 lastMessageHashAfter = l1messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l1messenger.lastMinGasLimit(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrowAfter = gem.balanceOf(escrow); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 4"; + assert localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore + _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20To +rule bridgeERC20To_revert(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenLocalToken = l1ToL2Token(_localToken); + + address escrow = escrow(); + + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + mathint localTokenBalanceOfEscrow = gem.balanceOf(escrow); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfSender + localTokenBalanceOfEscrow; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20To@withrevert(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = isOpen != 1; + bool revert3 = _remoteToken == 0 || l1ToL2TokenLocalToken != _remoteToken; + + assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting finalizeBridgeERC20 +rule finalizeBridgeERC20(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + address escrow = escrow(); + + mathint localTokenBalanceOfEscrowBefore = gem.balanceOf(escrow); + mathint localTokenBalanceOfToBefore = gem.balanceOf(_to); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfEscrowBefore + localTokenBalanceOfToBefore; + + finalizeBridgeERC20(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + mathint localTokenBalanceOfEscrowAfter = gem.balanceOf(escrow); + mathint localTokenBalanceOfToAfter = gem.balanceOf(_to); + + assert escrow != _to => localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore - _amount, "Assert 1"; + assert escrow != _to => localTokenBalanceOfToAfter == localTokenBalanceOfToBefore + _amount, "Assert 2"; + assert escrow == _to => localTokenBalanceOfEscrowAfter == localTokenBalanceOfEscrowBefore, "Assert 3"; +} + +// Verify revert rules on finalizeBridgeERC20 +rule finalizeBridgeERC20_revert(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + address messenger = messenger(); + address otherBridge = otherBridge(); + address xDomainMessageSender = l1messenger.xDomainMessageSender(); + address escrow = escrow(); + + mathint localTokenBalanceOfEscrow = gem.balanceOf(escrow); + + // ERC20 assumption + require gem.totalSupply() >= localTokenBalanceOfEscrow + gem.balanceOf(_to); + // Bridge assumption + require localTokenBalanceOfEscrow >= _amount; + // Set up assumption + require gem.allowance(escrow, currentContract) == max_uint256; + + finalizeBridgeERC20@withrevert(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != messenger || xDomainMessageSender != otherBridge; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/L2GovernanceRelay.conf b/certora/L2GovernanceRelay.conf new file mode 100644 index 0000000..fa08736 --- /dev/null +++ b/certora/L2GovernanceRelay.conf @@ -0,0 +1,21 @@ +{ + "files": [ + "src/L2GovernanceRelay.sol", + "test/mocks/MessengerMock.sol", + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "L2GovernanceRelay": "200", + "MessengerMock": "0" + }, + "link": [ + "L2GovernanceRelay:messenger=MessengerMock" + ], + "verify": "L2GovernanceRelay:certora/L2GovernanceRelay.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["L2GovernanceRelay"], + "build_cache": true, + "optimistic_hashing": true, + "msg": "L2GovernanceRelay" +} diff --git a/certora/L2GovernanceRelay.spec b/certora/L2GovernanceRelay.spec new file mode 100644 index 0000000..725971d --- /dev/null +++ b/certora/L2GovernanceRelay.spec @@ -0,0 +1,51 @@ +// L2GovernanceRelay.spec + +using MessengerMock as l2messenger; + +methods { + // immutables + function l1GovernanceRelay() external returns (address) envfree; + function messenger() external returns (address) envfree; + // + function l2messenger.xDomainMessageSender() external returns (address) envfree; +} + +persistent ghost bool called; +persistent ghost address calledAddr; +persistent ghost mathint dataLength; +persistent ghost bool success; +hook DELEGATECALL(uint256 g, address addr, uint256 argsOffset, uint256 argsLength, uint256 retOffset, uint256 retLength) uint256 rc { + called = true; + calledAddr = addr; + dataLength = argsLength; + success = rc != 0; +} + +// Verify correct storage changes for non reverting relay +rule relay(address target, bytes targetData) { + env e; + + relay(e, target, targetData); + + assert called, "Assert 1"; + assert calledAddr == target, "Assert 2"; + assert dataLength == targetData.length, "Assert 3"; + assert success, "Assert 4"; +} + +// Verify revert rules on relay +rule relay_revert(address target, bytes targetData) { + env e; + + address l1GovernanceRelay = l1GovernanceRelay(); + address messenger = messenger(); + address xDomainMessageSender = l2messenger.xDomainMessageSender(); + + relay@withrevert(e, target, targetData); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != messenger || xDomainMessageSender != l1GovernanceRelay; + bool revert3 = !success; + + assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed"; +} diff --git a/certora/L2TokenBridge.conf b/certora/L2TokenBridge.conf new file mode 100644 index 0000000..f0db0b9 --- /dev/null +++ b/certora/L2TokenBridge.conf @@ -0,0 +1,31 @@ +{ + "files": [ + "src/L2TokenBridge.sol", + "certora/harness/Auxiliar.sol", + "test/mocks/MessengerMock.sol", + "test/mocks/GemMock.sol", + "certora/harness/ImplementationMock.sol" + ], + "solc": "solc-0.8.21", + "solc_optimize_map": { + "L2TokenBridge": "200", + "Auxiliar": "0", + "MessengerMock": "0", + "GemMock": "0", + "ImplementationMock": "0" + }, + "link": [ + "L2TokenBridge:messenger=MessengerMock" + ], + "verify": "L2TokenBridge:certora/L2TokenBridge.spec", + "rule_sanity": "basic", + "multi_assert_check": true, + "parametric_contracts": ["L2TokenBridge"], + "build_cache": true, + "optimistic_hashing": true, + "hashing_length_bound": "512", + "prover_args": [ + "-enableStorageSplitting false" + ], + "msg": "L2TokenBridge" +} diff --git a/certora/L2TokenBridge.spec b/certora/L2TokenBridge.spec new file mode 100644 index 0000000..5d6eb93 --- /dev/null +++ b/certora/L2TokenBridge.spec @@ -0,0 +1,478 @@ +// L2TokenBridge.spec + +using Auxiliar as aux; +using MessengerMock as l2messenger; +using GemMock as gem; + +methods { + // storage variables + function wards(address) external returns (uint256) envfree; + function l1ToL2Token(address) external returns (address) envfree; + function maxWithdraws(address) external returns (uint256) envfree; + function isOpen() external returns (uint256) envfree; + // immutables + function otherBridge() external returns (address) envfree; + function messenger() external returns (address) envfree; + // getter + function getImplementation() external returns (address) envfree; + // + function gem.wards(address) external returns (uint256) envfree; + function gem.allowance(address,address) external returns (uint256) envfree; + function gem.totalSupply() external returns (uint256) envfree; + function gem.balanceOf(address) external returns (uint256) envfree; + function aux.getBridgeMessageHash(address,address,address,address,uint256,bytes) external returns (bytes32) envfree; + function l2messenger.xDomainMessageSender() external returns (address) envfree; + function l2messenger.lastTarget() external returns (address) envfree; + function l2messenger.lastMessageHash() external returns (bytes32) envfree; + function l2messenger.lastMinGasLimit() external returns (uint32) envfree; + // + function _.proxiableUUID() external => DISPATCHER(true); + function _.burn(address,uint256) external => DISPATCHER(true); + function _.mint(address,uint256) external => DISPATCHER(true); +} + +definition INITIALIZABLE_STORAGE() returns uint256 = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; +definition IMPLEMENTATION_SLOT() returns uint256 = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + +persistent ghost bool firstRead; +persistent ghost mathint initializedBefore; +persistent ghost bool initializingBefore; +persistent ghost mathint initializedAfter; +persistent ghost bool initializingAfter; +hook ALL_SLOAD(uint256 slot) uint256 val { + if (slot == INITIALIZABLE_STORAGE() && firstRead) { + firstRead = false; + initializedBefore = val % (max_uint64 + 1); + initializingBefore = (val / 2^64) % (max_uint8 + 1) != 0; + } else if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} +hook ALL_SSTORE(uint256 slot, uint256 val) { + if (slot == INITIALIZABLE_STORAGE()) { + initializedAfter = val % (max_uint64 + 1); + initializingAfter = (val / 2^64) % (max_uint8 + 1) != 0; + } +} + +// Verify no more entry points exist +rule entryPoints(method f) filtered { f -> !f.isView } { + env e; + + calldataarg args; + f(e, args); + + assert f.selector == sig:initialize().selector || + f.selector == sig:upgradeToAndCall(address,bytes).selector || + f.selector == sig:rely(address).selector || + f.selector == sig:deny(address).selector || + f.selector == sig:close().selector || + f.selector == sig:registerToken(address,address).selector || + f.selector == sig:setMaxWithdraw(address,uint256).selector || + f.selector == sig:bridgeERC20(address,address,uint256,uint32,bytes).selector || + f.selector == sig:bridgeERC20To(address,address,address,uint256,uint32,bytes).selector || + f.selector == sig:finalizeBridgeERC20(address,address,address,address,uint256,bytes).selector; +} + +// Verify that each storage layout is only modified in the corresponding functions +rule storageAffected(method f) filtered { f -> f.selector != sig:upgradeToAndCall(address, bytes).selector } { + env e; + + address anyAddr; + + initializedAfter = initializedBefore; + + mathint wardsBefore = wards(anyAddr); + address l1ToL2TokenBefore = l1ToL2Token(anyAddr); + mathint maxWithdrawsBefore = maxWithdraws(anyAddr); + mathint isOpenBefore = isOpen(); + + calldataarg args; + f(e, args); + + mathint wardsAfter = wards(anyAddr); + address l1ToL2TokenAfter = l1ToL2Token(anyAddr); + mathint maxWithdrawsAfter = maxWithdraws(anyAddr); + mathint isOpenAfter = isOpen(); + + assert initializedAfter != initializedBefore => f.selector == sig:initialize().selector, "Assert 1"; + assert wardsAfter != wardsBefore => f.selector == sig:rely(address).selector || f.selector == sig:deny(address).selector || f.selector == sig:initialize().selector, "Assert 2"; + assert l1ToL2TokenAfter != l1ToL2TokenBefore => f.selector == sig:registerToken(address,address).selector, "Assert 3"; + assert maxWithdrawsAfter != maxWithdrawsBefore => f.selector == sig:setMaxWithdraw(address,uint256).selector, "Assert 4"; + assert isOpenAfter != isOpenBefore => f.selector == sig:close().selector || f.selector == sig:initialize().selector, "Assert 5"; +} + +// Verify correct storage changes for non reverting initialize +rule initialize() { + env e; + + address other; + require other != e.msg.sender; + + mathint wardsOtherBefore = wards(other); + + initialize(e); + + mathint wardsSenderAfter = wards(e.msg.sender); + mathint wardsOtherAfter = wards(other); + mathint isOpenAfter = isOpen(); + + assert initializedAfter == 1, "Assert 1"; + assert !initializingAfter, "Assert 2"; + assert wardsSenderAfter == 1, "Assert 3"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 4"; + assert isOpenAfter == 1, "Assert 5"; +} + +// Verify revert rules on initialize +rule initialize_revert() { + env e; + + firstRead = true; + mathint bridgeCodeSize = nativeCodesize[currentContract]; // This should actually be always > 0 + + initialize@withrevert(e); + + bool initialSetup = initializedBefore == 0 && !initializingBefore; + bool construction = initializedBefore == 1 && bridgeCodeSize == 0; + + bool revert1 = e.msg.value > 0; + bool revert2 = !initialSetup && !construction; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting initialize +rule upgradeToAndCall(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + upgradeToAndCall(e, newImplementation, data); + + address implementationAfter = getImplementation(); + + assert implementationAfter == newImplementation, "Assert 1"; +} + +// Verify revert rules on upgradeToAndCall +rule upgradeToAndCall_revert(address newImplementation, bytes data) { + env e; + + require data.length == 0; // Avoid evaluating the delegatecCall part + + address self = currentContract.__self; + address implementation = getImplementation(); + mathint wardsSender = wards(e.msg.sender); + bytes32 newImplementationProxiableUUID = newImplementation.proxiableUUID(e); + + upgradeToAndCall@withrevert(e, newImplementation, data); + + bool revert1 = e.msg.value > 0; + bool revert2 = self == currentContract || implementation != self; + bool revert3 = wardsSender != 1; + bool revert4 = newImplementationProxiableUUID != to_bytes32(IMPLEMENTATION_SLOT()); + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting rely +rule rely(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + rely(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 1, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on rely +rule rely_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + rely@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting deny +rule deny(address usr) { + env e; + + address other; + require other != usr; + + mathint wardsOtherBefore = wards(other); + + deny(e, usr); + + mathint wardsUsrAfter = wards(usr); + mathint wardsOtherAfter = wards(other); + + assert wardsUsrAfter == 0, "Assert 1"; + assert wardsOtherAfter == wardsOtherBefore, "Assert 2"; +} + +// Verify revert rules on deny +rule deny_revert(address usr) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + deny@withrevert(e, usr); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting close +rule close() { + env e; + + close(e); + + mathint isOpenAfter = isOpen(); + + assert isOpenAfter == 0, "Assert 1"; +} + +// Verify revert rules on close +rule close_revert() { + env e; + + mathint wardsSender = wards(e.msg.sender); + + close@withrevert(e); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting registerToken +rule registerToken(address l1Token, address l2Token) { + env e; + + registerToken(e, l1Token, l2Token); + + address l1ToL2TokenAfter = l1ToL2Token(l1Token); + + assert l1ToL2TokenAfter == l2Token, "Assert 1"; +} + +// Verify revert rules on registerToken +rule registerToken_revert(address l1Token, address l2Token) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + registerToken@withrevert(e, l1Token, l2Token); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting setMaxWithdraw +rule setMaxWithdraw(address l2Token, uint256 maxWithdraw) { + env e; + + setMaxWithdraw(e, l2Token, maxWithdraw); + + mathint maxWithdrawsAfter = maxWithdraws(l2Token); + + assert maxWithdrawsAfter == maxWithdraw, "Assert 1"; +} + +// Verify revert rules on setMaxWithdraw +rule setMaxWithdraw_revert(address l2Token, uint256 maxWithdraw) { + env e; + + mathint wardsSender = wards(e.msg.sender); + + setMaxWithdraw@withrevert(e, l2Token, maxWithdraw); + + bool revert1 = e.msg.value > 0; + bool revert2 = wardsSender != 1; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20 +rule bridgeERC20(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, e.msg.sender, _amount, _extraData); + mathint localTokenTotalSupplyBefore = gem.totalSupply(); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupplyBefore >= localTokenBalanceOfSenderBefore; + + bridgeERC20(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l2messenger.lastTarget(); + bytes32 lastMessageHashAfter = l2messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l2messenger.lastMinGasLimit(); + mathint localTokenTotalSupplyAfter = gem.totalSupply(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenTotalSupplyAfter == localTokenTotalSupplyBefore - _amount, "Assert 4"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20 +rule bridgeERC20_revert(address _localToken, address _remoteToken, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenRemoteToken = l1ToL2Token(_remoteToken); + mathint maxWithdrawsLocatOken = maxWithdraws(_localToken); + + mathint localTokenTotalSupply = gem.totalSupply(); + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupply >= localTokenBalanceOfSender; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20@withrevert(e, _localToken, _remoteToken, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = nativeCodesize[e.msg.sender] != 0; + bool revert3 = isOpen != 1; + bool revert4 = _localToken == 0 || l1ToL2TokenRemoteToken != _localToken; + bool revert5 = _amount > maxWithdrawsLocatOken; + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4 || revert5, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting bridgeERC20To +rule bridgeERC20To(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + require _localToken == gem; + + address otherBridge = otherBridge(); + + bytes32 message = aux.getBridgeMessageHash(_localToken, _remoteToken, e.msg.sender, _to, _amount, _extraData); + mathint localTokenTotalSupplyBefore = gem.totalSupply(); + mathint localTokenBalanceOfSenderBefore = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupplyBefore >= localTokenBalanceOfSenderBefore; + + bridgeERC20To(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + address lastTargetAfter = l2messenger.lastTarget(); + bytes32 lastMessageHashAfter = l2messenger.lastMessageHash(); + uint32 lastMinGasLimitAfter = l2messenger.lastMinGasLimit(); + mathint localTokenTotalSupplyAfter = gem.totalSupply(); + mathint localTokenBalanceOfSenderAfter = gem.balanceOf(e.msg.sender); + + assert lastTargetAfter == otherBridge, "Assert 1"; + assert lastMessageHashAfter == message, "Assert 2"; + assert lastMinGasLimitAfter == _minGasLimit, "Assert 3"; + assert localTokenTotalSupplyAfter == localTokenTotalSupplyBefore - _amount, "Assert 4"; + assert localTokenBalanceOfSenderAfter == localTokenBalanceOfSenderBefore - _amount, "Assert 5"; +} + +// Verify revert rules on bridgeERC20To +rule bridgeERC20To_revert(address _localToken, address _remoteToken, address _to, uint256 _amount, uint32 _minGasLimit, bytes _extraData) { + env e; + + mathint isOpen = isOpen(); + address l1ToL2TokenRemoteToken = l1ToL2Token(_remoteToken); + mathint maxWithdrawsLocatOken = maxWithdraws(_localToken); + + mathint localTokenTotalSupply = gem.totalSupply(); + mathint localTokenBalanceOfSender = gem.balanceOf(e.msg.sender); + // ERC20 assumption + require localTokenTotalSupply >= localTokenBalanceOfSender; + // User assumptions + require localTokenBalanceOfSender >= _amount; + require gem.allowance(e.msg.sender, currentContract) >= _amount; + + bridgeERC20To@withrevert(e, _localToken, _remoteToken, _to, _amount, _minGasLimit, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = isOpen != 1; + bool revert3 = _localToken == 0 || l1ToL2TokenRemoteToken != _localToken; + bool revert4 = _amount > maxWithdrawsLocatOken; + + assert lastReverted <=> revert1 || revert2 || revert3 || + revert4, "Revert rules failed"; +} + +// Verify correct storage changes for non reverting finalizeBridgeERC20 +rule finalizeBridgeERC20(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + mathint localTokenTotalSupplyBefore = gem.totalSupply(); + mathint localTokenBalanceOfToBefore = gem.balanceOf(_to); + // ERC20 assumption + require localTokenTotalSupplyBefore >= localTokenBalanceOfToBefore; + + finalizeBridgeERC20(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + mathint localTokenTotalSupplyAfter = gem.totalSupply(); + mathint localTokenBalanceOfToAfter = gem.balanceOf(_to); + + assert localTokenTotalSupplyAfter == localTokenTotalSupplyBefore + _amount, "Assert 1"; + assert localTokenBalanceOfToAfter == localTokenBalanceOfToBefore + _amount, "Assert 2"; +} + +// Verify revert rules on finalizeBridgeERC20 +rule finalizeBridgeERC20_revert(address _localToken, address _remoteToken, address _from, address _to, uint256 _amount, bytes _extraData) { + env e; + + require _localToken == gem; + + address messenger = messenger(); + address otherBridge = otherBridge(); + address xDomainMessageSender = l2messenger.xDomainMessageSender(); + + mathint localTokenTotalSupply = gem.totalSupply(); + mathint localTokenBalanceOfTo = gem.balanceOf(_to); + // ERC20 assumption + require localTokenTotalSupply >= localTokenBalanceOfTo; + // Practical assumption + require localTokenTotalSupply + _amount <= max_uint256; + // Set up assumption + require gem.wards(currentContract) == 1; + + finalizeBridgeERC20@withrevert(e, _localToken, _remoteToken, _from, _to, _amount, _extraData); + + bool revert1 = e.msg.value > 0; + bool revert2 = e.msg.sender != messenger || xDomainMessageSender != otherBridge; + + assert lastReverted <=> revert1 || revert2, "Revert rules failed"; +} diff --git a/certora/harness/Auxiliar.sol b/certora/harness/Auxiliar.sol new file mode 100644 index 0000000..03ca10d --- /dev/null +++ b/certora/harness/Auxiliar.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +interface GovernanceRelayLike { + function relay(address, bytes calldata) external; +} + +interface BridgeLike { + function finalizeBridgeERC20(address, address, address, address, uint256, bytes calldata) external; +} + +contract Auxiliar { + function getGovMessageHash(address target, bytes calldata targetData) public pure returns (bytes32) { + return keccak256(abi.encodeCall(GovernanceRelayLike.relay, (target, targetData))); + } + + function getBridgeMessageHash(address token1, address token2, address sender, address to, uint256 amount, bytes calldata extraData) public pure returns (bytes32) { + return keccak256(abi.encodeCall(BridgeLike.finalizeBridgeERC20, ( + token1, + token2, + sender, + to, + amount, + extraData + ))); + } +} diff --git a/certora/harness/ImplementationMock.sol b/certora/harness/ImplementationMock.sol new file mode 100644 index 0000000..ec2e28d --- /dev/null +++ b/certora/harness/ImplementationMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +pragma solidity ^0.8.21; + +contract ImplementationMock { + bytes32 public proxiableUUID; +} diff --git a/test/mocks/MessengerMock.sol b/test/mocks/MessengerMock.sol index e744690..9a11ef0 100644 --- a/test/mocks/MessengerMock.sol +++ b/test/mocks/MessengerMock.sol @@ -1,24 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -// Copyright (C) 2024 Dai Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - pragma solidity ^0.8.21; contract MessengerMock { address public xDomainMessageSender; + address public lastTarget; + bytes32 public lastMessageHash; + uint32 public lastMinGasLimit; event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); @@ -26,7 +14,10 @@ contract MessengerMock { xDomainMessageSender = xDomainMessageSender_; } - function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable { - emit SentMessage(_target, msg.sender, _message, 0, _minGasLimit); + function sendMessage(address target, bytes calldata message, uint32 minGasLimit) external payable { + lastTarget = target; + lastMessageHash = keccak256(message); + lastMinGasLimit = minGasLimit; + emit SentMessage(target, msg.sender, message, 0, minGasLimit); } }