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

chore: Safe boilerplate & simple account automated tests #7

Merged
merged 15 commits into from
Sep 4, 2024
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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- run: rustup update stable && rustup default stable
- run: rustup toolchain install nightly -c rustfmt
- run: git submodule update --init --recursive
- run: make setup-thirdparty
- run: docker compose up -d
working-directory: test/scripts/forked_state
- run: while ! curl localhost:8545/health; do sleep 1; done
- run: while ! curl localhost:4337/health; do sleep 1; done
- run: while ! curl localhost:3000/ping; do sleep 1; done
- run: cargo test --all-features --lib --bins
# - run: cargo clippy --workspace --all-features --all-targets -- -D warnings
# - run: cargo +nightly fmt --all -- --check
Expand All @@ -33,6 +41,9 @@ jobs:
- debug
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- run: rustup update stable && rustup default stable
- run: git submodule update --init --recursive
- run: make setup-thirdparty
Expand Down
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[submodule "crates/yttrium/src/contracts"]
path = crates/yttrium/src/contracts
url = https://github.com/eth-infinitism/account-abstraction.git
[submodule "crates/yttrium/safe-smart-account"]
path = crates/yttrium/safe-smart-account
url = https://github.com/safe-global/safe-smart-account
[submodule "crates/yttrium/safe-modules"]
path = crates/yttrium/safe-modules
url = https://github.com/safe-global/safe-modules
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ fetch-thirdparty:

setup-thirdparty:
cd crates/yttrium/src/contracts/ && yarn install --frozen-lockfile --immutable && yarn compile
cd crates/yttrium/safe-smart-account/ && npm install
cd crates/yttrium/safe-modules/ && pnpm install

build-ios-bindings:
sh crates/ffi/build-rust-ios.sh
Expand Down
1 change: 1 addition & 0 deletions crates/ffi/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ impl Into<yttrium::config::Endpoints> for ffi::FFIEndpoints {
yttrium::config::Endpoints {
rpc: self.rpc.into(),
bundler: self.bundler.into(),
paymaster: self.paymaster.into(),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ mod ffi {
pub struct FFIEndpoints {
pub rpc: FFIEndpoint,
pub bundler: FFIEndpoint,
pub paymaster: FFIEndpoint,
}

#[derive(Debug, Clone)]
Expand Down
4 changes: 4 additions & 0 deletions crates/yttrium/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ hex = "0.4.3"
[dev-dependencies]
# mocking
wiremock = "0.6.0"

[build-dependencies]
alloy-primitives = { version = "0.7.0" }
serde_json = "1"
107 changes: 107 additions & 0 deletions crates/yttrium/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use {
// serde_json::Value,
std::process::{Command, Stdio},
};

fn main() {
build_contracts();
}

const CONTRACTS_DIR: &str = "crates/yttrium/safe-smart-account/contracts";

fn build_contracts() {
println!("cargo::rerun-if-changed={CONTRACTS_DIR}");
install_foundry();
compile_contracts(&format!("{CONTRACTS_DIR}/proxies"));
// extract_bytecodes();
}

fn format_foundry_dir(path: &str) -> String {
format!(
"{}/../../../../.foundry/{}",
std::env::var("OUT_DIR").unwrap(),
path
)
}

fn install_foundry() {
let bin_finished_flag = format_foundry_dir("bin/.finished");
if std::fs::metadata(&bin_finished_flag).is_ok() {
return;
}

let bin_folder = format_foundry_dir("bin");
std::fs::remove_dir_all(&bin_folder).ok();
std::fs::create_dir_all(&bin_folder).unwrap();
let output = Command::new("bash")
.args(["-c", &format!("curl https://raw.githubusercontent.com/foundry-rs/foundry/e0ea59cae26d945445d9cf21fdf22f4a18ac5bb2/foundryup/foundryup | FOUNDRY_DIR={} bash", format_foundry_dir(""))])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap()
.wait_with_output()
.unwrap();
println!("foundryup status: {:?}", output.status);
let stdout = String::from_utf8(output.stdout).unwrap();
println!("foundryup stdout: {stdout:?}");
let stderr = String::from_utf8(output.stderr).unwrap();
println!("foundryup stderr: {stderr:?}");
assert!(output.status.success());

std::fs::write(bin_finished_flag, "").unwrap();
}

fn compile_contracts(contracts_dir: &str) {
let output = Command::new(format_foundry_dir("bin/forge"))
.args([
"build",
&format!("--contracts={contracts_dir}"),
"--skip=test",
"--cache-path",
&format_foundry_dir("forge/cache"),
"--out",
&format_foundry_dir("forge/out"),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap()
.wait_with_output()
.unwrap();
println!("forge status: {:?}", output.status);
let stdout = String::from_utf8(output.stdout).unwrap();
println!("forge stdout: {stdout:?}");
let stderr = String::from_utf8(output.stderr).unwrap();
println!("forge stderr: {stderr:?}");
assert!(output.status.success());
}

// const ERC6492_FILE: &str = "forge/out/Erc6492.sol/ValidateSigOffchain.json";
// const ERC6492_BYTECODE_FILE: &str = "forge/out/Erc6492.sol/ValidateSigOffchain.bytecode";
// const ERC1271_MOCK_FILE: &str = "forge/out/Erc1271Mock.sol/Erc1271Mock.json";
// const ERC1271_MOCK_BYTECODE_FILE: &str = "forge/out/Erc1271Mock.sol/Erc1271Mock.bytecode";
// fn extract_bytecodes() {
// extract_bytecode(
// &format_foundry_dir(ERC6492_FILE),
// &format_foundry_dir(ERC6492_BYTECODE_FILE),
// );
// extract_bytecode(
// &format_foundry_dir(ERC1271_MOCK_FILE),
// &format_foundry_dir(ERC1271_MOCK_BYTECODE_FILE),
// );
// }

// fn extract_bytecode(input_file: &str, output_file: &str) {
// let contents = serde_json::from_slice::<Value>(&std::fs::read(input_file).unwrap()).unwrap();
// let bytecode = contents
// .get("bytecode")
// .unwrap()
// .get("object")
// .unwrap()
// .as_str()
// .unwrap()
// .strip_prefix("0x")
// .unwrap();
// let bytecode = alloy_primitives::hex::decode(bytecode).unwrap();
// std::fs::write(output_file, bytecode).unwrap();
// }
21 changes: 21 additions & 0 deletions crates/yttrium/contracts/Account7702.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
pragma solidity ^0.8.20;

contract Account7702 {
constructor() {} // TODO need owner

struct Call {
bytes data;
address to;
uint256 value;
bytes signature; // TODO proper type
}

function execute(Call[] calldata calls) external payable {
// TODO how to authenticate signture
for (uint256 i = 0; i < calls.length; i++) {
Call memory call = calls[i];
(bool success, ) = call.to.call{value: call.value}(call.data);
require(success, "call reverted");
}
}
}
1 change: 1 addition & 0 deletions crates/yttrium/safe-modules
Submodule safe-modules added at bc76ff
1 change: 1 addition & 0 deletions crates/yttrium/safe-smart-account
Submodule safe-smart-account added at c266ff
14 changes: 12 additions & 2 deletions crates/yttrium/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::env;

const LOCAL_RPC_URL: &str = "http://localhost:8545";
const LOCAL_BUNDLER_URL: &str = "http://localhost:4337";
const LOCAL_PAYMASTER_URL: &str = "http://localhost:3000";

#[derive(Clone, Debug, PartialEq)]
pub struct Config {
Expand All @@ -23,6 +24,7 @@ impl Config {
pub struct Endpoints {
pub rpc: Endpoint,
pub bundler: Endpoint,
pub paymaster: Endpoint,
}

impl Endpoints {
Expand All @@ -45,13 +47,14 @@ impl Endpoints {
Endpoint { api_key, base_url }
};

Endpoints { rpc, bundler }
Endpoints { rpc, paymaster: bundler.clone(), bundler }
}

pub fn local() -> Self {
Endpoints {
rpc: Endpoint::local_rpc(),
bundler: Endpoint::local_bundler(),
paymaster: Endpoint::local_paymaster(),
}
}

Expand All @@ -73,7 +76,7 @@ impl Endpoints {
Endpoint { api_key: api_key.clone(), base_url }
};

Endpoints { rpc, bundler }
Endpoints { rpc, paymaster: bundler.clone(), bundler }
}
}

Expand All @@ -97,4 +100,11 @@ impl Endpoint {
api_key: "".to_string(),
}
}

pub fn local_paymaster() -> Self {
Endpoint {
base_url: LOCAL_PAYMASTER_URL.to_string(),
api_key: "".to_string(),
}
}
}
1 change: 1 addition & 0 deletions crates/yttrium/src/smart_accounts.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod nonce;
pub mod safe;
pub mod simple_account;
115 changes: 115 additions & 0 deletions crates/yttrium/src/smart_accounts/safe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use alloy::{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved this to safe.rs rather than mod.rs as I find it easier find things and navigate Rust codebases

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't like the module organization you are using. I find files by navigating in the file tree, not by searching for them. This current module structure has related files spread out.

dyn_abi::DynSolValue,
primitives::{address, Address, Bytes, U256},
sol,
sol_types::SolCall,
};

sol!(
#[allow(missing_docs)]
#[sol(rpc)]
SafeProxyFactory,
"safe-smart-account/build/artifacts/contracts/proxies/SafeProxyFactory.sol/SafeProxyFactory.json"
// "../../target/.foundry/forge/out/SafeProxyFactory.sol/SafeProxyFactory.json"
// concat!(env!("OUT_DIR"), "/../../../../.foundry/forge/out/SafeProxyFactory.sol/SafeProxyFactory.json")
);

sol!(
#[allow(clippy::too_many_arguments)]
#[allow(missing_docs)]
#[sol(rpc)]
Safe,
"safe-smart-account/build/artifacts/contracts/Safe.sol/Safe.json"
);

// https://github.com/WalletConnect/secure-web3modal/blob/c19a1e7b21c6188261728f4d521a17f94da4f055/src/core/SmartAccountSdk/utils.ts#L178
// https://github.com/WalletConnect/secure-web3modal/blob/c19a1e7b21c6188261728f4d521a17f94da4f055/src/core/SmartAccountSdk/constants.ts#L24
const SEPOLIA_SAFE_ERC_7579_LAUNCHPAD_ADDRESS: Address =
address!("EBe001b3D534B9B6E2500FB78E67a1A137f561CE");
const SEPOLIA_SAFE_4337_MODULE_ADDRESS: Address =
address!("3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2");

// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L438C36-L438C76
const SAFE_MULTI_SEND_ADDRESS: Address =
address!("38869bf66a61cF6bDB996A6aE40D5853Fd43B526");

// https://github.com/safe-global/safe-modules-deployments/blob/d6642d90659de19e54bb4a20d646b30bd0a51885/src/assets/safe-4337-module/v0.3.0/safe-module-setup.json#L7
// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L431
const SAFE_MODULE_SETUP_ADDRESS: Address =
address!("2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47");

sol!(
#[allow(missing_docs)]
#[sol(rpc)]
SafeModuleSetup,
"safe-modules/modules/4337/build/artifacts/contracts/SafeModuleSetup.sol/SafeModuleSetup.json"
);

sol!(
#[allow(missing_docs)]
#[sol(rpc)]
MultiSend,
"safe-smart-account/build/artifacts/contracts/libraries/MultiSend.sol/MultiSend.json"
);

// https://github.com/WalletConnect/secure-web3modal/blob/c19a1e7b21c6188261728f4d521a17f94da4f055/src/core/SmartAccountSdk/constants.ts#L10
// const APPKIT_SALT: U256 = U256::from_str("zg3ijy0p46");

fn encode_internal_transaction(
to: Address,
data: Vec<u8>,
value: U256,
operation: bool,
) -> Bytes {
// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L486
DynSolValue::Tuple(vec![
DynSolValue::Uint(U256::from(if operation { 1 } else { 0 }), 8),
DynSolValue::Address(to),
DynSolValue::Uint(value, 256),
DynSolValue::Uint(U256::from(data.len()), 256),
DynSolValue::Bytes(data),
])
.abi_encode()
.into()
}

fn init_code_call_data(
owner: Address,
) -> SafeProxyFactory::createProxyWithNonceCall {
// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L714C31-L714C46
let enable_modules = SafeModuleSetup::enableModulesCall {
modules: vec![SEPOLIA_SAFE_4337_MODULE_ADDRESS],
}
.abi_encode();

// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L486
let txn = encode_internal_transaction(
SAFE_MODULE_SETUP_ADDRESS,
enable_modules,
U256::ZERO,
true,
); // TODO join any setupTransactions

let multi_send_call_data =
MultiSend::multiSendCall { transactions: txn }.abi_encode().into();

// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L728
let initializer = Safe::setupCall {
_owners: vec![owner],
_threshold: U256::from(1),
to: SAFE_MULTI_SEND_ADDRESS,
data: multi_send_call_data,
fallbackHandler: SAFE_MODULE_SETUP_ADDRESS,
paymentToken: Address::ZERO,
payment: U256::ZERO,
paymentReceiver: Address::ZERO,
}
.abi_encode()
.into();
// https://github.com/pimlicolabs/permissionless.js/blob/b8798c121eecba6a71f96f8ddf8e0ad2e98a3236/packages/permissionless/accounts/safe/toSafeSmartAccount.ts#L840
SafeProxyFactory::createProxyWithNonceCall {
_singleton: SEPOLIA_SAFE_ERC_7579_LAUNCHPAD_ADDRESS,
initializer,
saltNonce: U256::ZERO,
}
}
20 changes: 20 additions & 0 deletions crates/yttrium/src/test_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
fn format_foundry_dir(path: &str) -> String {
format!(
"{}/../../../../.foundry/{}",
std::env::var("OUT_DIR").unwrap(),
path
)
}

pub fn spawn_anvil() -> (AnvilInstance, String, ReqwestProvider, SigningKey) {
let anvil = Anvil::at(format_foundry_dir("bin/anvil")).spawn();
let rpc_url = anvil.endpoint();
let provider = ReqwestProvider::<Ethereum>::new_http(anvil.endpoint_url());
let private_key = anvil.keys().first().unwrap().clone();
(
anvil,
rpc_url,
provider,
SigningKey::from_bytes(&private_key.to_bytes()).unwrap(),
)
}
Loading