Skip to main content

polyester/chain/
safe.rs

1//! CREATE2 Polyester Safe address prediction (permissionless / TS parity).
2
3use alloy_primitives::{Address, B256, Bytes, U256, keccak256};
4use alloy_sol_types::{SolCall, sol};
5use std::sync::LazyLock;
6
7use crate::chain::environment::{
8    POLYESTER_TESTNET_ENVIRONMENT, PolyesterChainEnvironment, SafeDeploymentConfig,
9};
10use crate::errors::{Error, Result};
11
12// SafeProxy creation bytecode from @safe-global/safe-contracts v1.4.1
13// (must match SafeProxyFactory.proxyCreationCode on Polyester).
14static SAFE_PROXY_CREATION_CODE: LazyLock<Vec<u8>> = LazyLock::new(|| {
15    hex::decode(concat!(
16        "608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b",
17        "8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffff",
18        "ffffffffffffffffffffffff1614156100ca576040517f08c379a0000000000000000000000000000000000000000000000000",
19        "0000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b8060",
20        "00806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffff",
21        "ffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff",
22        "600054167fa619486e000000000000000000000000000000000000000000000000000000006000351415605057806000526020",
23        "6000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea2646970667358221220",
24        "03d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c696420",
25        "73696e676c65746f6e20616464726573732070726f7669646564",
26    ))
27    .expect("SAFE_PROXY_CREATION_CODE hex")
28});
29
30sol! {
31    function setup(
32        address[] owners,
33        uint256 threshold,
34        address to,
35        bytes data,
36        address fallbackHandler,
37        address paymentToken,
38        uint256 payment,
39        address paymentReceiver
40    );
41    function enableModules(address[] modules);
42    function multiSend(bytes transactions);
43    function createProxyWithNonce(address singleton, bytes initializer, uint256 saltNonce);
44}
45
46/// Predicted Safe address plus factory deploy payloads.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct PredictedSafe {
49    pub address: String,
50    pub initializer: Vec<u8>,
51    pub factory_calldata: Vec<u8>,
52}
53
54fn parse_address(value: &str) -> Result<Address> {
55    let text = value.trim();
56    let hex = text.strip_prefix("0x").unwrap_or(text);
57    if hex.len() != 40 {
58        return Err(Error::validation(
59            "address must be a 20-byte 0x-prefixed hex string",
60        ));
61    }
62    let raw = hex::decode(hex).map_err(|_| Error::validation("address is not valid hex"))?;
63    Ok(Address::from_slice(&raw))
64}
65
66fn encode_internal_tx(to: Address, data: &[u8], value: U256, operation: u8) -> Vec<u8> {
67    let mut out = Vec::with_capacity(1 + 20 + 32 + 32 + data.len());
68    out.push(operation);
69    out.extend_from_slice(to.as_slice());
70    out.extend_from_slice(&value.to_be_bytes::<32>());
71    out.extend_from_slice(&U256::from(data.len()).to_be_bytes::<32>());
72    out.extend_from_slice(data);
73    out
74}
75
76fn get_initializer(
77    owners: &[Address],
78    threshold: u64,
79    safe: &SafeDeploymentConfig,
80) -> Result<Vec<u8>> {
81    let module = parse_address(safe.safe_4337_module_address)?;
82    let setup_addr = parse_address(safe.safe_module_setup_address)?;
83    let multi_send = parse_address(safe.multi_send_address)?;
84
85    let enable_modules = enableModulesCall {
86        modules: vec![module],
87    }
88    .abi_encode();
89
90    let multi_calls = [encode_internal_tx(
91        setup_addr,
92        &enable_modules,
93        U256::ZERO,
94        1,
95    )];
96    let packed: Vec<u8> = multi_calls.into_iter().flatten().collect();
97    let multi_send_calldata = multiSendCall {
98        transactions: Bytes::from(packed),
99    }
100    .abi_encode();
101
102    Ok(setupCall {
103        owners: owners.to_vec(),
104        threshold: U256::from(threshold),
105        to: multi_send,
106        data: Bytes::from(multi_send_calldata),
107        fallbackHandler: module,
108        paymentToken: Address::ZERO,
109        payment: U256::ZERO,
110        paymentReceiver: Address::ZERO,
111    }
112    .abi_encode())
113}
114
115/// Deterministic CREATE2 Safe address + factory deploy data (zero RPC).
116pub fn predict_safe_address_with_data(
117    owners: &[&str],
118    salt_nonce: u64,
119    threshold: Option<u64>,
120    safe: Option<&SafeDeploymentConfig>,
121    environment: Option<&PolyesterChainEnvironment>,
122) -> Result<PredictedSafe> {
123    if owners.is_empty() {
124        return Err(Error::validation("owners must be non-empty"));
125    }
126    let env = environment.unwrap_or(&POLYESTER_TESTNET_ENVIRONMENT);
127    let cfg = safe.unwrap_or(&env.account_abstraction.safe);
128    let owner_addrs: Result<Vec<Address>> = owners.iter().map(|o| parse_address(o)).collect();
129    let owner_addrs = owner_addrs?;
130    let thresh = threshold.unwrap_or(owner_addrs.len() as u64);
131    let initializer = get_initializer(&owner_addrs, thresh, cfg)?;
132    let singleton = parse_address(cfg.safe_singleton_address)?;
133    let factory = parse_address(cfg.safe_proxy_factory_address)?;
134
135    let factory_calldata = createProxyWithNonceCall {
136        singleton,
137        initializer: Bytes::copy_from_slice(&initializer),
138        saltNonce: U256::from(salt_nonce),
139    }
140    .abi_encode();
141
142    let mut deployment_code = SAFE_PROXY_CREATION_CODE.clone();
143    let mut singleton_word = [0u8; 32];
144    singleton_word[12..].copy_from_slice(singleton.as_slice());
145    deployment_code.extend_from_slice(&singleton_word);
146
147    let mut salt_material = [0u8; 64];
148    salt_material[..32].copy_from_slice(keccak256(&initializer).as_slice());
149    salt_material[32..].copy_from_slice(&U256::from(salt_nonce).to_be_bytes::<32>());
150    let salt = B256::from(keccak256(salt_material));
151    let init_code_hash = B256::from(keccak256(&deployment_code));
152    let address = factory.create2(salt, init_code_hash);
153
154    Ok(PredictedSafe {
155        address: address.to_checksum(None),
156        initializer,
157        factory_calldata,
158    })
159}
160
161/// Predict the Polyester Safe for a single owner (main account = salt 0).
162pub fn predict_safe_address(
163    owner_address: &str,
164    salt_nonce: u64,
165    environment: Option<&PolyesterChainEnvironment>,
166) -> Result<String> {
167    Ok(
168        predict_safe_address_with_data(&[owner_address], salt_nonce, None, None, environment)?
169            .address,
170    )
171}
172
173/// Alias matching the TypeScript `predictPolyesterSmartAccountAddress` name.
174pub fn predict_polyester_smart_account_address(
175    owner_address: &str,
176    salt_nonce: u64,
177    environment: Option<&PolyesterChainEnvironment>,
178) -> Result<String> {
179    predict_safe_address(owner_address, salt_nonce, environment)
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    const OWNER: &str = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf";
187    const SALT0: &str = "0xA244Ed1dc6B46C75F37E0119054fFa45E76c9B6f";
188    const SALT7: &str = "0x4AEdcc90537f9fb3828E6b431E5A16Cdc473D6f0";
189
190    #[test]
191    fn predict_safe_address_salt_zero_matches_typescript() {
192        assert_eq!(predict_safe_address(OWNER, 0, None).unwrap(), SALT0);
193    }
194
195    #[test]
196    fn predict_safe_address_salt_seven_matches_typescript() {
197        assert_eq!(predict_safe_address(OWNER, 7, None).unwrap(), SALT7);
198    }
199
200    #[test]
201    fn predict_safe_address_with_data_includes_factory_calldata() {
202        let predicted = predict_safe_address_with_data(&[OWNER], 0, None, None, None).unwrap();
203        assert_eq!(predicted.address, SALT0);
204        assert!(hex::encode(&predicted.initializer).starts_with("b63e800d"));
205        assert!(hex::encode(&predicted.factory_calldata).starts_with("1688f0b9"));
206    }
207
208    #[test]
209    fn proxy_creation_code_length() {
210        // 972 hex chars from Safe 1.4.1 proxyCreationCode (Python SAFE_PROXY_CREATION_CODE).
211        assert_eq!(SAFE_PROXY_CREATION_CODE.len(), 486);
212    }
213}