@aztec/ethereum 0.0.1-commit.0b941701 → 0.0.1-commit.0c875d939

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.
Files changed (68) hide show
  1. package/dest/config.js +1 -1
  2. package/dest/contracts/empire_slashing_proposer.d.ts +1 -1
  3. package/dest/contracts/empire_slashing_proposer.d.ts.map +1 -1
  4. package/dest/contracts/empire_slashing_proposer.js +13 -15
  5. package/dest/contracts/fee_asset_handler.d.ts +1 -1
  6. package/dest/contracts/fee_asset_handler.d.ts.map +1 -1
  7. package/dest/contracts/fee_asset_handler.js +2 -0
  8. package/dest/contracts/fee_asset_price_oracle.d.ts +100 -0
  9. package/dest/contracts/fee_asset_price_oracle.d.ts.map +1 -0
  10. package/dest/contracts/fee_asset_price_oracle.js +634 -0
  11. package/dest/contracts/governance.d.ts +1 -1
  12. package/dest/contracts/governance.d.ts.map +1 -1
  13. package/dest/contracts/governance.js +2 -0
  14. package/dest/contracts/governance_proposer.d.ts +1 -1
  15. package/dest/contracts/governance_proposer.d.ts.map +1 -1
  16. package/dest/contracts/governance_proposer.js +4 -1
  17. package/dest/contracts/index.d.ts +2 -1
  18. package/dest/contracts/index.d.ts.map +1 -1
  19. package/dest/contracts/index.js +1 -0
  20. package/dest/contracts/multicall.d.ts +1 -1
  21. package/dest/contracts/multicall.d.ts.map +1 -1
  22. package/dest/contracts/multicall.js +2 -1
  23. package/dest/contracts/rollup.d.ts +2 -1
  24. package/dest/contracts/rollup.d.ts.map +1 -1
  25. package/dest/contracts/rollup.js +9 -3
  26. package/dest/contracts/tally_slashing_proposer.d.ts +1 -1
  27. package/dest/contracts/tally_slashing_proposer.d.ts.map +1 -1
  28. package/dest/contracts/tally_slashing_proposer.js +8 -1
  29. package/dest/deploy_aztec_l1_contracts.d.ts +1 -1
  30. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  31. package/dest/deploy_aztec_l1_contracts.js +37 -22
  32. package/dest/l1_tx_utils/constants.d.ts +1 -1
  33. package/dest/l1_tx_utils/constants.js +2 -2
  34. package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +1 -1
  35. package/dest/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.js +1 -1
  36. package/dest/l1_tx_utils/l1_tx_utils.js +6 -6
  37. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +3 -3
  38. package/dest/publisher_manager.d.ts +3 -2
  39. package/dest/publisher_manager.d.ts.map +1 -1
  40. package/dest/publisher_manager.js +2 -2
  41. package/dest/test/rollup_cheat_codes.d.ts +3 -1
  42. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  43. package/dest/test/rollup_cheat_codes.js +9 -0
  44. package/dest/test/start_anvil.js +1 -1
  45. package/dest/utils.d.ts +2 -1
  46. package/dest/utils.d.ts.map +1 -1
  47. package/dest/utils.js +46 -0
  48. package/package.json +5 -5
  49. package/src/config.ts +1 -1
  50. package/src/contracts/empire_slashing_proposer.ts +16 -27
  51. package/src/contracts/fee_asset_handler.ts +2 -0
  52. package/src/contracts/fee_asset_price_oracle.ts +262 -0
  53. package/src/contracts/governance.ts +2 -0
  54. package/src/contracts/governance_proposer.ts +4 -1
  55. package/src/contracts/index.ts +1 -0
  56. package/src/contracts/multicall.ts +5 -2
  57. package/src/contracts/rollup.ts +15 -7
  58. package/src/contracts/tally_slashing_proposer.ts +5 -1
  59. package/src/deploy_aztec_l1_contracts.ts +41 -27
  60. package/src/l1_tx_utils/constants.ts +2 -2
  61. package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +1 -1
  62. package/src/l1_tx_utils/fee-strategies/p75_competitive_blob_txs_only.ts +1 -1
  63. package/src/l1_tx_utils/l1_tx_utils.ts +6 -6
  64. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +3 -3
  65. package/src/publisher_manager.ts +4 -2
  66. package/src/test/rollup_cheat_codes.ts +9 -0
  67. package/src/test/start_anvil.ts +1 -1
  68. package/src/utils.ts +53 -0
@@ -0,0 +1,262 @@
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
+ if ((await this.client.getCode({ address: STATE_VIEW_ADDRESS.toString() })) === '0x') {
38
+ this.log.warn('Uniswap V4 StateView contract not found, skipping fee asset price oracle');
39
+ return undefined;
40
+ }
41
+ this.log.info('Uniswap V4 StateView contract found, initializing fee asset price oracle');
42
+ return new UniswapPriceOracle(this.client, this.log);
43
+ }
44
+
45
+ /**
46
+ * Computes the fee asset price modifier to be used in the next checkpoint proposal.
47
+ *
48
+ * The modifier adjusts the on-chain fee asset price toward the oracle price,
49
+ * clamped to ±1% (±100 basis points) per checkpoint.
50
+ *
51
+ * Returns 0 if not on mainnet or if the oracle query fails.
52
+ *
53
+ * @returns The price modifier in basis points (positive to increase price, negative to decrease)
54
+ */
55
+ async computePriceModifier(): Promise<bigint> {
56
+ const uniswapOracle = await this.getUniswapOracle();
57
+ if (!uniswapOracle) {
58
+ return 0n;
59
+ }
60
+
61
+ try {
62
+ // Get current on-chain price (ETH per fee asset, E12)
63
+ const currentPriceE12 = await this.rollupContract.getEthPerFeeAsset();
64
+
65
+ // Get oracle price (median of last N blocks, ETH per fee asset, E12)
66
+ const oraclePriceE12 = await uniswapOracle.getMeanEthPerFeeAssetE12();
67
+
68
+ // Compute modifier in basis points
69
+ const modifier = this.computePriceModifierBps(currentPriceE12, oraclePriceE12);
70
+
71
+ this.log.debug('Computed price modifier', {
72
+ currentPriceE12: currentPriceE12.toString(),
73
+ oraclePriceE12: oraclePriceE12.toString(),
74
+ modifierBps: modifier.toString(),
75
+ });
76
+
77
+ return modifier;
78
+ } catch (err) {
79
+ this.log.warn(`Failed to compute price modifier, using 0: ${err}`);
80
+ return 0n;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Gets the current oracle price (ETH per fee asset, scaled by 1e12).
86
+ * Returns undefined if not on mainnet or if the oracle query fails.
87
+ */
88
+ async getOraclePrice(): Promise<bigint | undefined> {
89
+ const uniswapOracle = await this.getUniswapOracle();
90
+ if (!uniswapOracle) {
91
+ return undefined;
92
+ }
93
+
94
+ try {
95
+ return await uniswapOracle.getMeanEthPerFeeAssetE12();
96
+ } catch (err) {
97
+ this.log.warn(`Failed to get oracle price: ${err}`);
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Computes the basis points modifier needed to move from current price toward target price.
104
+ *
105
+ * @param currentPrice - Current ETH per fee asset (E12 scale)
106
+ * @param targetPrice - Target ETH per fee asset (E12 scale)
107
+ * @returns Basis points modifier clamped to ±100 (±1%)
108
+ */
109
+ computePriceModifierBps(currentPrice: bigint, targetPrice: bigint): bigint {
110
+ if (currentPrice === 0n) {
111
+ return MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
112
+ }
113
+
114
+ // Calculate percentage difference in basis points
115
+ // modifierBps = ((targetPrice - currentPrice) / currentPrice) * 10000
116
+ const diff = targetPrice - currentPrice;
117
+ const rawModifierBps = (diff * 10_000n) / currentPrice;
118
+
119
+ // Clamp to ±MAX_FEE_ASSET_PRICE_MODIFIER_BPS
120
+ if (rawModifierBps > MAX_FEE_ASSET_PRICE_MODIFIER_BPS) {
121
+ return MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
122
+ }
123
+ if (rawModifierBps < -MAX_FEE_ASSET_PRICE_MODIFIER_BPS) {
124
+ return -MAX_FEE_ASSET_PRICE_MODIFIER_BPS;
125
+ }
126
+ return rawModifierBps;
127
+ }
128
+ }
129
+
130
+ /** Mainnet Uniswap V4 StateView contract address */
131
+ export const STATE_VIEW_ADDRESS = EthAddress.fromString('0x7ffe42c4a5deea5b0fec41c94c136cf115597227');
132
+
133
+ const PRECISION_Q192 = 10n ** 12n * 2n ** 192n;
134
+
135
+ /**
136
+ * Converts Uniswap's sqrtPriceX96 directly to ETH per FeeAsset (E12).
137
+ *
138
+ * For an ETH/FeeAsset pool where ETH is currency0 and FeeAsset is currency1:
139
+ * - Uniswap's sqrtPriceX96 = sqrt(FeeAsset/ETH) * 2^96
140
+ * - We need: ETH/FeeAsset with 1e12 precision
141
+ *
142
+ * Math:
143
+ * price = (sqrtPriceX96 / 2^96)^2 = sqrtPriceX96^2 / 2^192 (FeeAsset per ETH)
144
+ * ethPerFeeAsset = 1 / price = 2^192 / sqrtPriceX96^2
145
+ * ethPerFeeAssetE12 = ethPerFeeAsset * 1e12 = 1e12 * 2^192 / sqrtPriceX96^2
146
+ */
147
+ export function sqrtPriceX96ToEthPerFeeAssetE12(sqrtPriceX96: bigint): bigint {
148
+ if (sqrtPriceX96 === 0n) {
149
+ throw new Error('Cannot convert zero sqrtPriceX96');
150
+ }
151
+ return PRECISION_Q192 / (sqrtPriceX96 * sqrtPriceX96);
152
+ }
153
+ /**
154
+ * Uniswap V4 StateView ABI - only the functions we need
155
+ */
156
+ const StateViewAbi = [
157
+ {
158
+ type: 'function',
159
+ name: 'getSlot0',
160
+ inputs: [{ name: 'poolId', type: 'bytes32', internalType: 'PoolId' }],
161
+ outputs: [
162
+ { name: 'sqrtPriceX96', type: 'uint160', internalType: 'uint160' },
163
+ { name: 'tick', type: 'int24', internalType: 'int24' },
164
+ { name: 'protocolFee', type: 'uint24', internalType: 'uint24' },
165
+ { name: 'lpFee', type: 'uint24', internalType: 'uint24' },
166
+ ],
167
+ stateMutability: 'view',
168
+ },
169
+ ] as const;
170
+
171
+ /**
172
+ * Client for querying the ETH/FeeAsset price from Uniswap V4.
173
+ * Returns prices in ETH per FeeAsset format (E12) to match the rollup contract.
174
+ */
175
+ class UniswapPriceOracle {
176
+ private readonly stateView;
177
+ private readonly poolId: Hex;
178
+ private readonly log: Logger;
179
+
180
+ constructor(
181
+ private readonly client: ViemClient,
182
+ log?: Logger,
183
+ ) {
184
+ this.log = log ?? createLogger('uniswap-price-oracle');
185
+ this.stateView = getContract({
186
+ address: STATE_VIEW_ADDRESS.toString(),
187
+ abi: StateViewAbi,
188
+ client,
189
+ });
190
+ this.poolId = this.computePoolId();
191
+ this.log.debug(`Initialized UniswapPriceOracle with poolId: ${this.poolId}`);
192
+ }
193
+
194
+ /**
195
+ * Computes the PoolId from the pool configuration by hashing its components.
196
+ * PoolId = keccak256(abi.encode(currency0, currency1, fee, tickSpacing, hooks))
197
+ * For mainnet, the value is expected to be: 0xce2899b16743cfd5a954d8122d5e07f410305b1aebee39fd73d9f3b9ebf10c2f
198
+ * Derived anyway to make it simpler to change if needed.
199
+ */
200
+ @memoize
201
+ computePoolId(): Hex {
202
+ /** ETH/FeeAsset pool configuration (hardcoded for mainnet) */
203
+ const encoded = encodeAbiParameters(parseAbiParameters('address, address, uint24, int24, address'), [
204
+ EthAddress.ZERO.toString(),
205
+ EthAddress.fromString('0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2').toString(),
206
+ 500, // 0.05%
207
+ 10,
208
+ EthAddress.fromString('0xd53006d1e3110fD319a79AEEc4c527a0d265E080').toString(),
209
+ ]);
210
+ return keccak256(encoded);
211
+ }
212
+
213
+ /**
214
+ * Gets the price as ETH per FeeAsset, scaled by 1e12.
215
+ * This is the format expected by the rollup contract.
216
+ *
217
+ * @param blockNumber - Optional block number to query at (defaults to latest)
218
+ */
219
+ async getEthPerFeeAssetE12(blockNumber?: bigint): Promise<bigint> {
220
+ const [sqrtPriceX96] = await this.stateView.read.getSlot0(
221
+ [this.poolId],
222
+ blockNumber !== undefined ? { blockNumber } : undefined,
223
+ );
224
+ return sqrtPriceX96ToEthPerFeeAssetE12(sqrtPriceX96);
225
+ }
226
+
227
+ /**
228
+ * Gets the median price over the last N blocks as ETH per FeeAsset (E12).
229
+ * Using median helps protect against single-block manipulation.
230
+ *
231
+ * @param numBlocks - Number of recent blocks to sample (default: 5)
232
+ * @returns Median price as ETH per FeeAsset, scaled by 1e12
233
+ */
234
+ async getMeanEthPerFeeAssetE12(numBlocks: number = 5): Promise<bigint> {
235
+ const currentBlock = await this.client.getBlockNumber();
236
+ const prices: bigint[] = [];
237
+
238
+ for (let i = 0; i < numBlocks; i++) {
239
+ const blockNumber = currentBlock - BigInt(i);
240
+ if (blockNumber < 0n) {
241
+ break;
242
+ }
243
+
244
+ try {
245
+ const price = await this.getEthPerFeeAssetE12(blockNumber);
246
+ prices.push(price);
247
+ } catch (err) {
248
+ this.log.warn(`Failed to get price at block ${blockNumber}: ${err}`);
249
+ // Continue with fewer samples
250
+ }
251
+ }
252
+
253
+ const filteredPrices = prices.filter(price => price !== 0n);
254
+
255
+ if (filteredPrices.length === 0) {
256
+ throw new Error('Failed to get any price samples from Uniswap oracle');
257
+ }
258
+
259
+ const mean = filteredPrices.reduce((a, b) => a + b, 0n) / BigInt(filteredPrices.length);
260
+ return mean;
261
+ }
262
+ }
@@ -211,6 +211,7 @@ export class GovernanceContract extends ReadOnlyGovernanceContract {
211
211
 
212
212
  const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
213
213
  to: this.governanceContract.address,
214
+ abi: GovernanceAbi,
214
215
  data: encodedVoteData,
215
216
  });
216
217
 
@@ -265,6 +266,7 @@ export class GovernanceContract extends ReadOnlyGovernanceContract {
265
266
 
266
267
  const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
267
268
  to: this.governanceContract.address,
269
+ abi: GovernanceAbi,
268
270
  data: encodedExecuteData,
269
271
  });
270
272
 
@@ -83,6 +83,7 @@ export class GovernanceProposerContract implements IEmpireBase {
83
83
  public createSignalRequest(payload: Hex): L1TxRequest {
84
84
  return {
85
85
  to: this.address.toString(),
86
+ abi: GovernanceProposerAbi,
86
87
  data: encodeSignal(payload),
87
88
  };
88
89
  }
@@ -104,6 +105,7 @@ export class GovernanceProposerContract implements IEmpireBase {
104
105
  );
105
106
  return {
106
107
  to: this.address.toString(),
108
+ abi: GovernanceProposerAbi,
107
109
  data: encodeSignalWithSignature(payload, signature),
108
110
  };
109
111
  }
@@ -117,8 +119,9 @@ export class GovernanceProposerContract implements IEmpireBase {
117
119
  }> {
118
120
  const { receipt } = await l1TxUtils.sendAndMonitorTransaction({
119
121
  to: this.address.toString(),
122
+ abi: GovernanceProposerAbi,
120
123
  data: encodeFunctionData({
121
- abi: this.proposer.abi,
124
+ abi: GovernanceProposerAbi,
122
125
  functionName: 'submitRoundWinner',
123
126
  args: [round],
124
127
  }),
@@ -1,6 +1,7 @@
1
1
  export * from './empire_base.js';
2
2
  export * from './errors.js';
3
3
  export * from './fee_asset_handler.js';
4
+ export * from './fee_asset_price_oracle.js';
4
5
  export * from './fee_juice.js';
5
6
  export * from './governance.js';
6
7
  export * from './governance_proposer.js';
@@ -34,10 +34,13 @@ export class Multicall3 {
34
34
  };
35
35
 
36
36
  const encodedForwarderData = encodeFunctionData(forwarderFunctionData);
37
-
38
37
  try {
39
38
  const { receipt, state } = await l1TxUtils.sendAndMonitorTransaction(
40
- { to: MULTI_CALL_3_ADDRESS, data: encodedForwarderData },
39
+ {
40
+ to: MULTI_CALL_3_ADDRESS,
41
+ data: encodedForwarderData,
42
+ abi: multicall3Abi,
43
+ },
41
44
  gasConfig,
42
45
  blobConfig,
43
46
  );
@@ -391,20 +391,24 @@ export class RollupContract {
391
391
  slotDuration: number;
392
392
  epochDuration: number;
393
393
  proofSubmissionEpochs: number;
394
+ targetCommitteeSize: number;
394
395
  }> {
395
- const [l1StartBlock, l1GenesisTime, slotDuration, epochDuration, proofSubmissionEpochs] = await Promise.all([
396
- this.getL1StartBlock(),
397
- this.getL1GenesisTime(),
398
- this.getSlotDuration(),
399
- this.getEpochDuration(),
400
- this.getProofSubmissionEpochs(),
401
- ]);
396
+ const [l1StartBlock, l1GenesisTime, slotDuration, epochDuration, proofSubmissionEpochs, targetCommitteeSize] =
397
+ await Promise.all([
398
+ this.getL1StartBlock(),
399
+ this.getL1GenesisTime(),
400
+ this.getSlotDuration(),
401
+ this.getEpochDuration(),
402
+ this.getProofSubmissionEpochs(),
403
+ this.getTargetCommitteeSize(),
404
+ ]);
402
405
  return {
403
406
  l1StartBlock,
404
407
  l1GenesisTime,
405
408
  slotDuration,
406
409
  epochDuration: Number(epochDuration),
407
410
  proofSubmissionEpochs: Number(proofSubmissionEpochs),
411
+ targetCommitteeSize,
408
412
  };
409
413
  }
410
414
 
@@ -805,6 +809,7 @@ export class RollupContract {
805
809
  ): L1TxRequest {
806
810
  return {
807
811
  to: this.address,
812
+ abi: RollupAbi,
808
813
  data: encodeFunctionData({
809
814
  abi: RollupAbi,
810
815
  functionName: 'invalidateBadAttestation',
@@ -826,6 +831,7 @@ export class RollupContract {
826
831
  ): L1TxRequest {
827
832
  return {
828
833
  to: this.address,
834
+ abi: RollupAbi,
829
835
  data: encodeFunctionData({
830
836
  abi: RollupAbi,
831
837
  functionName: 'invalidateInsufficientAttestations',
@@ -959,6 +965,7 @@ export class RollupContract {
959
965
  setupEpoch(l1TxUtils: L1TxUtils) {
960
966
  return l1TxUtils.sendAndMonitorTransaction({
961
967
  to: this.address,
968
+ abi: RollupAbi,
962
969
  data: encodeFunctionData({
963
970
  abi: RollupAbi,
964
971
  functionName: 'setupEpoch',
@@ -970,6 +977,7 @@ export class RollupContract {
970
977
  vote(l1TxUtils: L1TxUtils, proposalId: bigint) {
971
978
  return l1TxUtils.sendAndMonitorTransaction({
972
979
  to: this.address,
980
+ abi: RollupAbi,
973
981
  data: encodeFunctionData({
974
982
  abi: RollupAbi,
975
983
  functionName: 'vote',
@@ -1,11 +1,12 @@
1
1
  import type { L1TxRequest } from '@aztec/ethereum/l1-tx-utils';
2
2
  import type { ViemClient } from '@aztec/ethereum/types';
3
- import { tryExtractEvent } from '@aztec/ethereum/utils';
3
+ import { mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
4
4
  import { SlotNumber } from '@aztec/foundation/branded-types';
5
5
  import { Buffer32 } from '@aztec/foundation/buffer';
6
6
  import { EthAddress } from '@aztec/foundation/eth-address';
7
7
  import { Signature } from '@aztec/foundation/eth-signature';
8
8
  import { hexToBuffer } from '@aztec/foundation/string';
9
+ import { SlasherAbi } from '@aztec/l1-artifacts/SlasherAbi';
9
10
  import { TallySlashingProposerAbi } from '@aztec/l1-artifacts/TallySlashingProposerAbi';
10
11
 
11
12
  import {
@@ -160,6 +161,7 @@ export class TallySlashingProposerContract {
160
161
 
161
162
  return {
162
163
  to: this.contract.address,
164
+ abi: TallySlashingProposerAbi,
163
165
  data: encodeFunctionData({
164
166
  abi: TallySlashingProposerAbi,
165
167
  functionName: 'vote',
@@ -207,6 +209,7 @@ export class TallySlashingProposerContract {
207
209
  public buildVoteRequestWithSignature(votes: Hex, signature: { v: number; r: Hex; s: Hex }): L1TxRequest {
208
210
  return {
209
211
  to: this.contract.address,
212
+ abi: TallySlashingProposerAbi,
210
213
  data: encodeFunctionData({
211
214
  abi: TallySlashingProposerAbi,
212
215
  functionName: 'vote',
@@ -224,6 +227,7 @@ export class TallySlashingProposerContract {
224
227
  public buildExecuteRoundRequest(round: bigint, committees: EthAddress[][]): L1TxRequest {
225
228
  return {
226
229
  to: this.contract.address,
230
+ abi: mergeAbis([TallySlashingProposerAbi, SlasherAbi]),
227
231
  data: encodeFunctionData({
228
232
  abi: TallySlashingProposerAbi,
229
233
  functionName: 'executeRound',
@@ -15,7 +15,7 @@ import { tmpdir } from 'os';
15
15
  import { dirname, join, resolve } from 'path';
16
16
  import readline from 'readline';
17
17
  import type { Hex } from 'viem';
18
- import { foundry, mainnet, sepolia } from 'viem/chains';
18
+ import { mainnet, sepolia } from 'viem/chains';
19
19
 
20
20
  import { createEthereumChain, isAnvilTestChain } from './chain.js';
21
21
  import { createExtendedL1Client } from './client.js';
@@ -31,9 +31,9 @@ const logger = createLogger('ethereum:deploy_aztec_l1_contracts');
31
31
  const JSON_DEPLOY_RESULT_PREFIX = 'JSON DEPLOY RESULT:';
32
32
 
33
33
  /**
34
- * Runs a process with the given command, arguments, and environment.
35
- * If the process outputs a line starting with JSON_DEPLOY_RESULT_PREFIX,
36
- * the JSON is parsed and returned.
34
+ * Runs a process and parses JSON deploy results from stdout.
35
+ * Lines starting with JSON_DEPLOY_RESULT_PREFIX are parsed and returned.
36
+ * All other stdout goes to logger.info, stderr goes to logger.warn.
37
37
  */
38
38
  function runProcess<T>(
39
39
  command: string,
@@ -49,26 +49,41 @@ function runProcess<T>(
49
49
  });
50
50
 
51
51
  let result: T | undefined;
52
+ let parseError: Error | undefined;
53
+ let settled = false;
52
54
 
53
55
  readline.createInterface({ input: proc.stdout }).on('line', line => {
54
56
  const trimmedLine = line.trim();
55
57
  if (trimmedLine.startsWith(JSON_DEPLOY_RESULT_PREFIX)) {
56
58
  const jsonStr = trimmedLine.slice(JSON_DEPLOY_RESULT_PREFIX.length).trim();
57
- // TODO(AD): should this be a zod parse?
58
- result = JSON.parse(jsonStr);
59
+ try {
60
+ result = JSON.parse(jsonStr);
61
+ } catch {
62
+ parseError = new Error(`Failed to parse deploy result JSON: ${jsonStr.slice(0, 200)}`);
63
+ }
59
64
  } else {
60
65
  logger.info(line);
61
66
  }
62
67
  });
63
- readline.createInterface({ input: proc.stderr }).on('line', logger.error.bind(logger));
68
+ readline.createInterface({ input: proc.stderr }).on('line', logger.warn.bind(logger));
64
69
 
65
70
  proc.on('error', error => {
71
+ if (settled) {
72
+ return;
73
+ }
74
+ settled = true;
66
75
  reject(new Error(`Failed to spawn ${command}: ${error.message}`));
67
76
  });
68
77
 
69
78
  proc.on('close', code => {
79
+ if (settled) {
80
+ return;
81
+ }
82
+ settled = true;
70
83
  if (code !== 0) {
71
- reject(new Error(`${command} exited with code ${code}. See logs for details.\n`));
84
+ reject(new Error(`${command} exited with code ${code}`));
85
+ } else if (parseError) {
86
+ reject(parseError);
72
87
  } else {
73
88
  resolve(result);
74
89
  }
@@ -168,6 +183,9 @@ export function prepareL1ContractsForDeployment(): string {
168
183
  const foundryTomlPath = join(basePath, 'foundry.toml');
169
184
  let foundryToml = readFileSync(foundryTomlPath, 'utf-8');
170
185
  const solcPathMatch = foundryToml.match(/solc\s*=\s*"\.\/solc-([^"]+)"/);
186
+ // Did we find a hardcoded solc path that we need to make absolute?
187
+ // This code path happens in CI currently as we bundle solc there to avoid race conditions when
188
+ // downloading solc.
171
189
  if (solcPathMatch) {
172
190
  const solcVersion = solcPathMatch[1];
173
191
  const absoluteSolcPath = join(basePath, `solc-${solcVersion}`);
@@ -318,11 +336,8 @@ export async function deployAztecL1Contracts(
318
336
  );
319
337
  }
320
338
 
321
- // From heuristic testing. More caused issues with anvil.
322
- const MAGIC_ANVIL_BATCH_SIZE = 8;
323
- // Anvil seems to stall with unbounded batch size. Otherwise no max batch size is desirable.
339
+ const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
324
340
  const forgeArgs = [
325
- 'script',
326
341
  FORGE_SCRIPT,
327
342
  '--sig',
328
343
  'run()',
@@ -330,8 +345,6 @@ export async function deployAztecL1Contracts(
330
345
  privateKey,
331
346
  '--rpc-url',
332
347
  rpcUrl,
333
- '--broadcast',
334
- ...(chainId === foundry.id ? ['--batch-size', MAGIC_ANVIL_BATCH_SIZE.toString()] : []),
335
348
  ...(shouldVerify ? ['--verify'] : []),
336
349
  ];
337
350
  const forgeEnv = {
@@ -340,7 +353,12 @@ export async function deployAztecL1Contracts(
340
353
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
341
354
  ...getDeployAztecL1ContractsEnvVars(args),
342
355
  };
343
- const result = await runProcess<ForgeL1ContractsDeployResult>('forge', forgeArgs, forgeEnv, l1ContractsPath);
356
+ const result = await runProcess<ForgeL1ContractsDeployResult>(
357
+ process.execPath,
358
+ [scriptPath, ...forgeArgs],
359
+ forgeEnv,
360
+ l1ContractsPath,
361
+ );
344
362
  if (!result) {
345
363
  throw new Error('Forge script did not output deployment result');
346
364
  }
@@ -583,17 +601,8 @@ export const deployRollupForUpgrade = async (
583
601
  const FORGE_SCRIPT = 'script/deploy/DeployRollupForUpgrade.s.sol';
584
602
  await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
585
603
 
586
- const forgeArgs = [
587
- 'script',
588
- FORGE_SCRIPT,
589
- '--sig',
590
- 'run()',
591
- '--private-key',
592
- privateKey,
593
- '--rpc-url',
594
- rpcUrl,
595
- '--broadcast',
596
- ];
604
+ const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
605
+ const forgeArgs = [FORGE_SCRIPT, '--sig', 'run()', '--private-key', privateKey, '--rpc-url', rpcUrl];
597
606
  const forgeEnv = {
598
607
  FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
599
608
  // Env vars required by l1-contracts/script/deploy/RollupConfiguration.sol.
@@ -602,7 +611,12 @@ export const deployRollupForUpgrade = async (
602
611
  ...getDeployRollupForUpgradeEnvVars(args),
603
612
  };
604
613
 
605
- const result = await runProcess<ForgeRollupUpgradeResult>('forge', forgeArgs, forgeEnv, l1ContractsPath);
614
+ const result = await runProcess<ForgeRollupUpgradeResult>(
615
+ process.execPath,
616
+ [scriptPath, ...forgeArgs],
617
+ forgeEnv,
618
+ l1ContractsPath,
619
+ );
606
620
  if (!result) {
607
621
  throw new Error('Forge script did not output deployment result');
608
622
  }
@@ -3,8 +3,8 @@
3
3
  // 1_000_000_000_000_000_000 Wei = 1 ETH
4
4
  export const WEI_CONST = 1_000_000_000n;
5
5
 
6
- // @note using this large gas limit to avoid the issue of `gas limit too low` when estimating gas in reth
7
- export const LARGE_GAS_LIMIT = 12_000_000n;
6
+ // EIP-7825: protocol-level cap on tx gas limit (2^24). Clients reject above this.
7
+ export const MAX_L1_TX_LIMIT = 16_777_216n;
8
8
 
9
9
  // setting a minimum bump percentage to 10% due to geth's implementation
10
10
  // https://github.com/ethereum/go-ethereum/blob/e3d61e6db028c412f74bc4d4c7e117a9e29d0de0/core/txpool/legacypool/list.go#L298
@@ -134,7 +134,7 @@ export const P75AllTxsPriorityFeeStrategy: PriorityFeeStrategy = {
134
134
  // Sanity check: cap competitive fee at 100x network estimate to avoid using unrealistic fees
135
135
  const maxReasonableFee = networkEstimate * 100n;
136
136
  if (competitiveFee > maxReasonableFee && networkEstimate > 0n) {
137
- logger?.warn('Competitive fee exceeds sanity cap, using capped value', {
137
+ logger?.debug('Competitive fee exceeds sanity cap, using capped value', {
138
138
  competitiveFee: formatGwei(competitiveFee),
139
139
  networkEstimate: formatGwei(networkEstimate),
140
140
  cappedTo: formatGwei(maxReasonableFee),
@@ -207,7 +207,7 @@ export const P75BlobTxsOnlyPriorityFeeStrategy: PriorityFeeStrategy = {
207
207
 
208
208
  // Debug: Log suspicious fees from history
209
209
  if (medianHistoricalFee > 100n * WEI_CONST) {
210
- logger?.warn('Suspicious high fee in history', {
210
+ logger?.debug('Suspicious high fee in history', {
211
211
  historicalMedian: formatGwei(medianHistoricalFee),
212
212
  allP75Fees: percentile75Fees.map(f => formatGwei(f)),
213
213
  });
@@ -27,7 +27,7 @@ import { jsonRpc } from 'viem/nonce';
27
27
  import type { ViemClient } from '../types.js';
28
28
  import { formatViemError } from '../utils.js';
29
29
  import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
30
- import { LARGE_GAS_LIMIT } from './constants.js';
30
+ import { MAX_L1_TX_LIMIT } from './constants.js';
31
31
  import type { IL1TxMetrics, IL1TxStore } from './interfaces.js';
32
32
  import { ReadOnlyL1TxUtils } from './readonly_l1_tx_utils.js';
33
33
  import {
@@ -207,7 +207,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
207
207
 
208
208
  let gasLimit: bigint;
209
209
  if (this.debugMaxGasLimit) {
210
- gasLimit = LARGE_GAS_LIMIT;
210
+ gasLimit = MAX_L1_TX_LIMIT;
211
211
  } else if (gasConfig.gasLimit) {
212
212
  gasLimit = gasConfig.gasLimit;
213
213
  } else {
@@ -283,7 +283,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
283
283
  return { txHash, state: l1TxState };
284
284
  } catch (err: any) {
285
285
  const viemError = formatViemError(err, request.abi);
286
- this.logger.error(`Failed to send L1 transaction`, viemError, {
286
+ this.logger.error(`Failed to send L1 transaction: ${viemError.message}`, viemError, {
287
287
  request: pick(request, 'to', 'value'),
288
288
  });
289
289
  throw viemError;
@@ -631,12 +631,12 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
631
631
  from: request.from ?? this.getSenderAddress().toString(),
632
632
  maxFeePerGas: gasPrice.maxFeePerGas,
633
633
  maxPriorityFeePerGas: gasPrice.maxPriorityFeePerGas,
634
- gas: request.gas ?? LARGE_GAS_LIMIT,
634
+ gas: request.gas ?? MAX_L1_TX_LIMIT,
635
635
  };
636
636
 
637
637
  if (!request.gas && !gasConfig.ignoreBlockGasLimit) {
638
- // LARGE_GAS_LIMIT is set as call.gas, increase block gasLimit
639
- blockOverrides.gasLimit = LARGE_GAS_LIMIT * 2n;
638
+ // MAX_L1_TX_LIMIT is set as call.gas, ensure block gasLimit is sufficient
639
+ blockOverrides.gasLimit = MAX_L1_TX_LIMIT;
640
640
  }
641
641
 
642
642
  return this._simulate(call, blockOverrides, stateOverrides, gasConfig, abi);
@@ -27,7 +27,7 @@ import type { ViemClient } from '../types.js';
27
27
  import { type L1TxUtilsConfig, defaultL1TxUtilsConfig, l1TxUtilsConfigMappings } from './config.js';
28
28
  import {
29
29
  BLOCK_TIME_MS,
30
- LARGE_GAS_LIMIT,
30
+ MAX_L1_TX_LIMIT,
31
31
  MIN_BLOB_REPLACEMENT_BUMP_PERCENTAGE,
32
32
  MIN_REPLACEMENT_BUMP_PERCENTAGE,
33
33
  WEI_CONST,
@@ -249,7 +249,7 @@ export class ReadOnlyL1TxUtils {
249
249
  ...request,
250
250
  ..._blobInputs,
251
251
  maxFeePerBlobGas: gasPrice.maxFeePerBlobGas!,
252
- gas: LARGE_GAS_LIMIT,
252
+ gas: MAX_L1_TX_LIMIT,
253
253
  blockTag: 'latest',
254
254
  });
255
255
 
@@ -258,7 +258,7 @@ export class ReadOnlyL1TxUtils {
258
258
  initialEstimate = await this.client.estimateGas({
259
259
  account,
260
260
  ...request,
261
- gas: LARGE_GAS_LIMIT,
261
+ gas: MAX_L1_TX_LIMIT,
262
262
  blockTag: 'latest',
263
263
  });
264
264
  this.logger?.trace(`Estimated gas for non-blob tx: ${initialEstimate}`);