Skip to content

Commit

Permalink
feat: fee pricing to 0 for old instances (#9296)
Browse files Browse the repository at this point in the history
Fixes #7938.
  • Loading branch information
LHerskind authored Oct 24, 2024
1 parent 74a8ad1 commit 7bc3a21
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 40 deletions.
56 changes: 33 additions & 23 deletions l1-contracts/src/core/FeeJuicePortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,24 @@ import {Errors} from "@aztec/core/libraries/Errors.sol";
import {Hash} from "@aztec/core/libraries/crypto/Hash.sol";
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";

import {Ownable} from "@oz/access/Ownable.sol";

contract FeeJuicePortal is IFeeJuicePortal, Ownable {
contract FeeJuicePortal is IFeeJuicePortal {
using SafeERC20 for IERC20;

IRegistry public registry;
IERC20 public underlying;
bytes32 public l2TokenAddress;
IRegistry public immutable REGISTRY;
IERC20 public immutable UNDERLYING;
bytes32 public immutable L2_TOKEN_ADDRESS;

constructor(address _owner, address _registry, address _underlying, bytes32 _l2TokenAddress)
Ownable(_owner)
{
bool public initialized;

constructor(address _registry, address _underlying, bytes32 _l2TokenAddress) {
require(
_registry != address(0) && _underlying != address(0) && _l2TokenAddress != 0,
Errors.FeeJuicePortal__InvalidInitialization()
);

registry = IRegistry(_registry);
underlying = IERC20(_underlying);
l2TokenAddress = _l2TokenAddress;
REGISTRY = IRegistry(_registry);
UNDERLYING = IERC20(_underlying);
L2_TOKEN_ADDRESS = _l2TokenAddress;
}

/**
Expand All @@ -44,16 +42,16 @@ contract FeeJuicePortal is IFeeJuicePortal, Ownable {
* @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.
*/
function initialize() external override(IFeeJuicePortal) onlyOwner {
require(owner() != address(0), Errors.FeeJuicePortal__AlreadyInitialized());
function initialize() external override(IFeeJuicePortal) {
require(!initialized, Errors.FeeJuicePortal__AlreadyInitialized());

uint256 balance = underlying.balanceOf(address(this));
uint256 balance = UNDERLYING.balanceOf(address(this));
if (balance < Constants.FEE_JUICE_INITIAL_MINT) {
underlying.safeTransferFrom(
UNDERLYING.safeTransferFrom(
msg.sender, address(this), Constants.FEE_JUICE_INITIAL_MINT - balance
);
}
_transferOwnership(address(0));
initialized = true;
}

/**
Expand All @@ -69,18 +67,24 @@ contract FeeJuicePortal is IFeeJuicePortal, Ownable {
returns (bytes32)
{
// Preamble
IInbox inbox = IRollup(registry.getRollup()).INBOX();
DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2TokenAddress, 1);
address rollup = canonicalRollup();
uint256 version = REGISTRY.getVersionFor(rollup);
IInbox inbox = IRollup(rollup).INBOX();
DataStructures.L2Actor memory actor = DataStructures.L2Actor(L2_TOKEN_ADDRESS, version);

// Hash the message content to be reconstructed in the receiving contract
bytes32 contentHash =
Hash.sha256ToField(abi.encodeWithSignature("claim(bytes32,uint256)", _to, _amount));

// Hold the tokens in the portal
underlying.safeTransferFrom(msg.sender, address(this), _amount);
UNDERLYING.safeTransferFrom(msg.sender, address(this), _amount);

// Send message to rollup
return inbox.sendL2Message(actor, contentHash, _secretHash);
bytes32 key = inbox.sendL2Message(actor, contentHash, _secretHash);

emit DepositToAztecPublic(_to, _amount, _secretHash, key);

return key;
}

/**
Expand All @@ -94,7 +98,13 @@ contract FeeJuicePortal is IFeeJuicePortal, Ownable {
* @param _amount - The amount to pay them
*/
function distributeFees(address _to, uint256 _amount) external override(IFeeJuicePortal) {
require(msg.sender == registry.getRollup(), Errors.FeeJuicePortal__Unauthorized());
underlying.safeTransfer(_to, _amount);
require(msg.sender == canonicalRollup(), Errors.FeeJuicePortal__Unauthorized());
UNDERLYING.safeTransfer(_to, _amount);

emit FeesDistributed(_to, _amount);
}

function canonicalRollup() public view returns (address) {
return REGISTRY.getRollup();
}
}
26 changes: 16 additions & 10 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
) Leonidas(_ares) {
epochProofVerifier = new MockVerifier();
FEE_JUICE_PORTAL = _fpcJuicePortal;
PROOF_COMMITMENT_ESCROW = new ProofCommitmentEscrow(_fpcJuicePortal.underlying(), address(this));
PROOF_COMMITMENT_ESCROW = new ProofCommitmentEscrow(_fpcJuicePortal.UNDERLYING(), address(this));
INBOX = IInbox(address(new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT)));
OUTBOX = IOutbox(address(new Outbox(address(this))));
vkTreeRoot = _vkTreeRoot;
Expand Down Expand Up @@ -260,15 +260,15 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {

tips.provenBlockNumber = endBlockNumber;

for (uint256 i = 0; i < Constants.AZTEC_EPOCH_DURATION; i++) {
address coinbase = address(uint160(uint256(publicInputs[9 + i * 2])));
uint256 fees = uint256(publicInputs[10 + i * 2]);

if (coinbase != address(0) && fees > 0) {
// @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(coinbase, fees);
// @note Only if the rollup is the canonical will it be able to meaningfully claim fees
// Otherwise, the fees are unbacked #7938.
if (address(this) == FEE_JUICE_PORTAL.canonicalRollup()) {
for (uint256 i = 0; i < Constants.AZTEC_EPOCH_DURATION; i++) {
address coinbase = address(uint160(uint256(publicInputs[9 + i * 2])));
uint256 fees = uint256(publicInputs[10 + i * 2]);
if (coinbase != address(0) && fees > 0) {
FEE_JUICE_PORTAL.distributeFees(coinbase, fees);
}
}
}

Expand Down Expand Up @@ -921,5 +921,11 @@ contract Rollup is EIP712("Aztec Rollup", "1"), Leonidas, IRollup, ITestRollup {
_flags.ignoreDA || _header.contentCommitment.txsEffectsHash == _txsEffectsHash,
Errors.Rollup__UnavailableTxs(_header.contentCommitment.txsEffectsHash)
);

// If not canonical rollup, require that the fees are zero
if (address(this) != FEE_JUICE_PORTAL.canonicalRollup()) {
require(_header.globalVariables.gasFees.feePerDaGas == 0, Errors.Rollup__NonZeroDaFee());
require(_header.globalVariables.gasFees.feePerL2Gas == 0, Errors.Rollup__NonZeroL2Fee());
}
}
}
10 changes: 9 additions & 1 deletion l1-contracts/src/core/interfaces/IFeeJuicePortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
pragma solidity >=0.8.27;

import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";

interface IFeeJuicePortal {
event DepositToAztecPublic(bytes32 indexed to, uint256 amount, bytes32 secretHash, bytes32 key);
event FeesDistributed(address indexed to, uint256 amount);

function initialize() external;
function distributeFees(address _to, uint256 _amount) external;
function depositToAztecPublic(bytes32 _to, uint256 _amount, bytes32 _secretHash)
external
returns (bytes32);
function underlying() external view returns (IERC20);
function canonicalRollup() external view returns (address);

function UNDERLYING() external view returns (IERC20);
function L2_TOKEN_ADDRESS() external view returns (bytes32);
function REGISTRY() external view returns (IRegistry);
}
2 changes: 2 additions & 0 deletions l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ library Errors {
error Rollup__TimestampTooOld(); // 0x72ed9c81
error Rollup__TryingToProveNonExistingBlock(); // 0x34ef4954
error Rollup__UnavailableTxs(bytes32 txsHash); // 0x414906c3
error Rollup__NonZeroDaFee();
error Rollup__NonZeroL2Fee();

//TxsDecoder
error TxsDecoder__InvalidLogsLength(uint256 expected, uint256 actual); // 0x829ca981
Expand Down
11 changes: 9 additions & 2 deletions l1-contracts/src/mock/MockFeeJuicePortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ pragma solidity >=0.8.27;
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol";
import {TestERC20} from "@aztec/mock/TestERC20.sol";
import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";

contract MockFeeJuicePortal is IFeeJuicePortal {
IERC20 public underlying;
IERC20 public immutable UNDERLYING;
bytes32 public constant L2_TOKEN_ADDRESS = bytes32(0);
IRegistry public constant REGISTRY = IRegistry(address(0));

constructor() {
underlying = new TestERC20();
UNDERLYING = new TestERC20();
}

function initialize() external override {}
Expand All @@ -20,4 +23,8 @@ contract MockFeeJuicePortal is IFeeJuicePortal {
function depositToAztecPublic(bytes32, uint256, bytes32) external pure override returns (bytes32) {
return bytes32(0);
}

function canonicalRollup() external pure override returns (address) {
return address(0);
}
}
39 changes: 38 additions & 1 deletion l1-contracts/test/Rollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ contract RollupTest is DecoderBase {
registry = new Registry(address(this));
testERC20 = new TestERC20();
feeJuicePortal = new FeeJuicePortal(
address(this), address(registry), address(testERC20), bytes32(Constants.FEE_JUICE_ADDRESS)
address(registry), address(testERC20), bytes32(Constants.FEE_JUICE_ADDRESS)
);
testERC20.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT);
feeJuicePortal.initialize();
rollup = new Rollup(feeJuicePortal, bytes32(0), bytes32(0), address(this), new address[](0));
inbox = Inbox(address(rollup.INBOX()));
outbox = Outbox(address(rollup.OUTBOX()));
Expand Down Expand Up @@ -500,6 +501,42 @@ contract RollupTest is DecoderBase {
assertEq(rollup.getProvenBlockNumber(), 0, "Invalid proven block number");
}

function testNonZeroDaFee() public setUpFor("mixed_block_1") {
registry.upgrade(address(0xbeef));

DecoderBase.Full memory full = load("mixed_block_1");
DecoderBase.Data memory data = full.block;
bytes memory header = data.header;
assembly {
mstore(add(header, add(0x20, 0x0208)), 1)
}
bytes32[] memory txHashes = new bytes32[](0);

// We jump to the time of the block. (unless it is in the past)
vm.warp(max(block.timestamp, data.decodedHeader.globalVariables.timestamp));

vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NonZeroDaFee.selector));
rollup.propose(header, data.archive, data.blockHash, txHashes, signatures, data.body);
}

function testNonZeroL2Fee() public setUpFor("mixed_block_1") {
registry.upgrade(address(0xbeef));

DecoderBase.Full memory full = load("mixed_block_1");
DecoderBase.Data memory data = full.block;
bytes memory header = data.header;
assembly {
mstore(add(header, add(0x20, 0x0228)), 1)
}
bytes32[] memory txHashes = new bytes32[](0);

// We jump to the time of the block. (unless it is in the past)
vm.warp(max(block.timestamp, data.decodedHeader.globalVariables.timestamp));

vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__NonZeroL2Fee.selector));
rollup.propose(header, data.archive, data.blockHash, txHashes, signatures, data.body);
}

function testBlockFee() public setUpFor("mixed_block_1") {
uint256 feeAmount = 2e18;

Expand Down
104 changes: 104 additions & 0 deletions l1-contracts/test/fee_portal/depositToAztecPublic.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.27;

import {Test} from "forge-std/Test.sol";
import {Registry} from "@aztec/governance/Registry.sol";
import {TestERC20} from "@aztec/mock/TestERC20.sol";
import {FeeJuicePortal} from "@aztec/core/FeeJuicePortal.sol";
import {IFeeJuicePortal} from "@aztec/core/interfaces/IFeeJuicePortal.sol";
import {Constants} from "@aztec/core/libraries/ConstantsGen.sol";
import {IERC20Errors} from "@oz/interfaces/draft-IERC6093.sol";
import {Rollup} from "@aztec/core/Rollup.sol";
import {DataStructures} from "@aztec/core/libraries/DataStructures.sol";
import {Hash} from "@aztec/core/libraries/crypto/Hash.sol";
import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
import {Inbox} from "@aztec/core/messagebridge/Inbox.sol";

contract DepositToAztecPublic is Test {
using Hash for DataStructures.L1ToL2Msg;

address internal constant OWNER = address(0x1);
Registry internal registry;
TestERC20 internal token;
FeeJuicePortal internal feeJuicePortal;
Rollup internal rollup;

function setUp() public {
registry = new Registry(OWNER);
token = new TestERC20();
feeJuicePortal =
new FeeJuicePortal(address(registry), address(token), bytes32(Constants.FEE_JUICE_ADDRESS));

token.mint(address(feeJuicePortal), Constants.FEE_JUICE_INITIAL_MINT);
feeJuicePortal.initialize();

rollup = new Rollup(feeJuicePortal, bytes32(0), bytes32(0), address(this), new address[](0));

vm.prank(OWNER);
registry.upgrade(address(rollup));
}

function test_RevertGiven_InsufficientBalance() external {
// it should revert
vm.expectRevert(
abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientAllowance.selector, address(feeJuicePortal), 0, 1
)
);
feeJuicePortal.depositToAztecPublic(bytes32(0x0), 1, bytes32(0x0));

token.approve(address(feeJuicePortal), 1);
vm.expectRevert(
abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(this), 0, 1)
);
feeJuicePortal.depositToAztecPublic(bytes32(0x0), 1, bytes32(0x0));
}

function test_GivenSufficientBalance(uint256 _numberOfRollups) external {
// it should create a message for the newest version
// it should transfer the tokens to the portal
// it should insert the message into the newest inbox
// it should emit a {DepositToAztecPublic} event
// it should return the key

uint256 numberOfRollups = bound(_numberOfRollups, 1, 5);
for (uint256 i = 0; i < numberOfRollups; i++) {
Rollup freshRollup =
new Rollup(feeJuicePortal, bytes32(0), bytes32(0), address(this), new address[](0));
vm.prank(OWNER);
registry.upgrade(address(freshRollup));
}

assertNotEq(registry.getRollup(), address(rollup));

bytes32 to = bytes32(0x0);
bytes32 secretHash = bytes32(uint256(0x01));
uint256 amount = 100 ether;

DataStructures.L1ToL2Msg memory message = DataStructures.L1ToL2Msg({
sender: DataStructures.L1Actor(address(feeJuicePortal), block.chainid),
recipient: DataStructures.L2Actor(feeJuicePortal.L2_TOKEN_ADDRESS(), 1 + numberOfRollups),
content: Hash.sha256ToField(abi.encodeWithSignature("claim(bytes32,uint256)", to, amount)),
secretHash: secretHash
});

bytes32 expectedKey = message.sha256ToField();

token.mint(address(this), amount);
token.approve(address(feeJuicePortal), amount);

Inbox inbox = Inbox(address(Rollup(address(registry.getRollup())).INBOX()));
assertEq(inbox.totalMessagesInserted(), 0);
uint256 index = 2 ** Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT;

vm.expectEmit(true, true, true, true, address(inbox));
emit IInbox.MessageSent(2, index, expectedKey);
vm.expectEmit(true, true, true, true, address(feeJuicePortal));
emit IFeeJuicePortal.DepositToAztecPublic(to, amount, secretHash, expectedKey);

bytes32 key = feeJuicePortal.depositToAztecPublic(to, amount, secretHash);

assertEq(inbox.totalMessagesInserted(), 1);
assertEq(key, expectedKey);
}
}
9 changes: 9 additions & 0 deletions l1-contracts/test/fee_portal/depositToAztecPublic.tree
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DepositToAztecPublic
├── given insufficient balance
│ └── it should revert
└── given sufficient balance
├── it should create a message for the newest version
├── it should transfer the tokens to the portal
├── it should insert the message into the newest inbox
├── it should emit a {DepositToAztecPublic} event
└── it should return the key
Loading

0 comments on commit 7bc3a21

Please sign in to comment.