Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions l1-contracts/src/core/FeeJuicePortal.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 10 additions & 7 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions l1-contracts/src/core/interfaces/IFeeJuicePortal.sol
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 deletions l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
57 changes: 51 additions & 6 deletions l1-contracts/test/Rollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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.
Expand All @@ -35,6 +36,7 @@ contract RollupTest is DecoderBase {
MerkleTestUtil internal merkleTestUtil;
TxsDecoderHelper internal txsHelper;
PortalERC20 internal portalERC20;
FeeJuicePortal internal feeJuicePortal;

AvailabilityOracle internal availabilityOracle;

Expand All @@ -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();
_;
Expand Down Expand Up @@ -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);

Expand Down
55 changes: 0 additions & 55 deletions l1-contracts/test/portals/FeeJuicePortal.sol

This file was deleted.

10 changes: 4 additions & 6 deletions l1-contracts/test/portals/TokenPortal.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading