trident-evm-aa 0.5.0

Rust glue for the Trident minimal EIP-7702 smart account: deploy, install, and batch transactions.
Documentation
//! Deployment helpers for `TridentAccount`.
//!
//! Two flavors:
//! - [`deploy`] — standard `CREATE` deployment; address depends on the sender's
//!   nonce at send time.
//! - [`deploy_deterministic`] — `CREATE2` via Arachnid's canonical deployment
//!   proxy, yielding the same address on every chain for a given `salt`.

use alloy_network::{Network, ReceiptResponse, TransactionBuilder};
use alloy_primitives::{Address, B256, address, keccak256};
use alloy_provider::Provider;

use crate::abi::TridentAccount;

/// Arachnid's deterministic deployment proxy — same address on every major
/// EVM network. See <https://github.com/Arachnid/deterministic-deployment-proxy>.
const CREATE2_FACTORY: Address = address!("4e59b44847b379578588920cA78FbF26c0B4956C");

/// The canonical `TridentAccount` deployment address — the CREATE2 prediction
/// with an all-zero salt via `CREATE2_FACTORY`. Same on every chain that has
/// Arachnid's proxy.
pub const DEFAULT_ADDRESS: Address = address!("319A60309bA63b73624729Cc903CD6FeE78af696");

/// Errors surfaced by the deploy helpers.
#[derive(Debug, thiserror::Error)]
pub enum DeployError {
    /// Error originating in the sol!-generated contract deployer (includes
    /// RPC/transport failures bubbled up through `alloy-contract`).
    #[error(transparent)]
    Contract(#[from] alloy_contract::Error),

    /// Send or confirmation failure for the raw CREATE2 transaction.
    #[error("rpc: {0}")]
    Rpc(Box<dyn std::error::Error + Send + Sync>),

    /// CREATE2 deployment succeeded but the receipt-reported contract address
    /// does not match the locally predicted address — usually indicates the
    /// factory on this chain is not Arachnid's canonical proxy.
    #[error("create2 address mismatch: predicted {predicted}, observed {observed}")]
    AddressMismatch {
        predicted: Address,
        observed: Address,
    },
}

/// Standard `CREATE` deployment of [`TridentAccount`]. Returns the deployed address.
pub async fn deploy<P, N>(provider: P) -> Result<Address, DeployError>
where
    P: Provider<N>,
    N: Network,
{
    let instance = TridentAccount::deploy(provider).await?;
    Ok(*instance.address())
}

/// `CREATE2` deployment of [`TridentAccount`] through `CREATE2_FACTORY`.
///
/// The returned address is cross-checked against
/// [`predict_deterministic_address`].
pub async fn deploy_deterministic<P, N>(provider: P, salt: B256) -> Result<Address, DeployError>
where
    P: Provider<N>,
    N: Network,
{
    let predicted = predict_deterministic_address(salt);

    let init_code = creation_code();
    let mut input = Vec::with_capacity(32 + init_code.len());
    input.extend_from_slice(salt.as_slice());
    input.extend_from_slice(init_code);

    let tx = <N::TransactionRequest as Default>::default()
        .with_to(CREATE2_FACTORY)
        .with_input(input);

    let receipt = provider
        .send_transaction(tx)
        .await
        .map_err(|e| DeployError::Rpc(Box::new(e)))?
        .get_receipt()
        .await
        .map_err(|e| DeployError::Rpc(Box::new(e)))?;

    // Arachnid's proxy performs the CREATE2 inside its runtime code, so the
    // top-level receipt's `contract_address` is usually empty. When it IS
    // populated (non-standard factories), sanity-check it.
    if let Some(observed) = receipt.contract_address()
        && observed != predicted
    {
        return Err(DeployError::AddressMismatch {
            predicted,
            observed,
        });
    }

    Ok(predicted)
}

/// Pure, off-chain prediction of the CREATE2 address for a given `salt`.
pub fn predict_deterministic_address(salt: B256) -> Address {
    CREATE2_FACTORY.create2(salt, keccak256(creation_code()))
}

fn creation_code() -> &'static [u8] {
    TridentAccount::BYTECODE.as_ref()
}

#[cfg(test)]
mod tests {
    use alloy_primitives::b256;
    use alloy_provider::Provider;

    use super::*;
    use crate::test_util::TestSetup;

    #[test]
    fn default_address_matches_zero_salt_prediction() {
        // Catches bytecode drift: if the TridentAccount artifact changes, the
        // hardcoded `DEFAULT_ADDRESS` diverges from the live prediction and
        // this test fails loudly.
        assert_eq!(DEFAULT_ADDRESS, predict_deterministic_address(B256::ZERO));
    }

    #[test]
    fn predict_deterministic_address_is_pure_and_distinguishes_salts() {
        let salt_a = b256!("1111111111111111111111111111111111111111111111111111111111111111");
        let salt_b = b256!("2222222222222222222222222222222222222222222222222222222222222222");

        assert_eq!(
            predict_deterministic_address(salt_a),
            predict_deterministic_address(salt_a),
        );
        assert_ne!(
            predict_deterministic_address(salt_a),
            predict_deterministic_address(salt_b),
        );
    }

    #[tokio::test]
    async fn deploy_places_code_at_returned_address() {
        let setup = TestSetup::new().await;
        let provider = setup.provider();

        let addr = deploy(provider.clone()).await.expect("deploy");

        let code = provider.get_code_at(addr).await.expect("get_code_at");
        assert!(!code.is_empty(), "no code at deployed address");
    }

    #[tokio::test]
    async fn deploy_deterministic_matches_prediction_and_places_code() {
        let setup = TestSetup::new().await;
        let provider = setup.provider();

        let salt = b256!("abcdef0000000000000000000000000000000000000000000000000000000000");
        let predicted = predict_deterministic_address(salt);

        let deployed = deploy_deterministic(&provider, salt)
            .await
            .expect("deploy_deterministic");

        assert_eq!(deployed, predicted);

        let code = provider.get_code_at(deployed).await.expect("get_code_at");
        assert!(!code.is_empty(), "no code at deterministic address");
    }

    #[tokio::test]
    async fn deploy_deterministic_yields_same_address_across_chains() {
        // Two independent anvils model two different chains. Same salt +
        // same init_code + same factory address → same CREATE2 address.
        let setup_a = TestSetup::new().await;
        let setup_b = TestSetup::new().await;

        let salt = b256!("cafe000000000000000000000000000000000000000000000000000000000000");

        let addr_a = deploy_deterministic(&setup_a.provider(), salt)
            .await
            .expect("deploy chain a");
        let addr_b = deploy_deterministic(&setup_b.provider(), salt)
            .await
            .expect("deploy chain b");

        assert_eq!(
            addr_a, addr_b,
            "same salt should yield same address across chains"
        );
    }
}