Skip to content

Commit 95f952a

Browse files
authored
Implement beneficiary FIP-0029 (#496)
1 parent d19f9ff commit 95f952a

File tree

8 files changed

+1030
-49
lines changed

8 files changed

+1030
-49
lines changed

actors/miner/src/beneficiary.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use fvm_ipld_encoding::tuple::*;
2+
use fvm_ipld_encoding::Cbor;
3+
use fvm_shared::address::Address;
4+
use fvm_shared::bigint::bigint_ser;
5+
use fvm_shared::clock::ChainEpoch;
6+
use fvm_shared::econ::TokenAmount;
7+
use num_traits::Zero;
8+
use std::ops::Sub;
9+
10+
#[derive(Debug, PartialEq, Eq, Clone, Serialize_tuple, Deserialize_tuple)]
11+
pub struct BeneficiaryTerm {
12+
/// The total amount the current beneficiary can withdraw. Monotonic, but reset when beneficiary changes.
13+
#[serde(with = "bigint_ser")]
14+
pub quota: TokenAmount,
15+
/// The amount of quota the current beneficiary has already withdrawn
16+
#[serde(with = "bigint_ser")]
17+
pub used_quota: TokenAmount,
18+
/// The epoch at which the beneficiary's rights expire and revert to the owner
19+
pub expiration: ChainEpoch,
20+
}
21+
22+
impl Cbor for BeneficiaryTerm {}
23+
24+
impl BeneficiaryTerm {
25+
pub fn default() -> BeneficiaryTerm {
26+
BeneficiaryTerm {
27+
quota: TokenAmount::zero(),
28+
expiration: 0,
29+
used_quota: TokenAmount::zero(),
30+
}
31+
}
32+
33+
pub fn new(
34+
quota: TokenAmount,
35+
used_quota: TokenAmount,
36+
expiration: ChainEpoch,
37+
) -> BeneficiaryTerm {
38+
BeneficiaryTerm { quota, expiration, used_quota }
39+
}
40+
41+
/// Get the amount that the beneficiary has not yet withdrawn
42+
/// return 0 when expired
43+
/// return 0 when the usedQuota >= Quota for safe
44+
/// otherwise Return quota-used_quota
45+
pub fn available(&self, cur: ChainEpoch) -> TokenAmount {
46+
if self.expiration > cur {
47+
(&self.quota).sub(&self.used_quota).max(TokenAmount::zero())
48+
} else {
49+
TokenAmount::zero()
50+
}
51+
}
52+
}
53+
54+
#[derive(Debug, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)]
55+
pub struct PendingBeneficiaryChange {
56+
pub new_beneficiary: Address,
57+
#[serde(with = "bigint_ser")]
58+
pub new_quota: TokenAmount,
59+
pub new_expiration: ChainEpoch,
60+
pub approved_by_beneficiary: bool,
61+
pub approved_by_nominee: bool,
62+
}
63+
64+
impl Cbor for PendingBeneficiaryChange {}
65+
66+
impl PendingBeneficiaryChange {
67+
pub fn new(
68+
new_beneficiary: Address,
69+
new_quota: TokenAmount,
70+
new_expiration: ChainEpoch,
71+
) -> Self {
72+
PendingBeneficiaryChange {
73+
new_beneficiary,
74+
new_quota,
75+
new_expiration,
76+
approved_by_beneficiary: false,
77+
approved_by_nominee: false,
78+
}
79+
}
80+
}

actors/miner/src/lib.rs

Lines changed: 207 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ use fvm_shared::econ::TokenAmount;
3333
// The following errors are particular cases of illegal state.
3434
// They're not expected to ever happen, but if they do, distinguished codes can help us
3535
// diagnose the problem.
36+
37+
pub use beneficiary::*;
3638
use fil_actors_runtime::cbor::{deserialize, serialize, serialize_vec};
3739
use fil_actors_runtime::runtime::builtins::Type;
3840
use fvm_shared::error::*;
@@ -60,6 +62,7 @@ use crate::Code::Blake2b256;
6062
#[cfg(feature = "fil-actor")]
6163
fil_actors_runtime::wasm_trampoline!(Actor);
6264

65+
mod beneficiary;
6366
mod bitfield_queue;
6467
mod commd;
6568
mod deadline_assignment;
@@ -118,6 +121,8 @@ pub enum Method {
118121
ProveReplicaUpdates = 27,
119122
PreCommitSectorBatch2 = 28,
120123
ProveReplicaUpdates2 = 29,
124+
ChangeBeneficiary = 30,
125+
GetBeneficiary = 31,
121126
}
122127

123128
pub const ERR_BALANCE_INVARIANTS_BROKEN: ExitCode = ExitCode::new(1000);
@@ -314,6 +319,15 @@ impl Actor {
314319
new_address
315320
));
316321
}
322+
323+
// Change beneficiary address to new owner if current beneficiary address equal to old owner address
324+
if info.beneficiary == info.owner {
325+
info.beneficiary = pending_address;
326+
}
327+
// Cancel pending beneficiary term change when the owner changes
328+
info.pending_beneficiary_term = None;
329+
330+
// Set the new owner address
317331
info.owner = pending_address;
318332
}
319333

@@ -3235,13 +3249,13 @@ impl Actor {
32353249
));
32363250
}
32373251

3238-
let (info, newly_vested, fee_to_burn, available_balance, state) =
3252+
let (info, amount_withdrawn, newly_vested, fee_to_burn, state) =
32393253
rt.transaction(|state: &mut State, rt| {
3240-
let info = get_miner_info(rt.store(), state)?;
3254+
let mut info = get_miner_info(rt.store(), state)?;
32413255

32423256
// Only the owner is allowed to withdraw the balance as it belongs to/is controlled by the owner
32433257
// and not the worker.
3244-
rt.validate_immediate_caller_is(&[info.owner])?;
3258+
rt.validate_immediate_caller_is(&[info.owner, info.beneficiary])?;
32453259

32463260
// Ensure we don't have any pending terminations.
32473261
if !state.early_terminations.is_empty() {
@@ -3273,36 +3287,197 @@ impl Actor {
32733287
// Verify unlocked funds cover both InitialPledgeRequirement and FeeDebt
32743288
// and repay fee debt now.
32753289
let fee_to_burn = repay_debts_or_abort(rt, state)?;
3276-
3277-
Ok((info, newly_vested, fee_to_burn, available_balance, state.clone()))
3290+
let mut amount_withdrawn =
3291+
std::cmp::min(&available_balance, &params.amount_requested);
3292+
if amount_withdrawn.is_negative() {
3293+
return Err(actor_error!(
3294+
illegal_state,
3295+
"negative amount to withdraw: {}",
3296+
amount_withdrawn
3297+
));
3298+
}
3299+
if info.beneficiary != info.owner {
3300+
// remaining_quota always zero and positive
3301+
let remaining_quota = info.beneficiary_term.available(rt.curr_epoch());
3302+
amount_withdrawn = std::cmp::min(amount_withdrawn, &remaining_quota);
3303+
if amount_withdrawn.is_positive() {
3304+
info.beneficiary_term.used_quota += amount_withdrawn;
3305+
state.save_info(rt.store(), &info).map_err(|e| {
3306+
e.downcast_default(
3307+
ExitCode::USR_ILLEGAL_STATE,
3308+
"failed to save miner info",
3309+
)
3310+
})?;
3311+
}
3312+
Ok((info, amount_withdrawn.clone(), newly_vested, fee_to_burn, state.clone()))
3313+
} else {
3314+
Ok((info, amount_withdrawn.clone(), newly_vested, fee_to_burn, state.clone()))
3315+
}
32783316
})?;
32793317

3280-
let amount_withdrawn = std::cmp::min(&available_balance, &params.amount_requested);
3281-
if amount_withdrawn.is_negative() {
3282-
return Err(actor_error!(
3283-
illegal_state,
3284-
"negative amount to withdraw: {}",
3285-
amount_withdrawn
3286-
));
3287-
}
3288-
if amount_withdrawn > &available_balance {
3289-
return Err(actor_error!(
3290-
illegal_state,
3291-
"amount to withdraw {} < available {}",
3292-
amount_withdrawn,
3293-
available_balance
3294-
));
3295-
}
3296-
32973318
if amount_withdrawn.is_positive() {
3298-
rt.send(info.owner, METHOD_SEND, RawBytes::default(), amount_withdrawn.clone())?;
3319+
rt.send(info.beneficiary, METHOD_SEND, RawBytes::default(), amount_withdrawn.clone())?;
32993320
}
33003321

33013322
burn_funds(rt, fee_to_burn)?;
33023323
notify_pledge_changed(rt, &newly_vested.neg())?;
33033324

33043325
state.check_balance_invariants(&rt.current_balance()).map_err(balance_invariants_broken)?;
3305-
Ok(WithdrawBalanceReturn { amount_withdrawn: amount_withdrawn.clone() })
3326+
Ok(WithdrawBalanceReturn { amount_withdrawn })
3327+
}
3328+
3329+
/// Proposes or confirms a change of beneficiary address.
3330+
/// A proposal must be submitted by the owner, and takes effect after approval of both the proposed beneficiary and current beneficiary,
3331+
/// if applicable, any current beneficiary that has time and quota remaining.
3332+
//// See FIP-0029, https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0029.md
3333+
fn change_beneficiary<BS, RT>(
3334+
rt: &mut RT,
3335+
params: ChangeBeneficiaryParams,
3336+
) -> Result<(), ActorError>
3337+
where
3338+
BS: Blockstore,
3339+
RT: Runtime<BS>,
3340+
{
3341+
let caller = rt.message().caller();
3342+
let new_beneficiary =
3343+
Address::new_id(rt.resolve_address(&params.new_beneficiary).ok_or_else(|| {
3344+
actor_error!(
3345+
illegal_argument,
3346+
"unable to resolve address: {}",
3347+
params.new_beneficiary
3348+
)
3349+
})?);
3350+
3351+
rt.transaction(|state: &mut State, rt| {
3352+
let mut info = get_miner_info(rt.store(), state)?;
3353+
if caller == info.owner {
3354+
// This is a ChangeBeneficiary proposal when the caller is Owner
3355+
if new_beneficiary != info.owner {
3356+
// When beneficiary is not owner, just check quota in params,
3357+
// Expiration maybe an expiration value, but wouldn't cause problem, just the new beneficiary never get any benefit
3358+
if !params.new_quota.is_positive() {
3359+
return Err(actor_error!(
3360+
illegal_argument,
3361+
"beneficial quota {} must bigger than zero",
3362+
params.new_quota
3363+
));
3364+
}
3365+
} else {
3366+
// Expiration/quota must set to 0 while change beneficiary to owner
3367+
if !params.new_quota.is_zero() {
3368+
return Err(actor_error!(
3369+
illegal_argument,
3370+
"owner beneficial quota {} must be zero",
3371+
params.new_quota
3372+
));
3373+
}
3374+
3375+
if params.new_expiration != 0 {
3376+
return Err(actor_error!(
3377+
illegal_argument,
3378+
"owner beneficial expiration {} must be zero",
3379+
params.new_expiration
3380+
));
3381+
}
3382+
}
3383+
3384+
let mut pending_beneficiary_term = PendingBeneficiaryChange::new(
3385+
new_beneficiary,
3386+
params.new_quota,
3387+
params.new_expiration,
3388+
);
3389+
if info.beneficiary_term.available(rt.curr_epoch()).is_zero() {
3390+
// Set current beneficiary to approved when current beneficiary is not effective
3391+
pending_beneficiary_term.approved_by_beneficiary = true;
3392+
}
3393+
info.pending_beneficiary_term = Some(pending_beneficiary_term);
3394+
} else if let Some(pending_term) = &info.pending_beneficiary_term {
3395+
if caller != info.beneficiary && caller != pending_term.new_beneficiary {
3396+
return Err(actor_error!(
3397+
forbidden,
3398+
"message caller {} is neither proposal beneficiary{} nor current beneficiary{}",
3399+
caller,
3400+
params.new_beneficiary,
3401+
info.beneficiary
3402+
));
3403+
}
3404+
3405+
if pending_term.new_beneficiary != new_beneficiary {
3406+
return Err(actor_error!(
3407+
illegal_argument,
3408+
"new beneficiary address must be equal expect {}, but got {}",
3409+
pending_term.new_beneficiary,
3410+
params.new_beneficiary
3411+
));
3412+
}
3413+
if pending_term.new_quota != params.new_quota {
3414+
return Err(actor_error!(
3415+
illegal_argument,
3416+
"new beneficiary quota must be equal expect {}, but got {}",
3417+
pending_term.new_quota,
3418+
params.new_quota
3419+
));
3420+
}
3421+
if pending_term.new_expiration != params.new_expiration {
3422+
return Err(actor_error!(
3423+
illegal_argument,
3424+
"new beneficiary expire date must be equal expect {}, but got {}",
3425+
pending_term.new_expiration,
3426+
params.new_expiration
3427+
));
3428+
}
3429+
} else {
3430+
return Err(actor_error!(forbidden, "No changeBeneficiary proposal exists"));
3431+
}
3432+
3433+
if let Some(pending_term) = info.pending_beneficiary_term.as_mut() {
3434+
if caller == info.beneficiary {
3435+
pending_term.approved_by_beneficiary = true
3436+
}
3437+
3438+
if caller == new_beneficiary {
3439+
pending_term.approved_by_nominee = true
3440+
}
3441+
3442+
if pending_term.approved_by_beneficiary && pending_term.approved_by_nominee {
3443+
//approved by both beneficiary and nominee
3444+
if new_beneficiary != info.beneficiary {
3445+
//if beneficiary changes, reset used_quota to zero
3446+
info.beneficiary_term.used_quota = TokenAmount::zero();
3447+
}
3448+
info.beneficiary = new_beneficiary;
3449+
info.beneficiary_term.quota = pending_term.new_quota.clone();
3450+
info.beneficiary_term.expiration = pending_term.new_expiration;
3451+
// clear the pending proposal
3452+
info.pending_beneficiary_term = None;
3453+
}
3454+
}
3455+
3456+
state.save_info(rt.store(), &info).map_err(|e| {
3457+
e.downcast_default(ExitCode::USR_ILLEGAL_STATE, "failed to save miner info")
3458+
})?;
3459+
Ok(())
3460+
})
3461+
}
3462+
3463+
// GetBeneficiary retrieves the currently active and proposed beneficiary information.
3464+
// This method is for use by other actors (such as those acting as beneficiaries),
3465+
// and to abstract the state representation for clients.
3466+
fn get_beneficiary<BS, RT>(rt: &mut RT) -> Result<GetBeneficiaryReturn, ActorError>
3467+
where
3468+
BS: Blockstore,
3469+
RT: Runtime<BS>,
3470+
{
3471+
rt.validate_immediate_caller_accept_any()?;
3472+
let info = rt.transaction(|state: &mut State, rt| get_miner_info(rt.store(), state))?;
3473+
3474+
Ok(GetBeneficiaryReturn {
3475+
active: ActiveBeneficiary {
3476+
beneficiary: info.beneficiary,
3477+
term: info.beneficiary_term,
3478+
},
3479+
proposed: info.pending_beneficiary_term,
3480+
})
33063481
}
33073482

33083483
fn repay_debt<BS, RT>(rt: &mut RT) -> Result<(), ActorError>
@@ -4689,6 +4864,14 @@ impl ActorCode for Actor {
46894864
let res = Self::prove_replica_updates2(rt, cbor::deserialize_params(params)?)?;
46904865
Ok(RawBytes::serialize(res)?)
46914866
}
4867+
Some(Method::ChangeBeneficiary) => {
4868+
Self::change_beneficiary(rt, cbor::deserialize_params(params)?)?;
4869+
Ok(RawBytes::default())
4870+
}
4871+
Some(Method::GetBeneficiary) => {
4872+
let res = Self::get_beneficiary(rt)?;
4873+
Ok(RawBytes::serialize(res)?)
4874+
}
46924875
None => Err(actor_error!(unhandled_message, "Invalid method")),
46934876
}
46944877
}

0 commit comments

Comments
 (0)