Skip to content

Commit

Permalink
Mailbox unit testing (#559)
Browse files Browse the repository at this point in the history
Failing CI check is for coverage and for external contributors, they are not permissioned enough to run this check. The reason being Github doesn't allow granting anything except read for tokens, when PR is from fork https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
  • Loading branch information
tommysr authored Jul 3, 2024
1 parent fb3a5d8 commit 1c1b93b
Show file tree
Hide file tree
Showing 12 changed files with 758 additions and 21 deletions.
18 changes: 18 additions & 0 deletions l1-contracts/contracts/dev-contracts/test/DummyBridgehub.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {Bridgehub} from "../../bridgehub/Bridgehub.sol";

/// @title DummyBridgehub
/// @notice A test smart contract that allows to set State Transition Manager for a given chain
contract DummyBridgehub is Bridgehub {
// add this to be excluded from coverage report
function test() internal virtual {}

constructor() Bridgehub() {}

function setStateTransitionManager(uint256 _chainId, address _stm) external {
stateTransitionManager[_chainId] = _stm;
}
}
4 changes: 4 additions & 0 deletions l1-contracts/contracts/dev-contracts/test/DummyHyperchain.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ contract DummyHyperchain is MailboxFacet {
s.bridgehub = bridgeHubAddress;
}

function getEraChainId() public view returns (uint256) {
return ERA_CHAIN_ID;
}

function setBridgeHubAddress(address bridgeHubAddress) public {
s.bridgehub = bridgeHubAddress;
}
Expand Down
53 changes: 50 additions & 3 deletions l1-contracts/contracts/dev-contracts/test/DummySharedBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {L2TransactionRequestTwoBridgesInner} from "../../bridgehub/IBridgehub.sol";
import {TWO_BRIDGES_MAGIC_VALUE} from "../../common/Config.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import {UnsafeBytes} from "contracts/common/libraries/UnsafeBytes.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract DummySharedBridge is PausableUpgradeable {
using SafeERC20 for IERC20;

contract DummySharedBridge {
event BridgehubDepositBaseTokenInitiated(
uint256 indexed chainId,
address indexed from,
Expand All @@ -19,7 +25,7 @@ contract DummySharedBridge {

/// @dev Maps token balances for each chain to prevent unauthorized spending across hyperchains.
/// This serves as a security measure until hyperbridging is implemented.
mapping(uint256 chainId => mapping(address l1Token => uint256 balance)) internal chainBalance;
mapping(uint256 chainId => mapping(address l1Token => uint256 balance)) public chainBalance;

/// @dev Indicates whether the hyperbridging is enabled for a given chain.
mapping(uint256 chainId => bool enabled) internal hyperbridgingEnabled;
Expand All @@ -38,6 +44,8 @@ contract DummySharedBridge {
amountReturnInFinalizeWithdrawal = _amount;
}

function receiveEth(uint256 _chainId) external payable {}

function depositLegacyErc20Bridge(
address, //_msgSender,
address, //_l2Receiver,
Expand Down Expand Up @@ -75,12 +83,51 @@ contract DummySharedBridge {

event Debugger(uint256);

function pause() external {
_pause();
}

function unpause() external {
_unpause();
}

// This function expects abi encoded data
function _parseL2WithdrawalMessage(
bytes memory _l2ToL1message
) internal view returns (address l1Receiver, address l1Token, uint256 amount) {
(l1Receiver, l1Token, amount) = abi.decode(_l2ToL1message, (address, address, uint256));
}

// simple function to just transfer the funds
function finalizeWithdrawal(
uint256 _chainId,
uint256 _l2BatchNumber,
uint256 _l2MessageIndex,
uint16 _l2TxNumberInBatch,
bytes calldata _message,
bytes32[] calldata _merkleProof
) external {
(address l1Receiver, address l1Token, uint256 amount) = _parseL2WithdrawalMessage(_message);

if (l1Token == address(1)) {
bool callSuccess;
// Low-level assembly call, to avoid any memory copying (save gas)
assembly {
callSuccess := call(gas(), l1Receiver, amount, 0, 0, 0, 0)
}
require(callSuccess, "ShB: withdraw failed");
} else {
// Withdraw funds
IERC20(l1Token).safeTransfer(l1Receiver, amount);
}
}

function bridgehubDepositBaseToken(
uint256 _chainId,
address _prevMsgSender,
address _l1Token,
uint256 _amount
) external payable {
) external payable whenNotPaused {
if (_l1Token == address(1)) {
require(msg.value == _amount, "L1SharedBridge: msg.value not equal to amount");
} else {
Expand Down
8 changes: 6 additions & 2 deletions l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -244,19 +244,20 @@ library Utils {
}

function getMailboxSelectors() public pure returns (bytes4[] memory) {
bytes4[] memory selectors = new bytes4[](7);
bytes4[] memory selectors = new bytes4[](8);
selectors[0] = MailboxFacet.proveL2MessageInclusion.selector;
selectors[1] = MailboxFacet.proveL2LogInclusion.selector;
selectors[2] = MailboxFacet.proveL1ToL2TransactionStatus.selector;
selectors[3] = MailboxFacet.finalizeEthWithdrawal.selector;
selectors[4] = MailboxFacet.requestL2Transaction.selector;
selectors[5] = MailboxFacet.bridgehubRequestL2Transaction.selector;
selectors[6] = MailboxFacet.l2TransactionBaseCost.selector;
selectors[7] = MailboxFacet.transferEthToSharedBridge.selector;
return selectors;
}

function getUtilsFacetSelectors() public pure returns (bytes4[] memory) {
bytes4[] memory selectors = new bytes4[](38);
bytes4[] memory selectors = new bytes4[](41);
selectors[0] = UtilsFacet.util_setChainId.selector;
selectors[1] = UtilsFacet.util_getChainId.selector;
selectors[2] = UtilsFacet.util_setBridgehub.selector;
Expand Down Expand Up @@ -295,6 +296,9 @@ library Utils {
selectors[35] = UtilsFacet.util_getIsFrozen.selector;
selectors[36] = UtilsFacet.util_setTransactionFilterer.selector;
selectors[37] = UtilsFacet.util_setBaseTokenGasPriceMultiplierDenominator.selector;
selectors[38] = UtilsFacet.util_setTotalBatchesExecuted.selector;
selectors[39] = UtilsFacet.util_setL2LogsRootHash.selector;
selectors[40] = UtilsFacet.util_setBaseTokenGasPriceMultiplierNominator.selector;
return selectors;
}

Expand Down
12 changes: 12 additions & 0 deletions l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ contract UtilsFacet is ZkSyncHyperchainBase {
return s.isFrozen;
}

function util_setTotalBatchesExecuted(uint256 _numberOfBatches) external {
s.totalBatchesExecuted = _numberOfBatches;
}

function util_setL2LogsRootHash(uint256 _batchNumber, bytes32 _newHash) external {
s.l2LogsRootHashes[_batchNumber] = _newHash;
}

function util_setBaseTokenGasPriceMultiplierNominator(uint128 _nominator) external {
s.baseTokenGasPriceMultiplierNominator = _nominator;
}

// add this to be excluded from coverage report
function test() internal virtual {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {MailboxTest} from "./_Mailbox_Shared.t.sol";
import {FeeParams, PubdataPricingMode} from "contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol";
import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA} from "contracts/common/Config.sol";
import {DummyHyperchain} from "contracts/dev-contracts/test/DummyHyperchain.sol";

contract MailboxBaseTests is MailboxTest {
function setUp() public virtual {
setupDiamondProxy();
utilsFacet.util_setBaseTokenGasPriceMultiplierDenominator(1);
utilsFacet.util_setBaseTokenGasPriceMultiplierNominator(1);
}

function test_mailboxConstructor() public {
DummyHyperchain h = new DummyHyperchain(address(0), eraChainId);
assertEq(h.getEraChainId(), eraChainId);
}

function test_RevertWhen_badDenominatorInL2TransactionBaseCost() public {
utilsFacet.util_setBaseTokenGasPriceMultiplierDenominator(0);
vm.expectRevert("Mailbox: baseTokenGasPriceDenominator not set");
mailboxFacet.l2TransactionBaseCost(100, 10000, REQUIRED_L2_GAS_PRICE_PER_PUBDATA);
}

function test_successful_getL2TransactionBaseCostPricingModeValidium() public {
uint256 gasPrice = 10000000;
uint256 l2GasLimit = 1000000;
uint256 l2GasPerPubdataByteLimit = REQUIRED_L2_GAS_PRICE_PER_PUBDATA;

FeeParams memory feeParams = FeeParams({
pubdataPricingMode: PubdataPricingMode.Validium,
batchOverheadL1Gas: 1000000,
maxPubdataPerBatch: 120000,
maxL2GasPerBatch: 80000000,
priorityTxMaxPubdata: 99000,
minimalL2GasPrice: 250000000
});

utilsFacet.util_setFeeParams(feeParams);

// this was get from running the function, but more reasonable would be to
// have some invariants that the calculation should keep for min required gas
// price and also gas limit
uint256 l2TransactionBaseCost = 250125000000000;

assertEq(
mailboxFacet.l2TransactionBaseCost(gasPrice, l2GasLimit, l2GasPerPubdataByteLimit),
l2TransactionBaseCost
);
}

function test_successful_getL2TransactionBaseCostPricingModeRollup() public {
uint256 gasPrice = 10000000;
uint256 l2GasLimit = 1000000;
uint256 l2GasPerPubdataByteLimit = REQUIRED_L2_GAS_PRICE_PER_PUBDATA;

FeeParams memory feeParams = FeeParams({
pubdataPricingMode: PubdataPricingMode.Rollup,
batchOverheadL1Gas: 1000000,
maxPubdataPerBatch: 120000,
maxL2GasPerBatch: 80000000,
priorityTxMaxPubdata: 99000,
minimalL2GasPrice: 250000000
});

utilsFacet.util_setFeeParams(feeParams);

// this was get from running the function, but more reasonable would be to
// have some invariants that the calculation should keep for min required gas
// price and also gas limit
uint256 l2TransactionBaseCost = 250125000000000;

assertEq(
mailboxFacet.l2TransactionBaseCost(gasPrice, l2GasLimit, l2GasPerPubdataByteLimit),
l2TransactionBaseCost
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ pragma solidity 0.8.24;

import {MailboxTest} from "./_Mailbox_Shared.t.sol";
import {BridgehubL2TransactionRequest} from "contracts/common/Messaging.sol";
import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA} from "contracts/common/Config.sol";
import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA, MAX_NEW_FACTORY_DEPS} from "contracts/common/Config.sol";
import {TransactionFiltererTrue} from "contracts/dev-contracts/test/DummyTransactionFiltererTrue.sol";
import {TransactionFiltererFalse} from "contracts/dev-contracts/test/DummyTransactionFiltererFalse.sol";

contract BridgehubRequestL2TransactionTest is MailboxTest {
function test_successWithoutFilterer() public {
contract MailboxBridgehubRequestL2TransactionTest is MailboxTest {
function setUp() public virtual {
setupDiamondProxy();
}

function test_success_withoutFilterer() public {
address bridgehub = makeAddr("bridgehub");

utilsFacet.util_setBridgehub(bridgehub);
Expand All @@ -24,7 +28,7 @@ contract BridgehubRequestL2TransactionTest is MailboxTest {
assertTrue(canonicalTxHash != bytes32(0), "canonicalTxHash should not be 0");
}

function test_successWithFilterer() public {
function test_success_withFilterer() public {
address bridgehub = makeAddr("bridgehub");
TransactionFiltererTrue tf = new TransactionFiltererTrue();

Expand Down Expand Up @@ -58,6 +62,16 @@ contract BridgehubRequestL2TransactionTest is MailboxTest {
mailboxFacet.bridgehubRequestL2Transaction(req);
}

function test_revertWhen_notBridgehub() public {
address bridgehub = makeAddr("bridgehub");
utilsFacet.util_setBridgehub(bridgehub);
BridgehubL2TransactionRequest memory req = getBridgehubRequestL2TransactionRequest();
vm.deal(bridgehub, 100 ether);
vm.prank(address(sender));
vm.expectRevert("Hyperchain: not bridgehub");
mailboxFacet.bridgehubRequestL2Transaction(req);
}

function getBridgehubRequestL2TransactionRequest() private returns (BridgehubL2TransactionRequest memory req) {
bytes[] memory factoryDeps = new bytes[](1);
factoryDeps[0] = "11111111111111111111111111111111";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {MailboxTest} from "./_Mailbox_Shared.t.sol";
import {DummyBridgehub} from "contracts/dev-contracts/test/DummyBridgehub.sol";
import {L1SharedBridge} from "contracts/bridge/L1SharedBridge.sol";
import {IBridgehub} from "contracts/bridgehub/IBridgehub.sol";
import {IL1SharedBridge} from "contracts/bridge/interfaces/IL1SharedBridge.sol";
import {DummySharedBridge} from "contracts/dev-contracts/test/DummySharedBridge.sol";

contract MailboxFinalizeWithdrawal is MailboxTest {
bytes32[] proof;
bytes message;
DummySharedBridge l1SharedBridge;
address baseTokenBridgeAddress;

function setUp() public virtual {
setupDiamondProxy();

l1SharedBridge = new DummySharedBridge(keccak256("dummyDepositHash"));
baseTokenBridgeAddress = address(l1SharedBridge);

proof = new bytes32[](0);
message = "message";
}

function test_RevertWhen_notEra() public {
utilsFacet.util_setChainId(eraChainId + 1);

vm.expectRevert("Mailbox: finalizeEthWithdrawal only available for Era on mailbox");
mailboxFacet.finalizeEthWithdrawal({
_l2BatchNumber: 0,
_l2MessageIndex: 0,
_l2TxNumberInBatch: 0,
_message: message,
_merkleProof: proof
});
}

function test_success_withdrawal(uint256 amount) public {
address baseTokenBridge = makeAddr("baseTokenBridge");
utilsFacet.util_setChainId(eraChainId);
utilsFacet.util_setBaseTokenBridge(baseTokenBridgeAddress);

address l1Receiver = makeAddr("receiver");
address l1Token = address(1);
vm.deal(baseTokenBridgeAddress, amount);

bytes memory message = abi.encode(l1Receiver, l1Token, amount);

mailboxFacet.finalizeEthWithdrawal({
_l2BatchNumber: 0,
_l2MessageIndex: 0,
_l2TxNumberInBatch: 0,
_message: message,
_merkleProof: proof
});

assertEq(l1Receiver.balance, amount);
assertEq(baseTokenBridgeAddress.balance, 0);
}
}
Loading

0 comments on commit 1c1b93b

Please sign in to comment.