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

CU-1yrzbe3 - Bonded finance beneficiary #443

Merged
merged 7 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
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
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