diff --git a/l1-contracts/src/core/FeeJuicePortal.sol b/l1-contracts/src/core/FeeJuicePortal.sol new file mode 100644 index 00000000000..5b899bd577f --- /dev/null +++ b/l1-contracts/src/core/FeeJuicePortal.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; + +// Messaging +import {IRegistry} from "./interfaces/messagebridge/IRegistry.sol"; +import {IInbox} from "./interfaces/messagebridge/IInbox.sol"; +import {IFeeJuicePortal} from "./interfaces/IFeeJuicePortal.sol"; +import {DataStructures} from "./libraries/DataStructures.sol"; +import {Errors} from "./libraries/Errors.sol"; +import {Constants} from "./libraries/ConstantsGen.sol"; +import {Hash} from "./libraries/Hash.sol"; + +contract FeeJuicePortal is IFeeJuicePortal, Ownable { + using SafeERC20 for IERC20; + + IRegistry public registry; + IERC20 public underlying; + bytes32 public l2TokenAddress; + + constructor() Ownable(msg.sender) {} + + /** + * @notice Initialize the FeeJuicePortal + * + * @dev This function is only callable by the owner of the contract and only once + * + * @dev Must be funded with FEE_JUICE_INITIAL_MINT tokens before initialization to + * ensure that the L2 contract is funded and able to pay for its deployment. + * + * @param _registry - The address of the registry contract + * @param _underlying - The address of the underlying token + * @param _l2TokenAddress - The address of the L2 token + */ + function initialize(address _registry, address _underlying, bytes32 _l2TokenAddress) + external + override(IFeeJuicePortal) + onlyOwner + { + if (address(registry) != address(0) || address(underlying) != address(0) || l2TokenAddress != 0) + { + revert Errors.FeeJuicePortal__AlreadyInitialized(); + } + if (_registry == address(0) || _underlying == address(0) || _l2TokenAddress == 0) { + revert Errors.FeeJuicePortal__InvalidInitialization(); + } + + registry = IRegistry(_registry); + underlying = IERC20(_underlying); + l2TokenAddress = _l2TokenAddress; + uint256 balance = underlying.balanceOf(address(this)); + if (balance < Constants.FEE_JUICE_INITIAL_MINT) { + underlying.safeTransferFrom( + msg.sender, address(this), Constants.FEE_JUICE_INITIAL_MINT - balance + ); + } + _transferOwnership(address(0)); + } + + /** + * @notice Deposit funds into the portal and adds an L2 message which can only be consumed publicly on Aztec + * @param _to - The aztec address of the recipient + * @param _amount - The amount to deposit + * @param _secretHash - The hash of the secret consumable message. The hash should be 254 bits (so it can fit in a Field element) + * @return - The key of the entry in the Inbox + */ + function depositToAztecPublic(bytes32 _to, uint256 _amount, bytes32 _secretHash) + external + override(IFeeJuicePortal) + returns (bytes32) + { + // Preamble + IInbox inbox = registry.getRollup().INBOX(); + DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2TokenAddress, 1); + + // Hash the message content to be reconstructed in the receiving contract + bytes32 contentHash = + Hash.sha256ToField(abi.encodeWithSignature("mint_public(bytes32,uint256)", _to, _amount)); + + // Hold the tokens in the portal + underlying.safeTransferFrom(msg.sender, address(this), _amount); + + // Send message to rollup + return inbox.sendL2Message(actor, contentHash, _secretHash); + } + + /** + * @notice Let the rollup distribute fees to an account + * + * Since the assets cannot be exited the usual way, but only paid as fees to sequencers + * we include this function to allow the rollup to do just that, bypassing the usual + * flows. + * + * @param _to - The address to receive the payment + * @param _amount - The amount to pay them + */ + function distributeFees(address _to, uint256 _amount) external override(IFeeJuicePortal) { + if (msg.sender != address(registry.getRollup())) { + revert Errors.FeeJuicePortal__Unauthorized(); + } + underlying.safeTransfer(_to, _amount); + } +} diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 3f53f193acf..3b4e2b65f66 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -9,7 +9,7 @@ import {IInbox} from "./interfaces/messagebridge/IInbox.sol"; import {IOutbox} from "./interfaces/messagebridge/IOutbox.sol"; import {IRegistry} from "./interfaces/messagebridge/IRegistry.sol"; import {IVerifier} from "./interfaces/IVerifier.sol"; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {IFeeJuicePortal} from "./interfaces/IFeeJuicePortal.sol"; // Libraries import {HeaderLib} from "./libraries/HeaderLib.sol"; @@ -47,7 +47,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { IInbox public immutable INBOX; IOutbox public immutable OUTBOX; uint256 public immutable VERSION; - IERC20 public immutable FEE_JUICE; + IFeeJuicePortal public immutable FEE_JUICE_PORTAL; IVerifier public verifier; @@ -74,14 +74,14 @@ contract Rollup is Leonidas, IRollup, ITestRollup { constructor( IRegistry _registry, IAvailabilityOracle _availabilityOracle, - IERC20 _fpcJuice, + IFeeJuicePortal _fpcJuicePortal, bytes32 _vkTreeRoot, address _ares ) Leonidas(_ares) { verifier = new MockVerifier(); REGISTRY = _registry; AVAILABILITY_ORACLE = _availabilityOracle; - FEE_JUICE = _fpcJuice; + FEE_JUICE_PORTAL = _fpcJuicePortal; INBOX = new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT); OUTBOX = new Outbox(address(this)); vkTreeRoot = _vkTreeRoot; @@ -361,10 +361,13 @@ contract Rollup is Leonidas, IRollup, ITestRollup { header.globalVariables.blockNumber, header.contentCommitment.outHash, l2ToL1TreeMinHeight ); - // @todo This should be address at time of proving. Also, this contract should NOT have funds!!! - // pay the coinbase 1 Fee Juice if it is not empty and header.totalFees is not zero + // @note This should be addressed at the time of proving if sequential proving or at the time of + // inclusion into the proven chain otherwise. See #7622. if (header.globalVariables.coinbase != address(0) && header.totalFees > 0) { - FEE_JUICE.transfer(address(header.globalVariables.coinbase), header.totalFees); + // @note This will currently fail if there are insufficient funds in the bridge + // which WILL happen for the old version after an upgrade where the bridge follow. + // Consider allowing a failure. See #7938. + FEE_JUICE_PORTAL.distributeFees(header.globalVariables.coinbase, header.totalFees); } emit L2BlockProcessed(header.globalVariables.blockNumber); diff --git a/l1-contracts/src/core/interfaces/IFeeJuicePortal.sol b/l1-contracts/src/core/interfaces/IFeeJuicePortal.sol new file mode 100644 index 00000000000..19dd38a059e --- /dev/null +++ b/l1-contracts/src/core/interfaces/IFeeJuicePortal.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Aztec Labs. +pragma solidity >=0.8.18; + +interface IFeeJuicePortal { + function initialize(address _registry, address _underlying, bytes32 _l2TokenAddress) external; + function distributeFees(address _to, uint256 _amount) external; + function depositToAztecPublic(bytes32 _to, uint256 _amount, bytes32 _secretHash) + external + returns (bytes32); +} diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 1983d5e2d22..4ef594869b0 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -101,6 +101,7 @@ library Constants { uint256 internal constant BLOB_SIZE_IN_BYTES = 126976; uint256 internal constant ETHEREUM_SLOT_DURATION = 12; uint256 internal constant IS_DEV_NET = 1; + uint256 internal constant FEE_JUICE_INITIAL_MINT = 20000000000; uint256 internal constant MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 20000; uint256 internal constant MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS = 3000; uint256 internal constant MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS = 3000; diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 78505c56d46..19be2872e3f 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -84,4 +84,9 @@ library Errors { error Leonidas__InvalidProposer(address expected, address actual); // 0xd02d278e error Leonidas__InsufficientAttestations(uint256 minimumNeeded, uint256 provided); // 0xbf1ca4cb error Leonidas__InsufficientAttestationsProvided(uint256 minimumNeeded, uint256 provided); // 0x2e7debe9 + + // Fee Juice Portal + error FeeJuicePortal__AlreadyInitialized(); // 0xc7a172fe + error FeeJuicePortal__InvalidInitialization(); // 0xfd9b3208 + error FeeJuicePortal__Unauthorized(); // 0x67e3691e } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 2d3d349b357..c47e631adf4 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -2,8 +2,6 @@ // Copyright 2023 Aztec Labs. pragma solidity >=0.8.18; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - import {DecoderBase} from "./decoders/Base.sol"; import {DataStructures} from "../src/core/libraries/DataStructures.sol"; @@ -14,6 +12,8 @@ import {Inbox} from "../src/core/messagebridge/Inbox.sol"; import {Outbox} from "../src/core/messagebridge/Outbox.sol"; import {Errors} from "../src/core/libraries/Errors.sol"; import {Rollup} from "../src/core/Rollup.sol"; +import {IFeeJuicePortal} from "../src/core/interfaces/IFeeJuicePortal.sol"; +import {FeeJuicePortal} from "../src/core/FeeJuicePortal.sol"; import {Leonidas} from "../src/core/sequencer_selection/Leonidas.sol"; import {AvailabilityOracle} from "../src/core/availability_oracle/AvailabilityOracle.sol"; import {FrontierMerkle} from "../src/core/messagebridge/frontier_tree/Frontier.sol"; @@ -22,6 +22,7 @@ import {MerkleTestUtil} from "./merkle/TestUtil.sol"; import {PortalERC20} from "./portals/PortalERC20.sol"; import {TxsDecoderHelper} from "./decoders/helpers/TxsDecoderHelper.sol"; +import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol"; /** * Blocks are generated using the `integration_l1_publisher.test.ts` tests. @@ -35,6 +36,7 @@ contract RollupTest is DecoderBase { MerkleTestUtil internal merkleTestUtil; TxsDecoderHelper internal txsHelper; PortalERC20 internal portalERC20; + FeeJuicePortal internal feeJuicePortal; AvailabilityOracle internal availabilityOracle; @@ -54,17 +56,23 @@ contract RollupTest is DecoderBase { registry = new Registry(address(this)); availabilityOracle = new AvailabilityOracle(); portalERC20 = new PortalERC20(); + feeJuicePortal = new FeeJuicePortal(); + portalERC20.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT); + feeJuicePortal.initialize( + address(registry), address(portalERC20), bytes32(Constants.FEE_JUICE_ADDRESS) + ); rollup = new Rollup( - registry, availabilityOracle, IERC20(address(portalERC20)), bytes32(0), address(this) + registry, + availabilityOracle, + IFeeJuicePortal(address(feeJuicePortal)), + bytes32(0), + address(this) ); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); registry.upgrade(address(rollup)); - // mint some tokens to the rollup - portalERC20.mint(address(rollup), 1000000); - merkleTestUtil = new MerkleTestUtil(); txsHelper = new TxsDecoderHelper(); _; @@ -143,6 +151,43 @@ contract RollupTest is DecoderBase { assertNotEq(minHeightEmpty, minHeightMixed, "Invalid min height"); } + function testBlockFee() public setUpFor("mixed_block_1") { + uint256 feeAmount = 2e18; + + DecoderBase.Data memory data = load("mixed_block_1").block; + bytes memory header = data.header; + bytes32 archive = data.archive; + bytes memory body = data.body; + + assembly { + mstore(add(header, add(0x20, 0x0248)), feeAmount) + } + availabilityOracle.publish(body); + + assertEq(portalERC20.balanceOf(address(rollup)), 0, "invalid rollup balance"); + + uint256 portalBalance = portalERC20.balanceOf(address(feeJuicePortal)); + + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, + address(feeJuicePortal), + portalBalance, + feeAmount + ) + ); + rollup.process(header, archive); + + address coinbase = data.decodedHeader.globalVariables.coinbase; + uint256 coinbaseBalance = portalERC20.balanceOf(coinbase); + assertEq(coinbaseBalance, 0, "invalid initial coinbase balance"); + + portalERC20.mint(address(feeJuicePortal), feeAmount - portalBalance); + + rollup.process(header, archive); + assertEq(portalERC20.balanceOf(coinbase), feeAmount, "invalid coinbase balance"); + } + function testMixedBlock(bool _toProve) public setUpFor("mixed_block_1") { _testBlock("mixed_block_1", _toProve); diff --git a/l1-contracts/test/portals/FeeJuicePortal.sol b/l1-contracts/test/portals/FeeJuicePortal.sol deleted file mode 100644 index 75518c743c8..00000000000 --- a/l1-contracts/test/portals/FeeJuicePortal.sol +++ /dev/null @@ -1,55 +0,0 @@ -pragma solidity >=0.8.18; - -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; - -// Messaging -import {IRegistry} from "../../src/core/interfaces/messagebridge/IRegistry.sol"; -import {IInbox} from "../../src/core/interfaces/messagebridge/IInbox.sol"; -import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; -// docs:start:content_hash_sol_import -import {Hash} from "../../src/core/libraries/Hash.sol"; -// docs:end:content_hash_sol_import - -// docs:start:init -contract FeeJuicePortal { - using SafeERC20 for IERC20; - - IRegistry public registry; - IERC20 public underlying; - bytes32 public l2TokenAddress; - - function initialize(address _registry, address _underlying, bytes32 _l2TokenAddress) external { - registry = IRegistry(_registry); - underlying = IERC20(_underlying); - l2TokenAddress = _l2TokenAddress; - } - // docs:end:init - - // docs:start:deposit_public - /** - * @notice Deposit funds into the portal and adds an L2 message which can only be consumed publicly on Aztec - * @param _to - The aztec address of the recipient - * @param _amount - The amount to deposit - * @param _secretHash - The hash of the secret consumable message. The hash should be 254 bits (so it can fit in a Field element) - * @return - The key of the entry in the Inbox - */ - function depositToAztecPublic(bytes32 _to, uint256 _amount, bytes32 _secretHash) - external - returns (bytes32) - { - // Preamble - IInbox inbox = registry.getRollup().INBOX(); - DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2TokenAddress, 1); - - // Hash the message content to be reconstructed in the receiving contract - bytes32 contentHash = - Hash.sha256ToField(abi.encodeWithSignature("mint_public(bytes32,uint256)", _to, _amount)); - - // Hold the tokens in the portal - underlying.safeTransferFrom(msg.sender, address(this), _amount); - - // Send message to rollup - return inbox.sendL2Message(actor, contentHash, _secretHash); - } -} diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 476b72701ad..411579d3e91 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -14,7 +14,7 @@ import {Errors} from "../../src/core/libraries/Errors.sol"; // Interfaces import {IInbox} from "../../src/core/interfaces/messagebridge/IInbox.sol"; import {IOutbox} from "../../src/core/interfaces/messagebridge/IOutbox.sol"; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {IFeeJuicePortal} from "../../src/core/interfaces/IFeeJuicePortal.sol"; // Portal tokens import {TokenPortal} from "./TokenPortal.sol"; @@ -25,10 +25,10 @@ import {NaiveMerkle} from "../merkle/Naive.sol"; contract TokenPortalTest is Test { using Hash for DataStructures.L1ToL2Msg; - uint256 internal constant FIRST_REAL_TREE_NUM = Constants.INITIAL_L2_BLOCK_NUM + 1; - event MessageConsumed(bytes32 indexed messageHash, address indexed recipient); + uint256 internal constant FIRST_REAL_TREE_NUM = Constants.INITIAL_L2_BLOCK_NUM + 1; + Registry internal registry; IInbox internal inbox; @@ -62,16 +62,14 @@ contract TokenPortalTest is Test { registry = new Registry(address(this)); portalERC20 = new PortalERC20(); rollup = new Rollup( - registry, new AvailabilityOracle(), IERC20(address(portalERC20)), bytes32(0), address(this) + registry, new AvailabilityOracle(), IFeeJuicePortal(address(0)), bytes32(0), address(this) ); inbox = rollup.INBOX(); outbox = rollup.OUTBOX(); registry.upgrade(address(rollup)); - portalERC20.mint(address(rollup), 1000000); tokenPortal = new TokenPortal(); - tokenPortal.initialize(address(registry), address(portalERC20), l2TokenAddress); // Modify the proven block count diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index 104b7664530..be0144dd0ae 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -15,14 +15,12 @@ import {Errors} from "../../src/core/libraries/Errors.sol"; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; import {IOutbox} from "../../src/core/interfaces/messagebridge/IOutbox.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; +import {IFeeJuicePortal} from "../../src/core/interfaces/IFeeJuicePortal.sol"; // Portals import {TokenPortal} from "./TokenPortal.sol"; import {UniswapPortal} from "./UniswapPortal.sol"; -// Portal tokens -import {PortalERC20} from "./PortalERC20.sol"; - contract UniswapPortalTest is Test { using Hash for DataStructures.L2ToL1Msg; @@ -55,12 +53,10 @@ contract UniswapPortalTest is Test { vm.selectFork(forkId); registry = new Registry(address(this)); - PortalERC20 portalERC20 = new PortalERC20(); rollup = new Rollup( - registry, new AvailabilityOracle(), IERC20(address(portalERC20)), bytes32(0), address(this) + registry, new AvailabilityOracle(), IFeeJuicePortal(address(0)), bytes32(0), address(this) ); registry.upgrade(address(rollup)); - portalERC20.mint(address(rollup), 1000000); daiTokenPortal = new TokenPortal(); daiTokenPortal.initialize(address(registry), address(DAI), l2TokenAddress); diff --git a/l1-contracts/test/sparta/DevNet.t.sol b/l1-contracts/test/sparta/DevNet.t.sol index cb591d3be32..3e0ec8e7679 100644 --- a/l1-contracts/test/sparta/DevNet.t.sol +++ b/l1-contracts/test/sparta/DevNet.t.sol @@ -2,8 +2,6 @@ // Copyright 2023 Aztec Labs. pragma solidity >=0.8.18; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - import {DecoderBase} from "../decoders/Base.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; @@ -19,8 +17,8 @@ import {Leonidas} from "../../src/core/sequencer_selection/Leonidas.sol"; import {AvailabilityOracle} from "../../src/core/availability_oracle/AvailabilityOracle.sol"; import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; -import {PortalERC20} from "../portals/PortalERC20.sol"; import {TxsDecoderHelper} from "../decoders/helpers/TxsDecoderHelper.sol"; +import {IFeeJuicePortal} from "../../src/core/interfaces/IFeeJuicePortal.sol"; /** * We are using the same blocks as from Rollup.t.sol. @@ -35,7 +33,6 @@ contract DevNetTest is DecoderBase { Rollup internal rollup; MerkleTestUtil internal merkleTestUtil; TxsDecoderHelper internal txsHelper; - PortalERC20 internal portalERC20; AvailabilityOracle internal availabilityOracle; @@ -59,18 +56,14 @@ contract DevNetTest is DecoderBase { registry = new Registry(address(this)); availabilityOracle = new AvailabilityOracle(); - portalERC20 = new PortalERC20(); rollup = new Rollup( - registry, availabilityOracle, IERC20(address(portalERC20)), bytes32(0), address(this) + registry, availabilityOracle, IFeeJuicePortal(address(0)), bytes32(0), address(this) ); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); registry.upgrade(address(rollup)); - // mint some tokens to the rollup - portalERC20.mint(address(rollup), 1000000); - merkleTestUtil = new MerkleTestUtil(); txsHelper = new TxsDecoderHelper(); diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index eb8638e070a..01b36aa665d 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -2,8 +2,6 @@ // Copyright 2023 Aztec Labs. pragma solidity >=0.8.18; -import {IERC20} from "@oz/token/ERC20/IERC20.sol"; - import {DecoderBase} from "../decoders/Base.sol"; import {DataStructures} from "../../src/core/libraries/DataStructures.sol"; @@ -22,13 +20,14 @@ import {NaiveMerkle} from "../merkle/Naive.sol"; import {MerkleTestUtil} from "../merkle/TestUtil.sol"; import {PortalERC20} from "../portals/PortalERC20.sol"; import {TxsDecoderHelper} from "../decoders/helpers/TxsDecoderHelper.sol"; - +import {IFeeJuicePortal} from "../../src/core/interfaces/IFeeJuicePortal.sol"; /** * We are using the same blocks as from Rollup.t.sol. * The tests in this file is testing the sequencer selection * * We will skip these test if we are running with IS_DEV_NET = true */ + contract SpartaTest is DecoderBase { using MessageHashUtils for bytes32; @@ -64,16 +63,13 @@ contract SpartaTest is DecoderBase { availabilityOracle = new AvailabilityOracle(); portalERC20 = new PortalERC20(); rollup = new Rollup( - registry, availabilityOracle, IERC20(address(portalERC20)), bytes32(0), address(this) + registry, availabilityOracle, IFeeJuicePortal(address(0)), bytes32(0), address(this) ); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); registry.upgrade(address(rollup)); - // mint some tokens to the rollup - portalERC20.mint(address(rollup), 1000000); - merkleTestUtil = new MerkleTestUtil(); txsHelper = new TxsDecoderHelper(); diff --git a/noir-projects/noir-contracts/contracts/fee_juice_contract/src/main.nr b/noir-projects/noir-contracts/contracts/fee_juice_contract/src/main.nr index 632516b985e..6a7cf65b303 100644 --- a/noir-projects/noir-contracts/contracts/fee_juice_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/fee_juice_contract/src/main.nr @@ -5,7 +5,7 @@ contract FeeJuice { protocol_types::{ contract_class_id::ContractClassId, abis::function_selector::FunctionSelector, address::{AztecAddress, EthAddress}, - constants::{DEPLOYER_CONTRACT_ADDRESS, REGISTERER_CONTRACT_ADDRESS} + constants::{DEPLOYER_CONTRACT_ADDRESS, REGISTERER_CONTRACT_ADDRESS, FEE_JUICE_INITIAL_MINT} }, state_vars::{SharedImmutable, PublicMutable, Map}, oracle::get_contract_instance::get_contract_instance, deploy::deploy_contract @@ -46,8 +46,7 @@ contract FeeJuice { ); // Increase self balance and set as fee payer, and end setup - let deploy_fees = 20000000000; - FeeJuice::at(self)._increase_public_balance(self, deploy_fees).enqueue(&mut context); + FeeJuice::at(self)._increase_public_balance(self, FEE_JUICE_INITIAL_MINT).enqueue(&mut context); context.set_as_fee_payer(); context.end_setup(); diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index f6f94f77777..14dc49d4220 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -133,6 +133,9 @@ global INITIAL_L2_BLOCK_NUM: Field = 1; global BLOB_SIZE_IN_BYTES: Field = 31 * 4096; global ETHEREUM_SLOT_DURATION: u32 = 12; global IS_DEV_NET: bool = true; +// The following and the value in `deploy_l1_contracts´ must match. We should not have the code both places, but +// we are running into circular dependency issues. #3342 +global FEE_JUICE_INITIAL_MINT: Field = 20000000000; // CONTRACT CLASS CONSTANTS global MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS: u32 = 20000; diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index 5f2c6dbc988..2348c3ec7ac 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -87,6 +87,7 @@ export const INITIAL_L2_BLOCK_NUM = 1; export const BLOB_SIZE_IN_BYTES = 126976; export const ETHEREUM_SLOT_DURATION = 12; export const IS_DEV_NET = 1; +export const FEE_JUICE_INITIAL_MINT = 20000000000; export const MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS = 20000; export const MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS = 3000; export const MAX_PACKED_BYTECODE_SIZE_PER_UNCONSTRAINED_FUNCTION_IN_FIELDS = 3000; diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index 3288cef8284..45d5b34bde2 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -85,7 +85,7 @@ describe('e2e_fees account_init', () => { describe('account pays its own fee', () => { it('pays natively in the Fee Juice after Alice bridges funds', async () => { - await t.feeJuiceContract.methods.mint_public(bobsAddress, t.INITIAL_GAS_BALANCE).send().wait(); + await t.mintFeeJuice(bobsAddress, t.INITIAL_GAS_BALANCE); const [bobsInitialGas] = await t.getGasBalanceFn(bobsAddress); expect(bobsInitialGas).toEqual(t.INITIAL_GAS_BALANCE); @@ -179,7 +179,7 @@ describe('e2e_fees account_init', () => { describe('another account pays the fee', () => { it('pays natively in the Fee Juice', async () => { // mint Fee Juice to alice - await t.feeJuiceContract.methods.mint_public(aliceAddress, t.INITIAL_GAS_BALANCE).send().wait(); + await t.mintFeeJuice(aliceAddress, t.INITIAL_GAS_BALANCE); const [alicesInitialGas] = await t.getGasBalanceFn(aliceAddress); // bob generates the private keys for his account on his own diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index 29d838ca1af..0c02c841bb2 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -33,10 +33,7 @@ import { getContract } from 'viem'; import { MNEMONIC } from '../fixtures/fixtures.js'; import { type ISnapshotManager, addAccounts, createSnapshotManager } from '../fixtures/snapshot_manager.js'; import { type BalancesFn, deployCanonicalFeeJuice, getBalancesFn, publicDeployAccounts } from '../fixtures/utils.js'; -import { - FeeJuicePortalTestingHarnessFactory, - type IGasBridgingTestHarness, -} from '../shared/gas_portal_test_harness.js'; +import { FeeJuicePortalTestingHarnessFactory, type GasBridgingTestHarness } from '../shared/gas_portal_test_harness.js'; const { E2E_DATA_PATH: dataPath } = process.env; @@ -74,7 +71,7 @@ export class FeesTest { public privateFPC!: PrivateFPCContract; public counterContract!: CounterContract; public subscriptionContract!: AppSubscriptionContract; - public feeJuiceBridgeTestHarness!: IGasBridgingTestHarness; + public feeJuiceBridgeTestHarness!: GasBridgingTestHarness; public getCoinbaseBalance!: () => Promise; public getGasBalanceFn!: BalancesFn; @@ -110,6 +107,14 @@ export class FeesTest { expect(balanceAfter).toEqual(balanceBefore + amount); } + // Mint fee juice AND mint funds to the portal to emulate the L1 -> L2 bridge + async mintFeeJuice(address: AztecAddress, amount: bigint) { + await this.feeJuiceContract.methods.mint_public(address, amount).send().wait(); + // Need to also mint funds to the portal + const portalAddress = EthAddress.fromString(this.feeJuiceBridgeTestHarness.tokenPortal.address); + await this.feeJuiceBridgeTestHarness.mintTokensOnL1(amount, portalAddress); + } + /** Alice mints bananaCoin tokens privately to the target address and redeems them. */ async mintPrivateBananas(amount: bigint, address: AztecAddress) { const balanceBefore = await this.bananaCoin.methods.balance_of_private(address).simulate(); @@ -187,7 +192,6 @@ export class FeesTest { walletClient: walletClient, wallet: this.aliceWallet, logger: this.logger, - mockL1: false, }); }, ); @@ -223,7 +227,6 @@ export class FeesTest { walletClient: walletClient, wallet: this.aliceWallet, logger: this.logger, - mockL1: false, }); }, ); @@ -362,7 +365,7 @@ export class FeesTest { await this.snapshotManager.snapshot( 'fund_alice_with_fee_juice', async () => { - await this.feeJuiceContract.methods.mint_public(this.aliceAddress, this.INITIAL_GAS_BALANCE).send().wait(); + await this.mintFeeJuice(this.aliceAddress, this.INITIAL_GAS_BALANCE); }, () => Promise.resolve(), ); @@ -392,11 +395,7 @@ export class FeesTest { // Mint some Fee Juice to the subscription contract // Could also use bridgeFromL1ToL2 from the harness, but this is more direct - await this.feeJuiceContract.methods - .mint_public(subscriptionContract.address, this.INITIAL_GAS_BALANCE) - .send() - .wait(); - + await this.mintFeeJuice(subscriptionContract.address, this.INITIAL_GAS_BALANCE); return { counterContractAddress: counterContract.address, subscriptionContractAddress: subscriptionContract.address, diff --git a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts index 12c8f801aec..d0b76ef852c 100644 --- a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts @@ -10,7 +10,7 @@ import { } from '@aztec/aztec.js'; import { FeeJuicePortalAbi, OutboxAbi, PortalERC20Abi } from '@aztec/l1-artifacts'; import { FeeJuiceContract } from '@aztec/noir-contracts.js'; -import { FeeJuiceAddress, getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; +import { FeeJuiceAddress } from '@aztec/protocol-contracts/fee-juice'; import { type Account, @@ -47,16 +47,6 @@ export interface FeeJuicePortalTestingHarnessFactoryConfig { export class FeeJuicePortalTestingHarnessFactory { private constructor(private config: FeeJuicePortalTestingHarnessFactoryConfig) {} - private async createMock() { - const wallet = this.config.wallet; - - // In this case we are not using a portal we just yolo it. - const gasL2 = await FeeJuiceContract.deploy(wallet) - .send({ contractAddressSalt: getCanonicalFeeJuice().instance.salt }) - .deployed(); - return Promise.resolve(new MockGasBridgingTestHarness(gasL2, EthAddress.ZERO)); - } - private async createReal() { const { aztecNode, pxeService, publicClient, walletClient, wallet, logger } = this.config; @@ -105,30 +95,9 @@ export class FeeJuicePortalTestingHarnessFactory { ); } - static create(config: FeeJuicePortalTestingHarnessFactoryConfig): Promise { + static create(config: FeeJuicePortalTestingHarnessFactoryConfig): Promise { const factory = new FeeJuicePortalTestingHarnessFactory(config); - if (config.mockL1) { - return factory.createMock(); - } else { - return factory.createReal(); - } - } -} - -class MockGasBridgingTestHarness implements IGasBridgingTestHarness { - constructor(public l2Token: FeeJuiceContract, public l1FeeJuiceAddress: EthAddress) {} - prepareTokensOnL1( - _l1TokenBalance: bigint, - _bridgeAmount: bigint, - _owner: AztecAddress, - ): Promise<{ secret: Fr; secretHash: Fr; msgHash: Fr }> { - throw new Error('Cannot prepare tokens on mocked L1.'); - } - async bridgeFromL1ToL2(_l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress): Promise { - await this.l2Token.methods.mint_public(owner, bridgeAmount).send().wait(); - } - getL1FeeJuiceBalance(_address: EthAddress): Promise { - throw new Error('Cannot get Fee Juice balance on mocked L1.'); + return factory.createReal(); } } @@ -136,7 +105,7 @@ class MockGasBridgingTestHarness implements IGasBridgingTestHarness { * A Class for testing cross chain interactions, contains common interactions * shared between cross chain tests. */ -class GasBridgingTestHarness implements IGasBridgingTestHarness { +export class GasBridgingTestHarness implements IGasBridgingTestHarness { constructor( /** Aztec node */ public aztecNode: AztecNode, @@ -177,12 +146,13 @@ class GasBridgingTestHarness implements IGasBridgingTestHarness { return [secret, secretHash]; } - async mintTokensOnL1(amount: bigint) { + async mintTokensOnL1(amount: bigint, to: EthAddress = this.ethAccount) { this.logger.info('Minting tokens on L1'); + const balanceBefore = await this.underlyingERC20.read.balanceOf([to.toString()]); await this.publicClient.waitForTransactionReceipt({ - hash: await this.underlyingERC20.write.mint([this.ethAccount.toString(), amount]), + hash: await this.underlyingERC20.write.mint([to.toString(), amount]), }); - expect(await this.underlyingERC20.read.balanceOf([this.ethAccount.toString()])).toBe(amount); + expect(await this.underlyingERC20.read.balanceOf([to.toString()])).toBe(balanceBefore + amount); } async getL1FeeJuiceBalance(address: EthAddress) { diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index c493809c632..9b63a51a5ae 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -183,13 +183,60 @@ export const deployL1Contracts = async ( logger.info(`Deployed Fee Juice at ${feeJuiceAddress}`); - const rollupAddress = await deployer.deploy(contractsToDeploy.rollup, [ - getAddress(registryAddress.toString()), - getAddress(availabilityOracleAddress.toString()), - getAddress(feeJuiceAddress.toString()), - args.vkTreeRoot.toString(), - account.address.toString(), - ]); + const feeJuicePortalAddress = await deployL1Contract( + walletClient, + publicClient, + contractsToDeploy.feeJuicePortal.contractAbi, + contractsToDeploy.feeJuicePortal.contractBytecode, + ); + + logger.info(`Deployed Gas Portal at ${feeJuicePortalAddress}`); + + const feeJuicePortal = getContract({ + address: feeJuicePortalAddress.toString(), + abi: contractsToDeploy.feeJuicePortal.contractAbi, + client: walletClient, + }); + + // fund the portal contract with Fee Juice + const feeJuice = getContract({ + address: feeJuiceAddress.toString(), + abi: contractsToDeploy.feeJuice.contractAbi, + client: walletClient, + }); + + // @note This value MUST match what is in `constants.nr`. It is currently specified here instead of just importing + // because there is circular dependency hell. This is a temporary solution. #3342 + const FEE_JUICE_INITIAL_MINT = 20000000000; + const receipt = await feeJuice.write.mint([feeJuicePortalAddress.toString(), FEE_JUICE_INITIAL_MINT], {} as any); + await publicClient.waitForTransactionReceipt({ hash: receipt }); + logger.info(`Funded fee juice portal contract with Fee Juice`); + + await publicClient.waitForTransactionReceipt({ + hash: await feeJuicePortal.write.initialize([ + registryAddress.toString(), + feeJuiceAddress.toString(), + args.l2FeeJuiceAddress.toString(), + ]), + }); + + logger.info( + `Initialized Gas Portal at ${feeJuicePortalAddress} to bridge between L1 ${feeJuiceAddress} to L2 ${args.l2FeeJuiceAddress}`, + ); + + const rollupAddress = await deployL1Contract( + walletClient, + publicClient, + contractsToDeploy.rollup.contractAbi, + contractsToDeploy.rollup.contractBytecode, + [ + getAddress(registryAddress.toString()), + getAddress(availabilityOracleAddress.toString()), + getAddress(feeJuicePortalAddress.toString()), + args.vkTreeRoot.toString(), + account.address.toString(), + ], + ); logger.info(`Deployed Rollup at ${rollupAddress}`); // Set initial blocks as proven if requested @@ -239,39 +286,6 @@ export const deployL1Contracts = async ( logger.verbose(`Registry ${registryAddress} has already registered rollup ${rollupAddress}`); } - // this contract remains uninitialized because at this point we don't know the address of the Fee Juice on L2 - const feeJuicePortalAddress = await deployer.deploy(contractsToDeploy.feeJuicePortal); - - logger.info(`Deployed Gas Portal at ${feeJuicePortalAddress}`); - - const feeJuicePortal = getContract({ - address: feeJuicePortalAddress.toString(), - abi: contractsToDeploy.feeJuicePortal.contractAbi, - client: walletClient, - }); - - await publicClient.waitForTransactionReceipt({ - hash: await feeJuicePortal.write.initialize([ - registryAddress.toString(), - feeJuiceAddress.toString(), - args.l2FeeJuiceAddress.toString(), - ]), - }); - - logger.info( - `Initialized Gas Portal at ${feeJuicePortalAddress} to bridge between L1 ${feeJuiceAddress} to L2 ${args.l2FeeJuiceAddress}`, - ); - - // fund the rollup contract with Fee Juice - const feeJuice = getContract({ - address: feeJuiceAddress.toString(), - abi: contractsToDeploy.feeJuice.contractAbi, - client: walletClient, - }); - const receipt = await feeJuice.write.mint([rollupAddress.toString(), 100000000000000000000n], {} as any); - await publicClient.waitForTransactionReceipt({ hash: receipt }); - logger.info(`Funded rollup contract with Fee Juice`); - const l1Contracts: L1ContractAddresses = { availabilityOracleAddress, rollupAddress,