use super::*;
use crate::prop::{
CollateralForfeited,
RolloverWithdrawalRecorded,
SessionRolledOver,
TierEnabledChanged,
TierVersionPublishedV2,
TurboTerms,
};
const TURBO_TIER_ID: u64 = 7;
const TURBO_DEPOSIT: u64 = 250_000;
const TURBO_FEE: u64 = 25_000;
const TURBO_ENTRY: u64 = TURBO_DEPOSIT + TURBO_FEE;
const ROLLOVER_BAR: u64 = 900_000;
fn turbo_tier_params() -> TierParams {
turbo_tier_params_for(LINE)
}
fn turbo_tier_params_for(line: u64) -> TierParams {
TierParams {
line,
leverage: 40,
duration: 0,
maintenance_bps: 100,
open_buffer_bps: 101,
liq_price_factor: 9_900,
prolong_fee: [0, 0, 0, 0],
max_credit_line_bps: 20_000,
max_price_age: u64::MAX,
open_fee: line / 400,
profit_share_bps: 0,
price_band_bps: 1_000,
}
}
fn turbo_terms(max_loss_bps: [u64; 4]) -> TurboTerms {
TurboTerms {
forfeit_collateral: true,
max_loss_bps,
rollover_profit_bps: 900,
max_rollovers: 3,
}
}
const DRAWDOWN_SCHEDULE: [u64; 4] = [150, 200, 250, 300];
async fn publish_turbo_tier(fixture: &PropFixture, terms: TurboTerms) -> u32 {
let response = fixture
.deployment
.pool
.methods()
.publish_tier_v2(
TURBO_TIER_ID,
turbo_tier_params(),
terms.clone(),
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap();
let version = response.value;
let published = response
.decode_logs_with_type::<TierVersionPublishedV2>()
.unwrap();
assert_eq!(published.len(), 1);
assert_eq!(published[0].tier_id, TURBO_TIER_ID);
assert_eq!(published[0].version, version);
assert_eq!(published[0].terms, terms);
assert_eq!(published[0].rollover_window_seconds, 21_600);
assert_eq!(published[0].threshold_floor, 9_850_000);
assert!(
response
.decode_logs_with_type::<TierVersionPublished>()
.unwrap()
.is_empty()
);
version
}
async fn open_turbo_session(fixture: &PropFixture) -> SessionOpened {
fixture.close_session().await;
start_turbo_session(fixture).await
}
async fn start_turbo_session(fixture: &PropFixture) -> SessionOpened {
start_turbo_session_paying(fixture, TURBO_ENTRY).await
}
async fn start_turbo_session_paying(fixture: &PropFixture, entry: u64) -> SessionOpened {
let response = fixture
.account
.methods()
.start_session(TURBO_TIER_ID, entry, ProlongPeriod::Week)
.call_params(CallParameters::new(
entry,
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::Exactly(1))
.call()
.await
.unwrap();
let opened = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionOpened>(&response.tx_status.receipts)
.unwrap();
assert_eq!(opened.len(), 1);
opened.into_iter().next().unwrap()
}
async fn refresh_session_key(fixture: &PropFixture) {
let chain = fixture
.user
.try_provider()
.unwrap()
.latest_block_time()
.await
.unwrap()
.map(|time| time.timestamp() as u64)
.unwrap_or(0);
fixture
.account
.methods()
.set_session(ParallelSessionArgs {
nonce: U256::zero(),
session_id: Identity::Address(fixture.user.address()),
expiry: Time {
unix: chain + 86_400,
},
contract_ids: vec![],
})
.with_contract_ids(&[fixture.deployment.oracle_id])
.call()
.await
.unwrap();
}
async fn advance_past_expiry(fixture: &PropFixture) {
let session = fixture.session().await;
let provider = fixture.outsider.try_provider().unwrap();
let latest = provider
.latest_block_time()
.await
.unwrap()
.expect("local chain has a latest block time");
let seconds = session.expires_at.saturating_sub(latest.timestamp() as u64) + 1;
let target = latest
.checked_add_signed(chrono::TimeDelta::seconds(seconds as i64))
.expect("test expiry timestamp is representable");
provider.produce_blocks(1, Some(target)).await.unwrap();
}
async fn try_expire(fixture: &PropFixture) -> Result<CallResponse<()>> {
Ok(fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.expire_session(fixture.child_id, vec![])
.with_contracts(&[
&fixture.account,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await?)
}
async fn try_rollover(fixture: &PropFixture) -> Result<CallResponse<()>> {
Ok(fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.rollover_session(fixture.child_id)
.with_contracts(&[
&fixture.account,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.call()
.await?)
}
async fn rollover_view(fixture: &PropFixture) -> crate::prop::RolloverView {
fixture
.deployment
.pool
.methods()
.get_rollover(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.unwrap()
}
#[tokio::test]
async fn a_turbo_publication_is_atomic_and_refuses_incoherent_terms_whole() {
let fixture = PropFixture::new().await;
let pool = fixture.deployment.pool.clone();
let before = pool
.methods()
.current_tier_version(TURBO_TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(before, None, "the turbo tier does not exist yet");
let mut anchorless = turbo_terms(DRAWDOWN_SCHEDULE);
anchorless.rollover_profit_bps = 0;
anchorless.max_rollovers = 0;
let error = pool
.methods()
.publish_tier_v2(
TURBO_TIER_ID,
turbo_tier_params(),
anchorless,
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(error.to_string().contains("InvalidTurboTerms"), "{error:#}");
for (schedule, what) in [
([0, 200, 250, 300], "a dead first step"),
([150, 0, 250, 300], "a dead middle step"),
([150, 200, 250, 0], "a dead last step"),
([150, 100, 250, 300], "a step that narrows"),
] {
let mut bad = turbo_terms(DRAWDOWN_SCHEDULE);
bad.max_loss_bps = schedule;
let error = pool
.methods()
.publish_tier_v2(
TURBO_TIER_ID,
turbo_tier_params(),
bad,
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidTurboTerms"),
"{what} must be refused: {error:#}"
);
}
let mut short = turbo_terms([150, 200, 0, 0]);
short.max_rollovers = 1;
pool.methods()
.publish_tier_v2(
TURBO_TIER_ID + 1,
turbo_tier_params(),
short,
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.expect("only reachable steps are policed");
let error = pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.publish_tier_v2(
TURBO_TIER_ID,
turbo_tier_params(),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AccessControlNotAuthorized"),
"{error:#}"
);
let after = pool
.methods()
.current_tier_version(TURBO_TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(after, None, "a refused publication must leave no trace");
let version = publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
let stored = pool
.methods()
.get_turbo_terms(TURBO_TIER_ID, version)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(stored, Some(turbo_terms(DRAWDOWN_SCHEDULE)));
assert_eq!(
pool.methods()
.get_turbo_terms(TIER_ID, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
None,
"a legacy publication never carries terms"
);
assert_eq!(
pool.methods()
.session_turbo_terms(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
None,
);
}
#[tokio::test]
async fn switching_an_old_tier_off_stops_its_sales_and_strands_no_session() {
let mut fixture = PropFixture::new().await;
let pool = fixture.deployment.pool.clone();
assert!(
pool.methods()
.is_tier_enabled(TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
let error = pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.set_tier_enabled(TIER_ID, false)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AccessControlNotAuthorized"),
"{error:#}"
);
let response = pool
.methods()
.set_tier_enabled(TIER_ID, false)
.call()
.await
.unwrap();
let changed = response
.decode_logs_with_type::<TierEnabledChanged>()
.unwrap();
assert_eq!(changed.len(), 1);
assert_eq!(changed[0].tier_id, TIER_ID);
assert!(!changed[0].enabled);
assert!(
!pool
.methods()
.is_tier_enabled(TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
assert_eq!(
pool.methods()
.session_turbo_terms(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
None,
);
let response = fixture
.call_account(vec![fixture.prolong_call(1)], 1)
.await
.unwrap();
let prolonged = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionProlonged>(&response.tx_status.receipts)
.unwrap();
assert_eq!(prolonged.len(), 1);
assert_eq!(prolonged[0].fee, 6_000);
fixture.close_session().await;
let error = fixture
.account
.methods()
.start_session(TIER_ID, COLLATERAL, ProlongPeriod::SixHours)
.call_params(CallParameters::new(
COLLATERAL,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.deployment.registry,
])
.call()
.await
.unwrap_err();
assert!(error.to_string().contains("TierDisabled"), "{error:#}");
let opened = start_turbo_session(&fixture).await;
assert_eq!(opened.tier_id, TURBO_TIER_ID);
assert_eq!(
pool.methods()
.session_turbo_terms(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(turbo_terms(DRAWDOWN_SCHEDULE)),
);
pool.methods()
.set_tier_enabled(TIER_ID, true)
.call()
.await
.unwrap();
assert!(
pool.methods()
.is_tier_enabled(TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
#[tokio::test]
async fn a_turbo_entry_is_forfeited_whole_and_the_walkaway_is_profits_only() {
let fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms([0, 0, 0, 0])).await;
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let opened = open_turbo_session(&fixture).await;
assert_eq!(opened.collateral, TURBO_ENTRY);
assert_eq!(opened.open_fee, TURBO_FEE);
assert_eq!(opened.fees_accrued, TURBO_FEE);
assert_eq!(opened.credit_line, LINE);
let view = rollover_view(&fixture).await;
assert_eq!(view.anchor, LINE);
assert_eq!(view.rollovers_used, 0);
assert_eq!(view.max_rollovers, 3);
assert_eq!(view.profit_required, ROLLOVER_BAR);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
platform_before + TURBO_FEE as u128
);
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let response = fixture.close_session().await;
let closed = fixture.closed_event(&response);
assert_eq!(closed.user_net, 0);
assert!(closed.payout_parent.is_empty());
let forfeited = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralForfeited>(&response.tx_status.receipts)
.unwrap();
assert_eq!(forfeited.len(), 1);
assert_eq!(forfeited[0].amount, TURBO_DEPOSIT);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
platform_before + TURBO_ENTRY as u128,
"the platform's whole take is the price: fee at the door, deposit at the close"
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
parent_before,
"a break-even prepaid user gets exactly nothing back"
);
}
#[tokio::test]
async fn a_round_tripped_deposit_is_still_forfeited_whole() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms([0, 0, 0, 0])).await;
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
open_turbo_session(&fixture).await;
let draw = fixture.draw_call(TURBO_DEPOSIT);
fixture.call_account(vec![draw], 1).await.unwrap();
let repay = fixture.repay_from_collateral_call(TURBO_DEPOSIT);
fixture.call_account(vec![repay], 0).await.unwrap();
assert_eq!(
fixture.session().await.collateral,
TURBO_FEE,
"the netting drove funded collateral down to the accrued fee alone"
);
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let response = fixture.close_session().await;
let closed = fixture.closed_event(&response);
assert_eq!(closed.user_net, 0);
assert!(closed.payout_parent.is_empty());
let forfeited = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralForfeited>(&response.tx_status.receipts)
.unwrap();
assert_eq!(forfeited.len(), 1);
assert_eq!(
forfeited[0].amount, TURBO_DEPOSIT,
"the round-tripped deposit is forfeited in full, not handed back"
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
platform_before + TURBO_ENTRY as u128,
"the platform's take is the whole price despite the round-trip"
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
parent_before,
"the round-trip returns the user nothing"
);
}
#[tokio::test]
async fn no_one_may_top_up_a_turbo_session_but_a_legacy_one_still_can() {
let fixture = PropFixture::new().await;
fixture.fund_collateral(100_000).await;
assert_eq!(fixture.session().await.collateral, COLLATERAL + 100_000);
publish_turbo_tier(&fixture, turbo_terms([0, 0, 0, 0])).await;
open_turbo_session(&fixture).await;
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.add_collateral(fixture.child_id)
.call_params(CallParameters::new(
100_000,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("TopUpsDisabled"),
"a prepaid stake cannot be topped up, by anyone: {error:#}"
);
}
#[tokio::test]
async fn a_flat_turbo_session_earns_nothing_and_expires_into_forfeit() {
let fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms([0, 0, 0, 0])).await;
open_turbo_session(&fixture).await;
let error = try_rollover(&fixture).await.unwrap_err();
assert!(error.to_string().contains("RolloverNotDue"), "{error:#}");
advance_past_expiry(&fixture).await;
let error = try_rollover(&fixture).await.unwrap_err();
assert!(error.to_string().contains("RolloverNotEarned"), "{error:#}");
let response = try_expire(&fixture).await.unwrap();
let closed = fixture.closed_event(&response);
assert!(matches!(closed.reason, SettlementReason::Expiry));
let forfeited = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralForfeited>(&response.tx_status.receipts)
.unwrap();
assert_eq!(forfeited.len(), 1);
assert_eq!(forfeited[0].amount, TURBO_DEPOSIT);
}
#[tokio::test]
async fn a_nine_percent_winner_is_rolled_by_the_expiry_trigger() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture
.publish_base_price(4_000_000_000_000_000_000u128)
.await;
assert_eq!(fixture.session_value().await, 12_000_000);
let expires_before = fixture.session().await.expires_at;
advance_past_expiry(&fixture).await;
let response = try_expire(&fixture).await.unwrap();
let rolled = response
.decode_logs_with_type::<SessionRolledOver>()
.unwrap();
assert_eq!(rolled.len(), 1);
assert_eq!(rolled[0].rollovers_used, 1);
assert_eq!(rolled[0].max_rollovers, 3);
assert_eq!(rolled[0].anchor, 12_000_000);
assert_eq!(rolled[0].profit_required, ROLLOVER_BAR);
assert_eq!(rolled[0].new_expires_at, expires_before + 604_800);
assert_eq!(fixture.session().await.expires_at, expires_before + 604_800);
fixture
.publish_base_price(2_000_000_000_000_000_000u128)
.await;
assert_eq!(fixture.session_value().await, 10_000_000);
let lines = fixture.lines().await;
assert_eq!(lines.threshold, U256::from(9_820_000u64));
assert!(
!fixture.is_liquidatable().await,
"giving back the gain is not a loss of principal"
);
fixture
.publish_base_price(1_800_000_000_000_000_000u128)
.await;
assert_eq!(fixture.session_value().await, 9_800_000);
assert_eq!(fixture.lines().await.threshold, U256::from(9_818_000u64));
assert!(fixture.is_liquidatable().await);
let closed = fixture.closed_event(&fixture.liquidate().await);
assert!(matches!(closed.reason, SettlementReason::Liquidation));
assert!(
closed.payout_parent.is_empty(),
"the forfeited stake absorbed the loss and the rest went to the platform"
);
}
#[tokio::test]
async fn withdrawing_banked_profit_leaves_the_anchor_fixed() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms([0, 0, 0, 0])).await;
open_turbo_session(&fixture).await;
let banked = 100_000u64;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
banked,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let withdrawable = banked - TURBO_FEE;
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, withdrawable)),
);
let response = fixture.call_account(vec![withdraw], 2).await.unwrap();
let recorded = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<RolloverWithdrawalRecorded>(&response.tx_status.receipts)
.unwrap();
assert_eq!(recorded.len(), 1);
assert_eq!(recorded[0].amount, withdrawable);
assert_eq!(recorded[0].withdrawn_total, withdrawable);
let view = rollover_view(&fixture).await;
assert_eq!(view.anchor, LINE);
assert_eq!(view.withdrawn, withdrawable);
}
#[tokio::test]
async fn a_tier_can_sell_a_drawdown_wider_than_the_pools_principal() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms([300, 300, 300, 300])).await;
open_turbo_session(&fixture).await;
let k = LINE - LINE / 40;
let threshold = fixture.lines().await.threshold;
assert_eq!(threshold, U256::from(LINE - LINE * 300 / 10_000));
assert!(
threshold < U256::from(k),
"the sold drawdown reaches past k"
);
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture
.publish_base_price(1_500_000_000_000_000_000u128)
.await;
assert!(fixture.is_liquidatable().await);
let bad_debt_before = fixture.bad_debt().await;
let closed = fixture.closed_event(&fixture.liquidate().await);
assert!(matches!(closed.reason, SettlementReason::Liquidation));
assert!(
closed.bad_debt != 0,
"a liquidation below the principal must book the shortfall"
);
assert_eq!(fixture.bad_debt().await, bad_debt_before + closed.bad_debt);
}
#[tokio::test]
async fn a_banked_week_still_counts_toward_a_target_that_never_moves() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
assert_eq!(
fixture
.deployment
.pool
.methods()
.session_turbo_terms(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(turbo_terms(DRAWDOWN_SCHEDULE)),
);
let opening = rollover_view(&fixture).await;
assert_eq!(opening.anchor, LINE);
assert_eq!(opening.profit_required, ROLLOVER_BAR);
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture
.publish_base_price(4_000_000_000_000_000_000u128)
.await;
assert_eq!(fixture.session_value().await, 12_000_000);
fixture
.rest_order(4_000_000, quantity, fixture.collateral_asset, 4_000_000)
.await;
let sell = fixture.book_call(4_000_000, quantity, fixture.base_asset, quantity);
let return_quote = fixture.pool_call(
fn_selector!(return_quote()),
cost,
fixture.collateral_asset,
None,
);
fixture
.call_account(vec![sell, fixture.settle_book_call(), return_quote], 2)
.await
.unwrap();
let banked = fixture.account_balance(fixture.collateral_asset).await
- fixture.session().await.fees_accrued;
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, banked)),
);
fixture.call_account(vec![withdraw], 2).await.unwrap();
let nav = fixture.session_value().await;
assert!(
nav < (LINE + ROLLOVER_BAR) as i128,
"the banked user's NAV is back near the line: {nav}"
);
let banked_view = rollover_view(&fixture).await;
assert_eq!(
banked_view.profit_required, ROLLOVER_BAR,
"the weekly target is a flat share of the account size, never re-based"
);
assert_eq!(banked_view.anchor, LINE);
assert_eq!(banked_view.withdrawn, banked);
assert_eq!(
nav + banked_view.withdrawn as i128 - banked_view.anchor as i128,
2_000_000
);
advance_past_expiry(&fixture).await;
let response = try_expire(&fixture).await.unwrap();
let rolled = response
.decode_logs_with_type::<SessionRolledOver>()
.unwrap();
assert_eq!(rolled.len(), 1);
assert_eq!(rolled[0].rollovers_used, 1);
assert_eq!(rolled[0].profit_required, ROLLOVER_BAR);
assert_eq!(rolled[0].anchor as i128, nav);
assert_eq!(rollover_view(&fixture).await.profit_required, ROLLOVER_BAR);
}
#[tokio::test]
async fn a_partial_withdrawal_still_earns_the_rollover_on_the_sum() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
assert_eq!(rollover_view(&fixture).await.anchor, LINE);
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, 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;
assert_eq!(fixture.session_value().await, 11_000_000);
fixture
.rest_order(3_000_000, quantity, fixture.collateral_asset, 3_000_000)
.await;
let sell = fixture.book_call(3_000_000, quantity, fixture.base_asset, quantity);
let return_quote = fixture.pool_call(
fn_selector!(return_quote()),
cost,
fixture.collateral_asset,
None,
);
fixture
.call_account(vec![sell, fixture.settle_book_call(), return_quote], 2)
.await
.unwrap();
let taken = 500_000u64;
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, taken)),
);
fixture.call_account(vec![withdraw], 2).await.unwrap();
let nav = fixture.session_value().await;
assert_eq!(nav, 10_500_000);
let view = rollover_view(&fixture).await;
assert_eq!(view.anchor, LINE);
assert_eq!(view.withdrawn, taken);
let on_desk = nav - LINE as i128;
assert!(
on_desk < ROLLOVER_BAR as i128 && (view.withdrawn as i128) < ROLLOVER_BAR as i128,
"neither half alone clears the bar: on_desk={on_desk}, withdrawn={}",
view.withdrawn
);
assert_eq!(on_desk + view.withdrawn as i128, 1_000_000);
advance_past_expiry(&fixture).await;
let response = try_expire(&fixture).await.unwrap();
let rolled = response
.decode_logs_with_type::<SessionRolledOver>()
.unwrap();
assert_eq!(rolled.len(), 1);
assert_eq!(rolled[0].rollovers_used, 1);
assert_eq!(rolled[0].anchor as i128, nav);
assert_eq!(rollover_view(&fixture).await.withdrawn, 0);
}
#[tokio::test]
async fn a_turbo_account_sells_exactly_seven_days() {
let fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
fixture.close_session().await;
for period in [
ProlongPeriod::SixHours,
ProlongPeriod::Day,
ProlongPeriod::Month,
] {
let error = fixture
.account
.methods()
.start_session(TURBO_TIER_ID, TURBO_ENTRY, period.clone())
.call_params(CallParameters::new(
TURBO_ENTRY,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.deployment.registry,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("PeriodNotOffered"),
"{period:?} must be refused by name, not by price: {error:#}"
);
}
let opened = start_turbo_session(&fixture).await;
assert_eq!(
opened.expires_at - opened.started_at,
604_800,
"the week is the whole term - the tier's base duration is pinned to zero"
);
}
#[tokio::test]
async fn one_payment_buys_at_most_four_weeks_and_the_last_profit_goes_home() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
let mut price = 2_000_000_000_000_000_000u128;
for week in 1u64..=3 {
price += 1_000_000_000_000_000_000;
fixture.publish_base_price(price).await;
advance_past_expiry(&fixture).await;
let expires_before = fixture.session().await.expires_at;
let response = try_expire(&fixture).await.unwrap();
let rolled = response
.decode_logs_with_type::<SessionRolledOver>()
.unwrap();
assert_eq!(rolled.len(), 1);
assert_eq!(rolled[0].rollovers_used, week, "free week number {week}");
assert_eq!(rolled[0].new_expires_at, expires_before + 604_800);
let anchor = rolled[0].anchor;
assert!(
anchor > LINE,
"a winner's anchor climbs with him: {anchor} at week {}",
week + 1
);
let allowance = LINE * DRAWDOWN_SCHEDULE[week as usize] / 10_000;
let basket_value = (price / 1_000_000_000_000) as u64;
let absorption = basket_value / 100;
let lines = fixture.lines().await;
assert_eq!(
lines.threshold,
U256::from(LINE - allowance + absorption),
"week {}'s drawdown is {} bps of the LINE, not of the anchor \
the roll just set",
week + 1,
DRAWDOWN_SCHEDULE[week as usize],
);
}
fixture.publish_base_price(6_500_000_000_000_000_000).await;
advance_past_expiry(&fixture).await;
let error = try_rollover(&fixture).await.unwrap_err();
assert!(
error.to_string().contains("RolloverExhausted"),
"three free weeks is the whole grant: {error:#}"
);
let response = try_expire(&fixture).await.unwrap();
let closed = fixture.closed_event(&response);
assert!(matches!(closed.reason, SettlementReason::Expiry));
assert!(
closed.user_net != 0,
"the month's profit goes home with the user"
);
assert!(!closed.payout_parent.is_empty());
let forfeited = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralForfeited>(&response.tx_status.receipts)
.unwrap();
assert_eq!(forfeited.len(), 1);
assert_eq!(forfeited[0].amount, TURBO_DEPOSIT);
}
#[tokio::test]
async fn the_first_week_liquidates_at_the_sold_drawdown() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture.publish_base_price(1_800_000_000_000_000_000).await;
let lines = fixture.lines().await;
assert_eq!(lines.threshold, U256::from(9_868_000u64));
assert_eq!(fixture.session_value().await, 9_800_000);
assert!(fixture.is_liquidatable().await);
let response = fixture.liquidate().await;
let closed = fixture.closed_event(&response);
assert!(matches!(closed.reason, SettlementReason::Liquidation));
let forfeited = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralForfeited>(&response.tx_status.receipts)
.unwrap();
assert_eq!(forfeited.len(), 1);
assert_eq!(forfeited[0].amount, 32_000);
assert!(closed.payout_parent.is_empty());
assert_eq!(closed.user_net, 0);
}
#[tokio::test]
async fn every_price_point_forfeits_its_own_deposit() {
let fixture = PropFixture::new().await;
fixture.close_session().await;
for (tier_id, line) in [(8u64, 5_000_000u64), (9u64, 2_500_000u64)] {
let deposit = line / 40;
let fee = line / 400;
let price = deposit + fee;
fixture
.deployment
.pool
.methods()
.publish_tier_v2(
tier_id,
turbo_tier_params_for(line),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.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();
fixture
.account
.methods()
.start_session(tier_id, price, ProlongPeriod::Week)
.call_params(CallParameters::new(
price,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.deployment.registry,
])
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
let response = fixture.close_session().await;
let forfeited = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralForfeited>(&response.tx_status.receipts)
.unwrap();
assert_eq!(forfeited.len(), 1, "line {line}");
assert_eq!(forfeited[0].amount, deposit, "line {line}");
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
platform_before + price as u128,
"line {line}: the platform's whole take is the price"
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
parent_before - price as u128,
"line {line}: a break-even user is out exactly the price"
);
}
}
#[tokio::test]
async fn profits_withdraw_only_once_positions_are_closed() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let banked = 100_000u64;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
banked,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let quantity = 500_000_000u64;
let cost = 1_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
let withdrawable = banked - TURBO_FEE;
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, withdrawable)),
);
let error = fixture
.call_account(vec![withdraw.clone()], 2)
.await
.unwrap_err();
assert!(
error.to_string().contains("QuoteOutstanding"),
"an open position blocks the withdrawal: {error:#}"
);
fixture
.rest_order(2_000_000, quantity, fixture.collateral_asset, cost)
.await;
let sell = fixture.book_call(2_000_000, quantity, fixture.base_asset, quantity);
let return_quote = fixture.pool_call(
fn_selector!(return_quote()),
cost,
fixture.collateral_asset,
None,
);
fixture
.call_account(vec![sell, fixture.settle_book_call(), return_quote], 2)
.await
.unwrap();
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
fixture.call_account(vec![withdraw], 2).await.unwrap();
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
parent_before + withdrawable as u128,
"flat, the profit is the user's immediately"
);
}
#[tokio::test]
async fn a_rollover_keeps_the_users_resting_orders() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture.publish_base_price(4_000_000_000_000_000_000).await;
let resting = quantity / 2;
let sell = fixture.book_call(8_000_000, resting, fixture.base_asset, resting);
fixture.call_account(vec![sell], 1).await.unwrap();
let (base_on_book, _) = fixture
.order_book
.order_book
.methods()
.get_total_balance_of(Identity::ContractId(fixture.child_id))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(base_on_book, resting);
let session_before = fixture.session().await.session_id;
advance_past_expiry(&fixture).await;
try_rollover(&fixture).await.unwrap();
let (base_after, _) = fixture
.order_book
.order_book
.methods()
.get_total_balance_of(Identity::ContractId(fixture.child_id))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(
base_after, resting,
"a session that keeps its seat keeps its book"
);
assert_eq!(
fixture.session().await.session_id,
session_before,
"same session, alive"
);
}
#[tokio::test]
async fn a_repurchase_discount_prices_the_next_entry_fee_down() {
let fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
fixture.close_session().await;
let manager = Identity::Address(fixture.deployer.address());
fixture
.deployment
.pool
.methods()
.grant_role(DISCOUNT_MANAGER_ROLE, manager)
.call()
.await
.unwrap();
fixture
.deployment
.pool
.methods()
.set_user_discount(
Identity::Address(fixture.user.address()),
None,
Some(UserDiscount {
discount_bps: 2_000,
scope: DiscountScope::FirstPremium,
}),
)
.call()
.await
.unwrap();
let discounted_fee = TURBO_FEE * 8 / 10;
let opened =
start_turbo_session_paying(&fixture, TURBO_DEPOSIT + discounted_fee).await;
assert_eq!(
opened.open_fee, discounted_fee,
"20% off the entry fee, reported as charged"
);
assert_eq!(opened.fees_accrued, discounted_fee);
let response = fixture.close_session().await;
let forfeited = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<CollateralForfeited>(&response.tx_status.receipts)
.unwrap();
assert_eq!(forfeited.len(), 1);
assert_eq!(forfeited[0].amount, TURBO_DEPOSIT);
}
#[tokio::test]
async fn withdrawing_an_asset_lowers_the_anchor_at_the_oracle_bid() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let held = 500_000_000u64;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
held,
fixture.base_asset,
TxPolicies::default(),
)
.await
.unwrap();
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, held)),
);
let response = fixture.call_account(vec![withdraw], 2).await.unwrap();
let recorded = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<RolloverWithdrawalRecorded>(&response.tx_status.receipts)
.unwrap();
assert_eq!(recorded.len(), 1);
assert_eq!(recorded[0].amount, 1_000_000);
assert_eq!(recorded[0].withdrawn_total, 1_000_000);
let view = rollover_view(&fixture).await;
assert_eq!(view.anchor, LINE);
assert_eq!(view.withdrawn, 1_000_000);
}
#[tokio::test]
async fn a_turbo_account_takes_its_price_and_never_a_unit_more() {
let fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
fixture.close_session().await;
let error = fixture
.account
.methods()
.start_session(TURBO_TIER_ID, TURBO_ENTRY + 1, ProlongPeriod::Week)
.call_params(CallParameters::new(
TURBO_ENTRY + 1,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.deployment.registry,
])
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("TurboEntryNotExact"),
"a turbo entry over its price is refused: {error:#}"
);
let opened = start_turbo_session_paying(&fixture, TURBO_ENTRY).await;
assert_eq!(opened.collateral, TURBO_ENTRY);
assert_eq!(opened.open_fee, TURBO_FEE);
assert_eq!(opened.fees_accrued, TURBO_FEE, "charged once, at open");
assert_eq!(fixture.session().await.fees_accrued, TURBO_FEE);
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.user.clone())
.methods()
.add_collateral(fixture.child_id)
.call_params(CallParameters::new(
1_000,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.call()
.await
.unwrap_err();
assert!(error.to_string().contains("TopUpsDisabled"), "{error:#}");
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.add_collateral(fixture.child_id)
.call_params(CallParameters::new(
1_000,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("TopUpsDisabled"),
"a gifted top-up games the drawdown limit as much as the owner's: {error:#}"
);
assert_eq!(fixture.session().await.collateral, TURBO_ENTRY);
}
async fn earn_and_bank_a_week(fixture: &mut PropFixture) -> u64 {
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture.publish_base_price(2_000_000_000_000_000_000).await;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture.publish_base_price(4_000_000_000_000_000_000).await;
fixture
.rest_order(4_000_000, quantity, fixture.collateral_asset, 4_000_000)
.await;
let sell = fixture.book_call(4_000_000, quantity, fixture.base_asset, quantity);
let return_quote = fixture.pool_call(
fn_selector!(return_quote()),
cost,
fixture.collateral_asset,
None,
);
fixture
.call_account(vec![sell, fixture.settle_book_call(), return_quote], 2)
.await
.unwrap();
let banked = fixture.account_balance(fixture.collateral_asset).await
- fixture.session().await.fees_accrued;
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, banked)),
);
fixture.call_account(vec![withdraw], 2).await.unwrap();
banked
}
#[tokio::test]
async fn the_whole_life_of_a_turbo_account_good_weeks_and_a_bad_one() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let wallet_after_open = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let mut total_banked = 0u64;
let mut week = 0usize;
while week < 3 {
let banked = earn_and_bank_a_week(&mut fixture).await;
total_banked += banked;
advance_past_expiry(&fixture).await;
let response = try_expire(&fixture).await.unwrap();
let rolled = response
.decode_logs_with_type::<SessionRolledOver>()
.unwrap();
assert_eq!(rolled.len(), 1);
assert_eq!(
rolled[0].rollovers_used,
week as u64 + 1,
"free week number {}",
week + 1
);
let anchor = rolled[0].anchor;
let allowance = LINE * DRAWDOWN_SCHEDULE[week + 1] / 10_000;
fixture.publish_base_price(2_000_000_000_000_000_000).await;
assert_eq!(
fixture.lines().await.threshold,
U256::from(LINE - allowance),
"week {}'s drawdown is {} bps of the line, and the anchor at \
{anchor} does not move it",
week + 2,
DRAWDOWN_SCHEDULE[week + 1],
);
refresh_session_key(&fixture).await;
week += 1;
}
assert_eq!(
rollover_view(&fixture).await.rollovers_used,
3,
"three free weeks earned and consumed"
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
wallet_after_open + total_banked as u128,
"every banked week is in the wallet"
);
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture.publish_base_price(1_600_000_000_000_000_000).await;
assert!(fixture.is_liquidatable().await);
advance_past_expiry(&fixture).await;
let error = try_rollover(&fixture).await.unwrap_err();
assert!(
error.to_string().contains("Rollover"),
"the fourth week has no free roll left: {error:#}"
);
let bad_debt_before = fixture.bad_debt().await;
let wallet_before_liq = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let closed = fixture.closed_event(&fixture.liquidate().await);
assert!(matches!(closed.reason, SettlementReason::Liquidation));
assert!(
closed.profit_is_negative,
"the session's own result is a loss"
);
assert!(
closed.bad_debt != 0,
"a loss past the stake must land as bad debt"
);
assert_eq!(
fixture.bad_debt().await,
bad_debt_before + closed.bad_debt,
"the shortfall is booked, once"
);
assert_eq!(
closed.user_net, 0,
"a liquidated session pays the user nothing"
);
assert!(closed.payout_parent.is_empty());
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
wallet_before_liq,
"the bad week takes nothing further from the user's banked profit"
);
assert!(
fixture
.deployment
.pool
.methods()
.get_session(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
assert!(
fixture
.deployment
.pool
.methods()
.get_rollover(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
let final_wallet = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
assert_eq!(
final_wallet,
wallet_after_open + total_banked as u128,
"the bad week moved nothing in the wallet; the three good weeks stand"
);
assert!(
total_banked > TURBO_ENTRY,
"the banked profit dwarfs the one forfeited stake"
);
}
#[tokio::test]
async fn the_cohort_boundary_refuses_a_rollover_on_legacy_and_arming_on_turbo() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.rollover_session(fixture.child_id)
.with_contracts(&[
&fixture.account,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("RolloverNotOffered"),
"a legacy session was never sold a rollover: {error:#}"
);
open_turbo_session(&fixture).await;
let arm = fixture.pool_call(
fn_selector!(set_auto_prolong(Option<ProlongPeriod>)),
0,
AssetId::default(),
Some(call_data!(Some(ProlongPeriod::Day))),
);
let error = fixture.call_account(vec![arm], 0).await.unwrap_err();
assert!(
error.to_string().contains("PeriodNotOffered"),
"a prepaid tier prices no period to arm: {error:#}"
);
}
#[tokio::test]
async fn banking_profit_does_not_widen_the_sold_drawdown() {
let mut fixture = PropFixture::new().await;
publish_turbo_tier(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
assert_eq!(fixture.lines().await.threshold, U256::from(9_850_000u64));
let banked = earn_and_bank_a_week(&mut fixture).await;
assert!(banked > 0);
fixture.publish_base_price(2_000_000_000_000_000_000).await;
let view = rollover_view(&fixture).await;
assert_eq!(view.anchor, LINE);
assert_eq!(view.withdrawn, banked);
assert_eq!(fixture.lines().await.threshold, U256::from(9_850_000u64));
let quantity = 1_000_000_000u64;
let cost = 2_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.base_asset, quantity)
.await;
let draw = fixture.draw_call(cost);
let buy = fixture.book_call(2_000_000, quantity, fixture.collateral_asset, cost);
fixture
.call_account(vec![draw, buy, fixture.settle_book_call()], 2)
.await
.unwrap();
fixture.publish_base_price(1_800_000_000_000_000_000).await;
assert!(fixture.is_liquidatable().await);
}