From 1b99de81e58a97fb47604d2e94582e6f227f98dd Mon Sep 17 00:00:00 2001 From: just-mitch <68168980+just-mitch@users.noreply.github.com> Date: Wed, 15 May 2024 16:33:08 -0400 Subject: [PATCH] feat: pay out arbitrary fee to coinbase on L1 (#6436) Fix https://github.com/AztecProtocol/aztec-packages/issues/5007 Just some basic plumbing to allow us to pay out fees to coinbase. --- l1-contracts/src/core/Rollup.sol | 10 +++++- l1-contracts/test/Rollup.t.sol | 10 +++++- l1-contracts/test/portals/TokenPortal.t.sol | 6 ++-- l1-contracts/test/portals/UniswapPortal.t.sol | 7 +++- .../end-to-end/src/e2e_fees/fees_test.ts | 22 ++++++++++-- .../src/e2e_fees/private_payments.test.ts | 7 +++- .../src/shared/gas_portal_test_harness.ts | 17 +++++++--- .../ethereum/src/deploy_l1_contracts.ts | 34 +++++++++++++------ 8 files changed, 90 insertions(+), 23 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 948e21b1b3e..5c23833bb40 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -8,6 +8,7 @@ import {IAvailabilityOracle} from "./interfaces/IAvailabilityOracle.sol"; import {IInbox} from "./interfaces/messagebridge/IInbox.sol"; import {IOutbox} from "./interfaces/messagebridge/IOutbox.sol"; import {IRegistry} from "./interfaces/messagebridge/IRegistry.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; // Libraries import {HeaderLib} from "./libraries/HeaderLib.sol"; @@ -33,6 +34,7 @@ contract Rollup is IRollup { IInbox public immutable INBOX; IOutbox public immutable OUTBOX; uint256 public immutable VERSION; + IERC20 public immutable GAS_TOKEN; bytes32 public archive; // Root of the archive tree uint256 public lastBlockTs; @@ -40,10 +42,11 @@ contract Rollup is IRollup { // See https://github.com/AztecProtocol/aztec-packages/issues/1614 uint256 public lastWarpedBlockTs; - constructor(IRegistry _registry, IAvailabilityOracle _availabilityOracle) { + constructor(IRegistry _registry, IAvailabilityOracle _availabilityOracle, IERC20 _gasToken) { VERIFIER = new MockVerifier(); REGISTRY = _registry; AVAILABILITY_ORACLE = _availabilityOracle; + GAS_TOKEN = _gasToken; INBOX = new Inbox(address(this), Constants.L1_TO_L2_MSG_SUBTREE_HEIGHT); OUTBOX = new Outbox(address(this)); VERSION = 1; @@ -92,6 +95,11 @@ contract Rollup is IRollup { header.globalVariables.blockNumber, header.contentCommitment.outHash, l2ToL1TreeHeight ); + // pay the coinbase 1 gas token if it is not empty + if (header.globalVariables.coinbase != address(0)) { + GAS_TOKEN.transfer(address(header.globalVariables.coinbase), 1); + } + emit L2BlockProcessed(header.globalVariables.blockNumber); } diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index d02e1a6aeff..88f6405e094 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -2,6 +2,8 @@ // 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"; @@ -15,6 +17,7 @@ import {Rollup} from "../src/core/Rollup.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"; @@ -29,18 +32,23 @@ contract RollupTest is DecoderBase { Rollup internal rollup; MerkleTestUtil internal merkleTestUtil; TxsDecoderHelper internal txsHelper; + PortalERC20 internal portalERC20; AvailabilityOracle internal availabilityOracle; function setUp() public virtual { registry = new Registry(); availabilityOracle = new AvailabilityOracle(); - rollup = new Rollup(registry, availabilityOracle); + portalERC20 = new PortalERC20(); + rollup = new Rollup(registry, availabilityOracle, IERC20(address(portalERC20))); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); registry.upgrade(address(rollup), address(inbox), address(outbox)); + // mint some tokens to the rollup + portalERC20.mint(address(rollup), 1000000); + merkleTestUtil = new MerkleTestUtil(); txsHelper = new TxsDecoderHelper(); } diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 550afbe5bc8..e51f4c0dccb 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -14,6 +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"; // Portal tokens import {TokenPortal} from "./TokenPortal.sol"; @@ -57,13 +58,14 @@ contract TokenPortalTest is Test { function setUp() public { registry = new Registry(); - rollup = new Rollup(registry, new AvailabilityOracle()); + portalERC20 = new PortalERC20(); + rollup = new Rollup(registry, new AvailabilityOracle(), IERC20(address(portalERC20))); inbox = rollup.INBOX(); outbox = rollup.OUTBOX(); registry.upgrade(address(rollup), address(inbox), address(outbox)); - portalERC20 = new PortalERC20(); + portalERC20.mint(address(rollup), 1000000); tokenPortal = new TokenPortal(); tokenPortal.initialize(address(registry), address(portalERC20), l2TokenAddress); diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index b525e78a494..bd4e566ddf4 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -20,6 +20,9 @@ import {NaiveMerkle} from "../merkle/Naive.sol"; 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; @@ -48,8 +51,10 @@ contract UniswapPortalTest is Test { vm.selectFork(forkId); registry = new Registry(); - rollup = new Rollup(registry, new AvailabilityOracle()); + PortalERC20 portalERC20 = new PortalERC20(); + rollup = new Rollup(registry, new AvailabilityOracle(), IERC20(address(portalERC20))); registry.upgrade(address(rollup), address(rollup.INBOX()), address(rollup.OUTBOX())); + portalERC20.mint(address(rollup), 1000000); daiTokenPortal = new TokenPortal(); daiTokenPortal.initialize(address(registry), address(DAI), l2TokenAddress); 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 3f115a52c46..a13f625ff46 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 @@ -14,8 +14,9 @@ import { createDebugLogger, } from '@aztec/aztec.js'; import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; -import { GasSettings } from '@aztec/circuits.js'; +import { EthAddress, GasSettings } from '@aztec/circuits.js'; import { createL1Clients } from '@aztec/ethereum'; +import { PortalERC20Abi } from '@aztec/l1-artifacts'; import { AppSubscriptionContract, TokenContract as BananaCoin, @@ -24,6 +25,8 @@ import { GasTokenContract, } from '@aztec/noir-contracts.js'; +import { getContract } from 'viem'; + import { MNEMONIC } from '../fixtures/fixtures.js'; import { type ISnapshotManager, @@ -58,6 +61,7 @@ export class FeesTest { public bobWallet!: AccountWallet; public bobAddress!: AztecAddress; public sequencerAddress!: AztecAddress; + public coinbase!: EthAddress; public gasSettings = GasSettings.default(); public maxFee = this.gasSettings.getFeeLimit().toBigInt(); @@ -68,6 +72,7 @@ export class FeesTest { public counterContract!: CounterContract; public subscriptionContract!: AppSubscriptionContract; + public getCoinbaseBalance!: () => Promise; public gasBalances!: BalancesFn; public bananaPublicBalances!: BalancesFn; public bananaPrivateBalances!: BalancesFn; @@ -84,7 +89,7 @@ export class FeesTest { async setup() { const context = await this.snapshotManager.setup(); - await context.aztecNode.setConfig({ feeRecipient: this.sequencerAddress }); + await context.aztecNode.setConfig({ feeRecipient: this.sequencerAddress, coinbase: this.coinbase }); ({ pxe: this.pxe, aztecNode: this.aztecNode } = context); return this; } @@ -138,6 +143,7 @@ export class FeesTest { this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); [this.aliceWallet, this.bobWallet] = this.wallets.slice(0, 2); [this.aliceAddress, this.bobAddress, this.sequencerAddress] = this.wallets.map(w => w.getAddress()); + this.coinbase = EthAddress.random(); }, ); } @@ -185,6 +191,8 @@ export class FeesTest { bananaCoinAddress: bananaCoin.address, bananaFPCAddress: bananaFPC.address, gasTokenAddress: gasTokenContract.address, + l1GasTokenAddress: harness.l1GasTokenAddress, + rpcUrl: context.aztecNodeConfig.rpcUrl, }; }, async data => { @@ -199,6 +207,16 @@ export class FeesTest { this.bananaPublicBalances = getBalancesFn('🍌.public', bananaCoin.methods.balance_of_public, this.logger); this.bananaPrivateBalances = getBalancesFn('🍌.private', bananaCoin.methods.balance_of_private, this.logger); this.gasBalances = getBalancesFn('⛽', gasTokenContract.methods.balance_of_public, this.logger); + + this.getCoinbaseBalance = async () => { + const { walletClient } = createL1Clients(data.rpcUrl, MNEMONIC); + const gasL1 = getContract({ + address: data.l1GasTokenAddress.toString(), + abi: PortalERC20Abi, + client: walletClient, + }); + return await gasL1.read.balanceOf([this.coinbase.toString()]); + }; }, ); } diff --git a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts index e85adf4e678..ebca3276713 100644 --- a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts @@ -36,6 +36,8 @@ describe('e2e_fees private_payment', () => { await t.teardown(); }); + let InitialSequencerL1Gas: bigint; + let InitialAlicePublicBananas: bigint; let InitialAlicePrivateBananas: bigint; let InitialAliceGas: bigint; @@ -59,6 +61,8 @@ describe('e2e_fees private_payment', () => { expect(gasSettings.getFeeLimit().toBigInt()).toEqual(maxFee); + InitialSequencerL1Gas = await t.getCoinbaseBalance(); + [ [InitialAlicePrivateBananas, InitialBobPrivateBananas, InitialFPCPrivateBananas], [InitialAlicePublicBananas, InitialBobPublicBananas, InitialFPCPublicBananas], @@ -104,7 +108,6 @@ describe('e2e_fees private_payment', () => { */ const transferAmount = 5n; const interaction = bananaCoin.methods.transfer(aliceAddress, bobAddress, transferAmount, 0n); - const localTx = await interaction.prove({ fee: { gasSettings, @@ -115,6 +118,8 @@ describe('e2e_fees private_payment', () => { const tx = await interaction.send().wait(); + await expect(t.getCoinbaseBalance()).resolves.toEqual(InitialSequencerL1Gas + 1n); + /** * at present the user is paying DA gas for: * 3 nullifiers = 3 * DA_BYTES_PER_FIELD * DA_GAS_PER_BYTE = 3 * 32 * 16 = 1536 DA gas 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 dee68998224..56d0838e296 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 @@ -23,8 +23,10 @@ import { } from 'viem'; export interface IGasBridgingTestHarness { + getL1GasTokenBalance(address: EthAddress): Promise; bridgeFromL1ToL2(l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress): Promise; l2Token: GasTokenContract; + l1GasTokenAddress: EthAddress; } export interface GasPortalTestingHarnessFactoryConfig { @@ -49,7 +51,7 @@ export class GasPortalTestingHarnessFactory { contractAddressSalt: getCanonicalGasToken(EthAddress.ZERO).instance.salt, }) .deployed(); - return Promise.resolve(new MockGasBridgingTestHarness(gasL2)); + return Promise.resolve(new MockGasBridgingTestHarness(gasL2, EthAddress.ZERO)); } private async createReal() { @@ -111,10 +113,13 @@ export class GasPortalTestingHarnessFactory { } class MockGasBridgingTestHarness implements IGasBridgingTestHarness { - constructor(public l2Token: GasTokenContract) {} + constructor(public l2Token: GasTokenContract, public l1GasTokenAddress: EthAddress) {} async bridgeFromL1ToL2(_l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress): Promise { await this.l2Token.methods.mint_public(owner, bridgeAmount).send().wait(); } + getL1GasTokenBalance(_address: EthAddress): Promise { + throw new Error('Cannot get gas token balance on mocked L1.'); + } } /** @@ -150,6 +155,10 @@ class GasBridgingTestHarness implements IGasBridgingTestHarness { public walletClient: WalletClient, ) {} + get l1GasTokenAddress() { + return EthAddress.fromString(this.underlyingERC20.address); + } + generateClaimSecret(): [Fr, Fr] { this.logger.debug("Generating a claim secret using pedersen's hash function"); const secret = Fr.random(); @@ -166,7 +175,7 @@ class GasBridgingTestHarness implements IGasBridgingTestHarness { expect(await this.underlyingERC20.read.balanceOf([this.ethAccount.toString()])).toBe(amount); } - async getL1BalanceOf(address: EthAddress) { + async getL1GasTokenBalance(address: EthAddress) { return await this.underlyingERC20.read.balanceOf([address.toString()]); } @@ -211,7 +220,7 @@ class GasBridgingTestHarness implements IGasBridgingTestHarness { // 2. Deposit tokens to the TokenPortal const msgHash = await this.sendTokensToPortalPublic(bridgeAmount, owner, secretHash); - expect(await this.getL1BalanceOf(this.ethAccount)).toBe(l1TokenBalance - bridgeAmount); + expect(await this.getL1GasTokenBalance(this.ethAccount)).toBe(l1TokenBalance - bridgeAmount); // Perform an unrelated transactions on L2 to progress the rollup by 2 blocks. await this.l2Token.methods.check_balance(0).send().wait(); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 6cec140a978..a79a001ad8e 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -158,12 +158,25 @@ export const deployL1Contracts = async ( ); logger.info(`Deployed AvailabilityOracle at ${availabilityOracleAddress}`); + const gasTokenAddress = await deployL1Contract( + walletClient, + publicClient, + contractsToDeploy.gasToken.contractAbi, + contractsToDeploy.gasToken.contractBytecode, + ); + + logger.info(`Deployed Gas Token at ${gasTokenAddress}`); + const rollupAddress = await deployL1Contract( walletClient, publicClient, contractsToDeploy.rollup.contractAbi, contractsToDeploy.rollup.contractBytecode, - [getAddress(registryAddress.toString()), getAddress(availabilityOracleAddress.toString())], + [ + getAddress(registryAddress.toString()), + getAddress(availabilityOracleAddress.toString()), + getAddress(gasTokenAddress.toString()), + ], ); logger.info(`Deployed Rollup at ${rollupAddress}`); @@ -201,16 +214,6 @@ export const deployL1Contracts = async ( { account }, ); - // this contract remains uninitialized because at this point we don't know the address of the gas token on L2 - const gasTokenAddress = await deployL1Contract( - walletClient, - publicClient, - contractsToDeploy.gasToken.contractAbi, - contractsToDeploy.gasToken.contractBytecode, - ); - - logger.info(`Deployed Gas Token at ${gasTokenAddress}`); - // this contract remains uninitialized because at this point we don't know the address of the gas token on L2 const gasPortalAddress = await deployL1Contract( walletClient, @@ -221,6 +224,15 @@ export const deployL1Contracts = async ( logger.info(`Deployed Gas Portal at ${gasPortalAddress}`); + // fund the rollup contract with gas tokens + const gasToken = getContract({ + address: gasTokenAddress.toString(), + abi: contractsToDeploy.gasToken.contractAbi, + client: walletClient, + }); + await gasToken.write.mint([rollupAddress.toString(), 100000000000000000000n], {} as any); + logger.info(`Funded rollup contract with gas tokens`); + const l1Contracts: L1ContractAddresses = { availabilityOracleAddress, rollupAddress,