Skip to content

Commit

Permalink
Implement beneficiary FIP-0029 (#496)
Browse files Browse the repository at this point in the history
  • Loading branch information
hunjixin authored Aug 29, 2022
1 parent d19f9ff commit 95f952a
Show file tree
Hide file tree
Showing 8 changed files with 1,030 additions and 49 deletions.
80 changes: 80 additions & 0 deletions actors/miner/src/beneficiary.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
231 changes: 207 additions & 24 deletions actors/miner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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, &params.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, &params.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<BS, RT>(
rt: &mut RT,
params: ChangeBeneficiaryParams,
) -> Result<(), ActorError>
where
BS: Blockstore,
RT: Runtime<BS>,
{
let caller = rt.message().caller();
let new_beneficiary =
Address::new_id(rt.resolve_address(&params.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<BS, RT>(rt: &mut RT) -> Result<GetBeneficiaryReturn, ActorError>
where
BS: Blockstore,
RT: Runtime<BS>,
{
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<BS, RT>(rt: &mut RT) -> Result<(), ActorError>
Expand Down Expand Up @@ -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")),
}
}
Expand Down
Loading

0 comments on commit 95f952a

Please sign in to comment.