diff --git a/Cargo.lock b/Cargo.lock index 2a75af837..899e8c423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,7 +1200,7 @@ dependencies = [ [[package]] name = "arbos-revm" version = "0.1.0" -source = "git+https://github.com/iosiro/arbos-revm?rev=39794a701235cdb835efc185e3920ebd032f1bd6#39794a701235cdb835efc185e3920ebd032f1bd6" +source = "git+https://github.com/iosiro/arbos-revm#39794a701235cdb835efc185e3920ebd032f1bd6" dependencies = [ "alloy-rlp", "alloy-sol-types", diff --git a/Cargo.toml b/Cargo.toml index 5b72893b8..4c72e776b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -220,7 +220,7 @@ foundry-wallets = { path = "crates/wallets" } foundry-linking = { path = "crates/linking" } # arbos-revm -arbos-revm = { git = "https://github.com/iosiro/arbos-revm", rev = "39794a701235cdb835efc185e3920ebd032f1bd6", default-features = false } +arbos-revm = { git = "https://github.com/iosiro/arbos-revm", version = "0.1.0", default-features = false } # solc & compilation utilities foundry-block-explorers = { version = "0.22.0", default-features = false } diff --git a/README.md b/README.md index 3487c7873..7ca279554 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This project was developed by [iosiro](https://www.iosiro.com/) as part of the [ ## Features - **Native Stylus Execution**: Execute Stylus WASM programs directly in Forge tests without requiring a network fork -- **Stylus Deployment Cheatcodes**: Deploy Stylus contracts using `vm.deployStylusCode()` and `vm.getStylusCode()` +- **Stylus Deployment Cheatcodes**: Deploy Stylus contracts using `vm.deployStylusCode()`, `vm.getStylusCode()`, and `vm.getStylusInitCode()` - **Brotli Compression**: Built-in `vm.brotliCompress()` and `vm.brotliDecompress()` cheatcodes for Stylus bytecode handling - **ArbOS State**: Automatic initialization of ArbOS state with configurable parameters - **Arbitrum Precompiles**: Full support for 13 Arbitrum-specific precompiles (see [Supported Precompiles](#supported-precompiles)) @@ -141,8 +141,45 @@ address deployed = vm.deployStylusCode(string artifactPath, bytes32 salt); // Get Stylus bytecode (compressed with magic prefix) bytes memory code = vm.getStylusCode(string artifactPath); + +// Get init code for CREATE/CREATE2 deployment +bytes memory initCode = vm.getStylusInitCode(string artifactPath); ``` +#### How `deployStylusCode` Works + +`deployStylusCode` delegates to the **StylusDeployer** contract (at `0xcEcba2F1DC234f70Dd89F2041029807F8D03A990` on Arbitrum), which atomically: + +1. Deploys the compressed bytecode via CREATE (or CREATE2 when a `salt` is provided) +2. Activates the program via the ARB_WASM precompile (paying the activation data fee) +3. Calls the Stylus constructor if `constructorArgs` are provided + +This produces a **single CALL transaction** when broadcasting, ensuring on-chain replay deploys a fully activated contract. + +In local tests (no broadcast), the StylusDeployer is deployed on-demand. When broadcasting, the StylusDeployer must already exist on-chain (it is pre-deployed on Arbitrum networks). A custom deployer address can be configured via: + +```toml +# foundry.toml +[profile.default.stylus] +deployer_address = "0x..." +``` + +#### Broadcasting Stylus Deployments + +`deployStylusCode` is fully compatible with `forge script --broadcast`: + +```solidity +contract DeployStylus is Script { + function run() external { + vm.startBroadcast(); + address deployed = vm.deployStylusCode("path/to/program.wasm"); + vm.stopBroadcast(); + } +} +``` + +The broadcast captures a single transaction: a CALL to the StylusDeployer with the activation data fee automatically estimated (actual fee + 20% buffer). The StylusDeployer refunds any excess ETH to the sender. + ### Brotli Compression ```solidity @@ -155,7 +192,7 @@ bytes memory decompressed = vm.brotliDecompress(bytes compressed); ## WASM Processing -When you use `vm.deployStylusCode()` or `vm.getStylusCode()`, the WASM binary is automatically processed to match the behavior of `cargo stylus deploy`: +When you use `vm.deployStylusCode()`, `vm.getStylusCode()`, or `vm.getStylusInitCode()`, the WASM binary is automatically processed to match the behavior of `cargo stylus deploy`: ### 1. Metadata Stripping @@ -390,7 +427,7 @@ This fork is based on Foundry v1.5.1 with the following changes: - **Added**: Native Stylus/WASM execution via [arbos-revm](https://github.com/iosiro/arbos-revm) - **Added**: ArbOS state initialization with configurable parameters -- **Added**: Stylus deployment cheatcodes (`deployStylusCode`, `getStylusCode`) +- **Added**: Stylus deployment cheatcodes (`deployStylusCode`, `getStylusCode`, `getStylusInitCode`) - **Added**: Brotli compression cheatcodes (`brotliCompress`, `brotliDecompress`) - **Added**: 13 Arbitrum precompiles (ArbSys, ArbWasm, ArbGasInfo, etc.) - **Added**: Stylus configuration options (CLI, foundry.toml, inline) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 958f69a9a..025871410 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -6807,7 +6807,7 @@ { "func": { "id": "getStylusCode", - "description": "Returns the deployment bytecode for a Stylus contract suitable for use with the StylusDeployer contract.\nTakes in the relative path to the WASM or Brotli compressed WASM binary.\nApplies the same compression and prefixing logic as `deployStylusCode`.", + "description": "Returns the compressed and prefixed Stylus bytecode (runtime code) for a contract.\nTakes in the relative path to the WASM or Brotli compressed WASM binary.\nApplies the same compression and prefixing logic as `deployStylusCode`.\nThis returns raw runtime bytecode without an init code wrapper, suitable for use with `vm.etch`.", "declaration": "function getStylusCode(string calldata artifactPath) external view returns (bytes memory);", "visibility": "external", "mutability": "view", @@ -6824,6 +6824,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getStylusInitCode", + "description": "Returns init code for a Stylus contract suitable for CREATE/CREATE2 or the StylusDeployer contract.\nTakes in the relative path to the WASM or Brotli compressed WASM binary.\nThe returned bytecode wraps the compressed Stylus code in EVM init code that\ndeploys it as contract code when executed via CREATE or CREATE2.", + "declaration": "function getStylusInitCode(string calldata artifactPath) external view returns (bytes memory);", + "visibility": "external", + "mutability": "view", + "signature": "getStylusInitCode(string)", + "selector": "0x7fb2f6d1", + "selectorBytes": [ + 127, + 178, + 246, + 209 + ] + }, + "group": "filesystem", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "getWallets", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 38862d89f..9744fe8a5 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2040,12 +2040,20 @@ interface Vm { #[cheatcode(group = Filesystem)] function deployStylusCode(string calldata artifactPath, bytes calldata constructorArgs, uint256 value, bytes32 salt) external returns (address deployedAddress); - /// Returns the deployment bytecode for a Stylus contract suitable for use with the StylusDeployer contract. + /// Returns the compressed and prefixed Stylus bytecode (runtime code) for a contract. /// Takes in the relative path to the WASM or Brotli compressed WASM binary. /// Applies the same compression and prefixing logic as `deployStylusCode`. + /// This returns raw runtime bytecode without an init code wrapper, suitable for use with `vm.etch`. #[cheatcode(group = Filesystem)] function getStylusCode(string calldata artifactPath) external view returns (bytes memory); + /// Returns init code for a Stylus contract suitable for CREATE/CREATE2 or the StylusDeployer contract. + /// Takes in the relative path to the WASM or Brotli compressed WASM binary. + /// The returned bytecode wraps the compressed Stylus code in EVM init code that + /// deploys it as contract code when executed via CREATE or CREATE2. + #[cheatcode(group = Filesystem)] + function getStylusInitCode(string calldata artifactPath) external view returns (bytes memory); + /// Compresses the given data using Brotli compression (quality: 11, window: 22). #[cheatcode(group = String)] function brotliCompress(bytes calldata data) external pure returns (bytes memory compressed); diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 847f19b7c..5b2a0e27b 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -890,7 +890,7 @@ impl Cheatcodes { self.apply_accesslist(ecx); // Apply our broadcast - if let Some(broadcast) = &self.broadcast { + if let Some(broadcast) = &mut self.broadcast { // Additional check as transfers in forge scripts seem to be estimated at 2300 // by revm leading to "Intrinsic gas too low" failure when simulated on chain. let is_fixed_gas_limit = call.gas_limit >= 21_000 && !self.dynamic_gas_limit; @@ -899,8 +899,16 @@ impl Cheatcodes { // We only apply a broadcast *to a specific depth*. // // We do this because any subsequent contract calls *must* exist on chain and - // we only want to grab *this* call, not internal ones - if curr_depth == broadcast.depth && call.caller == broadcast.original_caller { + // we only want to grab *this* call, not internal ones. + // Additionally, `call_from_code` allows cheatcode-internal exec_call to be + // captured as a broadcastable CALL (e.g., deployStylusCode via StylusDeployer). + let is_call_from_code = broadcast.call_from_code; + if is_call_from_code { + broadcast.call_from_code = false; + } + if (curr_depth == broadcast.depth && call.caller == broadcast.original_caller) + || is_call_from_code + { // At the target depth we set `msg.sender` & tx.origin. // We are simulating the caller as being an EOA, so *both* must be set to the // broadcast.origin. diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index ca9e9e8cc..2af5ae377 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -287,6 +287,9 @@ pub struct Broadcast { pub single_call: bool, /// Whether `vm.deployCode` cheatcode is used to deploy from code. pub deploy_from_code: bool, + /// Whether a cheatcode's internal `exec_call` should be captured as a broadcastable CALL. + /// Analogous to `deploy_from_code` but for CALLs instead of CREATEs. + pub call_from_code: bool, } /// Contains context for wallet management. @@ -386,6 +389,7 @@ fn broadcast(ccx: &mut CheatsCtxt, new_origin: Option<&Address>, single_call: bo depth, single_call, deploy_from_code: false, + call_from_code: false, }; debug!(target: "cheatcodes", ?broadcast, "started"); ccx.state.broadcast = Some(broadcast); diff --git a/crates/cheatcodes/src/stylus.rs b/crates/cheatcodes/src/stylus.rs index 1dd849cef..9c7d2b449 100644 --- a/crates/cheatcodes/src/stylus.rs +++ b/crates/cheatcodes/src/stylus.rs @@ -1,24 +1,33 @@ use std::{fs, path::PathBuf}; -use alloy_primitives::{Address, Bytes, U256, address, hex}; -use alloy_sol_types::SolValue; +use alloy_primitives::{Address, Bytes, FixedBytes, U256, hex}; +use alloy_sol_types::{SolCall, SolValue, sol}; use arbos_revm::{ state::program::activate_program, stylus_executor::stylus_code, utils::{Dictionary, brotli_compress, brotli_decompress, strip_wasm_for_stylus}, }; use foundry_config::fs_permissions::FsAccessKind; +use foundry_evm_core::constants::{DEFAULT_STYLUS_DEPLOYER, DEFAULT_STYLUS_DEPLOYER_RUNTIME_CODE}; use revm::{ - context::{ContextTr, CreateScheme, JournalTr}, - interpreter::{CallInputs, CallScheme, CreateInputs}, + bytecode::Bytecode, + context::{ContextTr, JournalTr}, + interpreter::{CallInputs, CallScheme}, + primitives::KECCAK_EMPTY, }; use spec::Vm::*; use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result}; -/// Default address of the StylusDeployer contract. -const DEFAULT_STYLUS_DEPLOYER_ADDRESS: Address = - address!("0xcEcba2F1DC234f70Dd89F2041029807F8D03A990"); +/// Generous activation fee budget (1 ETH). StylusDeployer sends `msg.value - initValue` to +/// ARB_WASM for activation and refunds excess to `msg.sender`. +const ACTIVATION_FEE_BUDGET: U256 = U256::from_limbs([1_000_000_000_000_000_000u64, 0, 0, 0]); + +sol! { + /// StylusDeployer.deploy function ABI + function deploy(bytes initCode, bytes constructorArgs, uint256 value, bytes32 salt) + external payable returns (address); +} impl Cheatcode for deployStylusCode_0Call { fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { @@ -83,6 +92,13 @@ impl Cheatcode for getStylusCodeCall { } } +impl Cheatcode for getStylusInitCodeCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { artifactPath: path } = self; + get_stylus_init_code(state, path) + } +} + impl Cheatcode for brotliCompressCall { fn apply(&self, _state: &mut Cheatcodes) -> Result { let Self { data } = self; @@ -97,9 +113,15 @@ impl Cheatcode for brotliDecompressCall { } } -/// Helper function to deploy stylus contract from artifact code. -/// Matches StylusDeployer.sol behavior: deploys, activates via ARB_WASM precompile, then -/// initializes. Uses CREATE2 scheme if salt specified. +/// Deploys a Stylus contract via the StylusDeployer contract. +/// +/// Delegates to the StylusDeployer contract which atomically handles: +/// 1. Deploy compressed bytecode via CREATE/CREATE2 +/// 2. Activate via ARB_WASM precompile +/// 3. Call constructor if args provided +/// +/// This produces a single CALL transaction when broadcasting, ensuring on-chain +/// replay deploys a fully activated contract. fn deploy_stylus_code( ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor, @@ -111,72 +133,132 @@ fn deploy_stylus_code( let compressed_bytecode = get_stylus_bytecode(ccx.state, path)?; let init_code = get_init_code_of_empty_constructor(compressed_bytecode.to_vec()); - let scheme = - if let Some(salt) = salt { CreateScheme::Create2 { salt } } else { CreateScheme::Create }; - - // StylusDeployer.sol always deploys with 0 value; value is used for initialization only - let create_value = - if constructor_args.is_some() { U256::ZERO } else { value.unwrap_or(U256::ZERO) }; - - // Use the configured deployer address as the CREATE caller (matching StylusDeployer.sol) - let deployer_address = ccx - .state - .config - .evm_opts - .stylus_config - .deployer_address - .unwrap_or(DEFAULT_STYLUS_DEPLOYER_ADDRESS); - - let outcome = executor.exec_create( - CreateInputs { - caller: deployer_address, - scheme, - value: create_value, - init_code: init_code.into(), + // Build constructor calldata: stylus_constructor() selector + args, or empty + let constructor_calldata = if let Some(args) = constructor_args { + // cast sig 'stylus_constructor()' => 0x5585258d + let mut calldata = vec![0x55, 0x85, 0x25, 0x8d]; + calldata.extend_from_slice(args); + Bytes::from(calldata) + } else { + Bytes::new() + }; + + let init_value = value.unwrap_or(U256::ZERO); + + // salt: None → bytes32(0) → StylusDeployer uses CREATE + // salt: Some(val) → bytes32(val) → StylusDeployer uses CREATE2 + let salt_bytes: FixedBytes<32> = + if let Some(s) = salt { FixedBytes::from(s.to_be_bytes::<32>()) } else { FixedBytes::ZERO }; + + // ABI-encode the deploy(bytes,bytes,uint256,bytes32) call + let deploy_calldata = + deployCall::new((init_code.into(), constructor_calldata, init_value, salt_bytes)) + .abi_encode(); + + // Use the configured deployer address or the well-known default + let deployer_address = + ccx.state.config.evm_opts.stylus_config.deployer_address.unwrap_or(DEFAULT_STYLUS_DEPLOYER); + + // If broadcast is active, set call_from_code so this exec_call is captured + let is_broadcasting = ccx.state.broadcast.is_some(); + if let Some(broadcast) = &mut ccx.state.broadcast { + broadcast.call_from_code = true; + } + + // Temporarily clear caller's code so StylusDeployer can refund excess ETH. + // The refund uses a low-level call that requires the recipient to accept ETH; + // contracts without receive()/fallback() payable would reject it. + let caller = ccx.caller; + let caller_code = ccx.ecx.journal_mut().code(caller).ok().unwrap_or_default().data; + if !caller_code.is_empty() { + ccx.ecx.journaled_state.set_code_with_hash(caller, Bytecode::default(), KECCAK_EMPTY); + } + + // Record balance before to compute actual activation fee afterwards. + // When broadcasting, the inspector reroutes the transfer to broadcast.new_origin (the + // signer EOA), so we must track *that* account's balance — not ccx.caller (the script). + let balance_account = + if let Some(broadcast) = &ccx.state.broadcast { broadcast.new_origin } else { caller }; + let balance_before = ccx.ecx.journaled_state.load_account(balance_account)?.data.info.balance; + + // Ensure StylusDeployer is available. When broadcasting, the deployer must exist + // on-chain — etching it locally would produce a simulation that can't replay on-chain. + // For local tests/scripts (no broadcast), deploy on-demand to avoid polluting initial + // EVM state which would shift deterministic fuzz sequences for unrelated invariant tests. + let deployer_account = ccx.ecx.journaled_state.load_account(deployer_address)?; + if deployer_account.data.info.code.as_ref().is_none_or(|code| code.is_empty()) { + if is_broadcasting { + return Err(fmt_err!( + "StylusDeployer not found at {deployer_address}. \ + Deploy it on-chain or set a custom address via `stylus.deployer_address` in foundry.toml" + )); + } + ccx.ecx.journaled_state.set_code_with_hash( + deployer_address, + Bytecode::new_raw(Bytes::from_static(DEFAULT_STYLUS_DEPLOYER_RUNTIME_CODE)), + KECCAK_EMPTY, + ); + } + + // Call StylusDeployer with a generous activation fee budget (1 ETH). + // The deployer sends `msg.value - initValue` to ARB_WASM for activation + // and refunds any excess back to msg.sender. + let total_value = init_value + ACTIVATION_FEE_BUDGET; + let outcome = executor.exec_call( + CallInputs { + input: revm::interpreter::CallInput::Bytes(deploy_calldata.into()), + return_memory_offset: 0..0, gas_limit: ccx.gas_limit, + bytecode_address: deployer_address, + target_address: deployer_address, + caller, + value: revm::interpreter::CallValue::Transfer(total_value), + scheme: CallScheme::Call, + is_static: false, + known_bytecode: None, }, ccx, - )?; + ); + + // Restore caller's code before handling the result + if !caller_code.is_empty() { + ccx.ecx.journaled_state.set_code(caller, Bytecode::new_raw(caller_code)); + } + let outcome = outcome?; if !outcome.result.result.is_ok() { return Err(crate::Error::from(outcome.result.output)); } - let address = outcome.address.ok_or_else(|| fmt_err!("contract creation failed"))?; - - // Activate the program - activate_stylus_program(ccx, address)?; + // Compute actual activation data fee from balance change of the paying account. + // Net cost = init_value + data_fee (budget minus refund). + let balance_after = ccx.ecx.journaled_state.load_account(balance_account)?.data.info.balance; + let total_spent = balance_before.saturating_sub(balance_after); + let data_fee = total_spent.saturating_sub(init_value); + + // For broadcast, set the tx value to init_value + estimated fee with 20% buffer + // instead of the full 1 ETH budget used during simulation. + if is_broadcasting + && let Some(last_tx) = ccx.state.broadcastable_transactions.back_mut() + && let Some(tx) = last_tx.transaction.as_unsigned_mut() + { + let buffered_fee = data_fee * U256::from(120) / U256::from(100); + tx.value = Some(init_value + buffered_fee); + } - if let Some(constructor_args) = constructor_args { - // cast sig 'stylus_constructor()' => 0x5585258d - let mut calldata = vec![0x55, 0x85, 0x25, 0x8d]; - calldata.extend_from_slice(constructor_args); - - let outcome = executor.exec_call( - CallInputs { - input: revm::interpreter::CallInput::Bytes(calldata.into()), - return_memory_offset: 0..0, - gas_limit: outcome.gas().remaining(), - bytecode_address: address, - target_address: address, - caller: ccx.caller, - value: revm::interpreter::CallValue::Transfer(value.unwrap_or(U256::ZERO)), - scheme: CallScheme::Call, - is_static: false, - known_bytecode: None, - }, - ccx, - )?; - - if !outcome.result.result.is_ok() { - return Err(crate::Error::from(outcome.result.output)); - } + // StylusDeployer returns the deployed address as ABI-encoded address + let output = &outcome.result.output; + if output.len() < 32 { + return Err(fmt_err!("StylusDeployer returned invalid output")); } + let address = Address::from_slice(&output[12..32]); Ok(address.abi_encode()) } /// Activates a Stylus program by compiling and storing it directly. +/// Kept for potential future use (e.g., `vm.etch` scenarios). +#[allow(dead_code)] fn activate_stylus_program(ccx: &mut CheatsCtxt, program_address: Address) -> Result<()> { let code_hash = ccx .ecx @@ -275,14 +357,22 @@ fn get_init_code_of_empty_constructor(bytecode: Vec) -> Vec { init } -/// Returns the deployment bytecode for a Stylus contract suitable for use with the StylusDeployer -/// contract. This applies the same compression and prefixing logic as deployStylusCode, but returns -/// the raw bytecode without the init code wrapper, since StylusDeployer handles deployment itself. +/// Returns the compressed and prefixed Stylus bytecode (runtime code) for a contract. +/// This applies the same compression and prefixing logic as deployStylusCode, but returns +/// the raw bytecode without the init code wrapper. Suitable for use with `vm.etch`. fn get_stylus_code(state: &Cheatcodes, path: &str) -> Result { let bytecode = get_stylus_bytecode(state, path)?; Ok(bytecode.abi_encode()) } +/// Returns init code for a Stylus contract suitable for CREATE/CREATE2 or the StylusDeployer. +/// Wraps the compressed bytecode in EVM init code using `get_init_code_of_empty_constructor`. +fn get_stylus_init_code(state: &Cheatcodes, path: &str) -> Result { + let bytecode = get_stylus_bytecode(state, path)?; + let init_code = get_init_code_of_empty_constructor(bytecode.to_vec()); + Ok(Bytes::from(init_code).abi_encode()) +} + /// Compresses the given data using Brotli compression. /// Uses quality 11 and window size 22, without dictionary. fn compress_brotli(data: &Bytes) -> Result { diff --git a/crates/evm/core/src/constants.rs b/crates/evm/core/src/constants.rs index ee3bd818c..eafd29ff2 100644 --- a/crates/evm/core/src/constants.rs +++ b/crates/evm/core/src/constants.rs @@ -59,6 +59,16 @@ pub const DEFAULT_CREATE2_DEPLOYER_RUNTIME_CODE: &[u8] = &hex!( pub const DEFAULT_CREATE2_DEPLOYER_CODEHASH: B256 = b256!("0x2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989"); +/// The default StylusDeployer contract address on Arbitrum. +/// +/// See: +pub const DEFAULT_STYLUS_DEPLOYER: Address = address!("0xcEcba2F1DC234f70Dd89F2041029807F8D03A990"); + +/// The runtime code of the default StylusDeployer contract, fetched from Arbitrum mainnet. +pub const DEFAULT_STYLUS_DEPLOYER_RUNTIME_CODE: &[u8] = &hex!( + "6080604052600436106100345760003560e01c8063835d1d4c146100395780639f40b3851461006e578063a9a8e4e91461009c575b600080fd5b34801561004557600080fd5b50610059610054366004610605565b6100d4565b60405190151581526020015b60405180910390f35b34801561007a57600080fd5b5061008e610089366004610684565b6101f0565b604051908152602001610065565b6100af6100aa3660046106d0565b610226565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610065565b6040517fd70c0ca700000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff82163f6004820152600090819060719063d70c0ca790602401602060405180830381865afa925050508015610161575060408051601f3d908101601f1916820190925261015e91810190610765565b60015b61016d57506000610170565b90505b607173ffffffffffffffffffffffffffffffffffffffff1663a996e0c26040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101bc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101e09190610765565b61ffff9182169116141592915050565b600083838360405160200161020793929190610787565b6040516020818303038152906040528051906020012090509392505050565b6000811561023c576102398286866101f0565b91505b600061027f88888080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250879250610534915050565b9050600061028c826100d4565b90506000811561033c5760006102a287346107a1565b6040517f58c780c200000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff861660048201529091506071906358c780c2908390602401604080518083038185885af1158015610312573d6000803e3d6000fd5b50505050506040513d601f19601f8201168201806040525081019061033791906107db565b925050505b86156103ff576000808473ffffffffffffffffffffffffffffffffffffffff16888b8b60405161036d929190610807565b60006040518083038185875af1925050503d80600081146103aa576040519150601f19603f3d011682016040523d82523d6000602084013e6103af565b606091505b5091509150816103f85784816040517f88d8f57d0000000000000000000000000000000000000000000000000000000081526004016103ef92919061085d565b60405180910390fd5b5050610436565b8515610436576040517ecc797100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60008661044383346107a1565b61044d91906107a1565b905080156104dc57604051600090339083908381818185875af1925050503d8060008114610497576040519150601f19603f3d011682016040523d82523d6000602084013e61049c565b606091505b50509050806104da576040517f3ea99169000000000000000000000000000000000000000000000000000000008152600481018390526024016103ef565b505b60405173ffffffffffffffffffffffffffffffffffffffff851681527f8ffcdc15a283d706d38281f500270d8b5a656918f555de0913d7455e3e6bc1bf9060200160405180910390a150919998505050505050505050565b60008251600003610571576040517f21744a5900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000821561058a57828451602086016000f59050610596565b8351602085016000f090505b3d1519811516156105ad576040513d6000823e3d81fd5b73ffffffffffffffffffffffffffffffffffffffff81166105fc57836040517f794c92ce0000000000000000000000000000000000000000000000000000000081526004016103ef9190610894565b90505b92915050565b60006020828403121561061757600080fd5b813573ffffffffffffffffffffffffffffffffffffffff811681146105fc57600080fd5b60008083601f84011261064d57600080fd5b50813567ffffffffffffffff81111561066557600080fd5b60208301915083602082850101111561067d57600080fd5b9250929050565b60008060006040848603121561069957600080fd5b83359250602084013567ffffffffffffffff8111156106b757600080fd5b6106c38682870161063b565b9497909650939450505050565b600080600080600080608087890312156106e957600080fd5b863567ffffffffffffffff8082111561070157600080fd5b61070d8a838b0161063b565b9098509650602089013591508082111561072657600080fd5b5061073389828a0161063b565b979a9699509760408101359660609091013595509350505050565b805161ffff8116811461076057600080fd5b919050565b60006020828403121561077757600080fd5b6107808261074e565b9392505050565b838152818360208301376000910160200190815292915050565b818103818111156105ff577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080604083850312156107ee57600080fd5b6107f78361074e565b9150602083015190509250929050565b8183823760009101908152919050565b6000815180845260005b8181101561083d57602081850181015186830182015201610821565b506000602082860101526020601f19601f83011685010191505092915050565b73ffffffffffffffffffffffffffffffffffffffff8316815260406020820152600061088c6040830184610817565b949350505050565b602081526000610780602083018461081756fea26469706673582212202ffe15188103190347fe5bb6d173aaf7108665ba4de6a82347fb04622c2affa064736f6c63430008110033" +); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index d0aa25f91..8bd657124 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3422,3 +3422,75 @@ forgetest_async!(can_execute_script_with_createx_and_via_ir, |prj, cmd| { ]) .assert_success(); }); + +// Test broadcasting a deployStylusCode call against a local anvil with StylusDeployer deployed. +forgetest_async!(can_broadcast_deploy_stylus_code, |prj, cmd| { + foundry_test_utils::util::initialize(prj.root()); + + // Spawn anvil and set the StylusDeployer runtime code at the well-known address. + let (api, handle) = spawn(NodeConfig::test()).await; + api.anvil_set_code( + foundry_evm::constants::DEFAULT_STYLUS_DEPLOYER, + Bytes::from_static(foundry_evm::constants::DEFAULT_STYLUS_DEPLOYER_RUNTIME_CODE), + ) + .await + .unwrap(); + + // Copy the Stylus WASM fixture into the test project. + let fixtures_dir = prj.root().join("fixtures/Stylus"); + fs::create_dir_all(&fixtures_dir).unwrap(); + fs::copy( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../testdata/fixtures/Stylus/foundry_stylus_program.wasm"), + fixtures_dir.join("foundry_stylus_program.wasm"), + ) + .unwrap(); + + // Configure fs_permissions so the cheatcode can read the fixture. + prj.update_config(|config| { + config.fs_permissions = foundry_config::FsPermissions::new(vec![ + foundry_config::fs_permissions::PathPermission::read("fixtures"), + ]); + }); + + // Write a script that broadcasts a deployStylusCode call. + let script = prj.add_source( + "DeployStylusScript", + r#" +import "forge-std/Script.sol"; + +interface VmExt { + function deployStylusCode(string calldata artifactPath) external returns (address); +} + +contract DeployStylusScript is Script { + function run() external { + VmExt vmExt = VmExt(address(vm)); + vm.startBroadcast(); + address deployed = vmExt.deployStylusCode("fixtures/Stylus/foundry_stylus_program.wasm"); + vm.stopBroadcast(); + require(deployed != address(0), "deployed address is zero"); + } +} +"#, + ); + + let deploy_contract = script.display().to_string() + ":DeployStylusScript"; + let private_key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + + cmd.set_current_dir(prj.root()); + cmd.args([ + "script", + &deploy_contract, + "--root", + prj.root().to_str().unwrap(), + "--fork-url", + &handle.http_endpoint(), + "-vvvvv", + "--broadcast", + "--slow", + "--private-key", + private_key, + ]) + .assert_success(); +}); diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index cc11d53cb..1848dee64 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -705,7 +705,7 @@ mod tests { "default/linking/nested/Nested.t.sol:NestedLib", &[( "default/linking/nested/Nested.t.sol:Lib", - address!("0xd5d0c56ae71393246e46950ee40f14dcb0898849"), + address!("0x19fa389ba0eb91a67036dd918e951b47d850c4b0"), )], ) .assert_dependencies( @@ -715,12 +715,12 @@ mod tests { // have the same address and nonce. ( "default/linking/nested/Nested.t.sol:Lib", - Address::from_str("0xd5d0c56ae71393246e46950ee40f14dcb0898849") + Address::from_str("0x19fa389ba0eb91a67036dd918e951b47d850c4b0") .unwrap(), ), ( "default/linking/nested/Nested.t.sol:NestedLib", - Address::from_str("0x4182c6ba261accf5dc1202ba47496cfbf0650428") + Address::from_str("0x1c7edef214838be7723eca84ccbe91ee7cbc98cf") .unwrap(), ), ], @@ -730,12 +730,12 @@ mod tests { &[ ( "default/linking/nested/Nested.t.sol:Lib", - Address::from_str("0xd5d0c56ae71393246e46950ee40f14dcb0898849") + Address::from_str("0x19fa389ba0eb91a67036dd918e951b47d850c4b0") .unwrap(), ), ( "default/linking/nested/Nested.t.sol:NestedLib", - Address::from_str("0x4182c6ba261accf5dc1202ba47496cfbf0650428") + Address::from_str("0x1c7edef214838be7723eca84ccbe91ee7cbc98cf") .unwrap(), ), ], diff --git a/testdata/default/cheats/GetStylusInitCode.t.sol b/testdata/default/cheats/GetStylusInitCode.t.sol new file mode 100644 index 000000000..3afb192af --- /dev/null +++ b/testdata/default/cheats/GetStylusInitCode.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "utils/Test.sol"; + +contract GetStylusInitCodeTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testGetStylusInitCode() public { + bytes memory initCode = vm.getStylusInitCode("fixtures/Stylus/foundry_stylus_program.wasm"); + assertTrue(initCode.length > 0); + } + + function testInitCodeDeploysMatchingStylusCode() public { + bytes memory stylusCode = vm.getStylusCode("fixtures/Stylus/foundry_stylus_program.wasm"); + bytes memory initCode = vm.getStylusInitCode("fixtures/Stylus/foundry_stylus_program.wasm"); + + // Deploy using CREATE with the init code + address deployed; + assembly { + deployed := create(0, add(initCode, 0x20), mload(initCode)) + } + assertTrue(deployed != address(0), "CREATE failed"); + + // The runtime code at the deployed address should match getStylusCode output + bytes memory runtimeCode = deployed.code; + assertEq(runtimeCode, stylusCode); + } + + function testInitCodeDeploysWithCreate2() public { + bytes memory stylusCode = vm.getStylusCode("fixtures/Stylus/foundry_stylus_program.wasm"); + bytes memory initCode = vm.getStylusInitCode("fixtures/Stylus/foundry_stylus_program.wasm"); + bytes32 salt = keccak256("test-salt"); + + // Deploy using CREATE2 with the init code + address deployed; + assembly { + deployed := create2(0, add(initCode, 0x20), mload(initCode), salt) + } + assertTrue(deployed != address(0), "CREATE2 failed"); + + // The runtime code should match getStylusCode output + bytes memory runtimeCode = deployed.code; + assertEq(runtimeCode, stylusCode); + } +} diff --git a/testdata/default/repros/Issue2851.t.sol b/testdata/default/repros/Issue2851.t.sol deleted file mode 100644 index bafa7c0ed..000000000 --- a/testdata/default/repros/Issue2851.t.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.1; - -import "utils/Test.sol"; - -contract Backdoor { - uint256 public number = 1; - - function backdoor(uint256 newNumber) public payable { - uint256 x = newNumber - 1; - if (x == 6912213124124531) { - number = 0; - } - } -} - -// https://github.com/foundry-rs/foundry/issues/2851 -contract Issue2851Test is Test { - Backdoor back; - - function setUp() public { - back = new Backdoor(); - } - - /// forge-config: default.fuzz.dictionary.max_fuzz_dictionary_literals = 0 - /// forge-config: default.fuzz.seed = '111' - function invariantNotZero() public { - assertEq(back.number(), 1); - } -} diff --git a/testdata/utils/Vm.sol b/testdata/utils/Vm.sol index ba787983c..da0b8bd82 100644 --- a/testdata/utils/Vm.sol +++ b/testdata/utils/Vm.sol @@ -334,6 +334,7 @@ interface Vm { function getStorageAccesses() external view returns (StorageAccess[] memory storageAccesses); function getStorageSlots(address target, string calldata variableName) external view returns (uint256[] memory slots); function getStylusCode(string calldata artifactPath) external view returns (bytes memory); + function getStylusInitCode(string calldata artifactPath) external view returns (bytes memory); function getWallets() external view returns (address[] memory wallets); function indexOf(string calldata input, string calldata key) external pure returns (uint256); function interceptInitcode() external;