Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:implement beneficiary fip0029 #496

Merged
merged 6 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }
anorth marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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 {
anorth marked this conversation as resolved.
Show resolved Hide resolved
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::*;
hunjixin marked this conversation as resolved.
Show resolved Hide resolved
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.
hunjixin marked this conversation as resolved.
Show resolved Hide resolved
/// 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(())
anorth marked this conversation as resolved.
Show resolved Hide resolved
})
}

// 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