Skip to content

Commit 2863b7a

Browse files
castillaxgithub-actions[bot]pgherveou
authored
Fix generated address returned by Substrate RPC runtime call (#8504)
## Description When dry-running a contract deployment through the runtime API, the returned address does not match the actual address that will be used when the transaction is submitted. This inconsistency occurs because the address derivation logic doesn't properly account for the difference between transaction execution and dry-run execution contexts. The issue stems from the `create1` address derivation logic in `exec.rs`: ```rust address::create1( &deployer, // the Nonce from the origin has been incremented pre-dispatch, so we // need to subtract 1 to get the nonce at the time of the call. if origin_is_caller { account_nonce.saturating_sub(1u32.into()).saturated_into() } else { account_nonce.saturated_into() }, ) ``` The code correctly subtracts 1 from the account nonce during a transaction execution (because the nonce is incremented pre-dispatch), but doesn't account for execution context - whether it's a real transaction or a dry run through the RPC. ## Review Notes This PR adds a new condition to check for the `IncrementOnce` when calculating the nonce for address derivation: ```rust address::create1( &deployer, // the Nonce from the origin has been incremented pre-dispatch, so we // need to subtract 1 to get the nonce at the time of the call. if origin_is_caller && matches!(exec_context, IncrementOnce::AlreadyIncremented) { account_nonce.saturating_sub(1u32.into()).saturated_into() } else { account_nonce.saturated_into() }, ) ``` ## Before Fix - Dry-run contract deployment returns address derived with nonce N - Actual transaction deployment creates contract at address derived with nonce N-1 - Result: Inconsistent addresses between simulation and actual execution ## After Fix - Dry-run and actual transaction deployments both create contracts at the same address - Result: Consistent contract addresses regardless of execution context - Added test case to verify nonce handling in different execution contexts This fix ensures that users can rely on the address returned by a dry run to match the actual address that will be used when the transaction is submitted. Fixes paritytech/contract-issues#37 # Checklist * [x] My PR includes a detailed description as outlined in the "Description" and its two subsections above. * [x] My PR follows the [labeling requirements]( https://github.com/paritytech/polkadot-sdk/blob/master/docs/contributor/CONTRIBUTING.md#Process ) of this project (at minimum one label for `T` required) * External contributors: ask maintainers to put the right label on your PR. * [x] I have made corresponding changes to the documentation (if applicable) * [x] I have added tests that prove my fix is effective or that my feature works (if applicable) --------- Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: pgherveou <[email protected]>
1 parent ff5c67c commit 2863b7a

9 files changed

Lines changed: 168 additions & 10 deletions

File tree

cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ use frame_system::{
6262
};
6363
use pallet_asset_conversion_tx_payment::SwapAssetAdapter;
6464
use pallet_nfts::{DestroyWitness, PalletFeatures};
65-
use pallet_revive::{evm::runtime::EthExtra, AddressMapper};
65+
use pallet_revive::{evm::runtime::EthExtra, AddressMapper, NonceAlreadyIncremented};
6666
use pallet_xcm::EnsureXcm;
6767
use parachains_common::{
6868
impls::DealWithFees, message_queue::*, AccountId, AssetIdForTrustBackedAssets, AuraId, Balance,
@@ -2364,6 +2364,7 @@ impl_runtime_apis! {
23642364
code,
23652365
data,
23662366
salt,
2367+
NonceAlreadyIncremented::No,
23672368
)
23682369
}
23692370

prdoc/pr_8504.prdoc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
title: Fix generated address returned by Substrate RPC runtime call
2+
doc:
3+
- audience: Runtime Dev
4+
description: |-
5+
## Description
6+
7+
When dry-running a contract deployment through the runtime API, the returned address does not match the actual address that will be used when the transaction is submitted. This inconsistency occurs because the address derivation logic doesn't properly account for the difference between transaction execution and dry-run execution contexts.
8+
9+
The issue stems from the `create1` address derivation logic in `exec.rs`:
10+
11+
```rust
12+
address::create1(
13+
&deployer,
14+
// the Nonce from the origin has been incremented pre-dispatch, so we
15+
// need to subtract 1 to get the nonce at the time of the call.
16+
if origin_is_caller {
17+
account_nonce.saturating_sub(1u32.into()).saturated_into()
18+
} else {
19+
account_nonce.saturated_into()
20+
},
21+
)
22+
```
23+
24+
The code correctly subtracts 1 from the account nonce during a transaction execution (because the nonce is incremented pre-dispatch), but doesn't account for execution context - whether it's a real transaction or a dry run through the RPC.
25+
26+
## Review Notes
27+
28+
This PR adds a new condition to check for the `ExecContext` when calculating the nonce for address derivation:
29+
30+
```rust
31+
address::create1(
32+
&deployer,
33+
// the Nonce from the origin has been incremented pre-dispatch, so we
34+
// need to subtract 1 to get the nonce at the time of the call.
35+
if origin_is_caller && matches!(exec_context, ExecContext::Transaction) {
36+
account_nonce.saturating_sub(1u32.into()).saturated_into()
37+
} else {
38+
account_nonce.saturated_into()
39+
},
40+
)
41+
```
42+
43+
A new test `nonce_not_incremented_in_dry_run()` has been added to verify the behavior.
44+
45+
## Before Fix
46+
47+
- Dry-run contract deployment returns address derived with nonce N
48+
- Actual transaction deployment creates contract at address derived with nonce N-1
49+
- Result: Inconsistent addresses between simulation and actual execution
50+
51+
## After Fix
52+
53+
- Dry-run and actual transaction deployments both create contracts at the same address
54+
- Result: Consistent contract addresses regardless of execution context
55+
- Added test case to verify nonce handling in different execution contexts
56+
57+
This fix ensures that users can rely on the address returned by a dry run to match the actual address that will be used when the transaction is submitted.
58+
59+
Fixes https://github.com/paritytech/contract-issues/issues/37
60+
61+
# Checklist
62+
63+
* [x] My PR includes a detailed description as outlined in the "Description" and its two subsections above.
64+
* [x] My PR follows the [labeling requirements](
65+
https://github.com/paritytech/polkadot-sdk/blob/master/docs/contributor/CONTRIBUTING.md#Process
66+
) of this project (at minimum one label for `T` required)
67+
* External contributors: ask maintainers to put the right label on your PR.
68+
* [x] I have made corresponding changes to the documentation (if applicable)
69+
* [x] I have added tests that prove my fix is effective or that my feature works (if applicable)
70+
crates:
71+
- name: asset-hub-westend-runtime
72+
bump: patch
73+
- name: pallet-revive
74+
bump: major

substrate/bin/node/runtime/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ use pallet_im_online::sr25519::AuthorityId as ImOnlineId;
8585
use pallet_nfts::PalletFeatures;
8686
use pallet_nis::WithMaximumOf;
8787
use pallet_nomination_pools::PoolId;
88-
use pallet_revive::{evm::runtime::EthExtra, AddressMapper};
88+
use pallet_revive::{evm::runtime::EthExtra, AddressMapper, NonceAlreadyIncremented};
8989
use pallet_session::historical as pallet_session_historical;
9090
use sp_core::U256;
9191
use sp_runtime::traits::TransactionExtension;
@@ -3443,6 +3443,7 @@ impl_runtime_apis! {
34433443
code,
34443444
data,
34453445
salt,
3446+
NonceAlreadyIncremented::No,
34463447
)
34473448
}
34483449

substrate/frame/revive/src/call_builder.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ use crate::{
3333
transient_storage::MeterEntry,
3434
wasm::{PreparedCall, Runtime},
3535
BalanceOf, Code, CodeInfoOf, Config, ContractInfo, ContractInfoOf, DepositLimit, Error,
36-
GasMeter, MomentOf, Origin, Pallet as Contracts, PristineCode, WasmBlob, Weight,
36+
GasMeter, MomentOf, NonceAlreadyIncremented, Origin, Pallet as Contracts, PristineCode,
37+
WasmBlob, Weight,
3738
};
3839
use alloc::{vec, vec::Vec};
3940
use frame_support::{storage::child, traits::fungible::Mutate};
@@ -270,6 +271,7 @@ where
270271
Code::Upload(module.code),
271272
data,
272273
salt,
274+
NonceAlreadyIncremented::No,
273275
);
274276

275277
let address = outcome.result?.addr;

substrate/frame/revive/src/exec.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::{
2626
tracing::if_tracing,
2727
transient_storage::TransientStorage,
2828
BalanceOf, CodeInfo, CodeInfoOf, Config, ContractInfo, ContractInfoOf, ConversionPrecision,
29-
Error, Event, ImmutableData, ImmutableDataOf, Pallet as Contracts,
29+
Error, Event, ImmutableData, ImmutableDataOf, NonceAlreadyIncremented, Pallet as Contracts,
3030
};
3131
use alloc::vec::Vec;
3232
use core::{fmt::Debug, marker::PhantomData, mem};
@@ -614,6 +614,7 @@ enum FrameArgs<'a, T: Config, E> {
614614
salt: Option<&'a [u8; 32]>,
615615
/// The input data is used in the contract address derivation of the new contract.
616616
input_data: &'a [u8],
617+
nonce_already_incremented: NonceAlreadyIncremented,
617618
},
618619
}
619620

@@ -807,13 +808,15 @@ where
807808
input_data: Vec<u8>,
808809
salt: Option<&[u8; 32]>,
809810
skip_transfer: bool,
811+
nonce_already_incremented: NonceAlreadyIncremented,
810812
) -> Result<(H160, ExecReturnValue), ExecError> {
811813
let (mut stack, executable) = Self::new(
812814
FrameArgs::Instantiate {
813815
sender: origin.clone(),
814816
executable,
815817
salt,
816818
input_data: input_data.as_ref(),
819+
nonce_already_incremented,
817820
},
818821
Origin::from_account_id(origin),
819822
gas_meter,
@@ -874,7 +877,6 @@ where
874877
storage_meter,
875878
BalanceOf::<T>::max_value(),
876879
false,
877-
true,
878880
)?
879881
else {
880882
return Ok(None);
@@ -908,7 +910,6 @@ where
908910
storage_meter: &mut storage::meter::GenericMeter<T, S>,
909911
deposit_limit: BalanceOf<T>,
910912
read_only: bool,
911-
origin_is_caller: bool,
912913
) -> Result<Option<(Frame<T>, ExecutableOrPrecompile<T, E, Self>)>, ExecError> {
913914
let (account_id, contract_info, executable, delegate, entry_point) = match frame_args {
914915
FrameArgs::Call { dest, cached_info, delegated_call } => {
@@ -972,7 +973,13 @@ where
972973

973974
(dest, contract, executable, delegated_call, ExportedFunction::Call)
974975
},
975-
FrameArgs::Instantiate { sender, executable, salt, input_data } => {
976+
FrameArgs::Instantiate {
977+
sender,
978+
executable,
979+
salt,
980+
input_data,
981+
nonce_already_incremented,
982+
} => {
976983
let deployer = T::AddressMapper::to_address(&sender);
977984
let account_nonce = <System<T>>::account_nonce(&sender);
978985
let address = if let Some(salt) = salt {
@@ -983,7 +990,7 @@ where
983990
&deployer,
984991
// the Nonce from the origin has been incremented pre-dispatch, so we
985992
// need to subtract 1 to get the nonce at the time of the call.
986-
if origin_is_caller {
993+
if matches!(nonce_already_incremented, NonceAlreadyIncremented::Yes) {
987994
account_nonce.saturating_sub(1u32.into()).saturated_into()
988995
} else {
989996
account_nonce.saturated_into()
@@ -1059,7 +1066,6 @@ where
10591066
nested_storage,
10601067
deposit_limit,
10611068
read_only,
1062-
false,
10631069
)? {
10641070
self.frames.try_push(frame).map_err(|_| Error::<T>::MaxCallDepthReached)?;
10651071
Ok(Some(executable))
@@ -1665,6 +1671,7 @@ where
16651671
executable,
16661672
salt,
16671673
input_data: input_data.as_ref(),
1674+
nonce_already_incremented: NonceAlreadyIncremented::No,
16681675
},
16691676
value.try_into().map_err(|_| Error::<T>::BalanceConversionFailed)?,
16701677
gas_limit,

substrate/frame/revive/src/exec/tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ fn input_data_to_instantiate() {
566566
vec![1, 2, 3, 4],
567567
Some(&[0; 32]),
568568
false,
569+
NonceAlreadyIncremented::Yes,
569570
);
570571
assert_matches!(result, Ok(_));
571572
});
@@ -1069,6 +1070,7 @@ fn refuse_instantiate_with_value_below_existential_deposit() {
10691070
vec![],
10701071
Some(&[0; 32]),
10711072
false,
1073+
NonceAlreadyIncremented::Yes,
10721074
),
10731075
Err(_)
10741076
);
@@ -1104,6 +1106,7 @@ fn instantiation_work_with_success_output() {
11041106
vec![],
11051107
Some(&[0 ;32]),
11061108
false,
1109+
NonceAlreadyIncremented::Yes,
11071110
),
11081111
Ok((address, ref output)) if output.data == vec![80, 65, 83, 83] => address
11091112
);
@@ -1150,6 +1153,7 @@ fn instantiation_fails_with_failing_output() {
11501153
vec![],
11511154
Some(&[0; 32]),
11521155
false,
1156+
NonceAlreadyIncremented::Yes,
11531157
),
11541158
Ok((address, ref output)) if output.data == vec![70, 65, 73, 76] => address
11551159
);
@@ -1313,6 +1317,7 @@ fn termination_from_instantiate_fails() {
13131317
vec![],
13141318
Some(&[0; 32]),
13151319
false,
1320+
NonceAlreadyIncremented::Yes,
13161321
),
13171322
Err(ExecError {
13181323
error: Error::<Test>::TerminatedInConstructor.into(),
@@ -1441,6 +1446,7 @@ fn recursive_call_during_constructor_is_balance_transfer() {
14411446
vec![],
14421447
Some(&[0; 32]),
14431448
false,
1449+
NonceAlreadyIncremented::Yes,
14441450
);
14451451
assert_matches!(result, Ok(_));
14461452
});
@@ -1802,6 +1808,7 @@ fn nonce() {
18021808
vec![],
18031809
Some(&[0; 32]),
18041810
false,
1811+
NonceAlreadyIncremented::Yes,
18051812
)
18061813
.ok();
18071814
assert_eq!(System::account_nonce(&ALICE), 0);
@@ -1815,6 +1822,7 @@ fn nonce() {
18151822
vec![],
18161823
Some(&[0; 32]),
18171824
false,
1825+
NonceAlreadyIncremented::Yes,
18181826
));
18191827
assert_eq!(System::account_nonce(&ALICE), 1);
18201828

@@ -1827,6 +1835,7 @@ fn nonce() {
18271835
vec![],
18281836
Some(&[0; 32]),
18291837
false,
1838+
NonceAlreadyIncremented::Yes,
18301839
));
18311840
assert_eq!(System::account_nonce(&ALICE), 2);
18321841

@@ -1839,6 +1848,7 @@ fn nonce() {
18391848
vec![],
18401849
Some(&[0; 32]),
18411850
false,
1851+
NonceAlreadyIncremented::Yes,
18421852
));
18431853
assert_eq!(System::account_nonce(&ALICE), 3);
18441854
});
@@ -2825,6 +2835,7 @@ fn immutable_data_set_overrides() {
28252835
vec![],
28262836
None,
28272837
false,
2838+
NonceAlreadyIncremented::Yes,
28282839
)
28292840
.unwrap()
28302841
.0;

substrate/frame/revive/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,18 @@ pub mod pallet {
513513
AddressMapping,
514514
}
515515

516+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
517+
pub enum NonceAlreadyIncremented {
518+
/// Indicates that the nonce has not been incremented yet.
519+
///
520+
/// This happens when the instantiation is triggered by a dry-run or another contract.
521+
No,
522+
/// Indicates that the nonce has already been incremented.
523+
///
524+
/// This happens when the instantiation is triggered by a transaction.
525+
Yes,
526+
}
527+
516528
/// A mapping from a contract's code hash to its code.
517529
#[pallet::storage]
518530
pub(crate) type PristineCode<T: Config> = StorageMap<_, Identity, H256, CodeVec>;
@@ -797,6 +809,7 @@ pub mod pallet {
797809
Code::Existing(code_hash),
798810
data,
799811
salt,
812+
NonceAlreadyIncremented::Yes,
800813
);
801814
if let Ok(retval) = &output.result {
802815
if retval.result.did_revert() {
@@ -861,6 +874,7 @@ pub mod pallet {
861874
Code::Upload(code),
862875
data,
863876
salt,
877+
NonceAlreadyIncremented::Yes,
864878
);
865879
if let Ok(retval) = &output.result {
866880
if retval.result.did_revert() {
@@ -1075,6 +1089,7 @@ where
10751089
code: Code,
10761090
data: Vec<u8>,
10771091
salt: Option<[u8; 32]>,
1092+
nonce_already_incremented: NonceAlreadyIncremented,
10781093
) -> ContractResult<InstantiateReturnValue, BalanceOf<T>> {
10791094
let mut gas_meter = GasMeter::new(gas_limit);
10801095
let mut storage_deposit = Default::default();
@@ -1117,6 +1132,7 @@ where
11171132
data,
11181133
salt.as_ref(),
11191134
unchecked_deposit_limit,
1135+
nonce_already_incremented,
11201136
);
11211137
storage_deposit = storage_meter
11221138
.try_into_deposit(&instantiate_origin, unchecked_deposit_limit)?
@@ -1286,6 +1302,7 @@ where
12861302
Code::Upload(code.to_vec()),
12871303
data.to_vec(),
12881304
None,
1305+
NonceAlreadyIncremented::No,
12891306
);
12901307

12911308
let returned_data = match result.result {

substrate/frame/revive/src/test_utils/builder.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use super::{deposit_limit, GAS_LIMIT};
1919
use crate::{
2020
address::AddressMapper, AccountIdOf, BalanceOf, Code, Config, ContractResult, DepositLimit,
21-
ExecReturnValue, InstantiateReturnValue, OriginFor, Pallet, Weight,
21+
ExecReturnValue, InstantiateReturnValue, NonceAlreadyIncremented, OriginFor, Pallet, Weight,
2222
};
2323
use alloc::{vec, vec::Vec};
2424
use frame_support::pallet_prelude::DispatchResultWithPostInfo;
@@ -138,6 +138,7 @@ builder!(
138138
code: Code,
139139
data: Vec<u8>,
140140
salt: Option<[u8; 32]>,
141+
nonce_already_incremented: NonceAlreadyIncremented,
141142
) -> ContractResult<InstantiateReturnValue, BalanceOf<T>>;
142143

143144
/// Build the instantiate call and unwrap the result.
@@ -165,6 +166,7 @@ builder!(
165166
code,
166167
data: vec![],
167168
salt: Some([0; 32]),
169+
nonce_already_incremented: NonceAlreadyIncremented::Yes,
168170
}
169171
}
170172
);

0 commit comments

Comments
 (0)