diff --git a/l1-contracts/contracts/dev-contracts/test/DummyTransactionFiltererFalse.sol b/l1-contracts/contracts/dev-contracts/test/DummyTransactionFiltererFalse.sol new file mode 100644 index 000000000..b71d2fd1f --- /dev/null +++ b/l1-contracts/contracts/dev-contracts/test/DummyTransactionFiltererFalse.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {ITransactionFilterer} from "../../state-transition/chain-interfaces/ITransactionFilterer.sol"; + +contract TransactionFiltererFalse is ITransactionFilterer { + // add this to be excluded from coverage report + function test() internal virtual {} + + function isTransactionAllowed( + address, + address, + uint256, + uint256, + bytes memory, + address + ) external view returns (bool) { + return false; + } +} diff --git a/l1-contracts/contracts/dev-contracts/test/DummyTransactionFiltererTrue.sol b/l1-contracts/contracts/dev-contracts/test/DummyTransactionFiltererTrue.sol new file mode 100644 index 000000000..036e5407c --- /dev/null +++ b/l1-contracts/contracts/dev-contracts/test/DummyTransactionFiltererTrue.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {ITransactionFilterer} from "../../state-transition/chain-interfaces/ITransactionFilterer.sol"; + +contract TransactionFiltererTrue is ITransactionFilterer { + // add this to be excluded from coverage report + function test() internal virtual {} + + function isTransactionAllowed( + address, + address, + uint256, + uint256, + bytes memory, + address + ) external view returns (bool) { + return true; + } +} diff --git a/l1-contracts/contracts/state-transition/chain-deps/ZkSyncStateTransitionStorage.sol b/l1-contracts/contracts/state-transition/chain-deps/ZkSyncStateTransitionStorage.sol index 4beec8f61..4c9db3635 100644 --- a/l1-contracts/contracts/state-transition/chain-deps/ZkSyncStateTransitionStorage.sol +++ b/l1-contracts/contracts/state-transition/chain-deps/ZkSyncStateTransitionStorage.sol @@ -150,4 +150,6 @@ struct ZkSyncStateTransitionStorage { /// we multiply by the nominator, and divide by the denominator uint128 baseTokenGasPriceMultiplierNominator; uint128 baseTokenGasPriceMultiplierDenominator; + /// @dev The optional address of the contract that has to be used for transaction filtering/whitelisting + address transactionFilterer; } diff --git a/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol b/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol index 9be5b168c..7b3c52e2a 100644 --- a/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol +++ b/l1-contracts/contracts/state-transition/chain-deps/facets/Admin.sol @@ -92,6 +92,12 @@ contract AdminFacet is ZkSyncStateTransitionBase, IAdmin { emit ValidiumModeStatusUpdate(_validiumMode); } + function setTransactionFilterer(address _transactionFilterer) external onlyAdmin { + address oldTransactionFilterer = s.transactionFilterer; + s.transactionFilterer = _transactionFilterer; + emit NewTransactionFilterer(oldTransactionFilterer, _transactionFilterer); + } + /*////////////////////////////////////////////////////////////// UPGRADE EXECUTION //////////////////////////////////////////////////////////////*/ diff --git a/l1-contracts/contracts/state-transition/chain-deps/facets/Mailbox.sol b/l1-contracts/contracts/state-transition/chain-deps/facets/Mailbox.sol index 813c580ef..527a8f2d2 100644 --- a/l1-contracts/contracts/state-transition/chain-deps/facets/Mailbox.sol +++ b/l1-contracts/contracts/state-transition/chain-deps/facets/Mailbox.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.20; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IMailbox} from "../../chain-interfaces/IMailbox.sol"; +import {ITransactionFilterer} from "../../chain-interfaces/ITransactionFilterer.sol"; import {Merkle} from "../../libraries/Merkle.sol"; import {PriorityQueue, PriorityOperation} from "../../libraries/PriorityQueue.sol"; import {TransactionValidator} from "../../libraries/TransactionValidator.sol"; @@ -227,6 +228,20 @@ contract MailboxFacet is ZkSyncStateTransitionBase, IMailbox { function _requestL2TransactionSender( BridgehubL2TransactionRequest memory _request ) internal nonReentrant returns (bytes32 canonicalTxHash) { + // Check that the transaction is allowed by the filterer (if the filterer is set). + if (s.transactionFilterer != address(0)) { + require( + ITransactionFilterer(s.transactionFilterer).isTransactionAllowed({ + sender: _request.sender, + contractL2: _request.contractL2, + mintValue: _request.mintValue, + l2Value: _request.l2Value, + l2Calldata: _request.l2Calldata, + refundRecipient: _request.refundRecipient + }), + "tf" + ); + } // Change the sender address if it is a smart contract to prevent address collision between L1 and L2. // Please note, currently zkSync address derivation is different from Ethereum one, but it may be changed in the future. address l2Sender = _request.sender; diff --git a/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol b/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol index 0993515eb..ecc3ce655 100644 --- a/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol +++ b/l1-contracts/contracts/state-transition/chain-interfaces/IAdmin.sol @@ -42,6 +42,9 @@ interface IAdmin is IZkSyncStateTransitionBase { /// @notice Used to set to validium directly after genesis function setValidiumMode(PubdataPricingMode _validiumMode) external; + /// @notice Set the transaction filterer + function setTransactionFilterer(address _transactionFilterer) external; + function upgradeChainFromVersion(uint256 _protocolVersion, Diamond.DiamondCutData calldata _cutData) external; /// @notice Executes a proposed governor upgrade @@ -79,6 +82,9 @@ interface IAdmin is IZkSyncStateTransitionBase { /// @notice Validium mode status changed event ValidiumModeStatusUpdate(PubdataPricingMode validiumMode); + /// @notice The transaction filterer has been updated + event NewTransactionFilterer(address oldTransactionFilterer, address newTransactionFilterer); + /// @notice BaseToken multiplier for L1->L2 transactions changed event NewBaseTokenMultiplier( uint128 oldNominator, diff --git a/l1-contracts/contracts/state-transition/chain-interfaces/ITransactionFilterer.sol b/l1-contracts/contracts/state-transition/chain-interfaces/ITransactionFilterer.sol new file mode 100644 index 000000000..00d2b465d --- /dev/null +++ b/l1-contracts/contracts/state-transition/chain-interfaces/ITransactionFilterer.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +/// @title The interface of the L1 -> L2 transaction filterer. +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface ITransactionFilterer { + /// @notice Check if the transaction is allowed + /// @param sender The sender of the transaction + /// @param contractL2 The L2 receiver address + /// @param mintValue The value of the L1 transaction + /// @param l2Value The msg.value of the L2 transaction + /// @param l2Calldata The calldata of the L2 transaction + /// @param refundRecipient The address to refund the excess value + /// @return Whether the transaction is allowed + function isTransactionAllowed( + address sender, + address contractL2, + uint256 mintValue, + uint256 l2Value, + bytes memory l2Calldata, + address refundRecipient + ) external view returns (bool); +} diff --git a/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol b/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol index d90cc3249..d7afe10cb 100644 --- a/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol +++ b/l1-contracts/test/foundry/unit/concrete/Utils/Utils.sol @@ -15,6 +15,7 @@ import {IVerifier, VerifierParams} from "solpp/state-transition/chain-deps/ZkSyn import {FeeParams, PubdataPricingMode} from "solpp/state-transition/chain-deps/ZkSyncStateTransitionStorage.sol"; import {InitializeData, InitializeDataNewChain} from "solpp/state-transition/chain-interfaces/IDiamondInit.sol"; import {IExecutor, SystemLogKey} from "solpp/state-transition/chain-interfaces/IExecutor.sol"; +import {L2CanonicalTransaction} from "solpp/common/Messaging.sol"; bytes32 constant DEFAULT_L2_LOGS_TREE_ROOT_HASH = 0x0000000000000000000000000000000000000000000000000000000000000000; address constant L2_SYSTEM_CONTEXT_ADDRESS = 0x000000000000000000000000000000000000800B; @@ -233,7 +234,7 @@ library Utils { } function getUtilsFacetSelectors() public pure returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](36); + bytes4[] memory selectors = new bytes4[](38); selectors[0] = UtilsFacet.util_setChainId.selector; selectors[1] = UtilsFacet.util_getChainId.selector; selectors[2] = UtilsFacet.util_setBridgehub.selector; @@ -270,6 +271,8 @@ library Utils { selectors[33] = UtilsFacet.util_getProtocolVersion.selector; selectors[34] = UtilsFacet.util_setIsFrozen.selector; selectors[35] = UtilsFacet.util_getIsFrozen.selector; + selectors[36] = UtilsFacet.util_setTransactionFilterer.selector; + selectors[37] = UtilsFacet.util_setBaseTokenGasPriceMultiplierDenominator.selector; return selectors; } @@ -395,6 +398,30 @@ library Utils { return address(diamondProxy); } + function makeEmptyL2CanonicalTransaction() public returns (L2CanonicalTransaction memory) { + uint256[4] memory reserved; + uint256[] memory factoryDeps = new uint256[](1); + return + L2CanonicalTransaction({ + txType: 0, + from: 0, + to: 0, + gasLimit: 0, + gasPerPubdataByteLimit: 0, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + paymaster: 0, + nonce: 0, + value: 0, + reserved: reserved, + data: "", + signature: "", + factoryDeps: factoryDeps, + paymasterInput: "", + reservedDynamic: "" + }); + } + function createBatchCommitment( IExecutor.CommitBatchInfo calldata _newBatchData, bytes32 _stateDiffHash, diff --git a/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol b/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol index abd0ccbc8..ab711fca3 100644 --- a/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol +++ b/l1-contracts/test/foundry/unit/concrete/Utils/UtilsFacet.sol @@ -104,6 +104,14 @@ contract UtilsFacet is ZkSyncStateTransitionBase { return s.validators[_validator]; } + function util_setTransactionFilterer(address _filterer) external { + s.transactionFilterer = _filterer; + } + + function util_setBaseTokenGasPriceMultiplierDenominator(uint128 _denominator) external { + s.baseTokenGasPriceMultiplierDenominator = _denominator; + } + function util_setZkPorterAvailability(bool _available) external { s.zkPorterIsAvailable = _available; } diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/SetTransactionFilterer.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/SetTransactionFilterer.t.sol new file mode 100644 index 000000000..b9615e4a4 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/SetTransactionFilterer.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {AdminTest} from "./_Admin_Shared.t.sol"; + +contract SetTransactionFiltererTest is AdminTest { + event NewTransactionFilterer(address oldTransactionFilterer, address newTransactionFilterer); + + function test_initialFilterer() public { + address admin = utilsFacet.util_getAdmin(); + address transactionFilterer = makeAddr("transactionFilterer"); + + vm.expectEmit(true, true, true, true, address(adminFacet)); + emit NewTransactionFilterer(address(0), transactionFilterer); + + vm.startPrank(admin); + adminFacet.setTransactionFilterer(transactionFilterer); + } + + function test_replaceFilterer() public { + address admin = utilsFacet.util_getAdmin(); + address f1 = makeAddr("f1"); + address f2 = makeAddr("f2"); + utilsFacet.util_setTransactionFilterer(f1); + + vm.expectEmit(true, true, true, true, address(adminFacet)); + emit NewTransactionFilterer(f1, f2); + + vm.startPrank(admin); + adminFacet.setTransactionFilterer(f2); + } + + function test_revertWhen_notAdmin() public { + address transactionFilterer = makeAddr("transactionFilterer"); + + vm.expectRevert("StateTransition Chain: not admin"); + vm.startPrank(makeAddr("nonAdmin")); + adminFacet.setTransactionFilterer(transactionFilterer); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol index fae0c0439..c3f1f7fc3 100644 --- a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol +++ b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Admin/_Admin_Shared.t.sol @@ -15,7 +15,7 @@ contract AdminTest is Test { UtilsFacet internal utilsFacet; function getAdminSelectors() public pure returns (bytes4[] memory) { - bytes4[] memory selectors = new bytes4[](11); + bytes4[] memory selectors = new bytes4[](12); selectors[0] = IAdmin.setPendingAdmin.selector; selectors[1] = IAdmin.acceptAdmin.selector; selectors[2] = IAdmin.setValidator.selector; @@ -27,6 +27,7 @@ contract AdminTest is Test { selectors[8] = IAdmin.executeUpgrade.selector; selectors[9] = IAdmin.freezeDiamond.selector; selectors[10] = IAdmin.unfreezeDiamond.selector; + selectors[11] = IAdmin.setTransactionFilterer.selector; return selectors; } diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Mailbox/BridgehubRequestL2Transaction.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Mailbox/BridgehubRequestL2Transaction.t.sol new file mode 100644 index 000000000..5672799e4 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Mailbox/BridgehubRequestL2Transaction.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {MailboxTest} from "./_Mailbox_Shared.t.sol"; +import {BridgehubL2TransactionRequest} from "solpp/common/Messaging.sol"; +import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA} from "solpp/common/Config.sol"; +import {TransactionFiltererTrue} from "solpp/dev-contracts/test/DummyTransactionFiltererTrue.sol"; +import {TransactionFiltererFalse} from "solpp/dev-contracts/test/DummyTransactionFiltererFalse.sol"; + +contract BridgehubRequestL2TransactionTest is MailboxTest { + function test_successWithoutFilterer() public { + address bridgehub = makeAddr("bridgehub"); + + utilsFacet.util_setBridgehub(bridgehub); + utilsFacet.util_setBaseTokenGasPriceMultiplierDenominator(1); + utilsFacet.util_setPriorityTxMaxGasLimit(100000000); + + BridgehubL2TransactionRequest memory req = getBridgehubRequestL2TransactionRequest(); + + vm.deal(bridgehub, 100 ether); + vm.prank(address(bridgehub)); + bytes32 canonicalTxHash = mailboxFacet.bridgehubRequestL2Transaction{value: 10 ether}(req); + assertTrue(canonicalTxHash != bytes32(0), "canonicalTxHash should not be 0"); + } + + function test_successWithFilterer() public { + address bridgehub = makeAddr("bridgehub"); + TransactionFiltererTrue tf = new TransactionFiltererTrue(); + + utilsFacet.util_setBridgehub(bridgehub); + utilsFacet.util_setTransactionFilterer(address(tf)); + utilsFacet.util_setBaseTokenGasPriceMultiplierDenominator(1); + utilsFacet.util_setPriorityTxMaxGasLimit(100000000); + + BridgehubL2TransactionRequest memory req = getBridgehubRequestL2TransactionRequest(); + + vm.deal(bridgehub, 100 ether); + vm.prank(address(bridgehub)); + bytes32 canonicalTxHash = mailboxFacet.bridgehubRequestL2Transaction{value: 10 ether}(req); + assertTrue(canonicalTxHash != bytes32(0), "canonicalTxHash should not be 0"); + } + + function test_revertWhen_FalseFilterer() public { + address bridgehub = makeAddr("bridgehub"); + TransactionFiltererFalse tf = new TransactionFiltererFalse(); + + utilsFacet.util_setBridgehub(bridgehub); + utilsFacet.util_setTransactionFilterer(address(tf)); + utilsFacet.util_setBaseTokenGasPriceMultiplierDenominator(1); + utilsFacet.util_setPriorityTxMaxGasLimit(100000000); + + BridgehubL2TransactionRequest memory req = getBridgehubRequestL2TransactionRequest(); + + vm.deal(bridgehub, 100 ether); + vm.prank(address(bridgehub)); + vm.expectRevert(bytes("tf")); + mailboxFacet.bridgehubRequestL2Transaction{value: 10 ether}(req); + } + + function getBridgehubRequestL2TransactionRequest() private returns (BridgehubL2TransactionRequest memory req) { + bytes[] memory factoryDeps = new bytes[](1); + factoryDeps[0] = "11111111111111111111111111111111"; + + req = BridgehubL2TransactionRequest({ + sender: sender, + contractL2: makeAddr("contractL2"), + mintValue: 2 ether, + l2Value: 10000, + l2Calldata: "", + l2GasLimit: 10000000, + l2GasPerPubdataByteLimit: REQUIRED_L2_GAS_PRICE_PER_PUBDATA, + factoryDeps: factoryDeps, + refundRecipient: sender + }); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Mailbox/_Mailbox_Shared.t.sol b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Mailbox/_Mailbox_Shared.t.sol new file mode 100644 index 000000000..521a2f321 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/state-transition/chain-deps/facets/Mailbox/_Mailbox_Shared.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {Utils} from "foundry-test/unit/concrete/Utils/Utils.sol"; +import {UtilsFacet} from "foundry-test/unit/concrete/Utils/UtilsFacet.sol"; + +import {MailboxFacet} from "solpp/state-transition/chain-deps/facets/Mailbox.sol"; +import {Diamond} from "solpp/state-transition/libraries/Diamond.sol"; +import {IMailbox} from "solpp/state-transition/chain-interfaces/IMailbox.sol"; + +contract MailboxTest is Test { + IMailbox internal mailboxFacet; + UtilsFacet internal utilsFacet; + address sender; + + function getMailboxSelectors() public pure returns (bytes4[] memory) { + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = IMailbox.bridgehubRequestL2Transaction.selector; + return selectors; + } + + function setUp() public virtual { + sender = makeAddr("sender"); + vm.deal(sender, 100 ether); + + Diamond.FacetCut[] memory facetCuts = new Diamond.FacetCut[](2); + facetCuts[0] = Diamond.FacetCut({ + facet: address(new MailboxFacet()), + action: Diamond.Action.Add, + isFreezable: true, + selectors: getMailboxSelectors() + }); + facetCuts[1] = Diamond.FacetCut({ + facet: address(new UtilsFacet()), + action: Diamond.Action.Add, + isFreezable: true, + selectors: Utils.getUtilsFacetSelectors() + }); + + address diamondProxy = Utils.makeDiamondProxy(facetCuts); + mailboxFacet = IMailbox(diamondProxy); + utilsFacet = UtilsFacet(diamondProxy); + } + + // add this to be excluded from coverage report + function test() internal virtual {} +}