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
27 changes: 27 additions & 0 deletions l1-contracts/src/periphery/DateGatedRelayer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 Aztec Labs.
pragma solidity >=0.8.27;

import {Ownable} from "@oz/access/Ownable.sol";
import {Address} from "@oz/utils/Address.sol";
import {IDateGatedRelayer} from "./interfaces/IDateGatedRelayer.sol";

contract DateGatedRelayer is Ownable, IDateGatedRelayer {
uint256 public immutable GATED_UNTIL;

error GateIsClosed();

constructor(address owner, uint256 _gatedUntil) Ownable(owner) {
GATED_UNTIL = _gatedUntil;
}

function relay(address target, bytes calldata data)
external
override(IDateGatedRelayer)
onlyOwner
returns (bytes memory)
{
require(block.timestamp >= GATED_UNTIL, GateIsClosed());
return Address.functionCall(target, data);
}
}
7 changes: 7 additions & 0 deletions l1-contracts/src/periphery/interfaces/IDateGatedRelayer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 Aztec Labs.
pragma solidity >=0.8.27;

interface IDateGatedRelayer {
function relay(address target, bytes calldata data) external returns (bytes memory);
}
55 changes: 55 additions & 0 deletions l1-contracts/test/DateGatedRelayer.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.27;

import {Test} from "forge-std/Test.sol";
import {DateGatedRelayer} from "../src/periphery/DateGatedRelayer.sol";
import {Ownable} from "@oz/access/Ownable.sol";
import {TestERC20} from "@aztec/mock/TestERC20.sol";
import {CoinIssuer} from "@aztec/governance/CoinIssuer.sol";

contract DateGatedRelayerTest is Test {
function test_notOwner(address _owner, address _caller) public {
vm.assume(_owner != address(0));
vm.assume(_caller != address(0));
vm.assume(_caller != _owner);

DateGatedRelayer dateGatedRelayer = new DateGatedRelayer(_owner, block.timestamp + 100);

vm.prank(_caller);
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller));
dateGatedRelayer.relay(address(0), "");
}

function test_GateIsClosed(uint256 _gatedUntil, uint256 _warp) public {
uint256 gatedUntil = bound(_gatedUntil, block.timestamp + 1, type(uint32).max);
DateGatedRelayer dateGatedRelayer = new DateGatedRelayer(address(this), gatedUntil);

uint256 warp = bound(_warp, 0, gatedUntil - 1);

vm.warp(warp);
vm.expectRevert(abi.encodeWithSelector(DateGatedRelayer.GateIsClosed.selector));
dateGatedRelayer.relay(address(0), "");
}

function test_GateIsOpen(uint256 _gatedUntil, uint256 _warp) public {
uint256 gatedUntil = bound(_gatedUntil, block.timestamp + 1, type(uint32).max);

TestERC20 testERC20 = new TestERC20("test", "TEST", address(this));
CoinIssuer coinIssuer = new CoinIssuer(testERC20, 100, address(this));
testERC20.transferOwnership(address(coinIssuer));
coinIssuer.acceptTokenOwnership();

DateGatedRelayer dateGatedRelayer = new DateGatedRelayer(address(this), gatedUntil);
coinIssuer.transferOwnership(address(dateGatedRelayer));

uint256 warp = bound(_warp, gatedUntil, type(uint32).max);

vm.expectRevert();
coinIssuer.mint(address(this), 100);

vm.warp(warp);
dateGatedRelayer.relay(address(coinIssuer), abi.encodeWithSelector(CoinIssuer.mint.selector, address(this), 100));

assertEq(testERC20.balanceOf(address(this)), 100);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { type Logger, sleep } from '@aztec/aztec.js';
import { RollupContract } from '@aztec/ethereum/contracts';
import { ChainMonitor } from '@aztec/ethereum/test';

import { jest } from '@jest/globals';

import type { EndToEndContext } from '../fixtures/utils.js';
import { EpochsTestContext } from './epochs_test.js';

jest.setTimeout(1000 * 60 * 15);

describe('e2e_epochs/epochs_empty_blocks_proof', () => {
let context: EndToEndContext;
let rollup: RollupContract;
let logger: Logger;
let monitor: ChainMonitor;

let L1_BLOCK_TIME_IN_S: number;

let test: EpochsTestContext;

beforeEach(async () => {
test = await EpochsTestContext.setup();
({ context, rollup, logger, monitor, L1_BLOCK_TIME_IN_S } = test);
});

afterEach(async () => {
jest.restoreAllMocks();
await test.teardown();
});

it('submits proof even if there are no txs to build a block', async () => {
context.sequencer?.updateConfig({ minTxsPerBlock: 1 });
await test.waitUntilEpochStarts(1);

// Sleep to make sure any pending blocks are published
await sleep(L1_BLOCK_TIME_IN_S * 1000);
const blockNumberAtEndOfEpoch0 = Number(await rollup.getBlockNumber());
logger.info(`Starting epoch 1 after L2 block ${blockNumberAtEndOfEpoch0}`);

await test.waitUntilProvenL2BlockNumber(blockNumberAtEndOfEpoch0, 240);
expect(monitor.l2BlockNumber).toEqual(blockNumberAtEndOfEpoch0);
logger.info(`Test succeeded`);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type Logger, sleep } from '@aztec/aztec.js';
import type { Logger } from '@aztec/aztec.js';
import { RollupContract } from '@aztec/ethereum/contracts';
import { ChainMonitor } from '@aztec/ethereum/test';

import { jest } from '@jest/globals';

Expand All @@ -9,46 +8,30 @@ import { EpochsTestContext, WORLD_STATE_BLOCK_HISTORY } from './epochs_test.js';

jest.setTimeout(1000 * 60 * 15);

describe('e2e_epochs/epochs_empty_blocks', () => {
describe('e2e_epochs/epochs_multiple', () => {
let context: EndToEndContext;
let rollup: RollupContract;
let logger: Logger;
let monitor: ChainMonitor;

let L1_BLOCK_TIME_IN_S: number;

let test: EpochsTestContext;

beforeEach(async () => {
test = await EpochsTestContext.setup();
({ context, rollup, logger, monitor, L1_BLOCK_TIME_IN_S } = test);
({ context, rollup, logger } = test);
});

afterEach(async () => {
jest.restoreAllMocks();
await test.teardown();
});

it('submits proof even if there are no txs to build a block', async () => {
context.sequencer?.updateConfig({ minTxsPerBlock: 1 });
await test.waitUntilEpochStarts(1);

// Sleep to make sure any pending blocks are published
await sleep(L1_BLOCK_TIME_IN_S * 1000);
const blockNumberAtEndOfEpoch0 = Number(await rollup.getBlockNumber());
logger.info(`Starting epoch 1 after L2 block ${blockNumberAtEndOfEpoch0}`);

await test.waitUntilProvenL2BlockNumber(blockNumberAtEndOfEpoch0, 240);
expect(monitor.l2BlockNumber).toEqual(blockNumberAtEndOfEpoch0);
logger.info(`Test succeeded`);
});

it('successfully proves multiple epochs', async () => {
const targetProvenEpochs = process.env.TARGET_PROVEN_EPOCHS ? parseInt(process.env.TARGET_PROVEN_EPOCHS) : 3;
const targetProvenBlockNumber = targetProvenEpochs * test.epochDuration;

let provenBlockNumber = 0;
let epochNumber = 0;
logger.info(`Waiting for ${targetProvenEpochs} epochs to be proven at ${targetProvenBlockNumber} L2 blocks`);
while (provenBlockNumber < targetProvenBlockNumber) {
logger.info(`Waiting for the end of epoch ${epochNumber}`);
await test.waitUntilEpochStarts(epochNumber + 1);
Expand All @@ -71,7 +54,7 @@ describe('e2e_epochs/epochs_empty_blocks', () => {
await test.waitForNodeToSync(provenBlockNumber, 'proven');
await test.verifyHistoricBlock(provenBlockNumber, true);

// right now finalisation means a block is two L2 epochs deep. If this rule changes then we need this test needs to be updated
// right now finalization means a block is two L2 epochs deep. If this rule changes then we need this test needs to be updated
const finalizedBlockNumber = Math.max(provenBlockNumber - context.config.aztecEpochDuration * 2, 0);
const expectedOldestHistoricBlock = Math.max(finalizedBlockNumber - WORLD_STATE_BLOCK_HISTORY + 1, 1);
const expectedBlockRemoved = expectedOldestHistoricBlock - 1;
Expand Down
1 change: 1 addition & 0 deletions yarn-project/ethereum/src/contracts/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe('Registry', () => {
'feeAssetHandlerAddress',
'stakingAssetHandlerAddress',
'zkPassportVerifierAddress',
'dateGatedRelayerAddress',
);
registry = new RegistryContract(l1Client, deployedAddresses.registryAddress);

Expand Down
6 changes: 5 additions & 1 deletion yarn-project/ethereum/src/deploy_l1_contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ describe('deploy_l1_contracts', () => {
const registry = new RegistryContract(client, deployment.l1ContractAddresses.registryAddress);
const rollup = new RollupContract(client, deployment.l1ContractAddresses.rollupAddress);
const gse = new GSEContract(client, await rollup.getGSE());
const dateGatedRelayerAddress = deployment.l1ContractAddresses.dateGatedRelayerAddress!;

// Checking the shared
expect(await registry.getOwner()).toEqual(governance.address);
Expand All @@ -308,7 +309,10 @@ describe('deploy_l1_contracts', () => {
expect(await getOwner(deployment.l1ContractAddresses.rewardDistributorAddress, 'REGISTRY')).toEqual(
registry.address,
);
expect(await getOwner(deployment.l1ContractAddresses.coinIssuerAddress)).toEqual(governance.address);

// The coin issuer should be owned by governance, but indirectly through the date gated relayer
expect(await getOwner(deployment.l1ContractAddresses.coinIssuerAddress)).toEqual(dateGatedRelayerAddress);
expect(await getOwner(dateGatedRelayerAddress)).toEqual(governance.address);

expect(await getOwner(deployment.l1ContractAddresses.feeJuiceAddress)).toEqual(
deployment.l1ContractAddresses.coinIssuerAddress,
Expand Down
21 changes: 14 additions & 7 deletions yarn-project/ethereum/src/deploy_l1_contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { RegistryContract } from './contracts/registry.js';
import { RollupContract, SlashingProposerType } from './contracts/rollup.js';
import {
CoinIssuerArtifact,
DateGatedRelayerArtifact,
FeeAssetArtifact,
FeeAssetHandlerArtifact,
GSEArtifact,
Expand Down Expand Up @@ -935,28 +936,33 @@ export const handoverToGovernance = async (
logger.verbose('Skipping fee asset ownership transfer due to external token usage');
}

// Either deploy or at least predict the address of the date gated relayer
const dateGatedRelayer = await deployer.deploy(DateGatedRelayerArtifact, [
governanceAddress.toString(),
1798761600n, // 2027-01-01 00:00:00 UTC
]);

// If the owner is not the Governance contract, transfer ownership to the Governance contract
if (
acceleratedTestDeployments ||
(await coinIssuerContract.read.owner()) !== getAddress(governanceAddress.toString())
) {
if (acceleratedTestDeployments || (await coinIssuerContract.read.owner()) === deployer.client.account.address) {
const { txHash: transferOwnershipTxHash } = await deployer.sendTransaction({
to: coinIssuerContract.address,
data: encodeFunctionData({
abi: CoinIssuerArtifact.contractAbi,
functionName: 'transferOwnership',
args: [getAddress(governanceAddress.toString())],
args: [getAddress(dateGatedRelayer.address.toString())],
}),
});
logger.verbose(
`Transferring the ownership of the coin issuer contract at ${coinIssuerAddress} to the Governance ${governanceAddress} in tx ${transferOwnershipTxHash}`,
`Transferring the ownership of the coin issuer contract at ${coinIssuerAddress} to the DateGatedRelayer ${dateGatedRelayer.address} in tx ${transferOwnershipTxHash}`,
);
txHashes.push(transferOwnershipTxHash);
}

// Wait for all actions to be mined
await deployer.waitForDeployments();
await Promise.all(txHashes.map(txHash => extendedClient.waitForTransactionReceipt({ hash: txHash })));

return { dateGatedRelayerAddress: dateGatedRelayer.address };
};

/*
Expand Down Expand Up @@ -1249,7 +1255,7 @@ export const deployL1Contracts = async (
await deployer.waitForDeployments();

// Now that the rollup has been deployed and added to the registry, transfer ownership to governance
await handoverToGovernance(
const { dateGatedRelayerAddress } = await handoverToGovernance(
l1Client,
deployer,
registryAddress,
Expand Down Expand Up @@ -1444,6 +1450,7 @@ export const deployL1Contracts = async (
stakingAssetHandlerAddress,
zkPassportVerifierAddress,
coinIssuerAddress,
dateGatedRelayerAddress,
},
};
};
Expand Down
8 changes: 8 additions & 0 deletions yarn-project/ethereum/src/l1_artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
CoinIssuerAbi,
CoinIssuerBytecode,
DateGatedRelayerAbi,
DateGatedRelayerBytecode,
EmpireSlasherDeploymentExtLibAbi,
EmpireSlasherDeploymentExtLibBytecode,
EmpireSlashingProposerAbi,
Expand Down Expand Up @@ -149,6 +151,12 @@ export const CoinIssuerArtifact = {
contractBytecode: CoinIssuerBytecode as Hex,
};

export const DateGatedRelayerArtifact = {
name: 'DateGatedRelayer',
contractAbi: DateGatedRelayerAbi,
contractBytecode: DateGatedRelayerBytecode as Hex,
};

export const GovernanceProposerArtifact = {
name: 'GovernanceProposer',
contractAbi: GovernanceProposerAbi,
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/ethereum/src/l1_contract_addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type L1ContractAddresses = {
stakingAssetHandlerAddress?: EthAddress | undefined;
zkPassportVerifierAddress?: EthAddress | undefined;
gseAddress?: EthAddress | undefined;
dateGatedRelayerAddress?: EthAddress | undefined;
};

export const L1ContractAddressesSchema = z.object({
Expand All @@ -51,12 +52,13 @@ export const L1ContractAddressesSchema = z.object({
stakingAssetHandlerAddress: schemas.EthAddress.optional(),
zkPassportVerifierAddress: schemas.EthAddress.optional(),
gseAddress: schemas.EthAddress.optional(),
dateGatedRelayerAddress: schemas.EthAddress.optional(),
}) satisfies ZodFor<L1ContractAddresses>;

const parseEnv = (val: string) => EthAddress.fromString(val);

export const l1ContractAddressesMapping: ConfigMappingsType<
Omit<L1ContractAddresses, 'gseAddress' | 'zkPassportVerifierAddress'>
Omit<L1ContractAddresses, 'gseAddress' | 'zkPassportVerifierAddress' | 'dateGatedRelayerAddress'>
> = {
registryAddress: {
env: 'REGISTRY_CONTRACT_ADDRESS',
Expand Down
1 change: 1 addition & 0 deletions yarn-project/l1-artifacts/scripts/generate-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ contracts=(
"MultiAdder"
"GSE"
"MockZKPassportVerifier"
"DateGatedRelayer"
)

# Combine error ABIs once, removing duplicates by {type, name}.
Expand Down