Skip to content

Commit da9ac1c

Browse files
authored
feat: assert non revertible phase when setting fee payer (#24479)
The fee payer should only be set in the non-revertible phase, but we were not asserting this.
1 parent 7d8a4e3 commit da9ac1c

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [Aztec.nr] `set_as_fee_payer` now asserts it is called during the setup phase
13+
14+
`PrivateContext::set_as_fee_payer` now asserts that execution is still in the setup (non-revertible) phase, i.e. that `end_setup` has not yet been called by any function in the transaction. Electing a fee payer in the revertible phase was never safe: compensation collected by the fee payer after `end_setup` can be discarded if a public call later reverts, while the protocol still debits the fee payer's fee-juice balance.
15+
16+
**Impact**: A transaction in which `set_as_fee_payer` runs after the setup phase has ended now fails with `fee payer must be elected during the setup phase`. Standard fee payment flows, which call `set_as_fee_payer` before or together with `end_setup`, are unaffected.
17+
1218
### [PXE] Stores are now selected by `(l1ChainId, rollupAddress, schemaVersion)` instead of being wiped on mismatch
1319

1420
Previously, connecting a PXE or embedded wallet to a different or redeployed rollup, or bumping the store schema version, wiped the existing on-disk store in place. That meant master account keys could be destroyed simply by pointing a wallet at a different network. PXE data stores now exist per `(l1ChainId, rollupAddress, schemaVersion)` triple, and switching networks (or upgrading) selects or creates the matching store instead of overwriting previous ones. The embedded wallet's `wallet_data` store is partitioned the same way, so accounts and aliases are per network: switching networks starts with an empty account list until accounts are re-imported, and switching back finds the originals intact.
@@ -140,8 +146,6 @@ Registering classes and instances are now separate, unvalidated operations. `reg
140146
The new class is used automatically once the upgrade takes effect on chain; no further PXE action is needed. Registering it beforehand is harmless: until the update activates, the node still resolves the contract's current class to the previous one, so it keeps running its old code.
141147

142148
- `pxe.getContractInstance(address)` and `wallet.getContractMetadata(address).instance` now return the contract's **address preimage**, which no longer includes `currentContractClassId`.
143-
144-
145149
### [Aztec.js] `AccountWithSecretKey` removed, read account keys from the `AccountManager` or PXE
146150

147151
`AccountWithSecretKey` was a thin wrapper that bundled an account's transaction signer with its master secret key, used mainly to print or export the secret. It has been removed, and `AccountManager.getAccount()` now returns the plain `Account` signer. The wrapper's extra methods are no longer available on that value:

noir-projects/aztec-nr/aztec/src/context/private_context.nr

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,11 +537,22 @@ impl PrivateContext {
537537
/// Only one contract per transaction can declare itself as the fee payer, and it must have sufficient fee-juice
538538
/// balance (>= the gas limits specified in the TxContext) by the time we reach the public setup phase of the tx.
539539
///
540+
/// The fee payer must be elected during the setup (non-revertible) phase, i.e. before
541+
/// [`end_setup`](PrivateContext::end_setup) is called - this function asserts so. This is because any compensation
542+
/// collected by the fee payer during the revertible phase can be discarded if a public call later reverts, while
543+
/// the protocol still debits the fee payer's fee-juice balance. Note that `end_setup` does not need to be called
544+
/// by the electing function itself: it can be called later in the transaction (e.g. by the fee-juice contract when
545+
/// claiming fee juice that pays for the very same transaction).
540546
pub fn set_as_fee_payer(&mut self) {
547+
assert(!self.in_revertible_phase(), "fee payer must be elected during the setup phase");
541548
aztecnr_trace_log_format!("Setting {0} as fee payer")([self.this_address().to_field()]);
542549
self.is_fee_payer = true;
543550
}
544551

552+
/// Returns whether execution is currently in the revertible (app) phase of the transaction.
553+
///
554+
/// A transaction is in the revertible phase if [`end_setup`](PrivateContext::end_setup) has already been called -
555+
/// potentially by a different function of the same transaction.
545556
pub fn in_revertible_phase(&mut self) -> bool {
546557
let current_counter = self.side_effect_counter;
547558

yarn-project/end-to-end/src/automine/phase_check.parallel.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AztecAddress } from '@aztec/aztec.js/addresses';
2+
import { BatchCall } from '@aztec/aztec.js/contracts';
23
import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee';
34
import { SPONSORED_FPC_SALT } from '@aztec/constants';
45
import { Fr } from '@aztec/foundation/curves/bn254';
@@ -7,11 +8,23 @@ import { TestContract } from '@aztec/noir-test-contracts.js/Test';
78
import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-juice';
89
import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
910
import { PublicDataTreeLeaf } from '@aztec/stdlib/trees';
11+
import { ExecutionPayload } from '@aztec/stdlib/tx';
1012
import { defaultInitialAccountFeeJuice } from '@aztec/world-state/testing';
1113

1214
import type { TestWallet } from '../test-wallet/test_wallet.js';
1315
import { AutomineTestContext } from './automine_test_context.js';
1416

17+
/**
18+
* Declares a contract as the tx fee payer without contributing any call to the fee payload, unlike
19+
* SponsoredFeePaymentMethod which prepends the sponsor call (and thus makes the election run before any app call).
20+
* This lets a test place the electing call anywhere in the app payload, e.g. after the setup phase has ended.
21+
*/
22+
class DeferredSponsoredFeePaymentMethod extends SponsoredFeePaymentMethod {
23+
override async getExecutionPayload(): Promise<ExecutionPayload> {
24+
return new ExecutionPayload([], [], [], [], await this.getFeePayer());
25+
}
26+
}
27+
1528
// Private functions should receive automatically a phase check that avoids any nested call changing the phase.
1629
// Functions that opt out of this phase check can be marked with #[allow_phase_change].
1730
//
@@ -78,4 +91,21 @@ describe('automine/phase_check', () => {
7891
},
7992
});
8093
});
94+
95+
it('should fail when the fee payer is elected after the setup phase has ended', async () => {
96+
// BatchCall.simulate ignores the fee payment method, which would make the wallet fall back to
97+
// PREEXISTING_FEE_JUICE and end setup in the account entrypoint before any app call runs. Build the payload via
98+
// request() instead, which merges the payment method's payload (and thus its fee payer), and simulate it directly.
99+
const lateElection = await new BatchCall(wallet, [
100+
contract.methods.call_function_that_ends_setup_without_phase_check(),
101+
sponsoredFPC.methods.sponsor_unconditionally(),
102+
]).request({
103+
fee: {
104+
paymentMethod: new DeferredSponsoredFeePaymentMethod(sponsoredFPC.address),
105+
},
106+
});
107+
await expect(wallet.simulateTx(lateElection, { from: defaultAccountAddress })).rejects.toThrow(
108+
'fee payer must be elected during the setup phase',
109+
);
110+
});
81111
});

0 commit comments

Comments
 (0)