Skip to content

Commit

Permalink
chore: Safe boilerplate & simple account automated tests (#7)
Browse files Browse the repository at this point in the history
* feat: Safe

* chore: remove old code

* chore: more Safe stuff

* chore: test simple account on local

* chore: run infra forked

* fix: docker service dependencies

* fix: other contract builds

* chore: install pnpm

* fix: docker compose cmd

* fix: build path

* chore: fix health endpoints

* chore: fix Swift build

* chore: remove use of mnemonic

* chore: move `mod.rs` to `safe.rs`, fix warning

---------

Co-authored-by: Jack Pooley <[email protected]>
  • Loading branch information
chris13524 and jackpooleywc authored Sep 4, 2024
1 parent efa3553 commit 9c29b40
Show file tree
Hide file tree
Showing 19 changed files with 590 additions and 4 deletions.
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::{
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

0 comments on commit 9c29b40

Please sign in to comment.