diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml
new file mode 100644
index 0000000..63f9e84
--- /dev/null
+++ b/.github/workflows/certora.yml
@@ -0,0 +1,45 @@
+name: Certora
+
+on: [push, pull_request]
+
+jobs:
+ certora:
+ name: Certora
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ arbitrum-token-bridge:
+ - l1-token-gateway
+ - l2-token-gateway
+
+ 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.arbitrum-token-bridge }}
+ run: make certora-${{ matrix.arbitrum-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..a2008f4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,3 @@
+PATH := ~/.solc-select/artifacts/solc-0.8.21:$(PATH)
+certora-l1-token-gateway :; PATH=${PATH} certoraRun certora/L1TokenGateway.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
+certora-l2-token-gateway :; PATH=${PATH} certoraRun certora/L2TokenGateway.conf$(if $(rule), --rule $(rule),)$(if $(results), --wait_for_results all,)
diff --git a/certora/L1TokenGateway.conf b/certora/L1TokenGateway.conf
new file mode 100644
index 0000000..ccdc7e7
--- /dev/null
+++ b/certora/L1TokenGateway.conf
@@ -0,0 +1,37 @@
+{
+ "files": [
+ "src/L1TokenGateway.sol",
+ "certora/harness/Auxiliar.sol",
+ "test/mocks/InboxMock.sol",
+ "test/mocks/InboxMock.sol:BridgeMock",
+ "test/mocks/InboxMock.sol:OutboxMock",
+ "test/mocks/GemMock.sol",
+ "certora/harness/ImplementationMock.sol"
+ ],
+ "solc": "solc-0.8.21",
+ "solc_optimize_map": {
+ "L1TokenGateway": "200",
+ "Auxiliar": "0",
+ "InboxMock": "0",
+ "BridgeMock": "0",
+ "OutboxMock": "0",
+ "GemMock": "0",
+ "ImplementationMock": "0"
+ },
+ "link": [
+ "L1TokenGateway:inbox=InboxMock",
+ "InboxMock:bridge=BridgeMock",
+ "BridgeMock:activeOutbox=OutboxMock"
+ ],
+ "verify": "L1TokenGateway:certora/L1TokenGateway.spec",
+ "rule_sanity": "basic",
+ "multi_assert_check": true,
+ "parametric_contracts": ["L1TokenGateway"],
+ "build_cache": true,
+ "optimistic_hashing": true,
+ "hashing_length_bound": "512",
+ "prover_args": [
+ "-enableStorageSplitting false"
+ ],
+ "msg": "L1TokenGateway"
+}
diff --git a/certora/L1TokenGateway.spec b/certora/L1TokenGateway.spec
new file mode 100644
index 0000000..9315de3
--- /dev/null
+++ b/certora/L1TokenGateway.spec
@@ -0,0 +1,600 @@
+// L1TokenGateway.spec
+
+using Auxiliar as aux;
+using InboxMock as inbox;
+using BridgeMock as bridge;
+using OutboxMock as outbox;
+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 counterpartGateway() external returns (address) envfree;
+ function l1Router() external returns (address) envfree;
+ function inbox() 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.extractFrom(bytes) external returns (address,bytes) envfree;
+ function aux.extractSubmission(bytes) external returns (uint256,bytes) envfree;
+ function aux.getL1DataHash(address,address,address,uint256,bytes) external returns (bytes32) envfree;
+ function inbox.bridge() external returns (address) envfree;
+ function inbox.lastTo() external returns (address) envfree;
+ function inbox.lastL2CallValue() external returns (uint256) envfree;
+ function inbox.lastMaxSubmissionCost() external returns (uint256) envfree;
+ function inbox.lastRefundTo() external returns (address) envfree;
+ function inbox.lastUser() external returns (address) envfree;
+ function inbox.lastMaxGas() external returns (uint256) envfree;
+ function inbox.lastGasPriceBid() external returns (uint256) envfree;
+ function inbox.lastDataHash() external returns (bytes32) envfree;
+ function inbox.lastValue() external returns (uint256) envfree;
+ function bridge.activeOutbox() external returns (address) envfree;
+ function outbox.l2ToL1Sender() external returns (address) 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:outboundTransfer(address,address,uint256,uint256,uint256,bytes).selector ||
+ f.selector == sig:outboundTransferCustomRefund(address,address,address,uint256,uint256,uint256,bytes).selector ||
+ f.selector == sig:finalizeInboundTransfer(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 outboundTransfer
+rule outboundTransfer(address l1Token, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) {
+ env e;
+
+ require l1Token == gem;
+
+ address l1Router = l1Router();
+ address counterpartGateway = counterpartGateway();
+ address escrow = escrow();
+ require e.msg.sender != escrow;
+
+ address from;
+ bytes auxData;
+ if (e.msg.sender == l1Router) {
+ from, auxData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ auxData = data;
+ }
+ require from != escrow;
+ mathint maxSubmissionCost;
+ bytes extraData;
+ maxSubmissionCost, extraData = aux.extractSubmission(auxData);
+
+ bytes32 dataHash = aux.getL1DataHash(l1Token, from, counterpartGateway, amount, extraData);
+
+ mathint l1TokenBalanceOfFromBefore = gem.balanceOf(from);
+ mathint l1TokenBalanceOfEscrowBefore = gem.balanceOf(escrow);
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfFromBefore + l1TokenBalanceOfEscrowBefore;
+
+ outboundTransfer(e, l1Token, to, amount, maxGas, gasPriceBid, data);
+
+ address lastToAfter = inbox.lastTo();
+ mathint lastL2CallValueAfter = inbox.lastL2CallValue();
+ mathint lastMaxSubmissionCostAfter = inbox.lastMaxSubmissionCost();
+ address lastRefundToAfter = inbox.lastRefundTo();
+ address lastUserAfter = inbox.lastUser();
+ mathint lastMaxGasAfter = inbox.lastMaxGas();
+ mathint lastGasPriceBidAfter = inbox.lastGasPriceBid();
+ bytes32 lastDataHashAfter = inbox.lastDataHash();
+ mathint lastValueAfter = inbox.lastValue();
+ mathint l1TokenBalanceOfFromAfter = gem.balanceOf(from);
+ mathint l1TokenBalanceOfEscrowAfter = gem.balanceOf(escrow);
+
+ assert lastToAfter == counterpartGateway, "Assert 1";
+ assert lastL2CallValueAfter == 0, "Assert 2";
+ assert lastMaxSubmissionCostAfter == maxSubmissionCost, "Assert 3";
+ assert lastRefundToAfter == to, "Assert 4";
+ assert lastUserAfter == from, "Assert 5";
+ assert lastMaxGasAfter == maxGas, "Assert 6";
+ assert lastGasPriceBidAfter == gasPriceBid, "Assert 7";
+ assert lastDataHashAfter == dataHash, "Assert 8";
+ assert lastValueAfter == e.msg.value, "Assert 9";
+ assert l1TokenBalanceOfFromAfter == l1TokenBalanceOfFromBefore - amount, "Assert 10";
+ assert l1TokenBalanceOfEscrowAfter == l1TokenBalanceOfEscrowBefore + amount, "Assert 11";
+}
+
+// Verify revert rules on outboundTransfer
+rule outboundTransfer_revert(address l1Token, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) {
+ env e;
+
+ require l1Token == gem;
+
+ mathint isOpen = isOpen();
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+
+ address l1Router = l1Router();
+ address escrow = escrow();
+
+ address from;
+ bytes auxData;
+ if (e.msg.sender == l1Router) {
+ from, auxData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ auxData = data;
+ }
+ require from != escrow;
+ mathint a;
+ bytes extraData;
+ a, extraData = aux.extractSubmission(auxData);
+
+ mathint l1TokenBalanceOfFrom = gem.balanceOf(from);
+ mathint l1TokenBalanceOfEscrow = gem.balanceOf(escrow);
+ mathint senderBalance = nativeBalances[e.msg.sender];
+ mathint inboxBalance = nativeBalances[inbox];
+
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfFrom + l1TokenBalanceOfEscrow;
+ // User assumptions
+ require l1TokenBalanceOfFrom >= amount;
+ require gem.allowance(from, currentContract) >= amount;
+ // Practical assumption
+ require inboxBalance + e.msg.value <= max_uint256;
+
+ outboundTransfer@withrevert(e, l1Token, to, amount, maxGas, gasPriceBid, data);
+
+ bool revert1 = senderBalance < e.msg.value;
+ bool revert2 = isOpen != 1;
+ bool revert3 = l1ToL2TokenL1Token == 0;
+ bool revert4 = extraData.length > 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting outboundTransferCustomRefund
+rule outboundTransferCustomRefund(address l1Token, address refundTo, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) {
+ env e;
+
+ require l1Token == gem;
+
+ address l1Router = l1Router();
+ address counterpartGateway = counterpartGateway();
+ address escrow = escrow();
+ require e.msg.sender != escrow;
+
+ address from;
+ bytes auxData;
+ if (e.msg.sender == l1Router) {
+ from, auxData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ auxData = data;
+ }
+ require from != escrow;
+ mathint maxSubmissionCost;
+ bytes extraData;
+ maxSubmissionCost, extraData = aux.extractSubmission(auxData);
+
+ bytes32 dataHash = aux.getL1DataHash(l1Token, from, counterpartGateway, amount, extraData);
+
+ mathint l1TokenBalanceOfFromBefore = gem.balanceOf(from);
+ mathint l1TokenBalanceOfEscrowBefore = gem.balanceOf(escrow);
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfFromBefore + l1TokenBalanceOfEscrowBefore;
+
+ outboundTransferCustomRefund(e, l1Token, refundTo, to, amount, maxGas, gasPriceBid, data);
+
+ address lastToAfter = inbox.lastTo();
+ mathint lastL2CallValueAfter = inbox.lastL2CallValue();
+ mathint lastMaxSubmissionCostAfter = inbox.lastMaxSubmissionCost();
+ address lastRefundToAfter = inbox.lastRefundTo();
+ address lastUserAfter = inbox.lastUser();
+ mathint lastMaxGasAfter = inbox.lastMaxGas();
+ mathint lastGasPriceBidAfter = inbox.lastGasPriceBid();
+ bytes32 lastDataHashAfter = inbox.lastDataHash();
+ mathint lastValueAfter = inbox.lastValue();
+ mathint l1TokenBalanceOfFromAfter = gem.balanceOf(from);
+ mathint l1TokenBalanceOfEscrowAfter = gem.balanceOf(escrow);
+
+ assert lastToAfter == counterpartGateway, "Assert 1";
+ assert lastL2CallValueAfter == 0, "Assert 2";
+ assert lastMaxSubmissionCostAfter == maxSubmissionCost, "Assert 3";
+ assert lastRefundToAfter == refundTo, "Assert 4";
+ assert lastUserAfter == from, "Assert 5";
+ assert lastMaxGasAfter == maxGas, "Assert 6";
+ assert lastGasPriceBidAfter == gasPriceBid, "Assert 7";
+ assert lastDataHashAfter == dataHash, "Assert 8";
+ assert lastValueAfter == e.msg.value, "Assert 9";
+ assert l1TokenBalanceOfFromAfter == l1TokenBalanceOfFromBefore - amount, "Assert 10";
+ assert l1TokenBalanceOfEscrowAfter == l1TokenBalanceOfEscrowBefore + amount, "Assert 11";
+}
+
+// Verify revert rules on outboundTransferCustomRefund
+rule outboundTransferCustomRefund_revert(address l1Token, address refundTo, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) {
+ env e;
+
+ require l1Token == gem;
+
+ mathint isOpen = isOpen();
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+
+ address l1Router = l1Router();
+ address escrow = escrow();
+
+ address from;
+ bytes auxData;
+ if (e.msg.sender == l1Router) {
+ from, auxData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ auxData = data;
+ }
+ require from != escrow;
+ mathint a;
+ bytes extraData;
+ a, extraData = aux.extractSubmission(auxData);
+
+ mathint l1TokenBalanceOfFrom = gem.balanceOf(from);
+ mathint l1TokenBalanceOfEscrow = gem.balanceOf(escrow);
+ mathint senderBalance = nativeBalances[e.msg.sender];
+ mathint inboxBalance = nativeBalances[inbox];
+
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfFrom + l1TokenBalanceOfEscrow;
+ // User assumptions
+ require l1TokenBalanceOfFrom >= amount;
+ require gem.allowance(from, currentContract) >= amount;
+ // Practical assumption
+ require inboxBalance + e.msg.value <= max_uint256;
+
+ outboundTransferCustomRefund@withrevert(e, l1Token, refundTo, to, amount, maxGas, gasPriceBid, data);
+
+ bool revert1 = senderBalance < e.msg.value;
+ bool revert2 = isOpen != 1;
+ bool revert3 = l1ToL2TokenL1Token == 0;
+ bool revert4 = extraData.length > 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting finalizeInboundTransfer
+rule finalizeInboundTransfer(address l1Token, address from, address to, uint256 amount, bytes data) {
+ env e;
+
+ require l1Token == gem;
+
+ address escrow = escrow();
+
+ mathint l1TokenBalanceOfEscrowBefore = gem.balanceOf(escrow);
+ mathint l1TokenBalanceOfToBefore = gem.balanceOf(to);
+
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfEscrowBefore + l1TokenBalanceOfToBefore;
+
+ finalizeInboundTransfer(e, l1Token, from, to, amount, data);
+
+ mathint l1TokenBalanceOfEscrowAfter = gem.balanceOf(escrow);
+ mathint l1TokenBalanceOfToAfter = gem.balanceOf(to);
+
+ assert escrow != to => l1TokenBalanceOfEscrowAfter == l1TokenBalanceOfEscrowBefore - amount, "Assert 1";
+ assert escrow != to => l1TokenBalanceOfToAfter == l1TokenBalanceOfToBefore + amount, "Assert 2";
+ assert escrow == to => l1TokenBalanceOfEscrowAfter == l1TokenBalanceOfEscrowBefore, "Assert 3";
+}
+
+// Verify revert rules on finalizeInboundTransfer
+rule finalizeInboundTransfer_revert(address l1Token, address from, address to, uint256 amount, bytes data) {
+ env e;
+
+ require l1Token == gem;
+
+ address l2ToL1Sender = outbox.l2ToL1Sender();
+
+ address counterpartGateway = counterpartGateway();
+ address escrow = escrow();
+
+ mathint l1TokenBalanceOfEscrow = gem.balanceOf(escrow);
+ mathint senderBalance = nativeBalances[e.msg.sender];
+
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfEscrow + gem.balanceOf(to);
+ // Bridge assumption
+ require l1TokenBalanceOfEscrow >= amount;
+ // Set up assumption
+ require gem.allowance(escrow, currentContract) == max_uint256;
+
+ finalizeInboundTransfer@withrevert(e, l1Token, from, to, amount, data);
+
+ bool revert1 = senderBalance < e.msg.value;
+ bool revert2 = e.msg.sender != bridge;
+ bool revert3 = l2ToL1Sender == 0;
+ bool revert4 = l2ToL1Sender != counterpartGateway;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4, "Revert rules failed";
+}
diff --git a/certora/L2TokenGateway.conf b/certora/L2TokenGateway.conf
new file mode 100644
index 0000000..b3e8b99
--- /dev/null
+++ b/certora/L2TokenGateway.conf
@@ -0,0 +1,28 @@
+{
+ "files": [
+ "src/L2TokenGateway.sol",
+ "certora/harness/Auxiliar.sol",
+ "certora/harness/ArbSysMock.sol",
+ "test/mocks/GemMock.sol",
+ "certora/harness/ImplementationMock.sol"
+ ],
+ "solc": "solc-0.8.21",
+ "solc_optimize_map": {
+ "L2TokenGateway": "200",
+ "Auxiliar": "0",
+ "ArbSysMock": "0",
+ "GemMock": "0",
+ "ImplementationMock": "0"
+ },
+ "verify": "L2TokenGateway:certora/L2TokenGateway.spec",
+ "rule_sanity": "basic",
+ "multi_assert_check": true,
+ "parametric_contracts": ["L2TokenGateway"],
+ "build_cache": true,
+ "optimistic_hashing": true,
+ "hashing_length_bound": "512",
+ "prover_args": [
+ "-enableStorageSplitting false"
+ ],
+ "msg": "L2TokenGateway"
+}
diff --git a/certora/L2TokenGateway.spec b/certora/L2TokenGateway.spec
new file mode 100644
index 0000000..ee7fb76
--- /dev/null
+++ b/certora/L2TokenGateway.spec
@@ -0,0 +1,542 @@
+// L2TokenGateway.spec
+
+using Auxiliar as aux;
+using ArbSysMock as arb;
+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 l2Router() external returns (address) envfree;
+ function counterpartGateway() 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.extractFrom(bytes) external returns (address,bytes) envfree;
+ function aux.extractSubmission(bytes) external returns (uint256,bytes) envfree;
+ function aux.getL2DataHash(address,address,address,uint256,bytes) external returns (bytes32) envfree;
+ function aux.applyL1ToL2Alias(address) external returns (address) envfree;
+ function arb.lastTo() external returns (address) envfree;
+ function arb.lastDataHash() external returns (bytes32) envfree;
+ function arb.lastValue() external returns (uint256) envfree;
+ //
+ function _.proxiableUUID() external => DISPATCHER(true);
+ function _.burn(address,uint256) external => DISPATCHER(true);
+ function _.mint(address,uint256) external => DISPATCHER(true);
+ function _.sendTxToL1(address,bytes) 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:outboundTransfer(address,address,uint256,bytes).selector ||
+ f.selector == sig:outboundTransfer(address,address,uint256,uint256,uint256,bytes).selector ||
+ f.selector == sig:finalizeInboundTransfer(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 outboundTransfer
+rule outboundTransfer(address l1Token, address to, uint256 amount, bytes data) {
+ env e;
+
+ require arb == 100;
+
+ address l2Router = l2Router();
+ address counterpartGateway = counterpartGateway();
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+
+ require l1ToL2TokenL1Token == gem;
+
+ address from;
+ bytes extraData;
+ if (e.msg.sender == l2Router) {
+ from, extraData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ extraData = data;
+ }
+
+ bytes32 dataHash = aux.getL2DataHash(l1Token, from, counterpartGateway, amount, extraData);
+
+ mathint l1TokenTotalSupplyBefore = gem.totalSupply();
+ mathint l1TokenBalanceOfFromBefore = gem.balanceOf(from);
+ // ERC20 assumption
+ require l1TokenTotalSupplyBefore >= l1TokenBalanceOfFromBefore;
+
+ outboundTransfer(e, l1Token, to, amount, data);
+
+ address lastToAfter = arb.lastTo();
+ bytes32 lastDataHashAfter = arb.lastDataHash();
+ mathint lastValueAfter = arb.lastValue();
+ mathint l1TokenTotalSupplyAfter = gem.totalSupply();
+ mathint l1TokenBalanceOfFromAfter = gem.balanceOf(from);
+
+ assert lastToAfter == counterpartGateway, "Assert 1";
+ assert lastDataHashAfter == dataHash, "Assert 2";
+ assert lastValueAfter == 0, "Assert 3";
+ assert l1TokenTotalSupplyAfter == l1TokenTotalSupplyBefore - amount, "Assert 4";
+ assert l1TokenBalanceOfFromAfter == l1TokenBalanceOfFromBefore - amount, "Assert 5";
+}
+
+// Verify revert rules on outboundTransfer
+rule outboundTransfer_revert(address l1Token, address to, uint256 amount, bytes data) {
+ env e;
+
+ require arb == 100;
+
+ mathint isOpen = isOpen();
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+ mathint maxWithdrawsL2Token = maxWithdraws(l1ToL2TokenL1Token);
+
+ require l1ToL2TokenL1Token == gem;
+
+ address l2Router = l2Router();
+
+ address from;
+ bytes extraData;
+ if (e.msg.sender == l2Router) {
+ from, extraData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ extraData = data;
+ }
+
+ mathint l1TokenBalanceOfFrom = gem.balanceOf(from);
+
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfFrom;
+ // User assumptions
+ require l1TokenBalanceOfFrom >= amount;
+ require gem.allowance(from, currentContract) >= amount;
+
+ outboundTransfer@withrevert(e, l1Token, to, amount, data);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = isOpen != 1;
+ bool revert3 = l1ToL2TokenL1Token == 0;
+ bool revert4 = amount > maxWithdrawsL2Token;
+ bool revert5 = extraData.length > 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting outboundTransfer
+rule outboundTransfer2(address l1Token, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) {
+ env e;
+
+ require arb == 100;
+
+ address l2Router = l2Router();
+ address counterpartGateway = counterpartGateway();
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+
+ require l1ToL2TokenL1Token == gem;
+
+ address from;
+ bytes extraData;
+ if (e.msg.sender == l2Router) {
+ from, extraData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ extraData = data;
+ }
+
+ bytes32 dataHash = aux.getL2DataHash(l1Token, from, counterpartGateway, amount, extraData);
+
+ mathint l1TokenTotalSupplyBefore = gem.totalSupply();
+ mathint l1TokenBalanceOfFromBefore = gem.balanceOf(from);
+ // ERC20 assumption
+ require l1TokenTotalSupplyBefore >= l1TokenBalanceOfFromBefore;
+
+ outboundTransfer(e, l1Token, to, amount, maxGas, gasPriceBid, data);
+
+ address lastToAfter = arb.lastTo();
+ bytes32 lastDataHashAfter = arb.lastDataHash();
+ mathint lastValueAfter = arb.lastValue();
+ mathint l1TokenTotalSupplyAfter = gem.totalSupply();
+ mathint l1TokenBalanceOfFromAfter = gem.balanceOf(from);
+
+ assert lastToAfter == counterpartGateway, "Assert 1";
+ assert lastDataHashAfter == dataHash, "Assert 2";
+ assert lastValueAfter == 0, "Assert 3";
+ assert l1TokenTotalSupplyAfter == l1TokenTotalSupplyBefore - amount, "Assert 4";
+ assert l1TokenBalanceOfFromAfter == l1TokenBalanceOfFromBefore - amount, "Assert 5";
+}
+
+// Verify revert rules on outboundTransfer
+rule outboundTransfer2_revert(address l1Token, address to, uint256 amount, uint256 maxGas, uint256 gasPriceBid, bytes data) {
+ env e;
+
+ require arb == 100;
+
+ mathint isOpen = isOpen();
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+ mathint maxWithdrawsL2Token = maxWithdraws(l1ToL2TokenL1Token);
+
+ require l1ToL2TokenL1Token == gem;
+
+ address l2Router = l2Router();
+
+ address from;
+ bytes extraData;
+ if (e.msg.sender == l2Router) {
+ from, extraData = aux.extractFrom(data);
+ } else {
+ from = e.msg.sender;
+ extraData = data;
+ }
+
+ mathint l1TokenBalanceOfFrom = gem.balanceOf(from);
+
+ // ERC20 assumption
+ require gem.totalSupply() >= l1TokenBalanceOfFrom;
+ // User assumptions
+ require l1TokenBalanceOfFrom >= amount;
+ require gem.allowance(from, currentContract) >= amount;
+
+ outboundTransfer@withrevert(e, l1Token, to, amount, maxGas, gasPriceBid, data);
+
+ bool revert1 = e.msg.value > 0;
+ bool revert2 = isOpen != 1;
+ bool revert3 = l1ToL2TokenL1Token == 0;
+ bool revert4 = amount > maxWithdrawsL2Token;
+ bool revert5 = extraData.length > 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3 ||
+ revert4 || revert5, "Revert rules failed";
+}
+
+// Verify correct storage changes for non reverting finalizeInboundTransfer
+rule finalizeInboundTransfer(address l1Token, address from, address to, uint256 amount, bytes data) {
+ env e;
+
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+ require l1ToL2TokenL1Token == gem;
+
+ mathint l1TokenTotalSupplyBefore = gem.totalSupply();
+ mathint l1TokenBalanceOfToBefore = gem.balanceOf(to);
+
+ // ERC20 assumption
+ require l1TokenTotalSupplyBefore >= l1TokenBalanceOfToBefore;
+
+ finalizeInboundTransfer(e, l1Token, from, to, amount, data);
+
+ mathint l1TokenTotalSupplyAfter = gem.totalSupply();
+ mathint l1TokenBalanceOfToAfter = gem.balanceOf(to);
+
+ assert l1TokenTotalSupplyAfter == l1TokenTotalSupplyBefore + amount, "Assert 1";
+ assert l1TokenBalanceOfToAfter == l1TokenBalanceOfToBefore + amount, "Assert 2";
+}
+
+// Verify revert rules on finalizeInboundTransfer
+rule finalizeInboundTransfer_revert(address l1Token, address from, address to, uint256 amount, bytes data) {
+ env e;
+
+ address l1ToL2TokenL1Token = l1ToL2Token(l1Token);
+ require l1ToL2TokenL1Token == gem;
+
+ address aliasCounterpartGateway = aux.applyL1ToL2Alias(counterpartGateway());
+
+ mathint senderBalance = nativeBalances[e.msg.sender];
+
+ // ERC20 assumption
+ require gem.totalSupply() >= gem.balanceOf(to);
+ // Set up assumption
+ require gem.wards(currentContract) == 1;
+ // Practical assumption
+ require gem.totalSupply() + amount <= max_uint256;
+
+ finalizeInboundTransfer@withrevert(e, l1Token, from, to, amount, data);
+
+ bool revert1 = senderBalance < e.msg.value;
+ bool revert2 = e.msg.sender != aliasCounterpartGateway;
+ bool revert3 = l1ToL2TokenL1Token == 0;
+
+ assert lastReverted <=> revert1 || revert2 || revert3, "Revert rules failed";
+}
diff --git a/certora/harness/ArbSysMock.sol b/certora/harness/ArbSysMock.sol
new file mode 100644
index 0000000..6d2573c
--- /dev/null
+++ b/certora/harness/ArbSysMock.sol
@@ -0,0 +1,36 @@
+// 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 .
+
+pragma solidity ^0.8.21;
+
+contract ArbSysMock {
+ address public lastTo;
+ bytes32 public lastDataHash;
+ uint256 public lastValue;
+ uint256 public ret;
+
+ function sendTxToL1(
+ address _to,
+ bytes calldata _data
+ ) payable external returns (uint256) {
+ lastTo = _to;
+ lastDataHash = keccak256(_data);
+ lastValue = msg.value;
+
+ return ret;
+ }
+}
diff --git a/certora/harness/Auxiliar.sol b/certora/harness/Auxiliar.sol
new file mode 100644
index 0000000..2f90d0d
--- /dev/null
+++ b/certora/harness/Auxiliar.sol
@@ -0,0 +1,58 @@
+// 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 .
+
+pragma solidity ^0.8.21;
+
+interface TokenGatewayLike {
+ function finalizeInboundTransfer(address, address, address, uint256, bytes calldata) external;
+}
+
+contract Auxiliar {
+ function extractFrom(bytes calldata data) external pure returns (address from, bytes memory extraData) {
+ (from, extraData) = abi.decode(data, (address, bytes));
+ }
+
+ function extractSubmission(bytes calldata data) external pure returns (uint256 submission, bytes memory extraData) {
+ (submission, extraData) = abi.decode(data, (uint256, bytes));
+ }
+
+ function getL1DataHash(address l1Token, address from, address to, uint256 amount, bytes calldata data) external pure returns (bytes32) {
+ return keccak256(abi.encodeCall(TokenGatewayLike.finalizeInboundTransfer, (
+ l1Token,
+ from,
+ to,
+ amount,
+ abi.encode("", data)
+ )));
+ }
+
+ function getL2DataHash(address l1Token, address from, address to, uint256 amount, bytes calldata data) external pure returns (bytes32) {
+ return keccak256(abi.encodeCall(TokenGatewayLike.finalizeInboundTransfer, (
+ l1Token,
+ from,
+ to,
+ amount,
+ abi.encode(0, data)
+ )));
+ }
+
+ function applyL1ToL2Alias(address l1Address) external pure returns (address l2Address) {
+ unchecked {
+ l2Address = address(uint160(l1Address) + uint160(0x1111000000000000000000000000000000001111));
+ }
+ }
+}
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/InboxMock.sol b/test/mocks/InboxMock.sol
index 17c1e2d..814e41f 100644
--- a/test/mocks/InboxMock.sol
+++ b/test/mocks/InboxMock.sol
@@ -20,9 +20,21 @@ contract BridgeMock {
contract InboxMock {
address public immutable bridge;
+ address public lastTo;
+ uint256 public lastL2CallValue;
+ uint256 public lastMaxSubmissionCost;
+ address public lastRefundTo;
+ address public lastUser;
+ uint256 public lastMaxGas;
+ uint256 public lastGasPriceBid;
+ bytes32 public lastDataHash;
+ uint256 public lastValue;
+ uint256 public ret;
+
constructor() {
bridge = address(new BridgeMock());
}
+
function createRetryableTicket(
address _to,
uint256 _l2CallValue,
@@ -32,5 +44,17 @@ contract InboxMock {
uint256 _maxGas,
uint256 _gasPriceBid,
bytes calldata _data
- ) payable external returns (uint256) {}
+ ) payable external returns (uint256) {
+ lastTo = _to;
+ lastL2CallValue = _l2CallValue;
+ lastMaxSubmissionCost = _maxSubmissionCost;
+ lastRefundTo = _refundTo;
+ lastUser = _user;
+ lastMaxGas = _maxGas;
+ lastGasPriceBid = _gasPriceBid;
+ lastDataHash = keccak256(_data);
+ lastValue = msg.value;
+
+ return ret;
+ }
}