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 #494

Closed
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
83 changes: 83 additions & 0 deletions actors/miner/src/beneficiary_term.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 std::cmp::max;

#[derive(Debug, PartialEq, Serialize_tuple, Deserialize_tuple)]
pub struct BeneficiaryTerm {
// Quota: The total amount the current beneficiary can withdraw. Monotonic, but reset when beneficiary changes.
#[serde(with = "bigint_ser")]
pub quota: TokenAmount,
// UsedQuota: The amount of quota the current beneficiary has already withdrawn
#[serde(with = "bigint_ser")]
pub used_quota: TokenAmount,
// Expiration: 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::default(),
expiration: 0,
used_quota: TokenAmount::default(),
}
}

pub fn new(
quota: TokenAmount,
used_quota: TokenAmount,
expiration: ChainEpoch,
) -> BeneficiaryTerm {
BeneficiaryTerm { quota, expiration, used_quota }
}

// IsUsedUp check whether beneficiary has use up all quota
pub fn is_used_up(&self) -> bool {
self.used_quota >= self.quota
}

// IsExpire check if the beneficiary is within the validity period
pub fn is_expire(&self, cur: ChainEpoch) -> bool {
self.expiration <= cur
}

// Available get the amount that the beneficiary has not yet withdrawn
pub fn available(&self, cur: ChainEpoch) -> TokenAmount {
// Return 0 when the usedQuota > Quota for safe
if self.is_expire(cur) {
TokenAmount::default()
} else {
max(self.quota.clone() - self.used_quota.clone(), TokenAmount::default())
}
}
}

#[derive(Debug, PartialEq, 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,
}
}
}
186 changes: 165 additions & 21 deletions actors/miner/src/lib.rs
Original file line number Diff line number Diff line change
@@ -53,12 +53,14 @@ pub use state::*;
pub use termination::*;
pub use types::*;
pub use vesting_state::*;
pub use beneficiary_term::*;

use crate::Code::Blake2b256;

#[cfg(feature = "fil-actor")]
fil_actors_runtime::wasm_trampoline!(Actor);

mod beneficiary_term;
mod bitfield_queue;
mod deadline_assignment;
mod deadline_info;
@@ -114,6 +116,8 @@ pub enum Method {
PreCommitSectorBatch = 25,
ProveCommitAggregate = 26,
ProveReplicaUpdates = 27,
ChangeBeneficiary =28,
GetBeneficiary =29,
}

pub const ERR_BALANCE_INVARIANTS_BROKEN: ExitCode = ExitCode::new(1000);
@@ -310,6 +314,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;
}

@@ -3084,9 +3097,9 @@ 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.
@@ -3123,28 +3136,41 @@ impl Actor {
// 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 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
));
}
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 {
let remaining_quota = info.beneficiary_term.available(rt.curr_epoch());
if !remaining_quota.is_positive() {
return Err(actor_error!(
forbidden,
"beneficiary{} quota {} used quota {} expiration epoch {} current epoch {}",
info.beneficiary,
info.beneficiary_term.quota,
info.beneficiary_term.used_quota,
info.beneficiary_term.expiration,
rt.curr_epoch()
));
}
amount_withdrawn = std::cmp::min(amount_withdrawn, &remaining_quota);
info.beneficiary_term.used_quota = 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()))
}
})?;

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)?;
@@ -3154,6 +3180,116 @@ impl Actor {
Ok(WithdrawBalanceReturn { amount_withdrawn: amount_withdrawn.clone() })
}

/// Proposes or confirms a change of owner address.
/// If invoked by the current owner, proposes a new owner address for confirmation. If the proposed address is the
/// current owner address, revokes any existing proposal.
/// If invoked by the previously proposed address, with the same proposal, changes the current owner address to be
/// that proposed address.
fn change_beneficiary<BS, RT>(rt: &mut RT, params: ChangeBeneficiaryParams) -> Result<(), ActorError>
where
BS: Blockstore,
RT: Runtime<BS>,
{
let new_beneficiary = 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 rt.message().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 info.pending_beneficiary_term.is_none() {
return Err(actor_error!(forbidden, "No changeBeneficiary proposal exists"));
}
if let Some(pending_term) = &info.pending_beneficiary_term {
if pending_term.new_beneficiary != new_beneficiary {
return Err(actor_error!(forbidden, "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!(forbidden, "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!(forbidden, "new beneficiary expire date must be equal expect {}, but got {}", pending_term.new_expiration, params.new_expiration));
}
}
}

if let Some(pending_term) = info.pending_beneficiary_term.as_mut() {
if rt.message().caller() == info.beneficiary {
pending_term.approved_by_beneficiary = true
}

if rt.message().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
info.beneficiary = new_beneficiary;
info.beneficiary_term.quota = pending_term.new_quota.clone();
info.beneficiary_term.expiration = pending_term.new_expiration;
if new_beneficiary != info.beneficiary {
info.beneficiary_term.used_quota = TokenAmount::default();
}
// 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|{
Ok(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>
where
BS: Blockstore,
@@ -4536,6 +4672,14 @@ impl ActorCode for Actor {
let res = Self::prove_replica_updates(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")),
}
}
10 changes: 10 additions & 0 deletions actors/miner/src/state.rs
Original file line number Diff line number Diff line change
@@ -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_term::{BeneficiaryTerm, PendingBeneficiaryChange};
use super::deadlines::new_deadline_info;
use super::policy::*;
use super::types::*;
@@ -1230,6 +1231,12 @@ pub struct MinerInfo {
/// Optional worker key to update at an epoch
pub pending_worker_key: Option<WorkerKeyChange>,

// Beneficiary account for receive miner benefits, withdraw on miner must send to this address,
// beneficiary set owner address by default when create miner
pub beneficiary: Address,
pub beneficiary_term: BeneficiaryTerm,
pub pending_beneficiary_term: Option<PendingBeneficiaryChange>,

/// Libp2p identity that should be used when connecting to this miner
#[serde(with = "serde_bytes")]
pub peer_id: Vec<u8>,
@@ -1278,6 +1285,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,
31 changes: 31 additions & 0 deletions actors/miner/src/types.rs
Original file line number Diff line number Diff line change
@@ -17,6 +17,8 @@ use fvm_shared::sector::{
StoragePower,
};
use fvm_shared::smooth::FilterEstimate;
use crate::beneficiary_term::BeneficiaryTerm;
use crate::PendingBeneficiaryChange;

pub type CronEvent = i64;

@@ -375,3 +377,32 @@ pub struct ProveReplicaUpdatesParams {
}

impl Cbor for ProveReplicaUpdatesParams {}


#[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<PendingBeneficiaryChange>,
}

impl Cbor for GetBeneficiaryReturn {}