Skip to content

Commit

Permalink
Multisig vesting unit tests (#295)
Browse files Browse the repository at this point in the history
* First vesting test
* add blake2b to test vm

Co-authored-by: zenground0 <ZenGround0@users.noreply.github.com>
  • Loading branch information
ZenGround0 and ZenGround0 authored May 1, 2022
1 parent b9ef2b6 commit d42dd40
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 18 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

286 changes: 283 additions & 3 deletions actors/multisig/tests/multisig_actor_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use fil_actors_runtime::test_utils::*;
use fil_actors_runtime::{INIT_ACTOR_ADDR, SYSTEM_ACTOR_ADDR};
use fvm_ipld_encoding::RawBytes;
use fvm_shared::address::{Address, BLS_PUB_LEN};
use fvm_shared::bigint::Zero;
use fvm_shared::clock::ChainEpoch;
use fvm_shared::econ::TokenAmount;
use fvm_shared::error::ExitCode;
use fvm_shared::METHOD_SEND;
Expand Down Expand Up @@ -113,7 +115,7 @@ mod constructor_tests {
let st: State = rt.get_state();
assert_eq!(params.signers, st.signers);
assert_eq!(params.num_approvals_threshold, st.num_approvals_threshold);
assert_eq!(TokenAmount::from(0u8), st.initial_balance);
assert_eq!(TokenAmount::zero(), st.initial_balance);
assert_eq!(100, st.unlock_duration);
assert_eq!(1234, st.start_epoch);
h.assert_transactions(&rt, vec![]);
Expand Down Expand Up @@ -205,7 +207,7 @@ mod constructor_tests {
anne_non_id,
METHOD_SEND,
RawBytes::default(),
TokenAmount::from(0u8),
TokenAmount::zero(),
RawBytes::default(),
ExitCode::OK,
);
Expand Down Expand Up @@ -265,6 +267,284 @@ mod constructor_tests {
}
}

#[cfg(test)]
mod vesting_tests {
use super::*;

const MSIG: Address = Address::new_id(1000);
const ANNE: Address = Address::new_id(101);
const BOB: Address = Address::new_id(102);
const CHARLIE: Address = Address::new_id(103);
const DARLENE: Address = Address::new_id(104);

const UNLOCK_DURATION: ChainEpoch = 10;
const START_EPOCH: ChainEpoch = 0;
const MSIG_INITIAL_BALANCE: u8 = 100;

#[test]
fn happy_path_full_vesting() {
let mut rt = construct_runtime(MSIG);
let h = util::ActorHarness::new();

rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE));
rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE));
h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]);
rt.set_received(TokenAmount::zero());

// anne proposes that darlene receive inital balance
rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE);
let proposal_hash = h.propose_ok(
&mut rt,
DARLENE,
TokenAmount::from(MSIG_INITIAL_BALANCE),
METHOD_SEND,
RawBytes::default(),
);

// bob approves anne's tx too soon
rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, BOB);
expect_abort(ExitCode::USR_INSUFFICIENT_FUNDS, h.approve(&mut rt, TxnID(0), proposal_hash));
rt.reset();

// advance the epoch s.t. all funds are unlocked
rt.set_epoch(START_EPOCH + UNLOCK_DURATION);
rt.expect_send(
DARLENE,
METHOD_SEND,
RawBytes::default(),
TokenAmount::from(MSIG_INITIAL_BALANCE),
RawBytes::default(),
ExitCode::OK,
);
assert_eq!(RawBytes::default(), h.approve_ok(&mut rt, TxnID(0), proposal_hash))

// h.check_state()
}

#[test]
fn partial_vesting_propose_to_send_half_the_actor_balance_when_the_epoch_is_half_the_unlock_duration(
) {
let mut rt = construct_runtime(MSIG);
let h = util::ActorHarness::new();

rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE));
rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE));
h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]);
rt.set_received(TokenAmount::zero());

rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE);
let proposal_hash = h.propose_ok(
&mut rt,
DARLENE,
TokenAmount::from(MSIG_INITIAL_BALANCE / 2),
METHOD_SEND,
RawBytes::default(),
);
rt.set_epoch(START_EPOCH + UNLOCK_DURATION / 2);
rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, BOB);
rt.expect_send(
DARLENE,
METHOD_SEND,
RawBytes::default(),
TokenAmount::from(MSIG_INITIAL_BALANCE / 2),
RawBytes::default(),
ExitCode::OK,
);
h.approve_ok(&mut rt, TxnID(0), proposal_hash);

// h.check_state()
}

#[test]
fn propose_and_autoapprove_tx_above_locked_amount_fails() {
let mut rt = construct_runtime(MSIG);
let h = util::ActorHarness::new();

rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE));
rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE));
h.construct_and_verify(&mut rt, 1, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]);
rt.set_received(TokenAmount::zero());

rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE);
expect_abort(
ExitCode::USR_INSUFFICIENT_FUNDS,
h.propose(
&mut rt,
DARLENE,
TokenAmount::from(MSIG_INITIAL_BALANCE),
METHOD_SEND,
RawBytes::default(),
),
);
rt.reset();
rt.set_epoch(START_EPOCH + UNLOCK_DURATION / 10);
let amount_out = TokenAmount::from(MSIG_INITIAL_BALANCE / 10);
rt.expect_send(
DARLENE,
METHOD_SEND,
RawBytes::default(),
amount_out.clone(),
RawBytes::default(),
ExitCode::OK,
);
h.propose_ok(&mut rt, DARLENE, amount_out, METHOD_SEND, RawBytes::default());

// h.check_state()
}

#[test]
fn fail_to_vest_more_than_locked_amount() {
let mut rt = construct_runtime(MSIG);
let h = util::ActorHarness::new();

rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE));
rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE));
h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]);
rt.set_received(TokenAmount::zero());

rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE);
let proposal_hash = h.propose_ok(
&mut rt,
DARLENE,
TokenAmount::from(MSIG_INITIAL_BALANCE / 2),
METHOD_SEND,
RawBytes::default(),
);
rt.set_epoch(START_EPOCH + UNLOCK_DURATION / 10);
rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, BOB);
expect_abort(ExitCode::USR_INSUFFICIENT_FUNDS, h.approve(&mut rt, TxnID(0), proposal_hash));
}

#[test]
fn avoid_truncating_division() {
let mut rt = construct_runtime(MSIG);
let h = util::ActorHarness::new();

let locked_balance = TokenAmount::from(UNLOCK_DURATION - 1); // balance < duration
let one = TokenAmount::from(1u8);
rt.set_balance(locked_balance.clone());
rt.set_received(locked_balance.clone());
h.construct_and_verify(&mut rt, 1, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]);
rt.set_received(TokenAmount::zero());

// expect nothing vested yet
rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE);
expect_abort(
ExitCode::USR_INSUFFICIENT_FUNDS,
h.propose(&mut rt, ANNE, one.clone(), METHOD_SEND, RawBytes::default()),
);
rt.reset();

// expect nothing ( (x-1/x) <1 unit) vested after 1 epoch
rt.set_epoch(START_EPOCH + 1);
expect_abort(
ExitCode::USR_INSUFFICIENT_FUNDS,
h.propose(&mut rt, ANNE, one.clone(), METHOD_SEND, RawBytes::default()),
);
rt.reset();

// expect 1 unit available after 2 epochs
rt.set_epoch(START_EPOCH + 2);
rt.expect_send(
ANNE,
METHOD_SEND,
RawBytes::default(),
one.clone(),
RawBytes::default(),
ExitCode::OK,
);
h.propose_ok(&mut rt, ANNE, one.clone(), METHOD_SEND, RawBytes::default());
rt.set_balance(locked_balance.clone());

// do not expect full vesting before full duration elapsed
rt.set_epoch(START_EPOCH + UNLOCK_DURATION - 1);
expect_abort(
ExitCode::USR_INSUFFICIENT_FUNDS,
h.propose(&mut rt, ANNE, locked_balance.clone(), METHOD_SEND, RawBytes::default()),
);
rt.reset();

// expect all but one unit available after all but one epochs
rt.expect_send(
ANNE,
METHOD_SEND,
RawBytes::default(),
locked_balance.clone() - one.clone(),
RawBytes::default(),
ExitCode::OK,
);
h.propose_ok(&mut rt, ANNE, locked_balance.clone() - one, METHOD_SEND, RawBytes::default());
rt.set_balance(locked_balance.clone());

// expect everything after exactly lock duration
rt.set_epoch(START_EPOCH + UNLOCK_DURATION);
rt.expect_send(
ANNE,
METHOD_SEND,
RawBytes::default(),
locked_balance.clone(),
RawBytes::default(),
ExitCode::OK,
);
h.propose_ok(&mut rt, ANNE, locked_balance, METHOD_SEND, RawBytes::default());
}

#[test]
fn sending_zero_ok_when_nothing_vests() {
let mut rt = construct_runtime(MSIG);
let h = util::ActorHarness::new();

rt.set_balance(TokenAmount::from(MSIG_INITIAL_BALANCE));
rt.set_received(TokenAmount::from(MSIG_INITIAL_BALANCE));
h.construct_and_verify(&mut rt, 2, UNLOCK_DURATION, START_EPOCH, vec![ANNE, BOB, CHARLIE]);
rt.set_received(TokenAmount::zero());

rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE);
rt.expect_send(
BOB,
METHOD_SEND,
RawBytes::default(),
TokenAmount::zero(),
RawBytes::default(),
ExitCode::OK,
);
}

#[test]
fn sending_zero_when_lockup_exceeds_balance() {
let mut rt = construct_runtime(MSIG);
let h = util::ActorHarness::new();

h.construct_and_verify(&mut rt, 1, 0, START_EPOCH, vec![ANNE]);
rt.set_caller(*MULTISIG_ACTOR_CODE_ID, MSIG);
rt.set_balance(TokenAmount::from(10u8));
rt.set_received(TokenAmount::from(10u8));

// lock up funds the actor doesn't have yet
h.lock_balance(&mut rt, START_EPOCH, UNLOCK_DURATION, TokenAmount::from(10u8)).unwrap();

// make a tx that transfers no value
let send_amount = TokenAmount::zero();
rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, ANNE);
rt.expect_send(
BOB,
METHOD_SEND,
RawBytes::default(),
send_amount.clone(),
RawBytes::default(),
ExitCode::OK,
);
h.propose_ok(&mut rt, BOB, send_amount, METHOD_SEND, RawBytes::default());

// verify that sending any value is prevented
let send_amount = TokenAmount::from(1u8);
expect_abort(
ExitCode::USR_INSUFFICIENT_FUNDS,
h.propose(&mut rt, BOB, send_amount, METHOD_SEND, RawBytes::default()),
)
}
}

// Propose

#[test]
Expand Down Expand Up @@ -676,7 +956,7 @@ fn test_approve_simple_propose_and_approval() {
let fake_params = RawBytes::from(vec![1, 2, 3, 4]);
let fake_method = 42;
let fake_ret = RawBytes::from(vec![4, 3, 2, 1]);
let send_value = TokenAmount::from(10u8);
let send_value = TokenAmount::zero();
rt.set_caller(*ACCOUNT_ACTOR_CODE_ID, anne);
let proposal_hash =
h.propose_ok(&mut rt, chuck, send_value.clone(), fake_method, fake_params.clone());
Expand Down
28 changes: 21 additions & 7 deletions actors/multisig/tests/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use fil_actor_multisig::{
compute_proposal_hash, Actor, AddSignerParams, ApproveReturn, ConstructorParams, Method,
ProposeParams, RemoveSignerParams, State, SwapSignerParams, Transaction, TxnID, TxnIDParams,
ProposeParams, ProposeReturn, RemoveSignerParams, State, SwapSignerParams, Transaction, TxnID,
TxnIDParams,
};
use fil_actor_multisig::{ChangeNumApprovalsThresholdParams, LockBalanceParams};
use fil_actors_runtime::test_utils::*;
Expand Down Expand Up @@ -94,12 +95,8 @@ impl ActorHarness {
method: MethodNum,
params: RawBytes,
) -> [u8; 32] {
rt.expect_validate_caller_type(vec![*ACCOUNT_ACTOR_CODE_ID, *MULTISIG_ACTOR_CODE_ID]);
let propose_params =
ProposeParams { to, value: value.clone(), method, params: params.clone() };
rt.call::<Actor>(Method::Propose as u64, &RawBytes::serialize(propose_params).unwrap())
.unwrap();
rt.verify();
let ret = self.propose(rt, to, value.clone(), method, params.clone());
ret.unwrap().deserialize::<ProposeReturn>().unwrap();
// compute proposal hash
let txn = Transaction { to, value, method, params, approved: vec![rt.caller] };
compute_proposal_hash(&txn, rt).unwrap()
Expand All @@ -119,6 +116,23 @@ impl ActorHarness {
approve_ret.ret
}

pub fn propose(
&self,
rt: &mut MockRuntime,
to: Address,
value: TokenAmount,
method: MethodNum,
params: RawBytes,
) -> Result<RawBytes, ActorError> {
rt.expect_validate_caller_type(vec![*ACCOUNT_ACTOR_CODE_ID, *MULTISIG_ACTOR_CODE_ID]);
let propose_params =
ProposeParams { to, value: value.clone(), method, params: params.clone() };
let ret =
rt.call::<Actor>(Method::Propose as u64, &RawBytes::serialize(propose_params).unwrap());
rt.verify();
ret
}

pub fn approve(
&self,
rt: &mut MockRuntime,
Expand Down
1 change: 1 addition & 0 deletions test_vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ cid = { version = "0.8.3", default-features = false, features = ["serde-codec"]
serde = { version = "1.0.136", features = ["derive"] }
thiserror = "1.0.30"
anyhow = "1.0.56"
blake2b_simd = "1.0"


Loading

0 comments on commit d42dd40

Please sign in to comment.