Skip to content

Commit

Permalink
feat: new getMemory cheatcode
Browse files Browse the repository at this point in the history
  • Loading branch information
teddav committed Apr 3, 2023
1 parent f7a535d commit 7277e76
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 1 deletion.
4 changes: 4 additions & 0 deletions evm/src/executor/abi/mod.rs
Expand Up @@ -18,6 +18,7 @@ ethers::contract::abigen!(
struct Log {bytes32[] topics; bytes data;}
struct Rpc {string name; string url;}
struct FsMetadata {bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created;}
struct FormattedMemory {string header; string[] words;}
roll(uint256)
warp(uint256)
difficulty(uint256)
Expand Down Expand Up @@ -188,6 +189,9 @@ ethers::contract::abigen!(
writeJson(string, string, string)
pauseGasMetering()
resumeGasMetering()
getMemory(uint256,uint256)(bytes)
getMemoryFormattedAsString(uint256,uint256)(string)
getMemoryFormatted(uint256,uint256)(FormattedMemory)
]"#,
);
pub use hevm::{HEVMCalls, HEVM_ABI};
Expand Down
5 changes: 5 additions & 0 deletions evm/src/executor/inspector/cheatcodes/mod.rs
Expand Up @@ -165,6 +165,9 @@ pub struct Cheatcodes {
/// CREATE / CREATE2 frames. This is needed to make gas meter pausing work correctly when
/// paused and creating new contracts.
pub gas_metering_create: Option<Option<revm::Gas>>,

/// Current's call memory
pub memory: Vec<u8>,
}

impl Cheatcodes {
Expand Down Expand Up @@ -288,6 +291,8 @@ where
data: &mut EVMData<'_, DB>,
_: bool,
) -> Return {
self.memory = interpreter.memory.data().clone();

// reset gas if gas metering is turned off
match self.gas_metering {
Some(None) => {
Expand Down
109 changes: 108 additions & 1 deletion evm/src/executor/inspector/cheatcodes/util.rs
Expand Up @@ -21,7 +21,7 @@ use ethers::{
};
use foundry_common::{fmt::*, RpcUrl};
use revm::{Account, CreateInputs, Database, EVMData, JournaledState, TransactTo};
use std::collections::VecDeque;
use std::{collections::VecDeque, fmt::Write};
use tracing::trace;

const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
Expand Down Expand Up @@ -134,6 +134,68 @@ pub fn parse(s: &str, ty: &ParamType) -> Result<Bytes, Bytes> {
.map_err(|e| format!("Failed to parse `{s}` as type `{ty}`: {e}").encode().into())
}

struct FormattedMemory {
header: String,
words: Vec<String>,
}

fn check_format_memory_inputs(
start: U256,
end: U256,
memory_length: u32,
) -> Result<(u32, u32), String> {
// let start = u32::try_from(start).map_err(|err| err.to_string().encode())?;
let start = u32::try_from(start).map_err(|err| format!("start parameter: {}", err))?;
let end = u32::try_from(end).map_err(|err| format!("end parameter: {}", err))?;
if start > end {
return Err(format!("invalid parameters: start ({}) must be <= end ({})", start, end))
}
if end > memory_length - 1 {
return Err(format!(
"invalid parameters: end ({}). Max memory offset: {}",
end,
memory_length - 1
))
}
Ok((start, end))
}

fn format_memory(mem: &Vec<u8>, start: U256, end: U256) -> Result<FormattedMemory, String> {
let (start, end) = check_format_memory_inputs(start, end, mem.len() as u32)?;

let optional_mem = mem.iter().map(|v| Some(*v)).collect::<Vec<Option<u8>>>();
let mem_slice = &optional_mem[(start as usize)..=(end as usize)];

let pre_fill: Vec<Option<u8>> = vec![None; (start % 32) as usize];
let post_fill: Vec<Option<u8>> = vec![None; (31 - end % 32) as usize];

let mem = [pre_fill.as_slice(), mem_slice, post_fill.as_slice()].concat();
let mem = mem.chunks(32).collect::<Vec<&[Option<u8>]>>();

let mut formatted_mem = vec![];

let start_print: usize = (start - start % 32) as usize;

for (i, chunk) in mem.iter().enumerate() {
// println!("{:02x?} {1:#04x}({1})", chunk, i * 32);
let mut s = String::new();
for v in chunk.iter() {
if let Some(v) = v {
write!(&mut s, "{:02x?} ", v).map_err(|err| err.to_string())?;
} else {
write!(&mut s, " ").map_err(|err| err.to_string())?;
}
}
write!(&mut s, " {0:#04x} ({0})", start_print + i * 32).map_err(|err| err.to_string())?;

formatted_mem.push(s);
}

let header = (0..32).map(|x| format!("{:>2?}", x)).collect::<Vec<String>>().join(" ");

Ok(FormattedMemory { header, words: formatted_mem })
}

pub fn apply<DB: Database>(
state: &mut Cheatcodes,
data: &mut EVMData<'_, DB>,
Expand Down Expand Up @@ -175,6 +237,51 @@ pub fn apply<DB: Database>(
HEVMCalls::ParseInt(inner) => parse(&inner.0, &ParamType::Int(256)),
HEVMCalls::ParseBytes32(inner) => parse(&inner.0, &ParamType::FixedBytes(32)),
HEVMCalls::ParseBool(inner) => parse(&inner.0, &ParamType::Bool),
HEVMCalls::GetMemory(inner) => {
match check_format_memory_inputs(inner.0, inner.1, state.memory.len() as u32) {
Ok((start, end)) => {
let mem = state.memory[start as usize..=end as usize].to_vec();
Ok(ethers::abi::encode(&[Token::Bytes(mem)]).into())
}
Err(err) => Err(format!("Error getMemory: {}", err).encode().into()),
}
}
HEVMCalls::GetMemoryFormattedAsString(inner) => {
match format_memory(&state.memory, inner.0, inner.1) {
Ok(mem) => {
let mem_as_string = format!(
"{}\n{}",
mem.header,
// when using `console.log()` it seems like the first line is always
// indented with 2 spaces so we add 2 spaces before
// each line after the header
mem.words.iter().fold(String::new(), |acc, s| acc + " " + s + "\n")
);

Ok(ethers::abi::encode(&[Token::String(mem_as_string)]).into())
}
Err(err) => {
Err(format!("Error getMemoryFormattedAsString: {}", err).encode().into())
}
}
}
HEVMCalls::GetMemoryFormatted(inner) => {
match format_memory(&state.memory, inner.0, inner.1) {
Ok(mem) => {
let formatted_mem = mem
.words
.iter()
.map(|v| Token::String(v.to_owned()))
.collect::<Vec<Token>>();
Ok(abi::encode(&[Token::Tuple(vec![
Token::String(mem.header),
Token::Array(formatted_mem),
])])
.into())
}
Err(err) => Err(format!("Error getMemoryFormatted: {}", err).encode().into()),
}
}
_ => return None,
})
}
Expand Down
13 changes: 13 additions & 0 deletions testdata/cheats/Cheats.sol
Expand Up @@ -26,6 +26,11 @@ interface Cheats {
uint256 created;
}

struct FormattedMemory {
string header;
string[] words;
}

// Set block.timestamp (newTimestamp)
function warp(uint256) external;

Expand Down Expand Up @@ -483,4 +488,12 @@ interface Cheats {

// Resumes gas metering from where it left off
function resumeGasMetering() external;

// Gets the current memory as bytes
function getMemory(uint256,uint256) external view returns (bytes memory);

// Gets the current memory and returns it ready to be console.logged
function getMemoryFormattedAsString(uint256,uint256) external view returns (string memory);

function getMemoryFormatted(uint256,uint256) external view returns (FormattedMemory memory);
}
71 changes: 71 additions & 0 deletions testdata/cheats/GetMemory.t.sol
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.0;

import "ds-test/test.sol";
import "./Cheats.sol";

contract GetMemoryTest is DSTest {
Cheats constant vm = Cheats(HEVM_ADDRESS);

function testGetMemory() public {
assertEq(vm.getMemory(0, 31), abi.encodePacked(bytes32(0)));

assembly {
mstore(0, 0x4141414141414141414141414141414141414141414141414141414141414141)
mstore(0x20, 0xbabababababababababababababababababababababababababababababababa)
}
bytes memory mem1 = vm.getMemory(0, 12);
bytes memory mem2 = vm.getMemory(0x20, 0x3f);

assertEq(mem1.length, 13);
assertEq(mem2.length, 32);

assertEq(mem1, hex"41414141414141414141414141");
assertEq(mem2, hex"babababababababababababababababababababababababababababababababa");

bytes memory mem3 = vm.getMemory(0x60, 0x7f);
assertEq(mem3.length, 32);
assertEq(mem3, abi.encodePacked(bytes32(0)));
}

function testGetMemoryAsString() public {
string memory mem = vm.getMemoryFormattedAsString(10, 20);
assertEq(mem, " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31\n 00 00 00 00 00 00 00 00 00 00 00 0x00 (0)\n");

assembly {
mstore8(10, 0x41)
mstore8(20, 0xba)
}

assertEq(vm.getMemoryFormattedAsString(5, 20), " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31\n 00 00 00 00 00 41 00 00 00 00 00 00 00 00 00 ba 0x00 (0)\n");
}

function testGetMemoryFormatted() public {
Cheats.FormattedMemory memory mem = vm.getMemoryFormatted(0, 0x40);
assertEq(mem.header, " 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31");
assertEq(mem.words.length, 3);
assertEq(mem.words[0], "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x00 (0)");
assertEq(mem.words[2], "00 0x40 (64)");
}

// Reverts
function testFailGetMemoryStartOverEndIndex() public {
vm.getMemory(20, 12);
}

function testFailGetMemoryStartIndexTooHigh() public {
vm.getMemory(300, 301);
}

function testFailGetMemoryEndIndexTooHigh() public {
vm.getMemory(20, 300);
}

// // Let's make sure our error messages make sense
// function testRevertsErrorMessages() public {
// // This doesn't work, the error message and the expected revert message don't match
// // maybe we don't format the error message correctly in `check_format_memory_inputs()` ?
// vm.expectRevert("Error getMemory: invalid parameters: start (20) must be <= end (12)");
// vm.getMemory(20, 12);
// }
}

0 comments on commit 7277e76

Please sign in to comment.