use super::*;
use crate::prop::{
CollateralAdded,
MarginWithdrawn,
QuoteRepaidFromCollateral,
};
async fn earn_realised_profit(fixture: &mut PropFixture) -> u64 {
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
let sale = 3_000_000u64;
fixture
.rest_order(cost, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(cost, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture
.publish_base_price(3_000_000_000_000_000_000u128)
.await;
fixture
.rest_order(sale, quantity, fixture.collateral_asset, sale)
.await;
let sell = fixture.book_call(sale, quantity, fixture.base_asset, quantity);
let give_back = fixture.pool_call(
fn_selector!(return_quote()),
cost,
fixture.collateral_asset,
None,
);
fixture
.call_account(vec![sell, fixture.settle_book_call(), give_back], 2)
.await
.unwrap();
let profit = sale - cost;
assert_eq!(
fixture.account_balance(fixture.collateral_asset).await,
profit
);
assert_eq!(fixture.session().await.drawn_quote, 0);
profit
}
fn capitalise_call(fixture: &PropFixture, amount: u64) -> CallContractArg {
fixture.pool_call(
fn_selector!(add_collateral(ContractId)),
amount,
fixture.collateral_asset,
Some(call_data!(fixture.child_id)),
)
}
#[tokio::test]
async fn a_session_that_banked_its_profit_as_collateral_still_pays_the_share() {
let mut fixture = PropFixture::new().await;
let profit = earn_realised_profit(&mut fixture).await;
let capitalise = capitalise_call(&fixture, profit);
let response = fixture.call_account(vec![capitalise], 0).await.unwrap();
let added = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralAdded>(&response.tx_status.receipts)
.unwrap();
assert_eq!(added.len(), 1);
assert_eq!(added[0].capitalised_total, profit);
let banked = fixture.session().await;
assert_eq!(banked.collateral, COLLATERAL + profit);
assert_eq!(banked.capitalised, profit);
assert_eq!(banked.decapitalised, 0);
assert_eq!(
fixture.account_balance(fixture.collateral_asset).await,
0,
"the profit is funded collateral now, not cash on the account"
);
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let closed = fixture.closed_event(&fixture.close_session().await);
assert_eq!(closed.reason, SettlementReason::UserClose);
assert_eq!(closed.bad_debt, 0);
assert_eq!(
closed.platform_total,
profit / 10,
"the share is charged on the profit even though it now reads as collateral"
);
assert_eq!(closed.user_net, COLLATERAL + profit - profit / 10);
assert_eq!(
closed.payout_platform,
vec![(fixture.collateral_asset, profit / 10)]
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
(profit / 10) as u128
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- parent_before,
(COLLATERAL + profit - profit / 10) as u128
);
}
#[tokio::test]
async fn the_platforms_claim_can_never_outrun_the_basket_it_is_paid_from() {
let mut fixture = PropFixture::new().await;
let profit = earn_realised_profit(&mut fixture).await;
let capitalise = capitalise_call(&fixture, profit);
fixture.call_account(vec![capitalise], 0).await.unwrap();
assert_eq!(fixture.session().await.collateral, 3_000_000);
let burn = fixture.prolong_call(499);
fixture.call_account(vec![burn], 1).await.unwrap();
let starved = fixture.session().await;
assert_eq!(starved.fees_accrued, 2_994_000);
assert_eq!(starved.collateral, 3_000_000);
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let closed = fixture.closed_event(&fixture.close_session().await);
assert_eq!(
closed.platform_total, 6_000,
"the claim is capped by what there is, not by what is owed"
);
assert_eq!(closed.user_net, 0);
assert!(closed.payout_parent.is_empty());
assert_eq!(closed.bad_debt, 0, "an unpayable share is not bad debt");
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
6_000
);
}
#[tokio::test]
async fn withdrawing_capitalised_profit_charges_the_share_once_and_pays_it() {
let mut fixture = PropFixture::new().await;
let profit = earn_realised_profit(&mut fixture).await;
let capitalise = capitalise_call(&fixture, profit);
fixture.call_account(vec![capitalise], 0).await.unwrap();
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, profit)),
);
let response = fixture.call_account(vec![withdraw], 2).await.unwrap();
let events = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<MarginWithdrawn>(&response.tx_status.receipts)
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].from_account, 0);
assert_eq!(events[0].from_pool, profit);
assert_eq!(
events[0].from_capitalised, profit,
"the whole withdrawal is share-bearing profit"
);
assert_eq!(events[0].share_qty, profit / 10);
assert_eq!(events[0].capitalised_total, 0);
assert_eq!(events[0].new_collateral, COLLATERAL);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
(profit / 10) as u128,
"the share reaches the platform in the call that charges it"
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- parent_before,
(profit - profit / 10) as u128
);
let closed = fixture.closed_event(&fixture.close_session().await);
assert_eq!(
closed.platform_total, 0,
"settlement must not charge the same profit a second time"
);
assert_eq!(closed.user_net, COLLATERAL);
}
#[tokio::test]
async fn a_top_up_of_netted_principal_cancels_the_memo_instead_of_banking_it() {
let mut fixture = PropFixture::new().await;
let amount = 1_000_000u64;
fixture
.call_account(vec![fixture.draw_call(amount)], 1)
.await
.unwrap();
let net = fixture.repay_from_collateral_call(amount);
let response = fixture.call_account(vec![net], 0).await.unwrap();
let netted = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<QuoteRepaidFromCollateral>(&response.tx_status.receipts)
.unwrap();
assert_eq!(netted.len(), 1);
assert_eq!(netted[0].from_capitalised, 0);
assert_eq!(netted[0].decapitalised_total, amount);
let after_netting = fixture.session().await;
assert_eq!(after_netting.collateral, COLLATERAL - amount);
assert_eq!(after_netting.decapitalised, amount);
assert_eq!(
fixture.account_balance(fixture.collateral_asset).await,
amount
);
let top_up = capitalise_call(&fixture, amount);
let response = fixture.call_account(vec![top_up], 0).await.unwrap();
let added = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralAdded>(&response.tx_status.receipts)
.unwrap();
assert_eq!(added.len(), 1);
assert_eq!(
added[0].capitalised_total, 0,
"returned principal is not profit"
);
let restored = fixture.session().await;
assert_eq!(restored.collateral, COLLATERAL);
assert_eq!(restored.capitalised, 0);
assert_eq!(
restored.decapitalised, 0,
"the memo is cancelled, not left to subtract at settlement"
);
let closed = fixture.closed_event(&fixture.close_session().await);
assert_eq!(closed.platform_total, 0);
assert_eq!(closed.user_net, COLLATERAL);
assert_eq!(closed.bad_debt, 0);
}
async fn crash_a_maxed_long(fixture: &mut PropFixture) -> u64 {
let quantity = 5_000_000_000u64;
let cost = 2_000_000u64;
let drawn = 10_000_000u64;
fixture
.publish_base_price(2_000_000_000_000_000_000u128)
.await;
fixture
.rest_order(cost, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(drawn);
let buy = fixture.book_call(cost, quantity, fixture.collateral_asset, drawn);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture
.publish_base_price(100_000_000_000_000_000u128)
.await;
assert!(fixture.is_liquidatable().await);
let closed = fixture.closed_event(&fixture.liquidate().await);
assert_eq!(closed.reason, SettlementReason::Liquidation);
assert!(closed.bad_debt != 0, "the crash must outrun the collateral");
closed.bad_debt
}
#[tokio::test]
async fn bad_debt_accumulates_across_the_sessions_that_caused_it() {
let mut fixture = PropFixture::new().await;
assert_eq!(fixture.bad_debt().await, 0);
let first = crash_a_maxed_long(&mut fixture).await;
assert_eq!(fixture.bad_debt().await, first);
fixture
.account
.methods()
.start_session(TIER_ID, COLLATERAL, ProlongPeriod::SixHours)
.call_params(CallParameters::new(
COLLATERAL,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contract_ids(&[
fixture.deployment.oracle_id,
fixture.deployment.pool_id,
fixture.deployment.registry_id,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
assert_eq!(fixture.session().await.session_id, 2);
let second = crash_a_maxed_long(&mut fixture).await;
assert_eq!(
fixture.bad_debt().await,
first + second,
"the pool's cumulative loss is the sum of the losses that caused it"
);
}
#[tokio::test]
async fn a_forced_exit_reconciles_every_counterparty_to_the_coin() {
let mut fixture = PropFixture::new().await;
let liquidator = Identity::Address(fixture.outsider.address());
fixture
.deployment
.pool
.methods()
.set_liquidator(liquidator)
.call()
.await
.unwrap();
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(cost, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(3_000_000);
let buy = fixture.book_call(cost, quantity, fixture.collateral_asset, cost);
let park = fixture.book_call(
1_000_000,
1_000_000_000,
fixture.collateral_asset,
1_000_000,
);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call(), park], 2)
.await
.unwrap();
assert_eq!(fixture.account_balance(fixture.base_asset).await, quantity);
let provider = fixture.deployer.try_provider().unwrap().clone();
let holders = [
("pool", fixture.deployment.pool_id),
("account", fixture.child_id),
("book", fixture.order_book.contract_id),
];
let mut contract_before = Vec::new();
for (_, id) in holders {
contract_before.push((
provider
.get_contract_asset_balance(&id, &fixture.collateral_asset)
.await
.unwrap() as i128,
provider
.get_contract_asset_balance(&id, &fixture.base_asset)
.await
.unwrap() as i128,
));
}
let wallets = [
("parent", fixture.user.address()),
("platform", fixture.deployer.address()),
("liquidator", fixture.outsider.address()),
];
let mut wallet_before = Vec::new();
for (_, address) in wallets {
wallet_before.push((
provider
.get_asset_balance(&address, &fixture.collateral_asset)
.await
.unwrap() as i128,
provider
.get_asset_balance(&address, &fixture.base_asset)
.await
.unwrap() as i128,
));
}
let bad_debt_before = fixture.bad_debt().await;
let cleanup = crate::prop::PropOrderBookCleanup {
order_book: fixture.order_book.contract_id,
order_ids: {
let orders = fixture
.order_book
.order_book
.methods()
.get_orders_at_price(crate::order_book_deploy::Side::Buy, 1_000_000)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
orders
.into_iter()
.filter(|o| o.trader_id == Identity::ContractId(fixture.child_id))
.map(|o| o.order_id)
.collect()
},
};
assert_eq!(
cleanup.order_ids.len(),
1,
"the keeper has an order to cancel"
);
let session = fixture.session().await;
let latest = provider.latest_block_time().await.unwrap().unwrap();
let jump = session.expires_at.saturating_sub(latest.timestamp() as u64) + 1;
provider
.produce_blocks(
1,
Some(
latest
.checked_add_signed(chrono::TimeDelta::seconds(jump as i64))
.unwrap(),
),
)
.await
.unwrap();
let response = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.expire_session(fixture.child_id, vec![cleanup])
.with_contracts(&[
&fixture.account,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
let closed = fixture.closed_event(&response);
assert_eq!(closed.reason, SettlementReason::Expiry);
for (index, asset) in [fixture.collateral_asset, fixture.base_asset]
.into_iter()
.enumerate()
{
let mut total = 0i128;
for (position, (name, id)) in holders.into_iter().enumerate() {
let after = provider
.get_contract_asset_balance(&id, &asset)
.await
.unwrap() as i128;
let before = if index == 0 {
contract_before[position].0
} else {
contract_before[position].1
};
let delta = after - before;
println!(" {name}: {delta}");
total += delta;
}
for (position, (name, address)) in wallets.into_iter().enumerate() {
let after =
provider.get_asset_balance(&address, &asset).await.unwrap() as i128;
let before = if index == 0 {
wallet_before[position].0
} else {
wallet_before[position].1
};
let delta = after - before;
println!(" {name}: {delta}");
total += delta;
}
assert_eq!(
total, 0,
"settlement redistributes; it must not create or destroy any asset"
);
}
assert_eq!(fixture.account_balance(fixture.collateral_asset).await, 0);
assert_eq!(fixture.account_balance(fixture.base_asset).await, 0);
assert_eq!(
fixture.bad_debt().await - bad_debt_before,
closed.bad_debt,
"the running total moves by exactly what this close booked"
);
}