Skip to content

Commit f220fdd

Browse files
committed
feat: remove beneficiary from self destruct (#1838)
fixes #1837
1 parent 4a52b7b commit f220fdd

File tree

9 files changed

+52
-83
lines changed

9 files changed

+52
-83
lines changed

fvm/src/kernel/default.rs

+18-17
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use filecoin_proofs_api::{self as proofs, ProverId, PublicReplicaInfo, SectorId}
1111
use fvm_ipld_blockstore::Blockstore;
1212
use fvm_ipld_encoding::{bytes_32, IPLD_RAW};
1313
use fvm_shared::address::Payload;
14-
use fvm_shared::bigint::Zero;
1514
use fvm_shared::chainid::ChainID;
1615
use fvm_shared::consensus::ConsensusFault;
1716
use fvm_shared::crypto::signature;
@@ -36,7 +35,7 @@ use crate::call_manager::{CallManager, InvocationResult, NO_DATA_BLOCK_ID};
3635
use crate::externs::{Chain, Consensus, Rand};
3736
use crate::gas::GasTimer;
3837
use crate::init_actor::INIT_ACTOR_ID;
39-
use crate::machine::{MachineContext, NetworkConfig};
38+
use crate::machine::{MachineContext, NetworkConfig, BURNT_FUNDS_ACTOR_ID};
4039
use crate::state_tree::ActorState;
4140
use crate::{ipld, syscall_error};
4241

@@ -244,35 +243,37 @@ where
244243
t.record(Ok(self.get_self()?.map(|a| a.balance).unwrap_or_default()))
245244
}
246245

247-
fn self_destruct(&mut self, beneficiary: &Address) -> Result<()> {
246+
fn self_destruct(&mut self, burn_unspent: bool) -> Result<()> {
248247
if self.read_only {
249248
return Err(syscall_error!(ReadOnly; "cannot self-destruct when read-only").into());
250249
}
251250

252-
// Idempotentcy: If the actor doesn't exist, this won't actually do anything. The current
251+
// Idempotent: If the actor doesn't exist, this won't actually do anything. The current
253252
// balance will be zero, and `delete_actor_id` will be a no-op.
254253
let t = self
255254
.call_manager
256255
.charge_gas(self.call_manager.price_list().on_delete_actor())?;
257256

257+
// If there are remaining funds, burn them. We do this instead of letting the user to
258+
// specify the beneficiary as:
259+
//
260+
// 1. This lets the user handle transfer failure cases themselves. The only way _this_ can
261+
// fail is for the caller to run out of gas.
262+
// 2. If we ever decide to allow code on method 0, allowing transfers here would be
263+
// unfortunate.
258264
let balance = self.current_balance()?;
259-
if balance != TokenAmount::zero() {
260-
// Starting from network version v7, the runtime checks if the beneficiary
261-
// exists; if missing, it fails the self destruct.
262-
//
263-
// In FVM we check unconditionally, since we only support nv13+.
264-
let beneficiary_id = self.resolve_address(beneficiary)?;
265-
266-
if beneficiary_id == self.actor_id {
267-
return Err(syscall_error!(Forbidden, "benefactor cannot be beneficiary").into());
265+
if !balance.is_zero() {
266+
if !burn_unspent {
267+
return Err(
268+
syscall_error!(IllegalOperation; "self-destruct with unspent funds").into(),
269+
);
268270
}
269-
270-
// Transfer the entirety of funds to beneficiary.
271271
self.call_manager
272-
.transfer(self.actor_id, beneficiary_id, &balance)?;
272+
.transfer(self.actor_id, BURNT_FUNDS_ACTOR_ID, &balance)
273+
.or_fatal()?;
273274
}
274275

275-
// Delete the executing actor
276+
// Delete the executing actor.
276277
t.record(self.call_manager.delete_actor(self.actor_id))
277278
}
278279
}

fvm/src/kernel/mod.rs

+2-4
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,8 @@ pub trait SelfOps: IpldBlockOps {
176176
/// The balance of the receiver.
177177
fn current_balance(&self) -> Result<TokenAmount>;
178178

179-
/// Deletes the executing actor from the state tree, transferring any balance to beneficiary.
180-
/// Aborts if the beneficiary does not exist.
181-
/// May only be called by the actor itself.
182-
fn self_destruct(&mut self, beneficiary: &Address) -> Result<()>;
179+
/// Deletes the executing actor from the state tree, burning any remaining balance if requested.
180+
fn self_destruct(&mut self, burn_unspent: bool) -> Result<()>;
183181
}
184182

185183
/// Actors operations whose scope of action is actors other than the calling

fvm/src/syscalls/sself.rs

+2-7
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,7 @@ pub fn current_balance(context: Context<'_, impl Kernel>) -> Result<sys::TokenAm
3333
.or_fatal()
3434
}
3535

36-
pub fn self_destruct(
37-
context: Context<'_, impl Kernel>,
38-
addr_off: u32,
39-
addr_len: u32,
40-
) -> Result<()> {
41-
let addr = context.memory.read_address(addr_off, addr_len)?;
42-
context.kernel.self_destruct(&addr)?;
36+
pub fn self_destruct(context: Context<'_, impl Kernel>, burn_unspent: u32) -> Result<()> {
37+
context.kernel.self_destruct(burn_unspent > 0)?;
4338
Ok(())
4439
}

sdk/src/error.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ pub enum StateUpdateError {
1616

1717
#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1818
pub enum ActorDeleteError {
19-
#[error("deletion beneficiary is the current actor")]
20-
BeneficiaryIsSelf,
21-
#[error("deletion beneficiary does not exist")]
22-
BeneficiaryDoesNotExist,
23-
#[error("current execution context is read-only")]
19+
#[error("cannot self-destruct when read-only")]
2420
ReadOnly,
21+
#[error("actor did not request unspent funds to be burnt")]
22+
UnspentFunds,
2523
}
2624

2725
#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]

sdk/src/sself.rs

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright 2021-2023 Protocol Labs
22
// SPDX-License-Identifier: Apache-2.0, MIT
33
use cid::Cid;
4-
use fvm_shared::address::Address;
54
use fvm_shared::econ::TokenAmount;
65
use fvm_shared::error::ErrorNumber;
76
use fvm_shared::MAX_CID_LEN;
@@ -53,18 +52,12 @@ pub fn current_balance() -> TokenAmount {
5352
}
5453
}
5554

56-
/// Destroys the calling actor, sending its current balance
57-
/// to the supplied address, which cannot be itself.
58-
///
59-
/// Fails when calling actor has a non zero balance and the beneficiary doesn't
60-
/// exist or is the actor being deleted.
61-
pub fn self_destruct(beneficiary: &Address) -> Result<(), ActorDeleteError> {
62-
let bytes = beneficiary.to_bytes();
55+
/// Destroys the calling actor, burning any remaining balance.
56+
pub fn self_destruct(burn_funds: bool) -> Result<(), ActorDeleteError> {
6357
unsafe {
64-
sys::sself::self_destruct(bytes.as_ptr(), bytes.len() as u32).map_err(|e| match e {
65-
ErrorNumber::Forbidden => ActorDeleteError::BeneficiaryIsSelf,
58+
sys::sself::self_destruct(burn_funds).map_err(|e| match e {
59+
ErrorNumber::IllegalOperation => ActorDeleteError::UnspentFunds,
6660
ErrorNumber::ReadOnly => ActorDeleteError::ReadOnly,
67-
ErrorNumber::NotFound => ActorDeleteError::BeneficiaryDoesNotExist,
6861
_ => panic!("unexpected error from `self::self_destruct` syscall: {}", e),
6962
})
7063
}

sdk/src/sys/sself.rs

+10-13
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ super::fvm_syscalls! {
3838
/// | Error | Reason |
3939
/// |----------------------|------------------------------------------------|
4040
/// | [`IllegalOperation`] | actor has been deleted |
41+
/// | [`ReadOnly`] | the actor is executing in read-only mode |
4142
/// | [`NotFound`] | specified root CID is not in the reachable set |
4243
pub fn set_root(cid: *const u8) -> Result<()>;
4344

@@ -48,23 +49,19 @@ super::fvm_syscalls! {
4849
/// None.
4950
pub fn current_balance() -> Result<super::TokenAmount>;
5051

51-
/// Destroys the calling actor, sending its current balance
52-
/// to the supplied address, which cannot be itself.
53-
///
54-
/// Fails when calling actor has a non zero balance and the beneficiary doesn't
55-
/// exist or is the actor being deleted.
52+
/// Destroys the calling actor. If `burn_funds` is true, any unspent balance will be burnt
53+
/// (destroyed). Otherwise, if `burnt_funds` is false and there are unspent funds, this syscall
54+
/// will fail.
5655
///
5756
/// # Arguments
5857
///
59-
/// - `addr_off` and `addr_len` specify the location and length of beneficiary's address in wasm
60-
/// memory.
58+
/// - `burn_funds` must be true to delete an actor with unspent funds.
6159
///
6260
/// # Errors
6361
///
64-
/// | Error | Reason |
65-
/// |---------------------|----------------------------------------------------------------|
66-
/// | [`NotFound`] | beneficiary isn't found |
67-
/// | [`Forbidden`] | beneficiary is not allowed (usually means beneficiary is self) |
68-
/// | [`IllegalArgument`] | if the passed address buffer isn't valid, in memory, etc. |
69-
pub fn self_destruct(addr_off: *const u8, addr_len: u32) -> Result<()>;
62+
/// | Error | Reason |
63+
/// |-----------------------|-------------------------------------------|
64+
/// | [`IllegalOperation`] | the actor has unspent funds |
65+
/// | [`ReadOnly`] | the actor is executing in read-only mode |
66+
pub fn self_destruct(burn_funds: bool) -> Result<()>;
7067
}

testing/conformance/src/vm.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,8 @@ where
529529
self.0.current_balance()
530530
}
531531

532-
fn self_destruct(&mut self, beneficiary: &Address) -> Result<()> {
533-
self.0.self_destruct(beneficiary)
532+
fn self_destruct(&mut self, burn_unspent: bool) -> Result<()> {
533+
self.0.self_destruct(burn_unspent)
534534
}
535535
}
536536

testing/test_actors/actors/fil-readonly-actor/src/actor.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,10 @@ fn invoke_method(blk: u32, method: u64) -> u32 {
159159
assert_eq!(err, ErrorNumber::ReadOnly);
160160

161161
// Should not be able to delete self.
162-
let err =
163-
sdk::sself::self_destruct(&Address::new_id(sdk::message::origin())).unwrap_err();
164-
assert_eq!(err, ActorDeleteError::ReadOnly);
162+
assert_eq!(
163+
sdk::sself::self_destruct(true).unwrap_err(),
164+
ActorDeleteError::ReadOnly
165+
);
165166
}
166167
4 => {
167168
assert!(sdk::vm::read_only());

testing/test_actors/actors/fil-sself-actor/src/actor.rs

+7-21
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use cid::multihash::{Code, MultihashDigest};
44
use cid::Cid;
55
use fvm_ipld_encoding::{to_vec, DAG_CBOR};
66
use fvm_sdk as sdk;
7-
use fvm_shared::address::Address;
87
use fvm_shared::econ::TokenAmount;
98
use sdk::error::{ActorDeleteError, StateReadError, StateUpdateError};
109

@@ -31,21 +30,14 @@ pub fn invoke(_: u32) -> u32 {
3130
let balance = sdk::sself::current_balance();
3231
assert_eq!(TokenAmount::from_nano(1_000_000), balance);
3332

34-
// test that we can't destroy the calling actor when supplied beneficiary
35-
// address does not exist or when its itself
36-
//
37-
assert_eq!(
38-
sdk::sself::self_destruct(&Address::new_id(191919)),
39-
Err(ActorDeleteError::BeneficiaryDoesNotExist),
40-
);
33+
// Now destroy the actor without burning funds. This should fail because we have unspent funds.
4134
assert_eq!(
42-
sdk::sself::self_destruct(&Address::new_id(10000)),
43-
Err(ActorDeleteError::BeneficiaryIsSelf),
35+
sdk::sself::self_destruct(false).unwrap_err(),
36+
ActorDeleteError::UnspentFunds
4437
);
4538

46-
// now lets destroy the calling actor
47-
//
48-
sdk::sself::self_destruct(&Address::new_id(sdk::message::origin())).unwrap();
39+
// Now lets destroy the actor, burning the funds.
40+
sdk::sself::self_destruct(true).unwrap();
4941

5042
// test that root/set_root/self_destruct fail when the actor has been deleted
5143
// and balance is 0
@@ -56,14 +48,8 @@ pub fn invoke(_: u32) -> u32 {
5648
);
5749
assert_eq!(TokenAmount::from_nano(0), sdk::sself::current_balance());
5850

59-
// calling destroy on an already destroyed actor should succeed (since its
60-
// balance is 0)
61-
//
62-
// TODO (fridrik): we should consider changing this behaviour in the future
63-
// and disallow destroying actor with non-zero balance)
64-
//
65-
sdk::sself::self_destruct(&Address::new_id(sdk::message::origin()))
66-
.expect("deleting an already deleted actor should succeed since it has zero balance");
51+
// calling destroy on an already destroyed actor should succeed (no-op)
52+
sdk::sself::self_destruct(false).expect("deleting an already deleted actor should succeed");
6753

6854
#[cfg(coverage)]
6955
sdk::debug::store_artifact("sself_actor.profraw", minicov::capture_coverage());

0 commit comments

Comments
 (0)