Skip to content

Commit

Permalink
refactor: proper portal setup for fees + test (#7944)
Browse files Browse the repository at this point in the history
Fixes #7692. 

Gets rid of assets directly in the rollup contract. The rollup contract
will instead make a call to the portal to get it to pay some funds to
the sequencer, currently named `distributeFees`.

Calling initialize on the fee juice portal require that it have been
funded to ensure that the deployment of the l2 fee assets does not lead
to an undercollateralized case.
  • Loading branch information
LHerskind authored Aug 19, 2024
1 parent 1da5caf commit 9fec67e
Show file tree
Hide file tree
Showing 18 changed files with 278 additions and 192 deletions.
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

0 comments on commit 9fec67e

Please sign in to comment.