Skip to content

Commit

Permalink
feat: pay out arbitrary fee to coinbase on L1 (AztecProtocol#6436)
Browse files Browse the repository at this point in the history
Fix AztecProtocol#5007

Just some basic plumbing to allow us to pay out fees to coinbase.
  • Loading branch information
just-mitch authored May 15, 2024
1 parent d67ab1c commit 1b99de8
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 23 deletions.
10 changes: 9 additions & 1 deletion l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -33,17 +34,19 @@ 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;
// Tracks the last time time was warped on L2 ("warp" is the testing cheatcode).
// 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;
Expand Down Expand Up @@ -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);
}

Expand Down
10 changes: 9 additions & 1 deletion l1-contracts/test/Rollup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";

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

Expand Down Expand Up @@ -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);
Expand Down
22 changes: 20 additions & 2 deletions yarn-project/end-to-end/src/e2e_fees/fees_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,6 +25,8 @@ import {
GasTokenContract,
} from '@aztec/noir-contracts.js';

import { getContract } from 'viem';

import { MNEMONIC } from '../fixtures/fixtures.js';
import {
type ISnapshotManager,
Expand Down Expand Up @@ -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();
Expand All @@ -68,6 +72,7 @@ export class FeesTest {
public counterContract!: CounterContract;
public subscriptionContract!: AppSubscriptionContract;

public getCoinbaseBalance!: () => Promise<bigint>;
public gasBalances!: BalancesFn;
public bananaPublicBalances!: BalancesFn;
public bananaPrivateBalances!: BalancesFn;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
},
);
}
Expand Down Expand Up @@ -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 => {
Expand All @@ -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()]);
};
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ describe('e2e_fees private_payment', () => {
await t.teardown();
});

let InitialSequencerL1Gas: bigint;

let InitialAlicePublicBananas: bigint;
let InitialAlicePrivateBananas: bigint;
let InitialAliceGas: bigint;
Expand All @@ -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],
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
17 changes: 13 additions & 4 deletions yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import {
} from 'viem';

export interface IGasBridgingTestHarness {
getL1GasTokenBalance(address: EthAddress): Promise<bigint>;
bridgeFromL1ToL2(l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress): Promise<void>;
l2Token: GasTokenContract;
l1GasTokenAddress: EthAddress;
}

export interface GasPortalTestingHarnessFactoryConfig {
Expand All @@ -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() {
Expand Down Expand Up @@ -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<void> {
await this.l2Token.methods.mint_public(owner, bridgeAmount).send().wait();
}
getL1GasTokenBalance(_address: EthAddress): Promise<bigint> {
throw new Error('Cannot get gas token balance on mocked L1.');
}
}

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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()]);
}

Expand Down Expand Up @@ -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();
Expand Down
34 changes: 23 additions & 11 deletions yarn-project/ethereum/src/deploy_l1_contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 1b99de8

Please sign in to comment.