@aztec/ethereum 0.0.1-commit.6d63667d → 0.0.1-commit.7035c9bd6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client.d.ts +10 -2
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +13 -7
- package/dest/config.d.ts +3 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -3
- package/dest/contracts/empire_base.d.ts +3 -1
- package/dest/contracts/empire_base.d.ts.map +1 -1
- package/dest/contracts/empire_slashing_proposer.d.ts +3 -1
- package/dest/contracts/empire_slashing_proposer.d.ts.map +1 -1
- package/dest/contracts/empire_slashing_proposer.js +9 -0
- package/dest/contracts/fee_asset_price_oracle.d.ts +101 -0
- package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -0
- package/dest/contracts/fee_asset_price_oracle.js +651 -0
- package/dest/contracts/governance.js +3 -3
- package/dest/contracts/governance_proposer.d.ts +3 -1
- package/dest/contracts/governance_proposer.d.ts.map +1 -1
- package/dest/contracts/governance_proposer.js +9 -0
- package/dest/contracts/index.d.ts +2 -1
- package/dest/contracts/index.d.ts.map +1 -1
- package/dest/contracts/index.js +1 -0
- package/dest/contracts/registry.d.ts +3 -1
- package/dest/contracts/registry.d.ts.map +1 -1
- package/dest/contracts/registry.js +30 -1
- package/dest/contracts/rollup.d.ts +21 -7
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +62 -9
- package/dest/deploy_aztec_l1_contracts.d.ts +2 -3
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +33 -19
- package/dest/deploy_l1_contract.js +3 -3
- package/dest/generated/l1-contracts-defaults.d.ts +1 -1
- package/dest/generated/l1-contracts-defaults.js +1 -1
- package/dest/l1_artifacts.d.ts +1051 -42
- package/dest/l1_artifacts.d.ts.map +1 -1
- package/dest/l1_reader.d.ts +3 -1
- package/dest/l1_reader.d.ts.map +1 -1
- package/dest/l1_reader.js +6 -1
- package/dest/l1_tx_utils/config.d.ts +7 -1
- package/dest/l1_tx_utils/config.d.ts.map +1 -1
- package/dest/l1_tx_utils/config.js +14 -1
- package/dest/l1_tx_utils/factory.d.ts +18 -10
- package/dest/l1_tx_utils/factory.d.ts.map +1 -1
- package/dest/l1_tx_utils/factory.js +17 -7
- package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +1 -1
- package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +1 -1
- package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts +15 -15
- package/dest/l1_tx_utils/forwarder_l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/forwarder_l1_tx_utils.js +9 -15
- package/dest/l1_tx_utils/index-blobs.d.ts +3 -3
- package/dest/l1_tx_utils/index-blobs.d.ts.map +1 -1
- package/dest/l1_tx_utils/index-blobs.js +2 -2
- package/dest/l1_tx_utils/index.d.ts +2 -1
- package/dest/l1_tx_utils/index.d.ts.map +1 -1
- package/dest/l1_tx_utils/index.js +1 -0
- package/dest/l1_tx_utils/l1_tx_utils.d.ts +18 -7
- package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_tx_utils.js +52 -36
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.js +8 -4
- package/dest/l1_tx_utils/tx_delayer.d.ts +56 -0
- package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -0
- package/dest/{test → l1_tx_utils}/tx_delayer.js +62 -34
- package/dest/test/chain_monitor.d.ts +22 -3
- package/dest/test/chain_monitor.d.ts.map +1 -1
- package/dest/test/chain_monitor.js +33 -2
- package/dest/test/index.d.ts +1 -3
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +0 -2
- package/dest/test/start_anvil.d.ts +16 -3
- package/dest/test/start_anvil.d.ts.map +1 -1
- package/dest/test/start_anvil.js +129 -29
- package/dest/test/upgrade_utils.js +2 -2
- package/dest/utils.d.ts +1 -1
- package/dest/utils.d.ts.map +1 -1
- package/dest/utils.js +16 -12
- package/package.json +5 -7
- package/src/client.ts +10 -2
- package/src/config.ts +15 -2
- package/src/contracts/empire_base.ts +2 -0
- package/src/contracts/empire_slashing_proposer.ts +6 -0
- package/src/contracts/fee_asset_price_oracle.ts +280 -0
- package/src/contracts/governance.ts +3 -3
- package/src/contracts/governance_proposer.ts +6 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/registry.ts +31 -1
- package/src/contracts/rollup.ts +74 -12
- package/src/deploy_aztec_l1_contracts.ts +56 -29
- package/src/deploy_l1_contract.ts +3 -3
- package/src/generated/l1-contracts-defaults.ts +1 -1
- package/src/l1_reader.ts +13 -1
- package/src/l1_tx_utils/config.ts +20 -0
- package/src/l1_tx_utils/factory.ts +31 -31
- package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +1 -1
- package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +1 -1
- package/src/l1_tx_utils/forwarder_l1_tx_utils.ts +43 -54
- package/src/l1_tx_utils/index-blobs.ts +2 -2
- package/src/l1_tx_utils/index.ts +1 -0
- package/src/l1_tx_utils/l1_tx_utils.ts +54 -26
- package/src/l1_tx_utils/readonly_l1_tx_utils.ts +8 -4
- package/src/{test → l1_tx_utils}/tx_delayer.ts +78 -50
- package/src/test/chain_monitor.ts +60 -3
- package/src/test/index.ts +0 -2
- package/src/test/start_anvil.ts +154 -29
- package/src/test/upgrade_utils.ts +2 -2
- package/src/utils.ts +17 -14
- package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts +0 -26
- package/dest/l1_tx_utils/l1_tx_utils_with_blobs.d.ts.map +0 -1
- package/dest/l1_tx_utils/l1_tx_utils_with_blobs.js +0 -26
- package/dest/test/delayed_tx_utils.d.ts +0 -13
- package/dest/test/delayed_tx_utils.d.ts.map +0 -1
- package/dest/test/delayed_tx_utils.js +0 -28
- package/dest/test/tx_delayer.d.ts +0 -36
- package/dest/test/tx_delayer.d.ts.map +0 -1
- package/src/l1_tx_utils/l1_tx_utils_with_blobs.ts +0 -77
- package/src/test/delayed_tx_utils.ts +0 -52
package/dest/utils.js
CHANGED
|
@@ -112,6 +112,19 @@ function getNestedErrorData(error) {
|
|
|
112
112
|
// Not found
|
|
113
113
|
return undefined;
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Truncates an error message to a safe length for log renderers.
|
|
117
|
+
* LogExplorer can only render up to 2500 characters in its summary view.
|
|
118
|
+
* We cap at 2000 to leave room for decorating context added by callers.
|
|
119
|
+
*/ function truncateErrorMessage(message) {
|
|
120
|
+
const MAX = 2000;
|
|
121
|
+
const CHUNK = 950;
|
|
122
|
+
if (message.length <= MAX) {
|
|
123
|
+
return message;
|
|
124
|
+
}
|
|
125
|
+
const truncated = message.length - 2 * CHUNK;
|
|
126
|
+
return message.slice(0, CHUNK) + `...${truncated} characters truncated...` + message.slice(-CHUNK);
|
|
127
|
+
}
|
|
115
128
|
/**
|
|
116
129
|
* Formats a Viem error into a FormattedViemError instance.
|
|
117
130
|
* @param error - The error to format.
|
|
@@ -162,18 +175,9 @@ function getNestedErrorData(error) {
|
|
|
162
175
|
}
|
|
163
176
|
// If it's a regular Error instance, return it with its message
|
|
164
177
|
if (error instanceof Error) {
|
|
165
|
-
return new FormattedViemError(error.message, error?.metaMessages);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const length = body.length;
|
|
169
|
-
// LogExplorer can only render up to 2500 characters in it's summary view. Try to keep the whole message below this number
|
|
170
|
-
// Limit the error to 2000 chacaters in order to allow code higher up to decorate this error with extra details (up to 500 characters)
|
|
171
|
-
if (length > 2000) {
|
|
172
|
-
const chunk = 950;
|
|
173
|
-
const truncated = length - 2 * chunk;
|
|
174
|
-
return new FormattedViemError(body.slice(0, chunk) + `...${truncated} characters truncated...` + body.slice(-1 * chunk));
|
|
175
|
-
}
|
|
176
|
-
return new FormattedViemError(body);
|
|
178
|
+
return new FormattedViemError(truncateErrorMessage(error.message), error?.metaMessages);
|
|
179
|
+
}
|
|
180
|
+
return new FormattedViemError(truncateErrorMessage(String(error)));
|
|
177
181
|
}
|
|
178
182
|
function stripAbis(obj) {
|
|
179
183
|
if (!obj || typeof obj !== 'object') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/ethereum",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.7035c9bd6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./account": "./dest/account.js",
|
|
@@ -50,11 +50,10 @@
|
|
|
50
50
|
"../package.common.json"
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
54
|
-
"@aztec/constants": "0.0.1-commit.
|
|
55
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
56
|
-
"@aztec/l1-artifacts": "0.0.1-commit.
|
|
57
|
-
"@viem/anvil": "^0.0.10",
|
|
53
|
+
"@aztec/blob-lib": "0.0.1-commit.7035c9bd6",
|
|
54
|
+
"@aztec/constants": "0.0.1-commit.7035c9bd6",
|
|
55
|
+
"@aztec/foundation": "0.0.1-commit.7035c9bd6",
|
|
56
|
+
"@aztec/l1-artifacts": "0.0.1-commit.7035c9bd6",
|
|
58
57
|
"dotenv": "^16.0.3",
|
|
59
58
|
"lodash.chunk": "^4.2.0",
|
|
60
59
|
"lodash.pickby": "^4.5.0",
|
|
@@ -69,7 +68,6 @@
|
|
|
69
68
|
"@types/lodash.pickby": "^4",
|
|
70
69
|
"@types/node": "^22.15.17",
|
|
71
70
|
"@typescript/native-preview": "7.0.0-dev.20260113.1",
|
|
72
|
-
"@viem/anvil": "^0.0.10",
|
|
73
71
|
"get-port": "^7.1.0",
|
|
74
72
|
"jest": "^30.0.0",
|
|
75
73
|
"jest-mock-extended": "^4.0.0",
|
package/src/client.ts
CHANGED
|
@@ -25,10 +25,17 @@ type Config = {
|
|
|
25
25
|
l1ChainId: number;
|
|
26
26
|
/** The polling interval viem uses in ms */
|
|
27
27
|
viemPollingIntervalMS?: number;
|
|
28
|
+
/** Timeout for HTTP requests to the L1 RPC node in ms. */
|
|
29
|
+
l1HttpTimeoutMS?: number;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
export type { Config as EthereumClientConfig };
|
|
31
33
|
|
|
34
|
+
/** Creates a viem fallback HTTP transport for the given L1 RPC URLs. */
|
|
35
|
+
export function makeL1HttpTransport(rpcUrls: string[], opts?: { timeout?: number }) {
|
|
36
|
+
return fallback(rpcUrls.map(url => http(url, { batch: false, timeout: opts?.timeout })));
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
// TODO: Use these methods to abstract the creation of viem clients.
|
|
33
40
|
|
|
34
41
|
/** Returns a viem public client given the L1 config. */
|
|
@@ -36,7 +43,7 @@ export function getPublicClient(config: Config): ViemPublicClient {
|
|
|
36
43
|
const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
|
|
37
44
|
return createPublicClient({
|
|
38
45
|
chain: chain.chainInfo,
|
|
39
|
-
transport:
|
|
46
|
+
transport: makeL1HttpTransport(config.l1RpcUrls, { timeout: config.l1HttpTimeoutMS }),
|
|
40
47
|
pollingInterval: config.viemPollingIntervalMS,
|
|
41
48
|
});
|
|
42
49
|
}
|
|
@@ -77,6 +84,7 @@ export function createExtendedL1Client(
|
|
|
77
84
|
chain: Chain = foundry,
|
|
78
85
|
pollingIntervalMS?: number,
|
|
79
86
|
addressIndex?: number,
|
|
87
|
+
opts?: { httpTimeoutMS?: number },
|
|
80
88
|
): ExtendedViemWalletClient {
|
|
81
89
|
const hdAccount =
|
|
82
90
|
typeof mnemonicOrPrivateKeyOrHdAccount === 'string'
|
|
@@ -88,7 +96,7 @@ export function createExtendedL1Client(
|
|
|
88
96
|
const extendedClient = createWalletClient({
|
|
89
97
|
account: hdAccount,
|
|
90
98
|
chain,
|
|
91
|
-
transport:
|
|
99
|
+
transport: makeL1HttpTransport(rpcUrls, { timeout: opts?.httpTimeoutMS }),
|
|
92
100
|
pollingInterval: pollingIntervalMS,
|
|
93
101
|
}).extend(publicActions);
|
|
94
102
|
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getConfigFromMappings,
|
|
7
7
|
getDefaultConfig,
|
|
8
8
|
numberConfigHelper,
|
|
9
|
+
omitConfigMappings,
|
|
9
10
|
optionalNumberConfigHelper,
|
|
10
11
|
} from '@aztec/foundation/config';
|
|
11
12
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -18,6 +19,8 @@ export type GenesisStateConfig = {
|
|
|
18
19
|
testAccounts: boolean;
|
|
19
20
|
/** Whether to populate the genesis state with initial fee juice for the sponsored FPC */
|
|
20
21
|
sponsoredFPC: boolean;
|
|
22
|
+
/** Additional addresses to prefund with fee juice at genesis */
|
|
23
|
+
prefundAddresses: string[];
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
export type L1ContractsConfig = {
|
|
@@ -99,7 +102,7 @@ export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> =
|
|
|
99
102
|
},
|
|
100
103
|
aztecEpochDuration: {
|
|
101
104
|
env: 'AZTEC_EPOCH_DURATION',
|
|
102
|
-
description: `How many L2 slots an epoch lasts (maximum
|
|
105
|
+
description: `How many L2 slots an epoch lasts (maximum MAX_CHECKPOINTS_PER_EPOCH).`,
|
|
103
106
|
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_EPOCH_DURATION),
|
|
104
107
|
},
|
|
105
108
|
aztecTargetCommitteeSize: {
|
|
@@ -238,7 +241,7 @@ export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> =
|
|
|
238
241
|
description: 'The delay before a validator can exit the set',
|
|
239
242
|
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_EXIT_DELAY_SECONDS),
|
|
240
243
|
},
|
|
241
|
-
...l1TxUtilsConfigMappings,
|
|
244
|
+
...omitConfigMappings(l1TxUtilsConfigMappings, ['ethereumSlotDuration']),
|
|
242
245
|
};
|
|
243
246
|
|
|
244
247
|
/**
|
|
@@ -258,6 +261,16 @@ export const genesisStateConfigMappings: ConfigMappingsType<GenesisStateConfig>
|
|
|
258
261
|
description: 'Whether to populate the genesis state with initial fee juice for the sponsored FPC.',
|
|
259
262
|
...booleanConfigHelper(false),
|
|
260
263
|
},
|
|
264
|
+
prefundAddresses: {
|
|
265
|
+
env: 'PREFUND_ADDRESSES',
|
|
266
|
+
description: 'Comma-separated list of Aztec addresses to prefund with fee juice at genesis (local network only).',
|
|
267
|
+
parseEnv: (val: string) =>
|
|
268
|
+
val
|
|
269
|
+
.split(',')
|
|
270
|
+
.map(a => a.trim())
|
|
271
|
+
.filter(a => a.length > 0),
|
|
272
|
+
defaultValue: [],
|
|
273
|
+
},
|
|
261
274
|
};
|
|
262
275
|
|
|
263
276
|
export function getL1ContractsConfigEnvVars(): L1ContractsConfig {
|
|
@@ -22,6 +22,8 @@ export interface IEmpireBase {
|
|
|
22
22
|
signerAddress: Hex,
|
|
23
23
|
signer: (msg: TypedDataDefinition) => Promise<Hex>,
|
|
24
24
|
): Promise<L1TxRequest>;
|
|
25
|
+
/** Checks if a payload was ever submitted to governance via submitRoundWinner. */
|
|
26
|
+
hasPayloadBeenProposed(payload: Hex, fromBlock: bigint): Promise<boolean>;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function encodeSignal(payload: Hex): Hex {
|
|
@@ -126,6 +126,12 @@ export class EmpireSlashingProposerContract extends EventEmitter implements IEmp
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** Checks if a payload was ever submitted to governance via submitRoundWinner. */
|
|
130
|
+
public async hasPayloadBeenProposed(payload: Hex, fromBlock: bigint): Promise<boolean> {
|
|
131
|
+
const events = await this.proposer.getEvents.PayloadSubmitted({ payload }, { fromBlock, strict: true });
|
|
132
|
+
return events.length > 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
129
135
|
public listenToSubmittablePayloads(callback: (args: { payload: `0x${string}`; round: bigint }) => unknown) {
|
|
130
136
|
return this.proposer.watchEvent.PayloadSubmittable(
|
|
131
137
|
{},
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { memoize } from '@aztec/foundation/decorators';
|
|
2
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
4
|
+
|
|
5
|
+
import { type Hex, encodeAbiParameters, getContract, keccak256, parseAbiParameters } from 'viem';
|
|
6
|
+
|
|
7
|
+
import type { ViemClient } from '../types.js';
|
|
8
|
+
import { RollupContract } from './rollup.js';
|
|
9
|
+
|
|
10
|
+
/** Maximum price modifier per checkpoint in basis points. ±100 bps = ±1% */
|
|
11
|
+
export const MAX_FEE_ASSET_PRICE_MODIFIER_BPS = 100n;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates that a fee asset price modifier is within the allowed range.
|
|
15
|
+
* Validators should call this before attesting to a checkpoint proposal.
|
|
16
|
+
*
|
|
17
|
+
* @param modifier - The fee asset price modifier in basis points
|
|
18
|
+
* @returns true if the modifier is valid (between -100 and +100 bps)
|
|
19
|
+
*/
|
|
20
|
+
export function validateFeeAssetPriceModifier(modifier: bigint): boolean {
|
|
21
|
+
return modifier >= -MAX_FEE_ASSET_PRICE_MODIFIER_BPS && modifier <= MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Oracle for computing fee asset price modifiers based on Uniswap V4 pool prices.
|
|
26
|
+
* Only active on Ethereum mainnet - returns 0 on other chains.
|
|
27
|
+
*/
|
|
28
|
+
export class FeeAssetPriceOracle {
|
|
29
|
+
constructor(
|
|
30
|
+
private client: ViemClient,
|
|
31
|
+
private readonly rollupContract: RollupContract,
|
|
32
|
+
private log: Logger = createLogger('fee-asset-price-oracle'),
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
@memoize
|
|
36
|
+
async getUniswapOracle(): Promise<UniswapPriceOracle | undefined> {
|
|
37
|
+
const code = await this.client.getCode({ address: STATE_VIEW_ADDRESS.toString() });
|
|
38
|
+
if (code === undefined || code === '0x') {
|
|
39
|
+
this.log.warn('Uniswap V4 StateView contract not found, skipping fee asset price oracle');
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
this.log.info('Uniswap V4 StateView contract found, initializing fee asset price oracle');
|
|
43
|
+
const oracle = new UniswapPriceOracle(this.client, this.log);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (!(await oracle.isPoolInitialized())) {
|
|
47
|
+
this.log.warn('Uniswap V4 pool not initialized, skipping fee asset price oracle');
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
this.log.warn(`Failed to check if Uniswap V4 pool is initialized: ${err}`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return oracle;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Computes the fee asset price modifier to be used in the next checkpoint proposal.
|
|
60
|
+
*
|
|
61
|
+
* The modifier adjusts the on-chain fee asset price toward the oracle price,
|
|
62
|
+
* clamped to ±1% (±100 basis points) per checkpoint.
|
|
63
|
+
*
|
|
64
|
+
* Returns 0 if not on mainnet or if the oracle query fails.
|
|
65
|
+
*
|
|
66
|
+
* @returns The price modifier in basis points (positive to increase price, negative to decrease)
|
|
67
|
+
*/
|
|
68
|
+
async computePriceModifier(): Promise<bigint> {
|
|
69
|
+
const uniswapOracle = await this.getUniswapOracle();
|
|
70
|
+
if (!uniswapOracle) {
|
|
71
|
+
return 0n;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Get current on-chain price (ETH per fee asset, E12)
|
|
76
|
+
const currentPriceE12 = await this.rollupContract.getEthPerFeeAsset();
|
|
77
|
+
|
|
78
|
+
// Get oracle price (median of last N blocks, ETH per fee asset, E12)
|
|
79
|
+
const oraclePriceE12 = await uniswapOracle.getMeanEthPerFeeAssetE12();
|
|
80
|
+
|
|
81
|
+
// Compute modifier in basis points
|
|
82
|
+
const modifier = this.computePriceModifierBps(currentPriceE12, oraclePriceE12);
|
|
83
|
+
|
|
84
|
+
this.log.debug('Computed price modifier', {
|
|
85
|
+
currentPriceE12: currentPriceE12.toString(),
|
|
86
|
+
oraclePriceE12: oraclePriceE12.toString(),
|
|
87
|
+
modifierBps: modifier.toString(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return modifier;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.log.warn(`Failed to compute price modifier, using 0: ${err}`);
|
|
93
|
+
return 0n;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Gets the current oracle price (ETH per fee asset, scaled by 1e12).
|
|
99
|
+
* Returns undefined if not on mainnet or if the oracle query fails.
|
|
100
|
+
*/
|
|
101
|
+
async getOraclePrice(): Promise<bigint | undefined> {
|
|
102
|
+
const uniswapOracle = await this.getUniswapOracle();
|
|
103
|
+
if (!uniswapOracle) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
return await uniswapOracle.getMeanEthPerFeeAssetE12();
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this.log.warn(`Failed to get oracle price: ${err}`);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Computes the basis points modifier needed to move from current price toward target price.
|
|
117
|
+
*
|
|
118
|
+
* @param currentPrice - Current ETH per fee asset (E12 scale)
|
|
119
|
+
* @param targetPrice - Target ETH per fee asset (E12 scale)
|
|
120
|
+
* @returns Basis points modifier clamped to ±100 (±1%)
|
|
121
|
+
*/
|
|
122
|
+
computePriceModifierBps(currentPrice: bigint, targetPrice: bigint): bigint {
|
|
123
|
+
if (currentPrice === 0n) {
|
|
124
|
+
return MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Calculate percentage difference in basis points
|
|
128
|
+
// modifierBps = ((targetPrice - currentPrice) / currentPrice) * 10000
|
|
129
|
+
const diff = targetPrice - currentPrice;
|
|
130
|
+
const rawModifierBps = (diff * 10_000n) / currentPrice;
|
|
131
|
+
|
|
132
|
+
// Clamp to ±MAX_FEE_ASSET_PRICE_MODIFIER_BPS
|
|
133
|
+
if (rawModifierBps > MAX_FEE_ASSET_PRICE_MODIFIER_BPS) {
|
|
134
|
+
return MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
|
|
135
|
+
}
|
|
136
|
+
if (rawModifierBps < -MAX_FEE_ASSET_PRICE_MODIFIER_BPS) {
|
|
137
|
+
return -MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
|
|
138
|
+
}
|
|
139
|
+
return rawModifierBps;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Mainnet Uniswap V4 StateView contract address */
|
|
144
|
+
export const STATE_VIEW_ADDRESS = EthAddress.fromString('0x7ffe42c4a5deea5b0fec41c94c136cf115597227');
|
|
145
|
+
|
|
146
|
+
const PRECISION_Q192 = 10n ** 12n * 2n ** 192n;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Converts Uniswap's sqrtPriceX96 directly to ETH per FeeAsset (E12).
|
|
150
|
+
*
|
|
151
|
+
* For an ETH/FeeAsset pool where ETH is currency0 and FeeAsset is currency1:
|
|
152
|
+
* - Uniswap's sqrtPriceX96 = sqrt(FeeAsset/ETH) * 2^96
|
|
153
|
+
* - We need: ETH/FeeAsset with 1e12 precision
|
|
154
|
+
*
|
|
155
|
+
* Math:
|
|
156
|
+
* price = (sqrtPriceX96 / 2^96)^2 = sqrtPriceX96^2 / 2^192 (FeeAsset per ETH)
|
|
157
|
+
* ethPerFeeAsset = 1 / price = 2^192 / sqrtPriceX96^2
|
|
158
|
+
* ethPerFeeAssetE12 = ethPerFeeAsset * 1e12 = 1e12 * 2^192 / sqrtPriceX96^2
|
|
159
|
+
*/
|
|
160
|
+
export function sqrtPriceX96ToEthPerFeeAssetE12(sqrtPriceX96: bigint): bigint {
|
|
161
|
+
if (sqrtPriceX96 === 0n) {
|
|
162
|
+
throw new Error('Cannot convert zero sqrtPriceX96');
|
|
163
|
+
}
|
|
164
|
+
return PRECISION_Q192 / (sqrtPriceX96 * sqrtPriceX96);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Uniswap V4 StateView ABI - only the functions we need
|
|
168
|
+
*/
|
|
169
|
+
const StateViewAbi = [
|
|
170
|
+
{
|
|
171
|
+
type: 'function',
|
|
172
|
+
name: 'getSlot0',
|
|
173
|
+
inputs: [{ name: 'poolId', type: 'bytes32', internalType: 'PoolId' }],
|
|
174
|
+
outputs: [
|
|
175
|
+
{ name: 'sqrtPriceX96', type: 'uint160', internalType: 'uint160' },
|
|
176
|
+
{ name: 'tick', type: 'int24', internalType: 'int24' },
|
|
177
|
+
{ name: 'protocolFee', type: 'uint24', internalType: 'uint24' },
|
|
178
|
+
{ name: 'lpFee', type: 'uint24', internalType: 'uint24' },
|
|
179
|
+
],
|
|
180
|
+
stateMutability: 'view',
|
|
181
|
+
},
|
|
182
|
+
] as const;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Client for querying the ETH/FeeAsset price from Uniswap V4.
|
|
186
|
+
* Returns prices in ETH per FeeAsset format (E12) to match the rollup contract.
|
|
187
|
+
*/
|
|
188
|
+
class UniswapPriceOracle {
|
|
189
|
+
private readonly stateView;
|
|
190
|
+
private readonly poolId: Hex;
|
|
191
|
+
private readonly log: Logger;
|
|
192
|
+
|
|
193
|
+
constructor(
|
|
194
|
+
private readonly client: ViemClient,
|
|
195
|
+
log?: Logger,
|
|
196
|
+
) {
|
|
197
|
+
this.log = log ?? createLogger('uniswap-price-oracle');
|
|
198
|
+
this.stateView = getContract({
|
|
199
|
+
address: STATE_VIEW_ADDRESS.toString(),
|
|
200
|
+
abi: StateViewAbi,
|
|
201
|
+
client,
|
|
202
|
+
});
|
|
203
|
+
this.poolId = this.computePoolId();
|
|
204
|
+
this.log.debug(`Initialized UniswapPriceOracle with poolId: ${this.poolId}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Computes the PoolId from the pool configuration by hashing its components.
|
|
209
|
+
* PoolId = keccak256(abi.encode(currency0, currency1, fee, tickSpacing, hooks))
|
|
210
|
+
* For mainnet, the value is expected to be: 0xce2899b16743cfd5a954d8122d5e07f410305b1aebee39fd73d9f3b9ebf10c2f
|
|
211
|
+
* Derived anyway to make it simpler to change if needed.
|
|
212
|
+
*/
|
|
213
|
+
@memoize
|
|
214
|
+
computePoolId(): Hex {
|
|
215
|
+
/** ETH/FeeAsset pool configuration (hardcoded for mainnet) */
|
|
216
|
+
const encoded = encodeAbiParameters(parseAbiParameters('address, address, uint24, int24, address'), [
|
|
217
|
+
EthAddress.ZERO.toString(),
|
|
218
|
+
EthAddress.fromString('0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2').toString(),
|
|
219
|
+
500, // 0.05%
|
|
220
|
+
10,
|
|
221
|
+
EthAddress.fromString('0xd53006d1e3110fD319a79AEEc4c527a0d265E080').toString(),
|
|
222
|
+
]);
|
|
223
|
+
return keccak256(encoded);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async isPoolInitialized(): Promise<boolean> {
|
|
227
|
+
const [sqrtPriceX96] = await this.stateView.read.getSlot0([this.poolId], undefined);
|
|
228
|
+
return sqrtPriceX96 !== 0n;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Gets the price as ETH per FeeAsset, scaled by 1e12.
|
|
233
|
+
* This is the format expected by the rollup contract.
|
|
234
|
+
*
|
|
235
|
+
* @param blockNumber - Optional block number to query at (defaults to latest)
|
|
236
|
+
*/
|
|
237
|
+
async getEthPerFeeAssetE12(blockNumber?: bigint): Promise<bigint> {
|
|
238
|
+
const [sqrtPriceX96] = await this.stateView.read.getSlot0(
|
|
239
|
+
[this.poolId],
|
|
240
|
+
blockNumber !== undefined ? { blockNumber } : undefined,
|
|
241
|
+
);
|
|
242
|
+
return sqrtPriceX96ToEthPerFeeAssetE12(sqrtPriceX96);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Gets the median price over the last N blocks as ETH per FeeAsset (E12).
|
|
247
|
+
* Using median helps protect against single-block manipulation.
|
|
248
|
+
*
|
|
249
|
+
* @param numBlocks - Number of recent blocks to sample (default: 5)
|
|
250
|
+
* @returns Median price as ETH per FeeAsset, scaled by 1e12
|
|
251
|
+
*/
|
|
252
|
+
async getMeanEthPerFeeAssetE12(numBlocks: number = 5): Promise<bigint> {
|
|
253
|
+
const currentBlock = await this.client.getBlockNumber();
|
|
254
|
+
const prices: bigint[] = [];
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
257
|
+
const blockNumber = currentBlock - BigInt(i);
|
|
258
|
+
if (blockNumber < 0n) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const price = await this.getEthPerFeeAssetE12(blockNumber);
|
|
264
|
+
prices.push(price);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
this.log.warn(`Failed to get price at block ${blockNumber}: ${err}`);
|
|
267
|
+
// Continue with fewer samples
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const filteredPrices = prices.filter(price => price !== 0n);
|
|
272
|
+
|
|
273
|
+
if (filteredPrices.length === 0) {
|
|
274
|
+
throw new Error('Failed to get any price samples from Uniswap oracle');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const mean = filteredPrices.reduce((a, b) => a + b, 0n) / BigInt(filteredPrices.length);
|
|
278
|
+
return mean;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from 'viem';
|
|
15
15
|
|
|
16
16
|
import type { L1ContractAddresses } from '../l1_contract_addresses.js';
|
|
17
|
-
import {
|
|
17
|
+
import { createL1TxUtils } from '../l1_tx_utils/index.js';
|
|
18
18
|
import { type ExtendedViemWalletClient, type ViemClient, isExtendedClient } from '../types.js';
|
|
19
19
|
|
|
20
20
|
export type L1GovernanceContractAddresses = Pick<
|
|
@@ -194,7 +194,7 @@ export class GovernanceContract extends ReadOnlyGovernanceContract {
|
|
|
194
194
|
retries: number;
|
|
195
195
|
logger: Logger;
|
|
196
196
|
}) {
|
|
197
|
-
const l1TxUtils =
|
|
197
|
+
const l1TxUtils = createL1TxUtils(this.client, { logger });
|
|
198
198
|
const retryDelaySeconds = 12;
|
|
199
199
|
|
|
200
200
|
voteAmount = voteAmount ?? (await this.getPowerForProposal(proposalId));
|
|
@@ -252,7 +252,7 @@ export class GovernanceContract extends ReadOnlyGovernanceContract {
|
|
|
252
252
|
retries: number;
|
|
253
253
|
logger: Logger;
|
|
254
254
|
}) {
|
|
255
|
-
const l1TxUtils =
|
|
255
|
+
const l1TxUtils = createL1TxUtils(this.client, { logger });
|
|
256
256
|
const retryDelaySeconds = 12;
|
|
257
257
|
let success = false;
|
|
258
258
|
for (let i = 0; i < retries; i++) {
|
|
@@ -110,6 +110,12 @@ export class GovernanceProposerContract implements IEmpireBase {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** Checks if a payload was ever submitted to governance via submitRoundWinner. */
|
|
114
|
+
public async hasPayloadBeenProposed(payload: Hex, fromBlock: bigint): Promise<boolean> {
|
|
115
|
+
const events = await this.proposer.getEvents.PayloadSubmitted({ payload }, { fromBlock, strict: true });
|
|
116
|
+
return events.length > 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
public async submitRoundWinner(
|
|
114
120
|
round: bigint,
|
|
115
121
|
l1TxUtils: L1TxUtils,
|
package/src/contracts/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createLogger } from '@aztec/foundation/log';
|
|
|
3
3
|
import { RegistryAbi } from '@aztec/l1-artifacts/RegistryAbi';
|
|
4
4
|
import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi';
|
|
5
5
|
|
|
6
|
-
import { type GetContractReturnType, type Hex, getContract } from 'viem';
|
|
6
|
+
import { type GetContractReturnType, type Hex, getAbiItem, getContract } from 'viem';
|
|
7
7
|
|
|
8
8
|
import type { L1ContractAddresses } from '../l1_contract_addresses.js';
|
|
9
9
|
import type { ViemClient } from '../types.js';
|
|
@@ -128,4 +128,34 @@ export class RegistryContract {
|
|
|
128
128
|
public async getRewardDistributor(): Promise<EthAddress> {
|
|
129
129
|
return EthAddress.fromString(await this.registry.read.getRewardDistributor());
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
/** Returns the L1 timestamp at which the given rollup was registered via addRollup(). */
|
|
133
|
+
public async getCanonicalRollupRegistrationTimestamp(
|
|
134
|
+
rollupAddress: EthAddress,
|
|
135
|
+
fromBlock?: bigint,
|
|
136
|
+
): Promise<bigint | undefined> {
|
|
137
|
+
const event = getAbiItem({ abi: RegistryAbi, name: 'CanonicalRollupUpdated' });
|
|
138
|
+
const start = fromBlock ?? 0n;
|
|
139
|
+
const latestBlock = await this.client.getBlockNumber();
|
|
140
|
+
const chunkSize = 1_000n;
|
|
141
|
+
|
|
142
|
+
for (let from = start; from <= latestBlock; from += chunkSize) {
|
|
143
|
+
const to = from + chunkSize - 1n > latestBlock ? latestBlock : from + chunkSize - 1n;
|
|
144
|
+
const logs = await this.client.getLogs({
|
|
145
|
+
address: this.address.toString(),
|
|
146
|
+
fromBlock: from,
|
|
147
|
+
toBlock: to,
|
|
148
|
+
strict: true,
|
|
149
|
+
event,
|
|
150
|
+
args: { instance: rollupAddress.toString() },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (logs.length > 0) {
|
|
154
|
+
const block = await this.client.getBlock({ blockNumber: logs[0].blockNumber });
|
|
155
|
+
return block.timestamp;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
131
161
|
}
|