Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate era revm cheatcodes #202

Merged
merged 6 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/era-cheatcodes/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/target
/Cargo.lock
tests/*
tests/src/fixtures/File/write_file.txt
tests/src/fixtures/Json/write_test.json
!tests/src
!tests/lib
!tests/test.sh
Expand Down
157 changes: 155 additions & 2 deletions crates/era-cheatcodes/src/cheatcodes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::utils::{ToH160, ToH256};
use alloy_sol_types::SolInterface;
use alloy_sol_types::{SolInterface, SolValue};
use era_test_node::{fork::ForkStorage, utils::bytecode_to_factory_dep};
use ethers::utils::to_checksum;
use foundry_cheatcodes_spec::Vm;
Expand All @@ -17,7 +17,7 @@ use multivm::{
},
},
};
use std::{cell::RefMut, collections::HashMap, fmt::Debug};
use std::{cell::RefMut, collections::HashMap, fmt::Debug, fs, process::Command};
use zksync_basic_types::{AccountTreeId, H160, H256, U256};
use zksync_state::{ReadStorage, StoragePtr, StorageView, WriteStorage};
use zksync_types::{
Expand Down Expand Up @@ -253,6 +253,34 @@ impl CheatcodeTracer {
self.store_factory_dep(hash, code);
self.write_storage(code_key, u256_to_h256(hash), &mut storage.borrow_mut());
}
ffi(ffiCall { commandInput: command_input }) => {
tracing::info!("πŸ‘· Running ffi: {command_input:?}");
let Some(first_arg) = command_input.get(0) else {
tracing::error!("Failed to run ffi: no args");
return
};
// TODO: set directory to root
let Ok(output) = Command::new(first_arg).args(&command_input[1..]).output() else {
tracing::error!("Failed to run ffi");
return
};

// The stdout might be encoded on valid hex, or it might just be a string,
// so we need to determine which it is to avoid improperly encoding later.
let Ok(trimmed_stdout) = String::from_utf8(output.stdout) else {
tracing::error!("Failed to parse ffi output");
return
};
let trimmed_stdout = trimmed_stdout.trim();
let encoded_stdout =
if let Ok(hex) = hex::decode(trimmed_stdout.trim_start_matches("0x")) {
hex
} else {
trimmed_stdout.as_bytes().to_vec()
};

self.add_trimmed_return_data(&encoded_stdout);
}
getNonce_0(getNonce_0Call { account }) => {
tracing::info!("πŸ‘· Getting nonce for {account:?}");
let mut storage = storage.borrow_mut();
Expand All @@ -275,6 +303,52 @@ impl CheatcodeTracer {
let value = storage.read_value(&key);
self.return_data = Some(vec![h256_to_u256(value)]);
}
readCallers(readCallersCall {}) => {
tracing::info!("πŸ‘· Reading callers");

let current_origin = {
let key = StorageKey::new(
AccountTreeId::new(zksync_types::SYSTEM_CONTEXT_ADDRESS),
zksync_types::SYSTEM_CONTEXT_TX_ORIGIN_POSITION,
);

storage.borrow_mut().read_value(&key)
};

let mut mode = CallerMode::None;
let mut new_caller = current_origin;

if let Some(prank) = &self.permanent_actions.start_prank {
//TODO: vm.prank -> CallerMode::Prank
println!("PRANK");
mode = CallerMode::RecurrentPrank;
new_caller = prank.sender.into();
}
// TODO: vm.broadcast / vm.startBroadcast section
// else if let Some(broadcast) = broadcast {
// mode = if broadcast.single_call {
// CallerMode::Broadcast
// } else {
// CallerMode::RecurrentBroadcast
// };
// new_caller = &broadcast.new_origin;
// new_origin = &broadcast.new_origin;
// }

let caller_mode = (mode as u8).into();
let message_sender = h256_to_u256(new_caller);
let tx_origin = h256_to_u256(current_origin);

self.return_data = Some(vec![caller_mode, message_sender, tx_origin]);
}
readFile(readFileCall { path }) => {
tracing::info!("πŸ‘· Reading file in path {}", path);
let Ok(data) = fs::read(path) else {
tracing::error!("Failed to read file");
return
};
self.add_trimmed_return_data(&data);
}
roll(rollCall { newHeight: new_height }) => {
tracing::info!("πŸ‘· Setting block number to {}", new_height);
let key = StorageKey::new(
Expand Down Expand Up @@ -459,6 +533,42 @@ impl CheatcodeTracer {
let int_value = value.to_string();
self.add_trimmed_return_data(int_value.as_bytes());
}
tryFfi(tryFfiCall { commandInput: command_input }) => {
tracing::info!("πŸ‘· Running try ffi: {command_input:?}");
let Some(first_arg) = command_input.get(0) else {
tracing::error!("Failed to run ffi: no args");
return
};
// TODO: set directory to root
let Ok(output) = Command::new(first_arg).args(&command_input[1..]).output() else {
tracing::error!("Failed to run ffi");
return
};

// The stdout might be encoded on valid hex, or it might just be a string,
// so we need to determine which it is to avoid improperly encoding later.
let Ok(trimmed_stdout) = String::from_utf8(output.stdout) else {
tracing::error!("Failed to parse ffi output");
return
};
let trimmed_stdout = trimmed_stdout.trim();
let encoded_stdout =
if let Ok(hex) = hex::decode(trimmed_stdout.trim_start_matches("0x")) {
hex
} else {
trimmed_stdout.as_bytes().to_vec()
};

let ffi_result = FfiResult {
exitCode: output.status.code().unwrap_or(69), // Default from foundry
stdout: encoded_stdout,
stderr: output.stderr,
};
let encoded_ffi_result: Vec<u8> = ffi_result.abi_encode();
let return_data: Vec<U256> =
encoded_ffi_result.chunks(32).map(|b| b.into()).collect_vec();
self.return_data = Some(return_data);
}
warp(warpCall { newTimestamp: new_timestamp }) => {
tracing::info!("πŸ‘· Setting block timestamp {}", new_timestamp);

Expand All @@ -474,6 +584,49 @@ impl CheatcodeTracer {
&mut storage,
);
}
writeFile(writeFileCall { path, data }) => {
tracing::info!("πŸ‘· Writing data to file in path {}", path);
if fs::write(path, data).is_err() {
tracing::error!("Failed to write file");
}
}
writeJson_0(writeJson_0Call { json, path }) => {
tracing::info!("πŸ‘· Writing json data to file in path {}", path);
let Ok(json) = serde_json::from_str::<serde_json::Value>(&json) else {
tracing::error!("Failed to parse json");
return
};
let Ok(formatted_json) = serde_json::to_string_pretty(&json) else {
tracing::error!("Failed to format json");
return
};
if fs::write(path, formatted_json).is_err() {
tracing::error!("Failed to write file");
}
}
writeJson_1(writeJson_1Call { json, path, valueKey: value_key }) => {
tracing::info!("πŸ‘· Writing json data to file in path {path} with key {value_key}");
let Ok(file) = fs::read_to_string(&path) else {
tracing::error!("Failed to read file");
return
};
let Ok(mut file_json) = serde_json::from_str::<serde_json::Value>(&file) else {
tracing::error!("Failed to parse json");
return
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&json) else {
tracing::error!("Failed to parse json");
return
};
file_json[value_key] = json;
let Ok(formatted_json) = serde_json::to_string_pretty(&file_json) else {
tracing::error!("Failed to format json");
return
};
if fs::write(path, formatted_json).is_err() {
tracing::error!("Failed to write file");
}
}
_ => {
tracing::error!("πŸ‘· Unrecognized cheatcode");
}
Expand Down
45 changes: 45 additions & 0 deletions crates/era-cheatcodes/tests/src/cheatcodes/Ffi.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "./Constants.sol";
import {Utils} from "./Utils.sol";

contract FfiTest is Test {
function testFfi() public {
string[] memory inputs = new string[](3);
inputs[0] = "bash";
inputs[1] = "-c";
inputs[
2
] = "echo -n 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000966666920776f726b730000000000000000000000000000000000000000000000";

(bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("ffi(string[])", inputs)
);
require(success, "ffi failed");

bytes memory data = Utils.trimReturnBytes(rawData);
string memory output = abi.decode(data, (string));
require(
keccak256(bytes(output)) == keccak256(bytes("ffi works")),
"ffi failed"
);

console.log("failed?", failed());
}

function testFfiString() public {
string[] memory inputs = new string[](3);
inputs[0] = "echo";
inputs[1] = "-n";
inputs[2] = "gm";

(bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("ffi(string[])", inputs)
);
require(success, "ffi failed");
bytes memory data = Utils.trimReturnBytes(rawData);
require(keccak256(data) == keccak256(bytes("gm")), "ffi failed");
}
}
50 changes: 50 additions & 0 deletions crates/era-cheatcodes/tests/src/cheatcodes/Fs.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "./Constants.sol";
import {Utils} from "./Utils.sol";

contract FsTest is Test {
function testReadFile() public {
string memory path = "src/fixtures/File/read.txt";

(bool success, bytes memory rawData) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("readFile(string)", path)
);
require(success, "readFile failed");

bytes memory data = Utils.trimReturnBytes(rawData);

require(
keccak256(data) ==
keccak256("hello readable world\nthis is the second line!\n"),
"read data did not match expected data"
);
console.log("failed?", failed());
}

function testWriteFile() public {
string memory path = "src/fixtures/File/write_file.txt";
string memory writeData = "hello writable world";

(bool success, ) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("writeFile(string,string)", path, writeData)
);
require(success, "writeFile failed");

bytes memory readRawData;
(success, readRawData) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("readFile(string)", path)
);
require(success, "readFile failed");

bytes memory readData = Utils.trimReturnBytes(readRawData);

require(
keccak256(readData) == keccak256(bytes(writeData)),
"read data did not match write data"
);
console.log("failed?", failed());
}
}
66 changes: 66 additions & 0 deletions crates/era-cheatcodes/tests/src/cheatcodes/ReadCallers.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2 as console} from "../../lib/forge-std/src/Test.sol";
import {Constants} from "./Constants.sol";

contract CheatcodeReadCallers is Test {
address constant TEST_ADDRESS = 0x6Eb28604685b1F182dAB800A1Bfa4BaFdBA8a79a;
address constant TEST_ORIGIN = 0xdEBe90b7BFD87Af696B1966082F6515a6E72F3d8;

// enum CallerMode {
// None,
// Broadcast,
// RecurrentBroadcast,
// Prank,
// RecurrentPrank
// }

function testNormalReadCallers() public {
(bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("readCallers()"));
require(success, "readCallers failed");

(uint8 mode, address sender, address origin) = abi.decode(data, (uint8, address, address));
require(mode == 0, "normal call mode");
require(sender == msg.sender, "sender not overridden");
require(origin == tx.origin, "origin not overridden");
}

function testPrankedReadCallers() public {
(bool success1, ) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("startPrank(address)", TEST_ADDRESS)
);
require(success1, "startPrank failed");

(bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("readCallers()"));
require(success, "readCallers failed");

(uint8 mode, address sender, address origin) = abi.decode(data, (uint8, address, address));
require(mode == 4, "recurrent prank call mode");
require(sender == TEST_ADDRESS, "sender overridden");
require(origin == tx.origin, "origin not overridden");

console.log("failed?", failed());
}

function testFullyPrankedReadCallers() public {
(bool success1, ) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("startPrank(address,address)", TEST_ADDRESS, TEST_ORIGIN)
);
require(success1, "startPrank failed");

(bool success, bytes memory data) = Constants.CHEATCODE_ADDRESS.call(
abi.encodeWithSignature("readCallers()"));
require(success, "readCallers failed");

(uint8 mode, address sender, address origin) = abi.decode(data, (uint8, address, address));

require(mode == 4, "recurrent prank call mode");
require(sender == TEST_ADDRESS, "sender overridden");
require(origin == TEST_ORIGIN, "origin overridden");

console.log("failed?", failed());
}
}
Loading