From 3e5ad83eee0d20b8fdd3938fa27ec98e95f7c99a Mon Sep 17 00:00:00 2001 From: jonas089 Date: Tue, 10 Mar 2026 12:41:34 +0100 Subject: [PATCH 1/2] add permit2 deployment --- Makefile | 8 +- solver-cli/src/commands/deploy.rs | 3 + solver-cli/src/commands/deploy_permit2.rs | 158 ++++++++++++++++++++++ solver-cli/src/commands/mod.rs | 1 + solver-cli/src/main.rs | 10 +- 5 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 solver-cli/src/commands/deploy_permit2.rs diff --git a/Makefile b/Makefile index 5e2b332..5276b69 100644 --- a/Makefile +++ b/Makefile @@ -121,13 +121,9 @@ init: build @$(SOLVER_CLI) init $(FORCE_FLAG) .PHONY: init -## deploy-permit2: Deploy Permit2 contract to local Anvil chains (fetches bytecode from mainnet) +## deploy-permit2: Deploy (or inject) Permit2 to all configured chains via solver-cli deploy-permit2: - @echo "Deploying Permit2 to local Anvil chains..." - @PERMIT2_CODE=$$(cast code 0x000000000022D473030F116dDEE9F6B43aC78BA3 --rpc-url https://eth.llamarpc.com 2>/dev/null) && \ - cast rpc anvil_setCode 0x000000000022D473030F116dDEE9F6B43aC78BA3 "$$PERMIT2_CODE" --rpc-url http://127.0.0.1:8545 > /dev/null && \ - cast rpc anvil_setCode 0x000000000022D473030F116dDEE9F6B43aC78BA3 "$$PERMIT2_CODE" --rpc-url http://127.0.0.1:8546 > /dev/null && \ - echo " Permit2 deployed at 0x000000000022D473030F116dDEE9F6B43aC78BA3 on anvil1 + anvil2" + @$(SOLVER_CLI) deploy-permit2 .PHONY: deploy-permit2 ## deploy: Deploy OIF contracts to all configured chains (use FORCE=1 to redeploy, CHAINS=a,b to limit) diff --git a/solver-cli/src/commands/deploy.rs b/solver-cli/src/commands/deploy.rs index 9be32b0..f92e062 100644 --- a/solver-cli/src/commands/deploy.rs +++ b/solver-cli/src/commands/deploy.rs @@ -172,6 +172,9 @@ impl DeployCommand { if let Some(addr) = &chain.contracts.oracle { print_address("CentralizedOracle", addr); } + if let Some(addr) = &chain.contracts.permit2 { + print_address("Permit2", addr); + } for (symbol, token) in &chain.tokens { print_address(&format!("Token ({})", symbol), &token.address); } diff --git a/solver-cli/src/commands/deploy_permit2.rs b/solver-cli/src/commands/deploy_permit2.rs new file mode 100644 index 0000000..001ef45 --- /dev/null +++ b/solver-cli/src/commands/deploy_permit2.rs @@ -0,0 +1,158 @@ +use anyhow::{Context, Result}; +use clap::Args; +use std::env; +use std::path::PathBuf; + +use crate::state::StateManager; +use crate::utils::*; +use crate::OutputFormat; + +/// Canonical Permit2 address (same on all chains where it's deployed) +const PERMIT2_ADDRESS: &str = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; + +/// Public Ethereum mainnet RPC used to fetch Permit2 bytecode when not locally present +const DEFAULT_MAINNET_RPC: &str = "https://eth.llamarpc.com"; + +#[derive(Args)] +pub struct DeployPermit2Command { + /// Project directory + #[arg(long)] + pub dir: Option, + + /// Chains to deploy to (comma-separated). Defaults to all configured chains. + #[arg(long)] + pub chains: Option, + + /// Mainnet RPC URL to fetch Permit2 bytecode from when the chain doesn't have it + #[arg(long, default_value = DEFAULT_MAINNET_RPC)] + pub mainnet_rpc: String, +} + +impl DeployPermit2Command { + pub async fn run(self, output: OutputFormat) -> Result<()> { + use alloy::primitives::{address, Bytes}; + use alloy::providers::{Provider, ProviderBuilder}; + + let out = OutputFormatter::new(output); + let project_dir = self.dir.unwrap_or_else(|| env::current_dir().unwrap()); + let state_mgr = StateManager::new(&project_dir); + + out.header("Deploying Permit2"); + + let mut state = state_mgr.load_or_error().await?; + load_dotenv(&project_dir)?; + let env_config = EnvConfig::from_env()?; + + let permit2_addr = address!("000000000022D473030F116dDEE9F6B43aC78BA3"); + + // Determine which chains to target + let chain_names: Vec = if let Some(arg) = &self.chains { + arg.split(',').map(|s| s.trim().to_string()).collect() + } else { + env_config.chain_names() + }; + + if chain_names.is_empty() { + anyhow::bail!("No chains configured. Use --chains or set chain env vars."); + } + + // Lazily fetch bytecode from mainnet — only if needed + let mut mainnet_bytecode: Option = None; + + for name in &chain_names { + let chain = env_config.load_chain(name).ok_or_else(|| { + anyhow::anyhow!( + "Chain '{}' not found. Make sure {}_RPC and {}_PK are set.", + name, + name.to_uppercase(), + name.to_uppercase() + ) + })?; + + let url: reqwest::Url = chain.rpc_url.parse().context("Invalid RPC URL")?; + let provider = ProviderBuilder::new().connect_http(url); + + let chain_id = provider + .get_chain_id() + .await + .context("Failed to get chain ID")?; + + print_info(&format!("Chain {} (id={})", name, chain_id)); + + // Check if Permit2 already has code at the canonical address + let existing_code = provider + .get_code_at(permit2_addr) + .await + .context("Failed to get code at Permit2 address")?; + + if !existing_code.is_empty() { + print_kv(" Permit2", &format!("{} (already deployed)", PERMIT2_ADDRESS)); + } else { + // Need to inject bytecode — fetch from mainnet if we don't have it yet + if mainnet_bytecode.is_none() { + print_info(&format!( + " Fetching Permit2 bytecode from {}...", + self.mainnet_rpc + )); + let mainnet_url: reqwest::Url = + self.mainnet_rpc.parse().context("Invalid mainnet RPC URL")?; + let mainnet_provider = ProviderBuilder::new().connect_http(mainnet_url); + let code = mainnet_provider + .get_code_at(permit2_addr) + .await + .context("Failed to fetch Permit2 bytecode from mainnet")?; + if code.is_empty() { + anyhow::bail!( + "Permit2 has no code on mainnet RPC {}. \ + Try a different --mainnet-rpc.", + self.mainnet_rpc + ); + } + mainnet_bytecode = Some(code); + } + + let bytecode = mainnet_bytecode.as_ref().unwrap(); + + // Inject via anvil_setCode (works on Anvil/Hardhat nodes) + provider + .raw_request::<_, ()>( + "anvil_setCode".into(), + (permit2_addr, bytecode), + ) + .await + .context( + "anvil_setCode failed — is this an Anvil node? \ + For live networks Permit2 should already be deployed at the canonical address.", + )?; + + print_kv( + " Permit2", + &format!("{} (bytecode injected)", PERMIT2_ADDRESS), + ); + } + + // Store address in state for this chain + if let Some(chain_config) = state.chains.get_mut(&chain_id) { + chain_config.contracts.permit2 = Some(PERMIT2_ADDRESS.to_string()); + } else { + print_warning(&format!( + " Chain {} (id={}) not found in state — run 'solver-cli deploy' first", + name, chain_id + )); + } + } + + state_mgr.save(&state).await?; + print_success("Permit2 addresses saved to state"); + + print_summary_start(); + print_kv("Permit2", PERMIT2_ADDRESS); + print_summary_end(); + + if out.is_json() { + out.json(&serde_json::json!({ "permit2": PERMIT2_ADDRESS }))?; + } + + Ok(()) + } +} diff --git a/solver-cli/src/commands/mod.rs b/solver-cli/src/commands/mod.rs index d3a425e..a678028 100644 --- a/solver-cli/src/commands/mod.rs +++ b/solver-cli/src/commands/mod.rs @@ -3,6 +3,7 @@ pub mod balances; pub mod chain; pub mod configure; pub mod deploy; +pub mod deploy_permit2; pub mod fund; pub mod init; pub mod intent; diff --git a/solver-cli/src/main.rs b/solver-cli/src/main.rs index adc0042..da909d8 100644 --- a/solver-cli/src/main.rs +++ b/solver-cli/src/main.rs @@ -12,9 +12,9 @@ use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use commands::{ account::AccountCommand, balances::BalancesCommand, chain::ChainCommand, - configure::ConfigureCommand, deploy::DeployCommand, fund::FundCommand, init::InitCommand, - intent::IntentCommand, order::OrderCommand, rebalancer::RebalancerCommand, - solver::SolverCommand, token::TokenCommand, + configure::ConfigureCommand, deploy::DeployCommand, deploy_permit2::DeployPermit2Command, + fund::FundCommand, init::InitCommand, intent::IntentCommand, order::OrderCommand, + rebalancer::RebalancerCommand, solver::SolverCommand, token::TokenCommand, }; #[derive(Parser)] @@ -48,6 +48,9 @@ enum Commands { /// Deploy contracts to all configured chains Deploy(DeployCommand), + /// Deploy (or inject) Permit2 to configured chains + DeployPermit2(DeployPermit2Command), + /// Generate solver configuration Configure(ConfigureCommand), @@ -110,6 +113,7 @@ async fn main() -> anyhow::Result<()> { let result = match cli.command { Commands::Init(cmd) => cmd.run(cli.output).await, Commands::Deploy(cmd) => cmd.run(cli.output).await, + Commands::DeployPermit2(cmd) => cmd.run(cli.output).await, Commands::Configure(cmd) => cmd.run(cli.output).await, Commands::Fund(cmd) => cmd.run(cli.output).await, Commands::Chain(cmd) => cmd.run(cli.output).await, From c804a881372a57978a2a0b62b3c7cc03dd9170b2 Mon Sep 17 00:00:00 2001 From: jonas089 Date: Tue, 10 Mar 2026 13:05:39 +0100 Subject: [PATCH 2/2] clippy --- solver-cli/src/commands/deploy_permit2.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/solver-cli/src/commands/deploy_permit2.rs b/solver-cli/src/commands/deploy_permit2.rs index 001ef45..37fb6d8 100644 --- a/solver-cli/src/commands/deploy_permit2.rs +++ b/solver-cli/src/commands/deploy_permit2.rs @@ -86,7 +86,10 @@ impl DeployPermit2Command { .context("Failed to get code at Permit2 address")?; if !existing_code.is_empty() { - print_kv(" Permit2", &format!("{} (already deployed)", PERMIT2_ADDRESS)); + print_kv( + " Permit2", + format!("{} (already deployed)", PERMIT2_ADDRESS), + ); } else { // Need to inject bytecode — fetch from mainnet if we don't have it yet if mainnet_bytecode.is_none() { @@ -94,8 +97,10 @@ impl DeployPermit2Command { " Fetching Permit2 bytecode from {}...", self.mainnet_rpc )); - let mainnet_url: reqwest::Url = - self.mainnet_rpc.parse().context("Invalid mainnet RPC URL")?; + let mainnet_url: reqwest::Url = self + .mainnet_rpc + .parse() + .context("Invalid mainnet RPC URL")?; let mainnet_provider = ProviderBuilder::new().connect_http(mainnet_url); let code = mainnet_provider .get_code_at(permit2_addr) @@ -127,7 +132,7 @@ impl DeployPermit2Command { print_kv( " Permit2", - &format!("{} (bytecode injected)", PERMIT2_ADDRESS), + format!("{} (bytecode injected)", PERMIT2_ADDRESS), ); }