Skip to content

Commit f06a910

Browse files
yash-atreyarplusq
authored andcommitted
feat(cheatcodes): vm.rememberKeys (foundry-rs#9087)
* feat(`cheatcodes`): vm.rememberKeys * docs + return addresses + test * remeberKeys with language * doc nits * cargo cheats * set script wallet in config if unset * nit * test
1 parent 21eadeb commit f06a910

File tree

6 files changed

+244
-7
lines changed

6 files changed

+244
-7
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/vm.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2407,6 +2407,20 @@ interface Vm {
24072407
#[cheatcode(group = Crypto)]
24082408
function rememberKey(uint256 privateKey) external returns (address keyAddr);
24092409

2410+
/// Derive a set number of wallets from a mnemonic at the derivation path `m/44'/60'/0'/0/{0..count}`.
2411+
///
2412+
/// The respective private keys are saved to the local forge wallet for later use and their addresses are returned.
2413+
#[cheatcode(group = Crypto)]
2414+
function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs);
2415+
2416+
/// Derive a set number of wallets from a mnemonic in the specified language at the derivation path `m/44'/60'/0'/0/{0..count}`.
2417+
///
2418+
/// The respective private keys are saved to the local forge wallet for later use and their addresses are returned.
2419+
#[cheatcode(group = Crypto)]
2420+
function rememberKeys(string calldata mnemonic, string calldata derivationPath, string calldata language, uint32 count)
2421+
external
2422+
returns (address[] memory keyAddrs);
2423+
24102424
// -------- Uncategorized Utilities --------
24112425

24122426
/// Labels an address in call traces.

crates/cheatcodes/src/crypto.rs

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
//! Implementations of [`Crypto`](spec::Group::Crypto) Cheatcodes.
22
3-
use crate::{Cheatcode, Cheatcodes, Result, Vm::*};
3+
use crate::{Cheatcode, Cheatcodes, Result, ScriptWallets, Vm::*};
44
use alloy_primitives::{keccak256, Address, B256, U256};
55
use alloy_signer::{Signer, SignerSync};
66
use alloy_signer_local::{
77
coins_bip39::{
88
ChineseSimplified, ChineseTraditional, Czech, English, French, Italian, Japanese, Korean,
99
Portuguese, Spanish, Wordlist,
1010
},
11-
MnemonicBuilder, PrivateKeySigner,
11+
LocalSigner, MnemonicBuilder, PrivateKeySigner,
1212
};
1313
use alloy_sol_types::SolValue;
14+
use foundry_wallets::multi_wallet::MultiWallet;
1415
use k256::{
1516
ecdsa::SigningKey,
1617
elliptic_curve::{bigint::ArrayEncoding, sec1::ToEncodedPoint},
1718
};
1819
use p256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey as P256SigningKey};
20+
use std::sync::Arc;
1921

2022
/// The BIP32 default derivation path prefix.
2123
const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
@@ -89,14 +91,56 @@ impl Cheatcode for rememberKeyCall {
8991
fn apply(&self, state: &mut Cheatcodes) -> Result {
9092
let Self { privateKey } = self;
9193
let wallet = parse_wallet(privateKey)?;
92-
let address = wallet.address();
93-
if let Some(script_wallets) = state.script_wallets() {
94-
script_wallets.add_local_signer(wallet);
95-
}
94+
let address = inject_wallet(state, wallet);
9695
Ok(address.abi_encode())
9796
}
9897
}
9998

99+
impl Cheatcode for rememberKeys_0Call {
100+
fn apply(&self, state: &mut Cheatcodes) -> Result {
101+
let Self { mnemonic, derivationPath, count } = self;
102+
tracing::info!("Remembering {} keys", count);
103+
let wallets = derive_wallets::<English>(mnemonic, derivationPath, *count)?;
104+
105+
tracing::info!("Adding {} keys to script wallets", count);
106+
107+
let mut addresses = Vec::<Address>::with_capacity(wallets.len());
108+
for wallet in wallets {
109+
let addr = inject_wallet(state, wallet);
110+
addresses.push(addr);
111+
}
112+
113+
Ok(addresses.abi_encode())
114+
}
115+
}
116+
117+
impl Cheatcode for rememberKeys_1Call {
118+
fn apply(&self, state: &mut Cheatcodes) -> Result {
119+
let Self { mnemonic, derivationPath, language, count } = self;
120+
let wallets = derive_wallets_str(mnemonic, derivationPath, language, *count)?;
121+
let mut addresses = Vec::<Address>::with_capacity(wallets.len());
122+
for wallet in wallets {
123+
let addr = inject_wallet(state, wallet);
124+
addresses.push(addr);
125+
}
126+
127+
Ok(addresses.abi_encode())
128+
}
129+
}
130+
131+
fn inject_wallet(state: &mut Cheatcodes, wallet: LocalSigner<SigningKey>) -> Address {
132+
let address = wallet.address();
133+
if let Some(script_wallets) = state.script_wallets() {
134+
script_wallets.add_local_signer(wallet);
135+
} else {
136+
// This is needed in case of testing scripts, wherein script wallets are not set on setup.
137+
let script_wallets = ScriptWallets::new(MultiWallet::default(), None);
138+
script_wallets.add_local_signer(wallet);
139+
Arc::make_mut(&mut state.config).script_wallets = Some(script_wallets);
140+
}
141+
address
142+
}
143+
100144
impl Cheatcode for sign_1Call {
101145
fn apply(&self, _state: &mut Cheatcodes) -> Result {
102146
let Self { privateKey, digest } = self;
@@ -228,7 +272,7 @@ fn sign_with_wallet(
228272
} else if signers.len() == 1 {
229273
*signers.keys().next().unwrap()
230274
} else {
231-
bail!("could not determine signer");
275+
bail!("could not determine signer, there are multiple signers available use vm.sign(signer, digest) to specify one");
232276
};
233277

234278
let wallet = signers
@@ -309,6 +353,50 @@ fn derive_key<W: Wordlist>(mnemonic: &str, path: &str, index: u32) -> Result {
309353
Ok(private_key.abi_encode())
310354
}
311355

356+
fn derive_wallets_str(
357+
mnemonic: &str,
358+
path: &str,
359+
language: &str,
360+
count: u32,
361+
) -> Result<Vec<LocalSigner<SigningKey>>> {
362+
match language {
363+
"chinese_simplified" => derive_wallets::<ChineseSimplified>(mnemonic, path, count),
364+
"chinese_traditional" => derive_wallets::<ChineseTraditional>(mnemonic, path, count),
365+
"czech" => derive_wallets::<Czech>(mnemonic, path, count),
366+
"english" => derive_wallets::<English>(mnemonic, path, count),
367+
"french" => derive_wallets::<French>(mnemonic, path, count),
368+
"italian" => derive_wallets::<Italian>(mnemonic, path, count),
369+
"japanese" => derive_wallets::<Japanese>(mnemonic, path, count),
370+
"korean" => derive_wallets::<Korean>(mnemonic, path, count),
371+
"portuguese" => derive_wallets::<Portuguese>(mnemonic, path, count),
372+
"spanish" => derive_wallets::<Spanish>(mnemonic, path, count),
373+
_ => Err(fmt_err!("unsupported mnemonic language: {language:?}")),
374+
}
375+
}
376+
377+
fn derive_wallets<W: Wordlist>(
378+
mnemonic: &str,
379+
path: &str,
380+
count: u32,
381+
) -> Result<Vec<LocalSigner<SigningKey>>> {
382+
let mut out = path.to_string();
383+
384+
if !out.ends_with('/') {
385+
out.push('/');
386+
}
387+
388+
let mut wallets = Vec::with_capacity(count as usize);
389+
for idx in 0..count {
390+
let wallet = MnemonicBuilder::<W>::default()
391+
.phrase(mnemonic)
392+
.derivation_path(format!("{out}{idx}"))?
393+
.build()?;
394+
wallets.push(wallet);
395+
}
396+
397+
Ok(wallets)
398+
}
399+
312400
#[cfg(test)]
313401
mod tests {
314402
use super::*;

crates/forge/tests/cli/script.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2108,3 +2108,41 @@ Script ran successfully.
21082108
21092109
"#]]);
21102110
});
2111+
2112+
forgetest_init!(can_remeber_keys, |prj, cmd| {
2113+
let script = prj
2114+
.add_source(
2115+
"Foo",
2116+
r#"
2117+
import "forge-std/Script.sol";
2118+
2119+
interface Vm {
2120+
function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs);
2121+
}
2122+
2123+
contract WalletScript is Script {
2124+
function run() public {
2125+
string memory mnemonic = "test test test test test test test test test test test junk";
2126+
string memory derivationPath = "m/44'/60'/0'/0/";
2127+
address[] memory wallets = Vm(address(vm)).rememberKeys(mnemonic, derivationPath, 3);
2128+
for (uint256 i = 0; i < wallets.length; i++) {
2129+
console.log(wallets[i]);
2130+
}
2131+
}
2132+
}"#,
2133+
)
2134+
.unwrap();
2135+
cmd.arg("script").arg(script).assert_success().stdout_eq(str![[r#"
2136+
[COMPILING_FILES] with [SOLC_VERSION]
2137+
[SOLC_VERSION] [ELAPSED]
2138+
Compiler run successful!
2139+
Script ran successfully.
2140+
[GAS]
2141+
2142+
== Logs ==
2143+
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
2144+
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
2145+
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
2146+
2147+
"#]]);
2148+
});

crates/forge/tests/cli/test_cmd.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,3 +2219,58 @@ warning: specifying argument for --decode-internal is deprecated and will be rem
22192219
22202220
"#]]);
22212221
});
2222+
2223+
// Test a script that calls vm.rememberKeys
2224+
forgetest_init!(script_testing, |prj, cmd| {
2225+
prj
2226+
.add_source(
2227+
"Foo",
2228+
r#"
2229+
import "forge-std/Script.sol";
2230+
2231+
interface Vm {
2232+
function rememberKeys(string calldata mnemonic, string calldata derivationPath, uint32 count) external returns (address[] memory keyAddrs);
2233+
}
2234+
2235+
contract WalletScript is Script {
2236+
function run() public {
2237+
string memory mnemonic = "test test test test test test test test test test test junk";
2238+
string memory derivationPath = "m/44'/60'/0'/0/";
2239+
address[] memory wallets = Vm(address(vm)).rememberKeys(mnemonic, derivationPath, 3);
2240+
for (uint256 i = 0; i < wallets.length; i++) {
2241+
console.log(wallets[i]);
2242+
}
2243+
}
2244+
}
2245+
2246+
contract FooTest {
2247+
WalletScript public script;
2248+
2249+
2250+
function setUp() public {
2251+
script = new WalletScript();
2252+
}
2253+
2254+
function testWalletScript() public {
2255+
script.run();
2256+
}
2257+
}
2258+
2259+
"#,
2260+
)
2261+
.unwrap();
2262+
2263+
cmd.args(["test", "--mt", "testWalletScript", "-vvv"]).assert_success().stdout_eq(str![[r#"
2264+
[COMPILING_FILES] with [SOLC_VERSION]
2265+
[SOLC_VERSION] [ELAPSED]
2266+
Compiler run successful!
2267+
2268+
Ran 1 test for src/Foo.sol:FooTest
2269+
[PASS] testWalletScript() ([GAS])
2270+
Logs:
2271+
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
2272+
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
2273+
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
2274+
...
2275+
"#]]);
2276+
});

testdata/cheats/Vm.sol

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)