diff --git a/Cargo.lock b/Cargo.lock index 5defbdb5..7e3d7c2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2695,7 +2695,7 @@ dependencies = [ [[package]] name = "ipc_actors_abis" version = "0.1.0" -source = "git+https://github.com/consensus-shipyard/ipc-solidity-actors.git?branch=dev#7a396cf0a72a96b5e3b2e457b9ca7e42e5c6b924" +source = "git+https://github.com/consensus-shipyard/ipc-solidity-actors.git?branch=dev#b1e37055cfe11f90b606ad06a056d6f306747d2c" dependencies = [ "anyhow", "ethers", diff --git a/ipc/cli/src/commands/subnet/join.rs b/ipc/cli/src/commands/subnet/join.rs index c1aaca2a..50f79887 100644 --- a/ipc/cli/src/commands/subnet/join.rs +++ b/ipc/cli/src/commands/subnet/join.rs @@ -91,3 +91,43 @@ pub struct StakeSubnetArgs { )] pub collateral: f64, } + +/// The command to unstake in a subnet from validator +pub struct UnstakeSubnet; + +#[async_trait] +impl CommandLineHandler for UnstakeSubnet { + type Arguments = UnstakeSubnetArgs; + + async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { + log::debug!("join subnet 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(Address::from_str(address)?), + None => None, + }; + provider + .unstake(subnet, from, f64_to_token_amount(arguments.collateral)?) + .await + } +} + +#[derive(Debug, Args)] +#[command( + name = "unstake", + about = "Remove collateral to an already joined subnet" +)] +pub struct UnstakeSubnetArgs { + #[arg(long, short, help = "The address that unstakes in the subnet")] + pub from: Option, + #[arg(long, short, help = "The subnet to release collateral from")] + pub subnet: String, + #[arg( + long, + short, + help = "The collateral to unstake from the subnet (in whole FIL units)" + )] + pub collateral: f64, +} diff --git a/ipc/cli/src/commands/subnet/mod.rs b/ipc/cli/src/commands/subnet/mod.rs index 3d197a2e..7f4f662d 100644 --- a/ipc/cli/src/commands/subnet/mod.rs +++ b/ipc/cli/src/commands/subnet/mod.rs @@ -11,7 +11,7 @@ use crate::commands::subnet::send_value::{SendValue, SendValueArgs}; use crate::{CommandLineHandler, GlobalArguments}; use clap::{Args, Subcommand}; -use self::join::{StakeSubnet, StakeSubnetArgs}; +use self::join::{StakeSubnet, StakeSubnetArgs, UnstakeSubnet, UnstakeSubnetArgs}; use self::leave::{Claim, ClaimArgs}; pub mod create; @@ -44,6 +44,7 @@ impl SubnetCommandsArgs { Commands::Kill(args) => KillSubnet::handle(global, args).await, Commands::SendValue(args) => SendValue::handle(global, args).await, Commands::Stake(args) => StakeSubnet::handle(global, args).await, + Commands::Unstake(args) => UnstakeSubnet::handle(global, args).await, Commands::Claim(args) => Claim::handle(global, args).await, } } @@ -59,5 +60,6 @@ pub(crate) enum Commands { Kill(KillSubnetArgs), SendValue(SendValueArgs), Stake(StakeSubnetArgs), + Unstake(UnstakeSubnetArgs), Claim(ClaimArgs), } diff --git a/ipc/provider/src/lib.rs b/ipc/provider/src/lib.rs index 455d7710..55cce740 100644 --- a/ipc/provider/src/lib.rs +++ b/ipc/provider/src/lib.rs @@ -310,6 +310,24 @@ impl IpcProvider { conn.manager().stake(subnet, sender, collateral).await } + pub async fn unstake( + &mut self, + subnet: SubnetID, + from: Option
, + collateral: 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().unstake(subnet, sender, collateral).await + } + pub async fn leave_subnet( &mut self, subnet: SubnetID, diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 86bfaca4..9cf03cd4 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -314,7 +314,7 @@ impl SubnetManager for EthSubnetManager { let collateral = collateral .atto() .to_u128() - .ok_or_else(|| anyhow!("invalid min validator stake"))?; + .ok_or_else(|| anyhow!("invalid collateral amount"))?; let address = contract_address_from_subnet(&subnet)?; log::info!( @@ -334,6 +334,32 @@ impl SubnetManager for EthSubnetManager { Ok(()) } + async fn unstake( + &self, + subnet: SubnetID, + from: Address, + collateral: TokenAmount, + ) -> Result<()> { + let collateral = collateral + .atto() + .to_u128() + .ok_or_else(|| anyhow!("invalid collateral amount"))?; + + let address = contract_address_from_subnet(&subnet)?; + log::info!( + "interacting with evm subnet contract: {address:} with collateral: {collateral:}" + ); + + let signer = Arc::new(self.get_signer(&from)?); + let contract = + subnet_actor_manager_facet::SubnetActorManagerFacet::new(address, signer.clone()); + + let txn = call_with_premium_estimation(signer, contract.unstake(collateral.into())).await?; + txn.send().await?.await?; + + Ok(()) + } + async fn leave_subnet(&self, subnet: SubnetID, from: Address) -> Result<()> { let address = contract_address_from_subnet(&subnet)?; log::info!("leaving evm subnet: {subnet:} at contract: {address:}"); diff --git a/ipc/provider/src/manager/subnet.rs b/ipc/provider/src/manager/subnet.rs index 40ccf857..6e476742 100644 --- a/ipc/provider/src/manager/subnet.rs +++ b/ipc/provider/src/manager/subnet.rs @@ -41,6 +41,11 @@ pub trait SubnetManager: Send + Sync + TopDownCheckpointQuery + BottomUpCheckpoi /// and increase their power in the subnet async fn stake(&self, subnet: SubnetID, from: Address, collateral: TokenAmount) -> Result<()>; + /// Allows validators that have already joined the subnet to unstake collateral + /// and reduce their power in the subnet + async fn unstake(&self, subnet: SubnetID, from: Address, collateral: TokenAmount) + -> Result<()>; + /// Sends a request to leave a subnet from a wallet address. async fn leave_subnet(&self, subnet: SubnetID, from: Address) -> Result<()>;