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

import {Test} from "forge-std/Test.sol";
import {Math} from "@oz/utils/math/Math.sol";

interface IStateView {
function getSlot0(bytes32 poolId)
external
view
returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee);
}

contract UniswapLookupScript is Test {
function lookAtUniswap() public {
// Uniswap V4 StateView contract on mainnet
IStateView stateView = IStateView(0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227);

address currency0 = address(0); // Native ETH
address currency1 = 0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2; // Fee asset token
uint24 fee = 500; // 0.05%
int24 tickSpacing = 10;
address hooks = 0xd53006d1e3110fD319a79AEEc4c527a0d265E080;

// Compute pool ID: keccak256(abi.encode(currency0, currency1, fee, tickSpacing, hooks))
bytes32 poolId = keccak256(abi.encode(currency0, currency1, fee, tickSpacing, hooks));
emit log_named_bytes32("Pool ID", poolId);

// Query the real pool state
(uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee) = stateView.getSlot0(poolId);

emit log_named_uint("sqrtPriceX96", sqrtPriceX96);
emit log_named_int("tick", tick);
emit log_named_uint("protocolFee", protocolFee);
emit log_named_uint("lpFee", lpFee);

// Convert to ethPerFeeAssetE12
// ethPerFeeAssetE12 = 1e12 * 2^192 / sqrtPriceX96^2
uint256 Q192 = 2 ** 192;
uint256 sqrtPriceSquared = uint256(sqrtPriceX96) * uint256(sqrtPriceX96);
uint256 ethPerFeeAssetE12 = (1e12 * Q192) / sqrtPriceSquared;

emit log_named_decimal_uint("ethPerFeeAssetE12 (computed)", ethPerFeeAssetE12, 12);

// Compute what sqrtPriceX96 would give us a price 0.5% higher
uint256 targetPriceHalfPercentHigher = (ethPerFeeAssetE12 * 1005) / 1000;
emit log_named_decimal_uint("Target price (0.5% higher)", targetPriceHalfPercentHigher, 12);

// sqrtPriceX96^2 = 1e12 * 2^192 / targetPrice
uint256 targetSqrtSquared = (1e12 * Q192) / targetPriceHalfPercentHigher;
uint256 targetSqrtPriceX96 = Math.sqrt(targetSqrtSquared);
emit log_named_uint("Target sqrtPriceX96", targetSqrtPriceX96);

// Verify: compute the price back from the sqrt
uint256 verifyPrice = (1e12 * Q192) / (targetSqrtPriceX96 * targetSqrtPriceX96);
assertEq(verifyPrice, targetPriceHalfPercentHigher);
}
}
5 changes: 3 additions & 2 deletions yarn-project/archiver/src/l1/calldata_retriever.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ describe('CalldataRetriever', () => {
const attestations = makeViemCommitteeAttestations();
const archiveRoot = Fr.random();
const archive = archiveRoot.toString() as Hex;
const feeAssetPriceModifier = BigInt(0);
const feeAssetPriceModifier = BigInt(-1);

// Create propose calldata with known values
const proposeCalldata = encodeFunctionData({
Expand All @@ -355,8 +355,9 @@ describe('CalldataRetriever', () => {
publicClient.getTransaction.mockResolvedValue(tx);

// Compute the expected payloadDigest using ConsensusPayload (same logic as the validator)
// Note: feeAssetPriceModifier is 0n in makeProposeCalldata
const checkpointHeader = CheckpointHeader.fromViem(header);
const consensusPayload = new ConsensusPayload(checkpointHeader, archiveRoot);
const consensusPayload = new ConsensusPayload(checkpointHeader, archiveRoot, feeAssetPriceModifier);
const payloadToSign = consensusPayload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation);
const expectedPayloadDigest = keccak256(payloadToSign);

Expand Down
6 changes: 5 additions & 1 deletion yarn-project/archiver/src/l1/calldata_retriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class CalldataRetriever {
header: CheckpointHeader;
attestations: CommitteeAttestation[];
blockHash: string;
feeAssetPriceModifier: bigint;
}> {
this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}`, {
willValidateHashes: !!expectedHashes.attestationsHash || !!expectedHashes.payloadDigest,
Expand Down Expand Up @@ -403,6 +404,7 @@ export class CalldataRetriever {
header: CheckpointHeader;
attestations: CommitteeAttestation[];
blockHash: string;
feeAssetPriceModifier: bigint;
} {
const { functionName: rollupFunctionName, args: rollupArgs } = decodeFunctionData({
abi: RollupAbi,
Expand Down Expand Up @@ -458,7 +460,8 @@ export class CalldataRetriever {
if (expectedHashes.payloadDigest) {
// Use ConsensusPayload to compute the digest - this ensures we match the exact logic
// used by the network for signing and verification
const consensusPayload = new ConsensusPayload(header, archiveRoot);
const feeAssetPriceModifier = decodedArgs.oracleInput.feeAssetPriceModifier;
const consensusPayload = new ConsensusPayload(header, archiveRoot, feeAssetPriceModifier);
const payloadToSign = consensusPayload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation);
const computedPayloadDigest = keccak256(payloadToSign);

Expand Down Expand Up @@ -495,6 +498,7 @@ export class CalldataRetriever {
header,
attestations,
blockHash,
feeAssetPriceModifier: decodedArgs.oracleInput.feeAssetPriceModifier,
};
}
}
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/archiver/src/l1/data_retrieval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('data_retrieval', () => {
const retrievedCheckpoint: RetrievedCheckpoint = {
checkpointNumber: CheckpointNumber(1),
archiveRoot,
feeAssetPriceModifier: 0n,
header: CheckpointHeader.random(),
checkpointBlobData,
l1: new L1PublishedData(1n, 1000n, '0x1234'),
Expand Down Expand Up @@ -94,6 +95,7 @@ describe('data_retrieval', () => {
const retrievedCheckpoint: RetrievedCheckpoint = {
checkpointNumber: CheckpointNumber(1),
archiveRoot,
feeAssetPriceModifier: 0n,
header: CheckpointHeader.random(),
checkpointBlobData,
l1: new L1PublishedData(1n, 1000n, '0x1234'),
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/archiver/src/l1/data_retrieval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { CalldataRetriever } from './calldata_retriever.js';
export type RetrievedCheckpoint = {
checkpointNumber: CheckpointNumber;
archiveRoot: Fr;
feeAssetPriceModifier: bigint;
header: CheckpointHeader;
checkpointBlobData: CheckpointBlobData;
l1: L1PublishedData;
Expand All @@ -49,6 +50,7 @@ export type RetrievedCheckpoint = {
export async function retrievedToPublishedCheckpoint({
checkpointNumber,
archiveRoot,
feeAssetPriceModifier,
header: checkpointHeader,
checkpointBlobData,
l1,
Expand Down Expand Up @@ -128,6 +130,7 @@ export async function retrievedToPublishedCheckpoint({
header: checkpointHeader,
blocks: l2Blocks,
number: checkpointNumber,
feeAssetPriceModifier: feeAssetPriceModifier,
});

return PublishedCheckpoint.from({ checkpoint, l1, attestations });
Expand Down
22 changes: 20 additions & 2 deletions yarn-project/archiver/src/modules/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ describe('validateCheckpointAttestations', () => {

const constants = { epochDuration: 10 };

const makeCheckpoint = async (signers: Secp256k1Signer[], committee: EthAddress[], slot?: number) => {
const checkpoint = await Checkpoint.random(CheckpointNumber(1), { slotNumber: SlotNumber(slot ?? 1) });
const makeCheckpoint = async (
signers: Secp256k1Signer[],
committee: EthAddress[],
slot?: number,
feeAssetPriceModifier?: bigint,
) => {
const checkpoint = await Checkpoint.random(CheckpointNumber(1), {
slotNumber: SlotNumber(slot ?? 1),
feeAssetPriceModifier,
});
return makeSignedPublishedCheckpoint(checkpoint, signers, committee);
};

Expand Down Expand Up @@ -79,6 +87,16 @@ describe('validateCheckpointAttestations', () => {
setCommittee(committee);
});

it('uses feeAssetPriceModifier when recovering attestors', async () => {
const checkpoint = await makeCheckpoint(signers.slice(0, 4), committee, 1, 1n);

const attestationInfos = getAttestationInfoFromPublishedCheckpoint(checkpoint);
expect(attestationInfos.filter(a => a.status === 'recovered-from-signature').length).toBe(4);

const result = await validateCheckpointAttestations(checkpoint, epochCache, constants, logger);
expect(result.valid).toBe(true);
});

it('requests committee for the correct epoch', async () => {
const checkpoint = await makeCheckpoint(signers, committee, 28);
await validateCheckpointAttestations(checkpoint, epochCache, constants, logger);
Expand Down
119 changes: 119 additions & 0 deletions yarn-project/end-to-end/src/e2e_fee_asset_price_oracle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { Logger } from '@aztec/aztec.js/log';
import { EthCheatCodes } from '@aztec/aztec/testing';
import { createExtendedL1Client } from '@aztec/ethereum/client';
import { RollupContract, STATE_VIEW_ADDRESS } from '@aztec/ethereum/contracts';
import { retryUntil } from '@aztec/foundation/retry';
import { DateProvider } from '@aztec/foundation/timer';

import { jest } from '@jest/globals';
import type { Anvil } from '@viem/anvil';
import { mnemonicToAccount } from 'viem/accounts';
import { foundry } from 'viem/chains';

import { MNEMONIC } from './fixtures/fixtures.js';
import { getLogger, setup, startAnvil } from './fixtures/utils.js';
import { MockStateView, diffInBps } from './shared/mock_state_view.js';

describe('FeeAssetPriceOracle E2E', () => {
jest.setTimeout(300_000);

let logger: Logger;
let teardown: () => Promise<void>;
let anvil: Anvil;
let rollup: RollupContract;
let mockStateView: MockStateView;
let ethCheatCodes: EthCheatCodes;

// Beware, if you use "mainnet" here it will be completely broken due to blobs...
const chain = foundry;

beforeAll(async () => {
logger = getLogger();

const anvilResult = await startAnvil({ chainId: chain.id });
anvil = anvilResult.anvil;
const rpcUrl = anvilResult.rpcUrl;

// Set ETHEREUM_HOSTS so setup() uses our pre-started Anvil
process.env.ETHEREUM_HOSTS = rpcUrl;

// Deploy mock StateView BEFORE the full setup, so the oracle can read from it
ethCheatCodes = new EthCheatCodes([rpcUrl], new DateProvider());
const account = mnemonicToAccount(MNEMONIC, { addressIndex: 999 });
const walletClient = createExtendedL1Client([rpcUrl], account, chain);

await ethCheatCodes.setBalance(account.address, 100n * 10n ** 18n);

mockStateView = await MockStateView.deploy(ethCheatCodes, walletClient, STATE_VIEW_ADDRESS);
logger.info(`Deployed mock StateView at ${STATE_VIEW_ADDRESS}`);

// The initial oracle price (default value) is 1e7
await mockStateView.setEthPerFeeAsset(10n ** 7n);

await ethCheatCodes.mineEmptyBlock();
await ethCheatCodes.mine(10);
await ethCheatCodes.mineEmptyBlock();

const context = await setup(0, { l1ChainId: chain.id, minTxsPerBlock: 0 }, {}, chain);
teardown = context.teardown;

const l1Client = context.deployL1ContractsValues.l1Client;
rollup = new RollupContract(l1Client, context.deployL1ContractsValues.l1ContractAddresses.rollupAddress);
});

afterAll(async () => {
await teardown?.();
await anvil?.stop().catch(err => logger.error('Failed to stop anvil', err));
delete process.env.ETHEREUM_HOSTS;
});

it('on-chain price converges toward oracle price over multiple checkpoints', async () => {
// Move the price up 2.5% (2 moves of 1% and another smaller)
// Wait until we are within 1 bps or the price
// Then move the price down 0.5%
// Wait until 1 bps of the price
// Profit

const targetOraclePrice = (BigInt(10n ** 7n) * 1025n) / 1000n;
await mockStateView.setEthPerFeeAsset(targetOraclePrice);
logger.info(`Set uniswap price to ${targetOraclePrice}`);

// Get initial on-chain price
const initialOnChainPrice = await rollup.getEthPerFeeAsset();
logger.info(`Initial on-chain price: ${initialOnChainPrice}, target oracle price: ${targetOraclePrice}`);

await retryUntil(
async () => {
const currentPrice = await rollup.getEthPerFeeAsset();
logger.info(`Current on-chain price: ${currentPrice}, waiting for: ${targetOraclePrice}`);
return diffInBps(currentPrice, targetOraclePrice) == 0n;
},
'price convergence toward oracle',
120, // timeout in seconds
5, // check interval in seconds
);

const priceAfterFirstAlignment = await rollup.getEthPerFeeAsset();
const targetOraclePrice2 = (BigInt(priceAfterFirstAlignment) * 995n) / 1000n;
await mockStateView.setEthPerFeeAsset(targetOraclePrice2);
logger.info(`Set uniswap price to ${targetOraclePrice}`);

await retryUntil(
async () => {
const currentPrice = await rollup.getEthPerFeeAsset();
logger.info(`Current on-chain price: ${currentPrice}, waiting for: ${targetOraclePrice2}`);
return diffInBps(currentPrice, targetOraclePrice2) == 0n;
},
'price convergence toward oracle',
120, // timeout in seconds
5, // check interval in seconds
);

const finalPrice = await rollup.getEthPerFeeAsset();
logger.info(`Final on-chain price: ${finalPrice}`);

// Verify the price moved toward the oracle price
expect(finalPrice).toBeGreaterThan(initialOnChainPrice);
expect(diffInBps(finalPrice, targetOraclePrice2)).toBe(0n);
});
});
Loading
Loading