Skip to content

Commit

Permalink
CU-1yrzbe3 - Bonded finance beneficiary (#443)
Browse files Browse the repository at this point in the history
* refund remaining reward on offer cancellation

* remove unused errors and non required check

* introduce bond offer beneficiary

* introduce refund-on-cancel tests and refactor to use `composable-tests-helpers`

* add expected final asset owner tests

* update benchmarking to match new changes

* make sure cancellation is transactional
  • Loading branch information
hussein-aitlahcen authored Jan 5, 2022
1 parent 40c44c1 commit a150c48
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 64 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.

1 change: 1 addition & 0 deletions frame/bonded-finance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ proptest-derive = "0.3"
serde = { version = "1.0.124" }
orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "17a791edf431d7d7aee1ea3dfaeeb7bc21944301" }
pallet-vesting = { path = "../../frame/vesting" }
composable-tests-helpers = { path = "../composable-tests-helpers", default-features = false }

[features]
default = ["std"]
Expand Down
1 change: 1 addition & 0 deletions frame/bonded-finance/src/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ where
T: Config,
{
BondOffer {
beneficiary: whitelisted_caller(),
asset: bond_asset,
bond_price: BalanceOf::<T>::from(MIN_VESTED_TRANSFER),
maturity: BondDuration::Finite { return_in: BlockNumberOf::<T>::from(1u32) },
Expand Down
27 changes: 13 additions & 14 deletions frame/bonded-finance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ pub mod pallet {
<<T as Config>::Currency as FungiblesInspect<AccountIdOf<T>>>::Balance;
pub(crate) type NativeBalanceOf<T> =
<<T as Config>::NativeCurrency as FungibleInspect<AccountIdOf<T>>>::Balance;
pub(crate) type BondOfferOf<T> = BondOffer<AssetIdOf<T>, BalanceOf<T>, BlockNumberOf<T>>;
pub(crate) type BondOfferOf<T> =
BondOffer<AccountIdOf<T>, AssetIdOf<T>, BalanceOf<T>, BlockNumberOf<T>>;

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
Expand All @@ -101,10 +102,6 @@ pub mod pallet {
pub enum Error<T> {
/// The offer could not be found.
BondOfferNotFound,
/// Not enough native currency to create a new offer.
NotEnoughStake,
/// Not enough asset to bond.
NotEnoughAsset,
/// Someone tried to submit an invalid offer.
InvalidBondOffer,
/// Someone tried to bond an already completed offer.
Expand Down Expand Up @@ -224,8 +221,9 @@ pub mod pallet {
///
/// Emits a `OfferCancelled`.
#[pallet::weight(10_000)]
#[transactional]
pub fn cancel(origin: OriginFor<T>, offer_id: T::BondOfferId) -> DispatchResult {
let (issuer, _) = Self::get_offer(offer_id)?;
let (issuer, offer) = Self::get_offer(offer_id)?;
match (ensure_signed(origin.clone()), T::AdminOrigin::ensure_origin(origin)) {
// Continue on admin origin
(_, Ok(_)) => {},
Expand All @@ -238,6 +236,13 @@ pub mod pallet {
};
let offer_account = Self::account_id(offer_id);
T::NativeCurrency::transfer(&offer_account, &issuer, T::Stake::get(), true)?;
T::Currency::transfer(
offer.reward.asset,
&offer_account,
&issuer,
offer.reward.amount,
true,
)?;
BondOffers::<T>::remove(offer_id);
Self::deposit_event(Event::<T>::OfferCancelled { offer_id });
Ok(())
Expand Down Expand Up @@ -303,12 +308,6 @@ pub mod pallet {
// NOTE(hussein-aitlahcen): can't overflow, subsumed by `offer.valid()` in
// `do_offer`
let value = nb_of_bonds * offer.bond_price;
ensure!(
T::Currency::can_withdraw(offer.asset, from, value)
.into_result()
.is_ok(),
Error::<T>::NotEnoughAsset
);
let reward_share = T::Convert::convert(
multiply_by_rational(
T::Convert::convert(nb_of_bonds),
Expand All @@ -318,7 +317,7 @@ pub mod pallet {
.map_err(|_| ArithmeticError::Overflow)?,
);
let offer_account = Self::account_id(offer_id);
T::Currency::transfer(offer.asset, from, &offer_account, value, true)?;
T::Currency::transfer(offer.asset, from, &offer.beneficiary, value, true)?;
let current_block = frame_system::Pallet::<T>::current_block_number();
T::Vesting::vested_transfer(
offer.reward.asset,
Expand All @@ -335,7 +334,7 @@ pub mod pallet {
BondDuration::Finite { return_in } => {
T::Vesting::vested_transfer(
offer.asset,
&offer_account,
&offer.beneficiary,
from,
VestingSchedule {
start: current_block,
Expand Down
161 changes: 115 additions & 46 deletions frame/bonded-finance/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use super::*;
use crate::utils::MIN_VESTED_TRANSFER;
use composable_tests_helpers::{prop_assert_acceptable_computation_error, prop_assert_ok};
use composable_traits::bonded_finance::{BondDuration, BondOffer, BondOfferReward};
use frame_support::{
error::BadOrigin,
Expand All @@ -14,46 +15,11 @@ use frame_support::{
};
use mock::{Event, *};
use proptest::prelude::*;
use sp_runtime::helpers_128bit::multiply_by_rational;

macro_rules! prop_assert_epsilon {
($x:expr, $y:expr) => {{
let precision = 100;
let epsilon = 1;
let upper = precision + epsilon;
let lower = precision - epsilon;
let q = multiply_by_rational($x, precision, $y).expect("qed;");
prop_assert!(
upper >= q && q >= lower,
"({}) => {} >= {} * {} / {} >= {}",
q,
upper,
$x,
precision,
$y,
lower
);
}};
}

macro_rules! prop_assert_ok {
($cond:expr) => {
prop_assert_ok!($cond, concat!("assertion failed: ", stringify!($cond)))
};

($cond:expr, $($fmt:tt)*) => {
if let Err(e) = $cond {
let message = format!($($fmt)*);
let message = format!("Expected Ok(_), got {:?}, {} at {}:{}", e, message, file!(), line!());
return ::std::result::Result::Err(
proptest::test_runner::TestCaseError::fail(message));
}
};
}

#[test]
fn valid_offer() {
assert!(BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: MIN_VESTED_TRANSFER as _,
nb_of_bonds: 100_000_u128,
Expand All @@ -66,6 +32,7 @@ fn valid_offer() {
}
.valid(MinVestedTransfer::get() as _, MinReward::get()));
assert!(BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: MIN_VESTED_TRANSFER as _,
nb_of_bonds: 1_u128,
Expand All @@ -78,6 +45,7 @@ fn valid_offer() {
}
.valid(MinVestedTransfer::get() as _, MinReward::get()));
assert!(BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: 1_000_000 + MIN_VESTED_TRANSFER as u128,
nb_of_bonds: 100_000_u128,
Expand All @@ -95,6 +63,7 @@ fn valid_offer() {
fn invalid_offer() {
// invalid bond_price
assert!(!BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: MIN_VESTED_TRANSFER as u128 - 1,
nb_of_bonds: 100_000_u128,
Expand All @@ -109,6 +78,7 @@ fn invalid_offer() {

// invalid nb_of_bonds
assert!(!BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: MIN_VESTED_TRANSFER as _,
nb_of_bonds: 0,
Expand All @@ -123,6 +93,7 @@ fn invalid_offer() {

// invalid maturity
assert!(!BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: 1_000_000 + MIN_VESTED_TRANSFER as u128,
nb_of_bonds: 100_000_u128,
Expand All @@ -137,6 +108,7 @@ fn invalid_offer() {

// invalid reward
assert!(!BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: 1_000_000 + MIN_VESTED_TRANSFER as u128,
nb_of_bonds: 100_000_u128,
Expand All @@ -147,6 +119,7 @@ fn invalid_offer() {

// invalid reward: < MinVested
assert!(!BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: 1_000_000 + MIN_VESTED_TRANSFER as u128,
nb_of_bonds: 100_000_u128,
Expand All @@ -161,6 +134,7 @@ fn invalid_offer() {

// invalid reward maturity
assert!(!BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price: 1_000_000 + MIN_VESTED_TRANSFER as u128,
nb_of_bonds: 100_000_u128,
Expand All @@ -177,10 +151,10 @@ fn invalid_offer() {
prop_compose! {
// NOTE(hussein-aitlahcen): we use u32 before casting to avoid overflows
/// Pseudo random valid simple offer
fn simple_offer(min_contracts: Balance)
fn simple_offer(min_nb_of_bonds: Balance)
(
bond_price in MIN_VESTED_TRANSFER as u128..u32::MAX as Balance,
nb_of_bonds in min_contracts..u32::MAX as Balance,
nb_of_bonds in min_nb_of_bonds..u32::MAX as Balance,
maturity in prop_oneof![
Just(BondDuration::Infinite),
(1..BlockNumber::MAX / 2).prop_map(|return_in| BondDuration::Finite { return_in })
Expand All @@ -189,8 +163,9 @@ prop_compose! {
reward_amount in MIN_REWARD..Balance::MAX / 2,
reward_maturity in 1..BlockNumber::MAX / 2
)
-> BondOffer<MockCurrencyId, Balance, BlockNumber> {
-> BondOffer<AccountId, MockCurrencyId, Balance, BlockNumber> {
BondOffer {
beneficiary: ALICE,
asset: MockCurrencyId::BTC,
bond_price,
nb_of_bonds,
Expand Down Expand Up @@ -248,9 +223,101 @@ proptest! {
})?;
}

#[test]
fn cancel_refund_reward(offer in simple_offer(2)) {
ExtBuilder::build().execute_with(|| {
prop_assert_ok!(Tokens::mint_into(NATIVE_CURRENCY_ID, &ALICE, Stake::get()));
prop_assert_ok!(Tokens::mint_into(offer.reward.asset, &ALICE, offer.reward.amount));

prop_assert_eq!(Tokens::balance(offer.reward.asset, &ALICE), offer.reward.amount);
let offer_id = BondedFinance::do_offer(&ALICE, offer.clone());
prop_assert_ok!(offer_id);
let offer_id = offer_id.expect("impossible; qed");

// Bob bond and take half of the reward
let half_nb_of_bonds = offer.nb_of_bonds / 2;
let half_reward = offer.reward.amount / 2;
prop_assert_ok!(Tokens::mint_into(offer.asset, &BOB, half_nb_of_bonds * offer.bond_price));
prop_assert_ok!(BondedFinance::do_bond(offer_id, &BOB, half_nb_of_bonds));

// Alice cancel the offer
prop_assert_ok!(BondedFinance::cancel(Origin::signed(ALICE), offer_id));

// The remaining half is refunded to alice
prop_assert_acceptable_computation_error!(Tokens::balance(offer.reward.asset, &ALICE), half_reward);

Ok(())
})?;
}

#[test]
fn cancel_refund_stake(offer in simple_offer(1)) {
ExtBuilder::build().execute_with(|| {
prop_assert_ok!(Tokens::mint_into(NATIVE_CURRENCY_ID, &ALICE, Stake::get()));
prop_assert_ok!(Tokens::mint_into(offer.reward.asset, &ALICE, offer.reward.amount));

prop_assert_eq!(Tokens::balance(offer.reward.asset, &ALICE), offer.reward.amount);
let offer_id = BondedFinance::do_offer(&ALICE, offer.clone());
prop_assert_ok!(offer_id);
let offer_id = offer_id.expect("impossible; qed");

// Alice cancel the offer
prop_assert_ok!(BondedFinance::cancel(Origin::signed(ALICE), offer_id));

// The stake is refunded
prop_assert_eq!(Tokens::balance(NATIVE_CURRENCY_ID, &ALICE), Stake::get());

Ok(())
})?;
}

#[test]
fn expected_final_owner(offer in simple_offer(1)) {
ExtBuilder::build().execute_with(|| {
prop_assert_ok!(Tokens::mint_into(NATIVE_CURRENCY_ID, &ALICE, Stake::get()));
prop_assert_ok!(Tokens::mint_into(offer.reward.asset, &ALICE, offer.reward.amount));
let offer_id = BondedFinance::do_offer(&ALICE, offer.clone());
prop_assert_ok!(offer_id);
let offer_id = offer_id.expect("impossible; qed");

prop_assert_ok!(Tokens::mint_into(offer.asset, &BOB, offer.total_price().expect("impossible; qed;")));
prop_assert_ok!(BondedFinance::bond(Origin::signed(BOB), offer_id, offer.nb_of_bonds));
prop_assert_eq!(
BondedFinance::bond(Origin::signed(BOB), offer_id, offer.nb_of_bonds),
Err(Error::<Runtime>::OfferCompleted.into())
);


match offer.maturity {
BondDuration::Infinite => {
prop_assert_eq!(
Tokens::balance(offer.asset, &offer.beneficiary),
offer.total_price().expect("impossible; qed;")
);
}
BondDuration::Finite { return_in } => {
prop_assert_eq!(
Tokens::balance(offer.asset, &offer.beneficiary),
0
);
System::set_block_number(return_in);
prop_assert_ok!(Vesting::claim(Origin::signed(BOB), offer.asset));
prop_assert_eq!(
Tokens::balance(offer.asset, &BOB),
offer.total_price().expect("impossible; qed;")
);
}
}

Ok(())
})?;
}

#[test]
fn isolated_accounts(offer_a in simple_offer(1), offer_b in simple_offer(1)) {
ExtBuilder::build().execute_with(|| {
System::set_block_number(1);

prop_assert_ok!(Tokens::mint_into(NATIVE_CURRENCY_ID, &ALICE, Stake::get()));
prop_assert_ok!(Tokens::mint_into(offer_a.reward.asset, &ALICE, offer_a.reward.amount));
let offer_a_id = BondedFinance::do_offer(&ALICE, offer_a.clone());
Expand Down Expand Up @@ -321,21 +388,21 @@ proptest! {
prop_assert_ok!(offer_id);
let offer_id = offer_id.expect("impossible; qed");

let half_contracts = offer.nb_of_bonds / 2;
let half_nb_of_bonds = offer.nb_of_bonds / 2;
let half_reward = offer.reward.amount / 2;

prop_assert_ok!(Tokens::mint_into(offer.asset, &BOB, half_contracts * offer.bond_price));
let bob_reward = BondedFinance::do_bond(offer_id, &BOB, half_contracts);
prop_assert_ok!(Tokens::mint_into(offer.asset, &BOB, half_nb_of_bonds * offer.bond_price));
let bob_reward = BondedFinance::do_bond(offer_id, &BOB, half_nb_of_bonds);
prop_assert_ok!(bob_reward);
let bob_reward = bob_reward.expect("impossible; qed;");

prop_assert_ok!(Tokens::mint_into(offer.asset, &CHARLIE, half_contracts * offer.bond_price));
let charlie_reward = BondedFinance::do_bond(offer_id, &CHARLIE, half_contracts);
prop_assert_ok!(Tokens::mint_into(offer.asset, &CHARLIE, half_nb_of_bonds * offer.bond_price));
let charlie_reward = BondedFinance::do_bond(offer_id, &CHARLIE, half_nb_of_bonds);
prop_assert_ok!(charlie_reward);
let charlie_reward = charlie_reward.expect("impossible; qed;");

prop_assert_epsilon!(bob_reward, half_reward);
prop_assert_epsilon!(charlie_reward, half_reward);
prop_assert_acceptable_computation_error!(bob_reward, half_reward);
prop_assert_acceptable_computation_error!(charlie_reward, half_reward);

prop_assert!(Tokens::can_withdraw(offer.reward.asset, &BOB, bob_reward) == WithdrawConsequence::Frozen);
prop_assert!(Tokens::can_withdraw(offer.reward.asset, &CHARLIE, charlie_reward) == WithdrawConsequence::Frozen);
Expand Down Expand Up @@ -434,6 +501,7 @@ proptest! {

prop_assert_ok!(BondedFinance::cancel(Origin::signed(ALICE), offer_id));
prop_assert_eq!(Tokens::balance(NATIVE_CURRENCY_ID, &ALICE), Stake::get());
prop_assert_eq!(Tokens::balance(offer.reward.asset, &ALICE), offer.reward.amount);

prop_assert_eq!(
BondedFinance::bond(Origin::signed(BOB), offer_id, offer.nb_of_bonds),
Expand Down Expand Up @@ -464,6 +532,7 @@ proptest! {

prop_assert_ok!(BondedFinance::cancel(Origin::root(), offer_id));
prop_assert_eq!(Tokens::balance(NATIVE_CURRENCY_ID, &ALICE), Stake::get());
prop_assert_eq!(Tokens::balance(offer.reward.asset, &ALICE), offer.reward.amount);

prop_assert_eq!(
BondedFinance::bond(Origin::signed(BOB), offer_id, offer.nb_of_bonds),
Expand Down
Loading

0 comments on commit a150c48

Please sign in to comment.