diff --git a/actors/miner/src/beneficiary.rs b/actors/miner/src/beneficiary.rs new file mode 100644 index 000000000..911ae9d4b --- /dev/null +++ b/actors/miner/src/beneficiary.rs @@ -0,0 +1,80 @@ +use fvm_ipld_encoding::tuple::*; +use fvm_ipld_encoding::Cbor; +use fvm_shared::address::Address; +use fvm_shared::bigint::bigint_ser; +use fvm_shared::clock::ChainEpoch; +use fvm_shared::econ::TokenAmount; +use num_traits::Zero; +use std::ops::Sub; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize_tuple, Deserialize_tuple)] +pub struct BeneficiaryTerm { + /// The total amount the current beneficiary can withdraw. Monotonic, but reset when beneficiary changes. + #[serde(with = "bigint_ser")] + pub quota: TokenAmount, + /// The amount of quota the current beneficiary has already withdrawn + #[serde(with = "bigint_ser")] + pub used_quota: TokenAmount, + /// The epoch at which the beneficiary's rights expire and revert to the owner + pub expiration: ChainEpoch, +} + +impl Cbor for BeneficiaryTerm {} + +impl BeneficiaryTerm { + pub fn default() -> BeneficiaryTerm { + BeneficiaryTerm { + quota: TokenAmount::zero(), + expiration: 0, + used_quota: TokenAmount::zero(), + } + } + + pub fn new( + quota: TokenAmount, + used_quota: TokenAmount, + expiration: ChainEpoch, + ) -> BeneficiaryTerm { + BeneficiaryTerm { quota, expiration, used_quota } + } + + /// Get the amount that the beneficiary has not yet withdrawn + /// return 0 when expired + /// return 0 when the usedQuota >= Quota for safe + /// otherwise Return quota-used_quota + pub fn available(&self, cur: ChainEpoch) -> TokenAmount { + if self.expiration > cur { + (&self.quota).sub(&self.used_quota).max(TokenAmount::zero()) + } else { + TokenAmount::zero() + } + } +} + +#[derive(Debug, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)] +pub struct PendingBeneficiaryChange { + pub new_beneficiary: Address, + #[serde(with = "bigint_ser")] + pub new_quota: TokenAmount, + pub new_expiration: ChainEpoch, + pub approved_by_beneficiary: bool, + pub approved_by_nominee: bool, +} + +impl Cbor for PendingBeneficiaryChange {} + +impl PendingBeneficiaryChange { + pub fn new( + new_beneficiary: Address, + new_quota: TokenAmount, + new_expiration: ChainEpoch, + ) -> Self { + PendingBeneficiaryChange { + new_beneficiary, + new_quota, + new_expiration, + approved_by_beneficiary: false, + approved_by_nominee: false, + } + } +} diff --git a/actors/miner/src/lib.rs b/actors/miner/src/lib.rs index 98e963600..f3960ba48 100644 --- a/actors/miner/src/lib.rs +++ b/actors/miner/src/lib.rs @@ -33,6 +33,8 @@ use fvm_shared::econ::TokenAmount; // The following errors are particular cases of illegal state. // They're not expected to ever happen, but if they do, distinguished codes can help us // diagnose the problem. + +pub use beneficiary::*; use fil_actors_runtime::cbor::{deserialize, serialize, serialize_vec}; use fil_actors_runtime::runtime::builtins::Type; use fvm_shared::error::*; @@ -60,6 +62,7 @@ use crate::Code::Blake2b256; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(Actor); +mod beneficiary; mod bitfield_queue; mod commd; mod deadline_assignment; @@ -118,6 +121,8 @@ pub enum Method { ProveReplicaUpdates = 27, PreCommitSectorBatch2 = 28, ProveReplicaUpdates2 = 29, + ChangeBeneficiary = 30, + GetBeneficiary = 31, } pub const ERR_BALANCE_INVARIANTS_BROKEN: ExitCode = ExitCode::new(1000); @@ -314,6 +319,15 @@ impl Actor { new_address )); } + + // Change beneficiary address to new owner if current beneficiary address equal to old owner address + if info.beneficiary == info.owner { + info.beneficiary = pending_address; + } + // Cancel pending beneficiary term change when the owner changes + info.pending_beneficiary_term = None; + + // Set the new owner address info.owner = pending_address; } @@ -3235,13 +3249,13 @@ impl Actor { )); } - let (info, newly_vested, fee_to_burn, available_balance, state) = + let (info, amount_withdrawn, newly_vested, fee_to_burn, state) = rt.transaction(|state: &mut State, rt| { - let info = get_miner_info(rt.store(), state)?; + let mut info = get_miner_info(rt.store(), state)?; // Only the owner is allowed to withdraw the balance as it belongs to/is controlled by the owner // and not the worker. - rt.validate_immediate_caller_is(&[info.owner])?; + rt.validate_immediate_caller_is(&[info.owner, info.beneficiary])?; // Ensure we don't have any pending terminations. if !state.early_terminations.is_empty() { @@ -3273,36 +3287,197 @@ impl Actor { // Verify unlocked funds cover both InitialPledgeRequirement and FeeDebt // and repay fee debt now. let fee_to_burn = repay_debts_or_abort(rt, state)?; - - Ok((info, newly_vested, fee_to_burn, available_balance, state.clone())) + let mut amount_withdrawn = + std::cmp::min(&available_balance, ¶ms.amount_requested); + if amount_withdrawn.is_negative() { + return Err(actor_error!( + illegal_state, + "negative amount to withdraw: {}", + amount_withdrawn + )); + } + if info.beneficiary != info.owner { + // remaining_quota always zero and positive + let remaining_quota = info.beneficiary_term.available(rt.curr_epoch()); + amount_withdrawn = std::cmp::min(amount_withdrawn, &remaining_quota); + if amount_withdrawn.is_positive() { + info.beneficiary_term.used_quota += amount_withdrawn; + state.save_info(rt.store(), &info).map_err(|e| { + e.downcast_default( + ExitCode::USR_ILLEGAL_STATE, + "failed to save miner info", + ) + })?; + } + Ok((info, amount_withdrawn.clone(), newly_vested, fee_to_burn, state.clone())) + } else { + Ok((info, amount_withdrawn.clone(), newly_vested, fee_to_burn, state.clone())) + } })?; - let amount_withdrawn = std::cmp::min(&available_balance, ¶ms.amount_requested); - if amount_withdrawn.is_negative() { - return Err(actor_error!( - illegal_state, - "negative amount to withdraw: {}", - amount_withdrawn - )); - } - if amount_withdrawn > &available_balance { - return Err(actor_error!( - illegal_state, - "amount to withdraw {} < available {}", - amount_withdrawn, - available_balance - )); - } - if amount_withdrawn.is_positive() { - rt.send(info.owner, METHOD_SEND, RawBytes::default(), amount_withdrawn.clone())?; + rt.send(info.beneficiary, METHOD_SEND, RawBytes::default(), amount_withdrawn.clone())?; } burn_funds(rt, fee_to_burn)?; notify_pledge_changed(rt, &newly_vested.neg())?; state.check_balance_invariants(&rt.current_balance()).map_err(balance_invariants_broken)?; - Ok(WithdrawBalanceReturn { amount_withdrawn: amount_withdrawn.clone() }) + Ok(WithdrawBalanceReturn { amount_withdrawn }) + } + + /// Proposes or confirms a change of beneficiary address. + /// A proposal must be submitted by the owner, and takes effect after approval of both the proposed beneficiary and current beneficiary, + /// if applicable, any current beneficiary that has time and quota remaining. + //// See FIP-0029, https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0029.md + fn change_beneficiary( + rt: &mut RT, + params: ChangeBeneficiaryParams, + ) -> Result<(), ActorError> + where + BS: Blockstore, + RT: Runtime, + { + let caller = rt.message().caller(); + let new_beneficiary = + Address::new_id(rt.resolve_address(¶ms.new_beneficiary).ok_or_else(|| { + actor_error!( + illegal_argument, + "unable to resolve address: {}", + params.new_beneficiary + ) + })?); + + rt.transaction(|state: &mut State, rt| { + let mut info = get_miner_info(rt.store(), state)?; + if caller == info.owner { + // This is a ChangeBeneficiary proposal when the caller is Owner + if new_beneficiary != info.owner { + // When beneficiary is not owner, just check quota in params, + // Expiration maybe an expiration value, but wouldn't cause problem, just the new beneficiary never get any benefit + if !params.new_quota.is_positive() { + return Err(actor_error!( + illegal_argument, + "beneficial quota {} must bigger than zero", + params.new_quota + )); + } + } else { + // Expiration/quota must set to 0 while change beneficiary to owner + if !params.new_quota.is_zero() { + return Err(actor_error!( + illegal_argument, + "owner beneficial quota {} must be zero", + params.new_quota + )); + } + + if params.new_expiration != 0 { + return Err(actor_error!( + illegal_argument, + "owner beneficial expiration {} must be zero", + params.new_expiration + )); + } + } + + let mut pending_beneficiary_term = PendingBeneficiaryChange::new( + new_beneficiary, + params.new_quota, + params.new_expiration, + ); + if info.beneficiary_term.available(rt.curr_epoch()).is_zero() { + // Set current beneficiary to approved when current beneficiary is not effective + pending_beneficiary_term.approved_by_beneficiary = true; + } + info.pending_beneficiary_term = Some(pending_beneficiary_term); + } else if let Some(pending_term) = &info.pending_beneficiary_term { + if caller != info.beneficiary && caller != pending_term.new_beneficiary { + return Err(actor_error!( + forbidden, + "message caller {} is neither proposal beneficiary{} nor current beneficiary{}", + caller, + params.new_beneficiary, + info.beneficiary + )); + } + + if pending_term.new_beneficiary != new_beneficiary { + return Err(actor_error!( + illegal_argument, + "new beneficiary address must be equal expect {}, but got {}", + pending_term.new_beneficiary, + params.new_beneficiary + )); + } + if pending_term.new_quota != params.new_quota { + return Err(actor_error!( + illegal_argument, + "new beneficiary quota must be equal expect {}, but got {}", + pending_term.new_quota, + params.new_quota + )); + } + if pending_term.new_expiration != params.new_expiration { + return Err(actor_error!( + illegal_argument, + "new beneficiary expire date must be equal expect {}, but got {}", + pending_term.new_expiration, + params.new_expiration + )); + } + } else { + return Err(actor_error!(forbidden, "No changeBeneficiary proposal exists")); + } + + if let Some(pending_term) = info.pending_beneficiary_term.as_mut() { + if caller == info.beneficiary { + pending_term.approved_by_beneficiary = true + } + + if caller == new_beneficiary { + pending_term.approved_by_nominee = true + } + + if pending_term.approved_by_beneficiary && pending_term.approved_by_nominee { + //approved by both beneficiary and nominee + if new_beneficiary != info.beneficiary { + //if beneficiary changes, reset used_quota to zero + info.beneficiary_term.used_quota = TokenAmount::zero(); + } + info.beneficiary = new_beneficiary; + info.beneficiary_term.quota = pending_term.new_quota.clone(); + info.beneficiary_term.expiration = pending_term.new_expiration; + // clear the pending proposal + info.pending_beneficiary_term = None; + } + } + + state.save_info(rt.store(), &info).map_err(|e| { + e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to save miner info") + })?; + Ok(()) + }) + } + + // GetBeneficiary retrieves the currently active and proposed beneficiary information. + // This method is for use by other actors (such as those acting as beneficiaries), + // and to abstract the state representation for clients. + fn get_beneficiary(rt: &mut RT) -> Result + where + BS: Blockstore, + RT: Runtime, + { + rt.validate_immediate_caller_accept_any()?; + let info = rt.transaction(|state: &mut State, rt| get_miner_info(rt.store(), state))?; + + Ok(GetBeneficiaryReturn { + active: ActiveBeneficiary { + beneficiary: info.beneficiary, + term: info.beneficiary_term, + }, + proposed: info.pending_beneficiary_term, + }) } fn repay_debt(rt: &mut RT) -> Result<(), ActorError> @@ -4689,6 +4864,14 @@ impl ActorCode for Actor { let res = Self::prove_replica_updates2(rt, cbor::deserialize_params(params)?)?; Ok(RawBytes::serialize(res)?) } + Some(Method::ChangeBeneficiary) => { + Self::change_beneficiary(rt, cbor::deserialize_params(params)?)?; + Ok(RawBytes::default()) + } + Some(Method::GetBeneficiary) => { + let res = Self::get_beneficiary(rt)?; + Ok(RawBytes::serialize(res)?) + } None => Err(actor_error!(unhandled_message, "Invalid method")), } } diff --git a/actors/miner/src/state.rs b/actors/miner/src/state.rs index ce91e4110..2cec75857 100644 --- a/actors/miner/src/state.rs +++ b/actors/miner/src/state.rs @@ -27,6 +27,7 @@ use fvm_shared::sector::{RegisteredPoStProof, SectorNumber, SectorSize, MAX_SECT use fvm_shared::HAMT_BIT_WIDTH; use num_traits::{Signed, Zero}; +use super::beneficiary::*; use super::deadlines::new_deadline_info; use super::policy::*; use super::types::*; @@ -1254,6 +1255,17 @@ pub struct MinerInfo { /// A proposed new owner account for this miner. /// Must be confirmed by a message from the pending address itself. pub pending_owner_address: Option
, + + /// Account for receive miner benefits, withdraw on miner must send to this address, + /// set owner address by default when create miner + pub beneficiary: Address, + + /// beneficiary's total quota, how much quota has been withdraw, + /// and when this beneficiary expired + pub beneficiary_term: BeneficiaryTerm, + + /// A proposal new beneficiary message for this miner + pub pending_beneficiary_term: Option, } impl MinerInfo { @@ -1278,6 +1290,9 @@ impl MinerInfo { worker, control_addresses, pending_worker_key: None, + beneficiary: owner, + beneficiary_term: BeneficiaryTerm::default(), + pending_beneficiary_term: None, peer_id, multi_address, window_post_proof_type, diff --git a/actors/miner/src/types.rs b/actors/miner/src/types.rs index f0b5a2c98..24ec1cef5 100644 --- a/actors/miner/src/types.rs +++ b/actors/miner/src/types.rs @@ -1,6 +1,7 @@ // Copyright 2019-2022 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use super::beneficiary::*; use crate::commd::CompactCommD; use cid::Cid; use fil_actors_runtime::DealWeight; @@ -407,3 +408,29 @@ pub struct ProveReplicaUpdatesParams2 { } impl Cbor for ProveReplicaUpdatesParams2 {} + +#[derive(Debug, Serialize_tuple, Deserialize_tuple)] +pub struct ChangeBeneficiaryParams { + pub new_beneficiary: Address, + #[serde(with = "bigint_ser")] + pub new_quota: TokenAmount, + pub new_expiration: ChainEpoch, +} + +impl Cbor for ChangeBeneficiaryParams {} + +#[derive(Debug, Serialize_tuple, Deserialize_tuple)] +pub struct ActiveBeneficiary { + pub beneficiary: Address, + pub term: BeneficiaryTerm, +} + +impl Cbor for ActiveBeneficiary {} + +#[derive(Debug, Serialize_tuple, Deserialize_tuple)] +pub struct GetBeneficiaryReturn { + pub active: ActiveBeneficiary, + pub proposed: Option, +} + +impl Cbor for GetBeneficiaryReturn {} diff --git a/actors/miner/tests/change_beneficiary_test.rs b/actors/miner/tests/change_beneficiary_test.rs new file mode 100644 index 000000000..19bbe29c8 --- /dev/null +++ b/actors/miner/tests/change_beneficiary_test.rs @@ -0,0 +1,428 @@ +use fil_actor_miner::BeneficiaryTerm; +use fil_actors_runtime::test_utils::{expect_abort, MockRuntime}; +use fvm_shared::clock::ChainEpoch; +use fvm_shared::{address::Address, econ::TokenAmount, error::ExitCode}; +use num_traits::Zero; + +mod util; +use util::*; + +fn setup() -> (ActorHarness, MockRuntime) { + let big_balance = 20u128.pow(23); + let period_offset = 100; + + let h = ActorHarness::new(period_offset); + let mut rt = h.new_runtime(); + h.construct_and_verify(&mut rt); + rt.balance.replace(TokenAmount::from(big_balance)); + + (h, rt) +} + +#[test] +fn successfully_change_owner_to_another_address_two_message() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + + let beneficiary_change = + BeneficiaryChange::new(first_beneficiary_id, TokenAmount::from(100), ChainEpoch::from(200)); + // proposal beneficiary change + h.change_beneficiary(&mut rt, h.owner, &beneficiary_change, None).unwrap(); + // assert change has been made in state + let mut beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + let pending_beneficiary_term = beneficiary_return.proposed.unwrap(); + assert_eq!(beneficiary_change, BeneficiaryChange::from_pending(&pending_beneficiary_term)); + + //confirm proposal + h.change_beneficiary( + &mut rt, + first_beneficiary_id, + &beneficiary_change, + Some(first_beneficiary_id), + ) + .unwrap(); + + beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(None, beneficiary_return.proposed); + assert_eq!(beneficiary_change, BeneficiaryChange::from_active(&beneficiary_return.active)); + + h.check_state(&rt); +} + +#[test] +fn successfully_change_from_not_owner_beneficiary_to_another_address_three_message() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + let second_beneficiary_id = Address::new_id(1001); + + let first_beneficiary_term = + BeneficiaryTerm::new(TokenAmount::from(100), TokenAmount::zero(), ChainEpoch::from(200)); + h.propose_approve_initial_beneficiary(&mut rt, first_beneficiary_id, first_beneficiary_term) + .unwrap(); + + let second_beneficiary_change = BeneficiaryChange::new( + second_beneficiary_id, + TokenAmount::from(101), + ChainEpoch::from(201), + ); + h.change_beneficiary(&mut rt, h.owner, &second_beneficiary_change, None).unwrap(); + let mut beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(first_beneficiary_id, beneficiary_return.active.beneficiary); + + let mut pending_beneficiary_term = beneficiary_return.proposed.unwrap(); + assert_eq!( + second_beneficiary_change, + BeneficiaryChange::from_pending(&pending_beneficiary_term) + ); + assert!(!pending_beneficiary_term.approved_by_beneficiary); + assert!(!pending_beneficiary_term.approved_by_nominee); + + h.change_beneficiary(&mut rt, second_beneficiary_id, &second_beneficiary_change, None).unwrap(); + beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(first_beneficiary_id, beneficiary_return.active.beneficiary); + + pending_beneficiary_term = beneficiary_return.proposed.unwrap(); + assert_eq!( + second_beneficiary_change, + BeneficiaryChange::from_pending(&pending_beneficiary_term) + ); + assert!(!pending_beneficiary_term.approved_by_beneficiary); + assert!(pending_beneficiary_term.approved_by_nominee); + + h.change_beneficiary(&mut rt, first_beneficiary_id, &second_beneficiary_change, None).unwrap(); + beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(None, beneficiary_return.proposed); + assert_eq!( + second_beneficiary_change, + BeneficiaryChange::from_active(&beneficiary_return.active) + ); + assert!(beneficiary_return.active.term.used_quota.is_zero()); +} + +#[test] +fn successfully_change_from_not_owner_beneficiary_to_another_address_when_beneficiary_inefficient_two_message( +) { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + let second_beneficiary_id = Address::new_id(1000); + + let quota = TokenAmount::from(100); + let expiration = ChainEpoch::from(200); + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(quota, TokenAmount::zero(), expiration), + ) + .unwrap(); + + rt.set_epoch(201); + let another_quota = TokenAmount::from(1001); + let another_expiration = ChainEpoch::from(3); + let another_beneficiary_change = + BeneficiaryChange::new(second_beneficiary_id, another_quota, another_expiration); + h.change_beneficiary(&mut rt, h.owner, &another_beneficiary_change, None).unwrap(); + + let mut beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + let pending_beneficiary_term = beneficiary_return.proposed.unwrap(); + assert_eq!( + another_beneficiary_change, + BeneficiaryChange::from_pending(&pending_beneficiary_term) + ); + + h.change_beneficiary(&mut rt, second_beneficiary_id, &another_beneficiary_change, None) + .unwrap(); + beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(None, beneficiary_return.proposed); + assert_eq!( + another_beneficiary_change, + BeneficiaryChange::from_active(&beneficiary_return.active) + ); + assert!(beneficiary_return.active.term.used_quota.is_zero()); + h.check_state(&rt); +} + +#[test] +fn successfully_owner_immediate_revoking_unapproved_proposal() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + + let beneficiary_change = + BeneficiaryChange::new(first_beneficiary_id, TokenAmount::from(100), ChainEpoch::from(200)); + // proposal beneficiary change + h.change_beneficiary(&mut rt, h.owner, &beneficiary_change, None).unwrap(); + // assert change has been made in state + let mut beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + let pending_beneficiary_term = beneficiary_return.proposed.unwrap(); + assert_eq!(beneficiary_change, BeneficiaryChange::from_pending(&pending_beneficiary_term)); + + //revoking unapprovel proposal + let back_owner_beneficiary_change = + BeneficiaryChange::new(h.owner, TokenAmount::zero(), ChainEpoch::from(0)); + h.change_beneficiary(&mut rt, h.owner, &back_owner_beneficiary_change, Some(h.owner)).unwrap(); + + beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(None, beneficiary_return.proposed); + assert_eq!( + back_owner_beneficiary_change, + BeneficiaryChange::from_active(&beneficiary_return.active) + ); + assert!(beneficiary_return.active.term.quota.is_zero()); + + h.check_state(&rt); +} + +#[test] +fn successfully_immediately_change_back_to_owner_address_while_used_up_quota() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + + let quota = TokenAmount::from(100); + let expiration = ChainEpoch::from(200); + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(quota.clone(), TokenAmount::zero(), expiration), + ) + .unwrap(); + + h.withdraw_funds(&mut rt, h.beneficiary, "a, "a, &TokenAmount::zero()).unwrap(); + let back_owner_beneficiary_change = + BeneficiaryChange::new(h.owner, TokenAmount::zero(), ChainEpoch::from(0)); + h.change_beneficiary(&mut rt, h.owner, &back_owner_beneficiary_change, Some(h.owner)).unwrap(); + + let beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(None, beneficiary_return.proposed); + assert_eq!( + back_owner_beneficiary_change, + BeneficiaryChange::from_active(&beneficiary_return.active) + ); + assert!(beneficiary_return.active.term.quota.is_zero()); + h.check_state(&rt); +} + +#[test] +fn successfully_immediately_change_back_to_owner_while_expired() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + + let quota = TokenAmount::from(100); + let expiration = ChainEpoch::from(200); + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(quota, TokenAmount::zero(), expiration), + ) + .unwrap(); + + rt.set_epoch(201); + let back_owner_beneficiary_change = + BeneficiaryChange::new(h.owner, TokenAmount::zero(), ChainEpoch::from(0)); + h.change_beneficiary(&mut rt, h.owner, &back_owner_beneficiary_change, Some(h.owner)).unwrap(); + + let beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(None, beneficiary_return.proposed); + assert_eq!( + back_owner_beneficiary_change, + BeneficiaryChange::from_active(&beneficiary_return.active) + ); + assert!(beneficiary_return.active.term.quota.is_zero()); + h.check_state(&rt); +} + +#[test] +fn successfully_change_quota_and_test_withdraw() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + let beneficiary_term = + BeneficiaryTerm::new(TokenAmount::from(100), TokenAmount::zero(), ChainEpoch::from(200)); + h.propose_approve_initial_beneficiary(&mut rt, first_beneficiary_id, beneficiary_term.clone()) + .unwrap(); + + let withdraw_fund = TokenAmount::from(80); + h.withdraw_funds(&mut rt, h.beneficiary, &withdraw_fund, &withdraw_fund, &TokenAmount::zero()) + .unwrap(); + //decrease quota + let decrease_quota = TokenAmount::from(50); + let decrease_beneficiary_change = + BeneficiaryChange::new(first_beneficiary_id, decrease_quota, beneficiary_term.expiration); + h.change_beneficiary(&mut rt, h.owner, &decrease_beneficiary_change, None).unwrap(); + h.change_beneficiary( + &mut rt, + first_beneficiary_id, + &decrease_beneficiary_change, + Some(first_beneficiary_id), + ) + .unwrap(); + let mut beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!( + decrease_beneficiary_change, + BeneficiaryChange::from_active(&beneficiary_return.active) + ); + assert_eq!(withdraw_fund, beneficiary_return.active.term.used_quota); + + //withdraw 0 zero + let withdraw_left = TokenAmount::from(20); + h.withdraw_funds( + &mut rt, + h.beneficiary, + &withdraw_left, + &TokenAmount::zero(), + &TokenAmount::zero(), + ) + .unwrap(); + + let increase_quota = TokenAmount::from(120); + let increase_beneficiary_change = + BeneficiaryChange::new(first_beneficiary_id, increase_quota, beneficiary_term.expiration); + + h.change_beneficiary(&mut rt, h.owner, &increase_beneficiary_change, None).unwrap(); + h.change_beneficiary( + &mut rt, + first_beneficiary_id, + &increase_beneficiary_change, + Some(first_beneficiary_id), + ) + .unwrap(); + + beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!( + increase_beneficiary_change, + BeneficiaryChange::from_active(&beneficiary_return.active) + ); + assert_eq!(withdraw_fund, beneficiary_return.active.term.used_quota); + + //success withdraw 40 atto fil + let withdraw_left = TokenAmount::from(40); + h.withdraw_funds(&mut rt, h.beneficiary, &withdraw_left, &withdraw_left, &TokenAmount::zero()) + .unwrap(); + h.check_state(&rt); +} + +#[test] +fn fails_approval_message_with_invalidate_params() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + + // proposal beneficiary + let beneficiary_change = + &BeneficiaryChange::new(first_beneficiary_id, TokenAmount::from(100), 200); + h.change_beneficiary(&mut rt, h.owner, beneficiary_change, None).unwrap(); + let beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!( + beneficiary_change, + &BeneficiaryChange::from_pending(&beneficiary_return.proposed.unwrap()) + ); + + //expiration in approval message must equal with proposal + expect_abort( + ExitCode::USR_ILLEGAL_ARGUMENT, + h.change_beneficiary( + &mut rt, + first_beneficiary_id, + &BeneficiaryChange::new(first_beneficiary_id, TokenAmount::from(100), 201), + None, + ), + ); + + //quota in approval message must equal with proposal + expect_abort( + ExitCode::USR_ILLEGAL_ARGUMENT, + h.change_beneficiary( + &mut rt, + first_beneficiary_id, + &BeneficiaryChange::new(first_beneficiary_id, TokenAmount::from(101), 200), + None, + ), + ); + + //beneficiary in approval message must equal with proposal + let second_beneficiary_id = Address::new_id(1010); + expect_abort( + ExitCode::USR_ILLEGAL_ARGUMENT, + h.change_beneficiary( + &mut rt, + first_beneficiary_id, + &BeneficiaryChange::new(second_beneficiary_id, TokenAmount::from(100), 200), + None, + ), + ); +} + +#[test] +fn fails_proposal_beneficiary_with_invalidate_params() { + let (mut h, mut rt) = setup(); + let first_beneficiary_id = Address::new_id(999); + + //not-call unable to proposal beneficiary + expect_abort( + ExitCode::USR_FORBIDDEN, + h.change_beneficiary( + &mut rt, + first_beneficiary_id, + &BeneficiaryChange::new(first_beneficiary_id, TokenAmount::from(100), 200), + None, + ), + ); + + //quota must bigger than zero while change beneficiary to address(not owner) + expect_abort( + ExitCode::USR_ILLEGAL_ARGUMENT, + h.change_beneficiary( + &mut rt, + h.owner, + &BeneficiaryChange::new(first_beneficiary_id, TokenAmount::from(0), 200), + None, + ), + ); + + //quota must be zero while change beneficiary to owner address + expect_abort( + ExitCode::USR_ILLEGAL_ARGUMENT, + h.change_beneficiary( + &mut rt, + h.owner, + &BeneficiaryChange::new(h.owner, TokenAmount::from(20), 0), + None, + ), + ); + + //expiration must be zero while change beneficiary to owner address + expect_abort( + ExitCode::USR_ILLEGAL_ARGUMENT, + h.change_beneficiary( + &mut rt, + h.owner, + &BeneficiaryChange::new(h.owner, TokenAmount::from(0), 1), + None, + ), + ); +} + +#[test] +fn successfully_get_beneficiary() { + let (mut h, mut rt) = setup(); + let mut beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + assert_eq!(h.owner, beneficiary_return.active.beneficiary); + assert_eq!(BeneficiaryTerm::default(), beneficiary_return.active.term); + + let first_beneficiary_id = Address::new_id(999); + let beneficiary_term = + BeneficiaryTerm::new(TokenAmount::from(100), TokenAmount::zero(), ChainEpoch::from(200)); + h.propose_approve_initial_beneficiary(&mut rt, first_beneficiary_id, beneficiary_term).unwrap(); + + let beneficiary = h.get_beneficiary(&mut rt).unwrap(); + let mut info = h.get_info(&rt); + assert_eq!(beneficiary.active.beneficiary, info.beneficiary); + assert_eq!(beneficiary.active.term.expiration, info.beneficiary_term.expiration); + assert_eq!(beneficiary.active.term.quota, info.beneficiary_term.quota); + assert_eq!(beneficiary.active.term.used_quota, info.beneficiary_term.used_quota); + + let withdraw_fund = TokenAmount::from(40); + h.withdraw_funds(&mut rt, h.beneficiary, &withdraw_fund, &withdraw_fund, &TokenAmount::zero()) + .unwrap(); + + beneficiary_return = h.get_beneficiary(&mut rt).unwrap(); + info = h.get_info(&rt); + assert_eq!(beneficiary_return.active.beneficiary, info.beneficiary); + assert_eq!(beneficiary_return.active.term, info.beneficiary_term); +} diff --git a/actors/miner/tests/change_owner_address_test.rs b/actors/miner/tests/change_owner_address_test.rs index a6b6603f0..56fd905dc 100644 --- a/actors/miner/tests/change_owner_address_test.rs +++ b/actors/miner/tests/change_owner_address_test.rs @@ -37,6 +37,7 @@ fn successful_change() { let info = h.get_info(&rt); assert_eq!(NEW_ADDRESS, info.owner); + assert_eq!(NEW_ADDRESS, info.beneficiary); assert!(info.pending_owner_address.is_none()); h.check_state(&rt); diff --git a/actors/miner/tests/util.rs b/actors/miner/tests/util.rs index b2db036bd..27ea19755 100644 --- a/actors/miner/tests/util.rs +++ b/actors/miner/tests/util.rs @@ -12,19 +12,20 @@ use fil_actor_miner::{ aggregate_pre_commit_network_fee, aggregate_prove_commit_network_fee, consensus_fault_penalty, initial_pledge_for_power, locked_reward_from_reward, max_prove_commit_duration, new_deadline_info_from_offset_and_epoch, pledge_penalty_for_continued_fault, power_for_sectors, - qa_power_for_sector, qa_power_for_weight, reward_for_consensus_slash_report, Actor, - ApplyRewardParams, BitFieldQueue, ChangeMultiaddrsParams, ChangePeerIDParams, - ChangeWorkerAddressParams, CheckSectorProvenParams, CompactPartitionsParams, - CompactSectorNumbersParams, ConfirmSectorProofsParams, CronEventPayload, Deadline, - DeadlineInfo, Deadlines, DeclareFaultsParams, DeclareFaultsRecoveredParams, - DeferredCronEventParams, DisputeWindowedPoStParams, ExpirationQueue, ExpirationSet, - ExtendSectorExpirationParams, FaultDeclaration, GetControlAddressesReturn, Method, - MinerConstructorParams as ConstructorParams, MinerInfo, Partition, PoStPartition, PowerPair, - PreCommitSectorBatchParams, PreCommitSectorParams, ProveCommitSectorParams, - RecoveryDeclaration, ReportConsensusFaultParams, SectorOnChainInfo, SectorPreCommitOnChainInfo, - Sectors, State, SubmitWindowedPoStParams, TerminateSectorsParams, TerminationDeclaration, - VestingFunds, WindowedPoSt, WithdrawBalanceParams, WithdrawBalanceReturn, - CRON_EVENT_PROVING_DEADLINE, SECTORS_AMT_BITWIDTH, + qa_power_for_sector, qa_power_for_weight, reward_for_consensus_slash_report, ActiveBeneficiary, + Actor, ApplyRewardParams, BeneficiaryTerm, BitFieldQueue, ChangeBeneficiaryParams, + ChangeMultiaddrsParams, ChangePeerIDParams, ChangeWorkerAddressParams, CheckSectorProvenParams, + CompactPartitionsParams, CompactSectorNumbersParams, ConfirmSectorProofsParams, + CronEventPayload, Deadline, DeadlineInfo, Deadlines, DeclareFaultsParams, + DeclareFaultsRecoveredParams, DeferredCronEventParams, DisputeWindowedPoStParams, + ExpirationQueue, ExpirationSet, ExtendSectorExpirationParams, FaultDeclaration, + GetBeneficiaryReturn, GetControlAddressesReturn, Method, + MinerConstructorParams as ConstructorParams, MinerInfo, Partition, PendingBeneficiaryChange, + PoStPartition, PowerPair, PreCommitSectorBatchParams, PreCommitSectorParams, + ProveCommitSectorParams, RecoveryDeclaration, ReportConsensusFaultParams, SectorOnChainInfo, + SectorPreCommitOnChainInfo, Sectors, State, SubmitWindowedPoStParams, TerminateSectorsParams, + TerminationDeclaration, VestingFunds, WindowedPoSt, WithdrawBalanceParams, + WithdrawBalanceReturn, CRON_EVENT_PROVING_DEADLINE, SECTORS_AMT_BITWIDTH, }; use fil_actor_miner::{Method as MinerMethod, ProveCommitAggregateParams}; use fil_actor_power::{ @@ -103,6 +104,7 @@ pub struct ActorHarness { pub owner: Address, pub worker: Address, pub worker_key: Address, + pub beneficiary: Address, pub control_addrs: Vec
, @@ -141,6 +143,7 @@ impl ActorHarness { worker_key, control_addrs, + beneficiary: owner, seal_proof_type: proof_type, window_post_proof_type: proof_type.registered_window_post_proof().unwrap(), sector_size: proof_type.sector_size().unwrap(), @@ -1936,21 +1939,26 @@ impl ActorHarness { pub fn withdraw_funds( &self, rt: &mut MockRuntime, + from_address: Address, amount_requested: &TokenAmount, expected_withdrawn: &TokenAmount, expected_debt_repaid: &TokenAmount, ) -> Result<(), ActorError> { - rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, self.owner); - rt.expect_validate_caller_addr(vec![self.owner]); + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, from_address); + rt.expect_validate_caller_addr(vec![self.owner, self.beneficiary]); + + if expected_withdrawn.is_positive() { + //no send when real withdraw amount is zero + rt.expect_send( + self.beneficiary, + METHOD_SEND, + RawBytes::default(), + expected_withdrawn.clone(), + RawBytes::default(), + ExitCode::OK, + ); + } - rt.expect_send( - self.owner, - METHOD_SEND, - RawBytes::default(), - expected_withdrawn.clone(), - RawBytes::default(), - ExitCode::OK, - ); if expected_debt_repaid.is_positive() { rt.expect_send( *BURNT_FUNDS_ACTOR_ADDR, @@ -2051,6 +2059,74 @@ impl ActorHarness { Ok(()) } + pub fn propose_approve_initial_beneficiary( + &mut self, + rt: &mut MockRuntime, + beneficiary_id_addr: Address, + beneficiary_term: BeneficiaryTerm, + ) -> Result<(), ActorError> { + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, self.owner); + + let param = ChangeBeneficiaryParams { + new_beneficiary: beneficiary_id_addr, + new_quota: beneficiary_term.quota, + new_expiration: beneficiary_term.expiration, + }; + let raw_bytes = &RawBytes::serialize(param).unwrap(); + rt.call::(Method::ChangeBeneficiary as u64, raw_bytes)?; + rt.verify(); + + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, beneficiary_id_addr); + rt.call::(Method::ChangeBeneficiary as u64, raw_bytes)?; + rt.verify(); + + self.beneficiary = beneficiary_id_addr; + Ok(()) + } + + pub fn change_beneficiary( + &mut self, + rt: &mut MockRuntime, + expect_caller: Address, + beneficiary_change: &BeneficiaryChange, + expect_beneficiary_addr: Option
, + ) -> Result { + rt.set_address_actor_type( + beneficiary_change.beneficiary_addr.clone(), + *ACCOUNT_ACTOR_CODE_ID, + ); + let caller_id = rt.get_id_address(&expect_caller).unwrap(); + let param = ChangeBeneficiaryParams { + new_beneficiary: beneficiary_change.beneficiary_addr, + new_quota: beneficiary_change.quota.clone(), + new_expiration: beneficiary_change.expiration, + }; + rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, caller_id); + let ret = rt.call::( + Method::ChangeBeneficiary as u64, + &RawBytes::serialize(param).unwrap(), + )?; + rt.verify(); + + if let Some(beneficiary) = expect_beneficiary_addr { + let beneficiary_return = self.get_beneficiary(rt)?; + assert_eq!(beneficiary, beneficiary_return.active.beneficiary); + self.beneficiary = beneficiary.clone(); + } + + Ok(ret) + } + + pub fn get_beneficiary( + &mut self, + rt: &mut MockRuntime, + ) -> Result { + rt.expect_validate_caller_any(); + let ret = rt.call::(Method::GetBeneficiary as u64, &RawBytes::default())?; + rt.verify(); + Ok(ret.deserialize::().unwrap()) + } + pub fn extend_sectors( &self, rt: &mut MockRuntime, @@ -2273,6 +2349,38 @@ pub struct PoStDisputeResult { pub expected_reward: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BeneficiaryChange { + pub beneficiary_addr: Address, + pub quota: TokenAmount, + pub expiration: ChainEpoch, +} + +impl BeneficiaryChange { + #[allow(dead_code)] + pub fn new(beneficiary_addr: Address, quota: TokenAmount, expiration: ChainEpoch) -> Self { + BeneficiaryChange { beneficiary_addr, quota, expiration } + } + + #[allow(dead_code)] + pub fn from_pending(pending_beneficiary: &PendingBeneficiaryChange) -> Self { + BeneficiaryChange { + beneficiary_addr: pending_beneficiary.new_beneficiary, + quota: pending_beneficiary.new_quota.clone(), + expiration: pending_beneficiary.new_expiration, + } + } + + #[allow(dead_code)] + pub fn from_active(info: &ActiveBeneficiary) -> Self { + BeneficiaryChange { + beneficiary_addr: info.beneficiary, + quota: info.term.quota.clone(), + expiration: info.term.expiration, + } + } +} + #[allow(dead_code)] pub fn assert_bitfield_equals(bf: &BitField, bits: &[u64]) { let mut rbf = BitField::new(); diff --git a/actors/miner/tests/withdraw_balance.rs b/actors/miner/tests/withdraw_balance.rs index 224e2b108..01133796d 100644 --- a/actors/miner/tests/withdraw_balance.rs +++ b/actors/miner/tests/withdraw_balance.rs @@ -1,4 +1,6 @@ -use fil_actors_runtime::test_utils::expect_abort_contains_message; +use fil_actor_miner::BeneficiaryTerm; +use fil_actors_runtime::test_utils::{expect_abort, expect_abort_contains_message}; +use fvm_shared::address::Address; use fvm_shared::bigint::Zero; use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; @@ -21,6 +23,7 @@ fn happy_path_withdraws_funds() { h.withdraw_funds( &mut rt, + h.owner, &TokenAmount::from(ONE_PERCENT_BALANCE), &TokenAmount::from(ONE_PERCENT_BALANCE), &TokenAmount::zero(), @@ -44,6 +47,7 @@ fn fails_if_miner_cant_repay_fee_debt() { "unlocked balance can not repay fee debt", h.withdraw_funds( &mut rt, + h.owner, &TokenAmount::from(ONE_PERCENT_BALANCE), &TokenAmount::from(ONE_PERCENT_BALANCE), &TokenAmount::zero(), @@ -66,6 +70,141 @@ fn withdraw_only_what_we_can_after_fee_debt() { let requested = rt.balance.borrow().to_owned(); let expected_withdraw = &requested - &fee_debt; - h.withdraw_funds(&mut rt, &requested, &expected_withdraw, &fee_debt).unwrap(); + h.withdraw_funds(&mut rt, h.owner, &requested, &expected_withdraw, &fee_debt).unwrap(); + h.check_state(&rt); +} + +#[test] +fn successfully_withdraw() { + let mut h = ActorHarness::new(PERIOD_OFFSET); + let mut rt = h.new_runtime(); + rt.set_balance(TokenAmount::from(BIG_BALANCE)); + h.construct_and_verify(&mut rt); + + let one = TokenAmount::from(1); + h.withdraw_funds(&mut rt, h.owner, &one, &one, &TokenAmount::zero()).unwrap(); + + let first_beneficiary_id = Address::new_id(999); + let quota = TokenAmount::from(ONE_PERCENT_BALANCE); + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(quota, TokenAmount::zero(), PERIOD_OFFSET + 100), + ) + .unwrap(); + h.withdraw_funds(&mut rt, h.owner, &one, &one, &TokenAmount::zero()).unwrap(); + h.withdraw_funds(&mut rt, h.beneficiary, &one, &one, &TokenAmount::zero()).unwrap(); + h.check_state(&rt); +} + +#[test] +fn successfully_withdraw_allow_zero() { + let mut h = ActorHarness::new(PERIOD_OFFSET); + let mut rt = h.new_runtime(); + rt.set_balance(TokenAmount::from(BIG_BALANCE)); + h.construct_and_verify(&mut rt); + + let first_beneficiary_id = Address::new_id(999); + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(TokenAmount::from(1), TokenAmount::zero(), PERIOD_OFFSET + 100), + ) + .unwrap(); + h.withdraw_funds( + &mut rt, + first_beneficiary_id, + &TokenAmount::zero(), + &TokenAmount::zero(), + &TokenAmount::zero(), + ) + .unwrap(); + h.check_state(&rt); +} + +#[test] +fn successfully_withdraw_limited_to_quota() { + let mut h = ActorHarness::new(PERIOD_OFFSET); + let mut rt = h.new_runtime(); + rt.set_balance(TokenAmount::from(BIG_BALANCE)); + h.construct_and_verify(&mut rt); + + let first_beneficiary_id = Address::new_id(999); + let quota = TokenAmount::from(ONE_PERCENT_BALANCE); + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(quota.clone(), TokenAmount::zero(), PERIOD_OFFSET + 100), + ) + .unwrap(); + + let withdraw_amount = TokenAmount::from(ONE_PERCENT_BALANCE * 2); + h.withdraw_funds(&mut rt, h.beneficiary, &withdraw_amount, "a, &TokenAmount::zero()) + .unwrap(); + h.check_state(&rt); +} + +#[test] +fn allow_withdraw_but_no_send_when_beneficiary_not_efficient() { + let mut h = ActorHarness::new(PERIOD_OFFSET); + let mut rt = h.new_runtime(); + rt.set_balance(TokenAmount::from(BIG_BALANCE)); + h.construct_and_verify(&mut rt); + + let first_beneficiary_id = Address::new_id(999); + let quota = TokenAmount::from(ONE_PERCENT_BALANCE); + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(quota.clone(), TokenAmount::zero(), PERIOD_OFFSET - 10), + ) + .unwrap(); + let info = h.get_info(&rt); + assert_eq!(PERIOD_OFFSET - 10, info.beneficiary_term.expiration); + rt.set_epoch(100); + h.withdraw_funds(&mut rt, h.beneficiary, "a, &TokenAmount::zero(), &TokenAmount::zero()) + .unwrap(); + h.check_state(&rt); +} + +#[test] +fn fail_withdraw_from_non_beneficiary() { + let mut h = ActorHarness::new(PERIOD_OFFSET); + let mut rt = h.new_runtime(); + rt.set_balance(TokenAmount::from(BIG_BALANCE)); + h.construct_and_verify(&mut rt); + + let first_beneficiary_id = Address::new_id(999); + let another_actor = Address::new_id(1000); + let quota = TokenAmount::from(ONE_PERCENT_BALANCE); + let one = TokenAmount::from(1); + + expect_abort( + ExitCode::USR_FORBIDDEN, + h.withdraw_funds( + &mut rt, + first_beneficiary_id, + &one, + &TokenAmount::zero(), + &TokenAmount::zero(), + ), + ); + + h.propose_approve_initial_beneficiary( + &mut rt, + first_beneficiary_id, + BeneficiaryTerm::new(quota, TokenAmount::zero(), PERIOD_OFFSET - 10), + ) + .unwrap(); + + expect_abort( + ExitCode::USR_FORBIDDEN, + h.withdraw_funds(&mut rt, another_actor, &one, &TokenAmount::zero(), &TokenAmount::zero()), + ); + + //allow owner withdraw + h.withdraw_funds(&mut rt, h.owner, &one, &one, &TokenAmount::zero()).unwrap(); + //allow beneficiary withdraw + h.withdraw_funds(&mut rt, first_beneficiary_id, &one, &one, &TokenAmount::zero()).unwrap(); h.check_state(&rt); }