diff --git a/fendermint/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/fendermint/vm/actor_interface/src/ipc.rs index 351ce66a1..d06fe163c 100644 --- a/fendermint/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/fendermint/vm/actor_interface/src/ipc.rs @@ -64,6 +64,10 @@ lazy_static! { name: "GatewayMessengerFacet", abi: ia::gateway_messenger_facet::GATEWAYMESSENGERFACET_ABI.to_owned(), }, + EthFacet { + name: "XnetMessagingFacet", + abi: ia::xnet_messaging_facet::XNETMESSAGINGFACET_ABI.to_owned(), + }, ], }, ), diff --git a/fendermint/fendermint/vm/interpreter/src/fvm/topdown.rs b/fendermint/fendermint/vm/interpreter/src/fvm/topdown.rs index 8226c6599..0fb69cca7 100644 --- a/fendermint/fendermint/vm/interpreter/src/fvm/topdown.rs +++ b/fendermint/fendermint/vm/interpreter/src/fvm/topdown.rs @@ -49,14 +49,17 @@ where DB: Blockstore + Sync + Send + 'static, { let minted_tokens = tokens_to_mint(&messages); + tracing::debug!(token = minted_tokens.to_string(), "tokens to mint in child"); - gateway_caller - .mint_to_gateway(state, minted_tokens.clone()) - .context("failed to mint to gateway")?; + if !minted_tokens.is_zero() { + gateway_caller + .mint_to_gateway(state, minted_tokens.clone()) + .context("failed to mint to gateway")?; - state.update_circ_supply(|circ_supply| { - *circ_supply += minted_tokens; - }); + state.update_circ_supply(|circ_supply| { + *circ_supply += minted_tokens; + }); + } gateway_caller.apply_cross_messages(state, messages) } diff --git a/ipc/ipc/cli/src/commands/crossmsg/fund.rs b/ipc/ipc/cli/src/commands/crossmsg/fund.rs index d7addf45f..245f665c0 100644 --- a/ipc/ipc/cli/src/commands/crossmsg/fund.rs +++ b/ipc/ipc/cli/src/commands/crossmsg/fund.rs @@ -114,3 +114,52 @@ pub struct PreFundArgs { #[arg(help = "Add an initial balance for the address in genesis in the subnet")] pub initial_balance: f64, } + +/// The command to send ERC20 tokens to a subnet from parent +pub(crate) struct FundWithToken; + +#[async_trait] +impl CommandLineHandler for FundWithToken { + type Arguments = FundWithTokenArgs; + + async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { + log::debug!("fund with token operation with args: {:?}", arguments); + + let mut provider = get_ipc_provider(global)?; + let subnet = SubnetID::from_str(&arguments.subnet)?; + let from = match &arguments.from { + Some(address) => Some(require_fil_addr_from_str(address)?), + None => None, + }; + let to = match &arguments.to { + Some(address) => Some(require_fil_addr_from_str(address)?), + None => None, + }; + + println!( + "fund with token performed in epoch: {:?}", + provider + .fund_with_token(subnet, from, to, f64_to_token_amount(arguments.amount)?,) + .await?, + ); + + Ok(()) + } +} + +#[derive(Debug, Args)] +#[command(about = "Send erc20 tokens from a parent to a child subnet")] +pub(crate) struct FundWithTokenArgs { + #[arg(long, short, help = "The address to send funds from")] + pub from: Option, + #[arg( + long, + short, + help = "The address to send funds to (if not set, amount sent to from address)" + )] + pub to: Option, + #[arg(long, short, help = "The subnet to fund")] + pub subnet: String, + #[arg(help = "The amount to fund in erc20, in ether, supports up to 9 decimal places")] + pub amount: f64, +} diff --git a/ipc/ipc/cli/src/commands/crossmsg/mod.rs b/ipc/ipc/cli/src/commands/crossmsg/mod.rs index 76ae591ee..bfbad6fd6 100644 --- a/ipc/ipc/cli/src/commands/crossmsg/mod.rs +++ b/ipc/ipc/cli/src/commands/crossmsg/mod.rs @@ -1,6 +1,6 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: MIT -use self::fund::{PreFund, PreFundArgs}; +use self::fund::{FundWithToken, FundWithTokenArgs, PreFund, PreFundArgs}; use self::release::{PreRelease, PreReleaseArgs}; use self::topdown_cross::{ LatestParentFinality, LatestParentFinalityArgs, ListTopdownMsgs, ListTopdownMsgsArgs, @@ -32,6 +32,7 @@ impl CrossMsgsCommandsArgs { pub async fn handle(&self, global: &GlobalArguments) -> anyhow::Result<()> { match &self.command { Commands::Fund(args) => Fund::handle(global, args).await, + Commands::FundWithToken(args) => FundWithToken::handle(global, args).await, Commands::PreFund(args) => PreFund::handle(global, args).await, Commands::Release(args) => Release::handle(global, args).await, Commands::PreRelease(args) => PreRelease::handle(global, args).await, @@ -45,6 +46,7 @@ impl CrossMsgsCommandsArgs { #[derive(Debug, Subcommand)] pub(crate) enum Commands { Fund(FundArgs), + FundWithToken(FundWithTokenArgs), PreFund(PreFundArgs), Release(ReleaseArgs), PreRelease(PreReleaseArgs), diff --git a/ipc/ipc/provider/src/lib.rs b/ipc/ipc/provider/src/lib.rs index 735318c22..24978a8d9 100644 --- a/ipc/ipc/provider/src/lib.rs +++ b/ipc/ipc/provider/src/lib.rs @@ -479,6 +479,30 @@ impl IpcProvider { .await } + /// Funds an account in a child subnet with erc20 token, provided that the supply source kind is + /// `ERC20`. If `from` is None, it will use the default address config in `ipc.toml`. + /// If `to` is `None`, the `from` account will be funded. + pub async fn fund_with_token( + &mut self, + subnet: SubnetID, + from: Option
, + to: Option
, + amount: TokenAmount, + ) -> anyhow::Result { + let parent = subnet.parent().ok_or_else(|| anyhow!("no parent found"))?; + let conn = match self.connection(&parent) { + None => return Err(anyhow!("target parent subnet not found")), + Some(conn) => conn, + }; + + let subnet_config = conn.subnet(); + let sender = self.check_sender(subnet_config, from)?; + + conn.manager() + .fund_with_token(subnet, sender, to.unwrap_or(sender), amount) + .await + } + /// Release to an account in a child subnet, if `to` is `None`, the self account /// is funded. pub async fn release( diff --git a/ipc/ipc/provider/src/manager/evm/manager.rs b/ipc/ipc/provider/src/manager/evm/manager.rs index d690f4897..ffe1b5374 100644 --- a/ipc/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/ipc/provider/src/manager/evm/manager.rs @@ -570,6 +570,36 @@ impl SubnetManager for EthSubnetManager { block_number_from_receipt(receipt) } + async fn fund_with_token( + &self, + subnet: SubnetID, + from: Address, + to: Address, + amount: TokenAmount, + ) -> Result { + log::debug!("fund with token, subnet: {subnet}, amount: {amount}, from: {from}, to: {to}"); + + let value = fil_amount_to_eth_amount(&amount)?; + let evm_subnet_id = gateway_manager_facet::SubnetID::try_from(&subnet)?; + + let signer = Arc::new(self.get_signer(&from)?); + let gateway_contract = gateway_manager_facet::GatewayManagerFacet::new( + self.ipc_contract_info.gateway_addr, + signer.clone(), + ); + + let txn = gateway_contract.fund_with_token( + evm_subnet_id, + gateway_manager_facet::FvmAddress::try_from(to)?, + value, + ); + let txn = call_with_premium_estimation(signer, txn).await?; + + let pending_tx = txn.send().await?; + let receipt = pending_tx.retries(TRANSACTION_RECEIPT_RETRIES).await?; + block_number_from_receipt(receipt) + } + async fn release( &self, gateway_addr: Address, @@ -1336,6 +1366,11 @@ fn into_genesis_balance_map( Ok(map) } +pub(crate) fn fil_amount_to_eth_amount(amount: &TokenAmount) -> Result { + let v = ethers::types::U256::from_dec_str(&amount.atto().to_string())?; + Ok(v) +} + /// Convert the ipc SubnetID type to an evm address. It extracts the last address from the Subnet id /// children and turns it into evm address. pub(crate) fn contract_address_from_subnet(subnet: &SubnetID) -> Result { diff --git a/ipc/ipc/provider/src/manager/subnet.rs b/ipc/ipc/provider/src/manager/subnet.rs index 50333926a..65f18aee6 100644 --- a/ipc/ipc/provider/src/manager/subnet.rs +++ b/ipc/ipc/provider/src/manager/subnet.rs @@ -88,6 +88,25 @@ pub trait SubnetManager: Send + Sync + TopDownFinalityQuery + BottomUpCheckpoint amount: TokenAmount, ) -> Result; + /// Sends funds to a specified subnet receiver using ERC20 tokens. + /// This function locks the amount of ERC20 tokens into custody and then mints the supply in the specified subnet. + /// It checks if the subnet's supply strategy is ERC20 and if not, the operation is reverted. + /// It allows for free injection of funds into a subnet and is protected against reentrancy. + /// + /// # Arguments + /// + /// * `subnetId` - The ID of the subnet where the funds will be sent to. + /// * `from` - The funding address. + /// * `to` - The funded address. + /// * `amount` - The amount of ERC20 tokens to be sent. + async fn fund_with_token( + &self, + subnet: SubnetID, + from: Address, + to: Address, + amount: TokenAmount, + ) -> Result; + /// Release creates a new check message to release funds in parent chain /// Returns the epoch that the released is executed in the child. async fn release(