use super::*;
use crate::{
order_book_deploy::Side,
prop::{
SessionView,
TierDailyLossLimitPublished,
TierEvalProfitTargetPublished,
},
};
async fn policy(fixture: &PropFixture, version: u32) -> Option<u64> {
fixture
.deployment
.pool
.methods()
.get_eval_profit_target(TURBO_TIER_ID, version)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
}
async fn publish(fixture: &PropFixture, target: Option<u64>) -> CallResponse<u32> {
fixture
.deployment
.pool
.methods()
.publish_tier_v4(
TURBO_TIER_ID,
turbo_tier_params(),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
Some(100),
target,
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap()
}
async fn set_policy(fixture: &PropFixture, version: u32, target: Option<u64>) {
fixture
.deployment
.pool
.methods()
.set_eval_profit_target(TURBO_TIER_ID, version, target)
.call()
.await
.unwrap();
}
async fn eval_fixture(target: Option<u64>) -> PropFixture {
let fixture = PropFixture::new().await;
publish(&fixture, target).await;
open_turbo_session(&fixture).await;
fixture
}
async fn state(fixture: &PropFixture) -> (Option<SessionView>, Vec<u128>) {
let mut balances = Vec::new();
for asset in [fixture.base_asset, fixture.collateral_asset] {
balances.push(fixture.pool_balance(asset).await as u128);
balances.push(fixture.account_balance(asset).await as u128);
for wallet in [&fixture.user, &fixture.deployer, &fixture.outsider] {
balances.push(wallet.get_asset_balance(&asset).await.unwrap());
}
}
(fixture.session_opt().await, balances)
}
#[tokio::test]
async fn v4_publishes_both_versioned_policies_and_preserves_the_tier_event() {
let fixture = PropFixture::new().await;
assert_eq!(policy(&fixture, 1).await, None);
for (index, target) in [Some(500), None, Some(0), Some(10_000)]
.into_iter()
.enumerate()
{
let response = publish(&fixture, target).await;
let version = index as u32 + 1;
assert_eq!(response.value, version);
assert_eq!(policy(&fixture, version).await, target);
let tier = response
.decode_logs_with_type::<TierVersionPublishedV2>()
.unwrap();
assert_eq!(tier.len(), 1);
assert_eq!(tier[0].params, turbo_tier_params());
assert_eq!(tier[0].terms, turbo_terms(DRAWDOWN_SCHEDULE));
assert_eq!(tier[0].books, vec![fixture.order_book.contract_id]);
assert_eq!(tier[0].k, LINE);
let eval = response
.decode_logs_with_type::<TierEvalProfitTargetPublished>()
.unwrap();
assert_eq!(eval.len(), 1);
assert_eq!(eval[0].tier_id, TURBO_TIER_ID);
assert_eq!(eval[0].version, version);
assert_eq!(eval[0].eval_profit_target_bps, target);
assert_eq!(eval[0].timestamp, tier[0].timestamp);
let daily = response
.decode_logs_with_type::<TierDailyLossLimitPublished>()
.unwrap();
assert_eq!(daily.len(), 1);
assert_eq!(daily[0].daily_loss_limit_bps, Some(100));
assert_eq!(daily[0].version, version);
assert_eq!(daily[0].timestamp, eval[0].timestamp);
assert_eq!(
fixture
.deployment
.pool
.methods()
.get_daily_loss_limit(TURBO_TIER_ID, version)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(100)
);
}
let v2 = publish_turbo_tier_unarmed(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
let v3 = fixture
.deployment
.pool
.methods()
.publish_tier_v3(
TURBO_TIER_ID,
turbo_tier_params(),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
Some(250),
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap()
.value;
assert_eq!(policy(&fixture, v2).await, None);
assert_eq!(policy(&fixture, v3).await, None);
assert_eq!(policy(&fixture, 1).await, Some(500));
assert_eq!(policy(&fixture, 3).await, Some(0));
assert_eq!(policy(&fixture, 4).await, Some(10_000));
}
#[tokio::test]
async fn eval_policy_is_admin_only_version_scoped_and_repairable_while_paused() {
let fixture = eval_fixture(Some(500)).await;
let session = fixture.session().await;
let newer = publish(&fixture, Some(800)).await.value;
let pool = &fixture.deployment.pool;
for caller in [&fixture.user, &fixture.outsider] {
let unauthorized = pool.clone().with_account(caller.clone());
let error = unauthorized
.methods()
.set_eval_profit_target(TURBO_TIER_ID, session.tier_version, None)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AccessControlNotAuthorized"),
"{error:#}"
);
let error = unauthorized
.methods()
.publish_tier_v4(
TURBO_TIER_ID,
turbo_tier_params(),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
None,
Some(1),
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AccessControlNotAuthorized"),
"{error:#}"
);
}
for (tier, version) in [(TURBO_TIER_ID, 0), (TURBO_TIER_ID, newer + 1), (999, 1)] {
let error = pool
.methods()
.set_eval_profit_target(tier, version, Some(1))
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidTierVersion"),
"{error:#}"
);
}
for invalid in [10_001, u64::MAX] {
let error = pool
.methods()
.set_eval_profit_target(TURBO_TIER_ID, session.tier_version, Some(invalid))
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidEvalProfitTarget"),
"{error:#}"
);
assert_eq!(policy(&fixture, session.tier_version).await, Some(500));
}
pool.methods().pause().call().await.unwrap();
for target in [Some(0), None, None, Some(10_000)] {
let response = pool
.methods()
.set_eval_profit_target(TURBO_TIER_ID, session.tier_version, target)
.call()
.await
.unwrap();
let events = response
.decode_logs_with_type::<TierEvalProfitTargetPublished>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].eval_profit_target_bps, target);
assert_eq!(events[0].version, session.tier_version);
assert_eq!(events[0].tier_id, TURBO_TIER_ID);
assert!(events[0].timestamp.unix > 0);
assert!(
response
.decode_logs_with_type::<TierVersionPublishedV2>()
.unwrap()
.is_empty()
);
assert_eq!(policy(&fixture, session.tier_version).await, target);
assert_eq!(policy(&fixture, newer).await, Some(800));
assert_eq!(fixture.session().await, session);
}
assert_eq!(
pool.methods()
.current_tier_version(TURBO_TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(newer)
);
assert_eq!(
pool.methods()
.get_daily_loss_limit(TURBO_TIER_ID, session.tier_version)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(100)
);
}
#[tokio::test]
async fn invalid_v4_publications_leave_no_partial_tier_or_policy() {
let fixture = PropFixture::new().await;
let version = publish(&fixture, Some(500)).await.value;
let pool = &fixture.deployment.pool;
let mut invalid_terms = turbo_terms(DRAWDOWN_SCHEDULE);
invalid_terms.max_loss_bps = [0; 4];
for (daily, target, terms, books, expected) in [
(
Some(100),
Some(10_001),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
"InvalidEvalProfitTarget",
),
(
Some(100),
Some(u64::MAX),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
"InvalidEvalProfitTarget",
),
(
Some(10_001),
Some(500),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
"InvalidDailyLossLimit",
),
(
None,
Some(500),
invalid_terms,
vec![fixture.order_book.contract_id],
"InvalidTurboTerms",
),
(
Some(100),
Some(500),
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id; 2],
"InvalidBook",
),
] {
let error = pool
.methods()
.publish_tier_v4(
TURBO_TIER_ID,
turbo_tier_params(),
terms,
books,
daily,
target,
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(error.to_string().contains(expected), "{error:#}");
assert_eq!(policy(&fixture, version).await, Some(500));
assert_eq!(policy(&fixture, version + 1).await, None);
assert_eq!(
pool.methods()
.get_daily_loss_limit(TURBO_TIER_ID, version + 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
None
);
assert_eq!(
pool.methods()
.get_tier(TURBO_TIER_ID, version + 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
None
);
assert_eq!(
pool.methods()
.get_turbo_terms(TURBO_TIER_ID, version + 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
None
);
assert_eq!(
pool.methods()
.current_tier_version(TURBO_TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(version)
);
}
assert_eq!(publish(&fixture, None).await.value, version + 1);
}
async fn gift(fixture: &PropFixture, asset: AssetId, amount: u64) {
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
amount,
asset,
TxPolicies::default(),
)
.await
.unwrap();
}
async fn assert_close_rejected(fixture: &PropFixture, expected: &str) {
let before = state(fixture).await;
let session = fixture.session().await.session_id;
let error = fixture
.close_eval_session(&fixture.outsider, session)
.await
.unwrap_err();
assert!(
error.to_string().contains(expected),
"expected {expected}: {error:#}"
);
assert_eq!(state(fixture).await, before);
}
async fn custom_fixture(target: Option<u64>, params: TierParams) -> PropFixture {
let fixture = PropFixture::new().await;
fixture
.deployment
.pool
.methods()
.publish_tier_v4(
TURBO_TIER_ID,
params,
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
None,
target,
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap();
open_turbo_session(&fixture).await;
fixture
}
fn assert_eval_events(
fixture: &PropFixture,
response: &CallResponse<()>,
) -> SessionClosed {
let closed = fixture.closed_event(response);
assert_eq!(closed.reason, SettlementReason::ProfitTargetReached);
assert_eq!(closed.user_net, 0);
assert_eq!(closed.platform_total, 0);
assert!(closed.payout_parent.is_empty());
assert_eq!(
closed.payout_platform,
vec![(fixture.collateral_asset, TURBO_DEPOSIT)]
);
assert!(closed.cancelled_debt.is_empty());
assert_eq!(closed.bad_debt, 0);
let seized = response.decode_logs_with_type::<SessionSeized>().unwrap();
assert_eq!(seized.len(), 1);
assert_eq!(seized[0].reason, SettlementReason::ProfitTargetReached);
assert!(seized[0].debt.is_empty());
assert!(seized[0].transferred.is_empty());
let ticket = response.decode_logs_with_type::<TicketSettled>().unwrap();
assert_eq!(ticket.len(), 1);
assert_eq!(ticket[0].to_platform, TURBO_DEPOSIT);
assert_eq!(ticket[0].absorbed_by_pool, 0);
closed
}
#[tokio::test]
async fn anyone_can_complete_at_or_above_the_profit_target_without_a_cosigner() {
let fixture = eval_fixture(Some(500)).await;
let pool = &fixture.deployment.pool;
assert_eq!(
pool.methods()
.expiry_cosigner()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
None
);
for role in [0, ACCOUNT_CLOSER_ROLE] {
assert!(
!pool
.methods()
.has_role(role, Identity::Address(fixture.outsider.address()))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
assert_close_rejected(&fixture, "ProfitTargetNotReached").await;
gift(&fixture, fixture.collateral_asset, 499_999).await;
assert_close_rejected(&fixture, "ProfitTargetNotReached").await;
gift(&fixture, fixture.collateral_asset, 1).await;
let session = fixture.session().await.session_id;
let response = fixture
.close_eval_session(&fixture.outsider, session)
.await
.unwrap();
let closed = assert_eval_events(&fixture, &response);
assert_eq!(closed.v, LINE + 500_000);
assert!(fixture.session_opt().await.is_none());
let reopened = start_turbo_session(&fixture).await;
gift(&fixture, fixture.collateral_asset, 500_001).await;
let response = fixture
.close_eval_session(&fixture.outsider, reopened.session_id)
.await
.unwrap();
assert_eq!(assert_eval_events(&fixture, &response).v, LINE + 500_001);
}
#[tokio::test]
async fn eval_target_rounds_up_from_the_pinned_account_size() {
let line = LINE + 1;
let fixture = custom_fixture(Some(1), turbo_tier_params_for(line)).await;
gift(&fixture, fixture.collateral_asset, 1_000).await;
assert_close_rejected(&fixture, "ProfitTargetNotReached").await;
gift(&fixture, fixture.collateral_asset, 1).await;
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
assert_eq!(assert_eval_events(&fixture, &response).v, line + 1_001);
}
#[tokio::test]
async fn eval_uses_live_overrides_on_the_sessions_pinned_policy() {
let fixture = PropFixture::new().await;
let version =
publish_turbo_tier_unarmed(&fixture, turbo_terms(DRAWDOWN_SCHEDULE)).await;
open_turbo_session(&fixture).await;
let newer = publish(&fixture, Some(0)).await.value;
assert!(newer > version);
assert_close_rejected(&fixture, "EvalProfitTargetNotConfigured").await;
set_policy(&fixture, version, Some(500)).await;
gift(&fixture, fixture.collateral_asset, 500_000).await;
set_policy(&fixture, version, Some(501)).await;
assert_close_rejected(&fixture, "ProfitTargetNotReached").await;
set_policy(&fixture, version, None).await;
assert_close_rejected(&fixture, "EvalProfitTargetNotConfigured").await;
set_policy(&fixture, newer, None).await;
set_policy(&fixture, version, Some(500)).await;
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
assert_eval_events(&fixture, &response);
set_policy(&fixture, newer, Some(0)).await;
let reopened = start_turbo_session(&fixture).await;
let response = fixture
.close_eval_session(&fixture.outsider, reopened.session_id)
.await
.unwrap();
assert_eq!(assert_eval_events(&fixture, &response).v, LINE);
}
#[tokio::test]
async fn outstanding_quote_and_base_debts_block_eval_until_repaid() {
let mut fixture = eval_fixture(Some(0)).await;
let draw = fixture.draw_call(100_000);
fixture.call_account(vec![draw], 1).await.unwrap();
assert_close_rejected(&fixture, "QuoteOutstanding").await;
let give_back = fixture.pool_call(
fn_selector!(return_quote()),
100_000,
fixture.collateral_asset,
None,
);
fixture.call_account(vec![give_back], 0).await.unwrap();
let quantity = 10_000_000u64;
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, quantity)),
);
fixture.call_account(vec![borrow], 1).await.unwrap();
assert_close_rejected(&fixture, "DebtsOutstanding").await;
assert_eq!(fixture.debt(fixture.base_asset).await, quantity);
let repay =
fixture.pool_call(fn_selector!(repay()), quantity, fixture.base_asset, None);
fixture.call_account(vec![repay], 0).await.unwrap();
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
assert_eval_events(&fixture, &response);
assert!(
!fixture
.deployment
.pool
.methods()
.has_debts(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
#[tokio::test]
async fn eval_requires_both_locked_and_settled_book_balances_to_be_empty() {
for side in [Side::Buy, Side::Sell] {
let mut fixture = eval_fixture(Some(0)).await;
let quantity = 1_000_000_000;
let (price, asset, amount, counter_asset, counter_amount) = match side {
Side::Buy => (
1_000_000,
fixture.collateral_asset,
1_000_000,
fixture.base_asset,
quantity,
),
Side::Sell => (
3_000_000,
fixture.base_asset,
quantity,
fixture.collateral_asset,
3_000_000,
),
};
gift(&fixture, asset, amount).await;
let order = fixture.book_call(price, quantity, asset, amount);
fixture.call_account(vec![order], 0).await.unwrap();
let trader = Identity::ContractId(fixture.child_id);
let book = &fixture.order_book.order_book;
let locked = book
.methods()
.get_total_balance_of(trader)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_ne!(locked, (0, 0));
assert_close_rejected(&fixture, "BookNotDrained").await;
assert_eq!(
book.methods()
.get_total_balance_of(trader)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
locked
);
fixture
.rest_order(price, quantity, counter_asset, counter_amount)
.await;
let settled = book
.methods()
.get_settled_balance_of(trader)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_ne!(settled, (0, 0));
assert_close_rejected(&fixture, "BookNotDrained").await;
assert_eq!(
book.methods()
.get_settled_balance_of(trader)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
settled
);
let settle = fixture.settle_book_call();
fixture.call_account(vec![settle], 1).await.unwrap();
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
assert_eval_events(&fixture, &response);
}
}
#[tokio::test]
async fn direct_and_received_base_holdings_use_normal_value_and_require_fresh_prices() {
let mut params = turbo_tier_params();
params.max_price_age = 1_000_100;
let mut fixture = custom_fixture(Some(500), params).await;
let quantity = 250_000_000;
let pool_before = fixture.pool_balance(fixture.base_asset).await;
gift(&fixture, fixture.base_asset, quantity).await;
let receive = fixture.pool_call(
fn_selector!(receive_settlement()),
quantity / 2,
fixture.base_asset,
None,
);
fixture.call_account(vec![receive], 0).await.unwrap();
assert_eq!(
fixture.account_balance(fixture.base_asset).await,
quantity / 2
);
assert_eq!(
fixture.pool_balance(fixture.base_asset).await,
pool_before + quantity / 2
);
assert_eq!(fixture.session_value().await, i128::from(LINE + 500_000));
advance_eight_days(&fixture).await;
assert_close_rejected(&fixture, "PriceStale").await;
let now = fixture
.user
.provider()
.latest_block_time()
.await
.unwrap()
.unwrap()
.timestamp() as u64;
fixture
.deployment
.price_feed
.methods()
.publish_prices(vec![PriceInput {
asset: fixture.base_asset,
bid: 2_000_000_000_000_000_000,
ask: 2_000_000_000_000_000_000,
timestamp: now,
}])
.call()
.await
.unwrap();
let parent_before = fixture
.user
.get_asset_balance(&fixture.base_asset)
.await
.unwrap();
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.base_asset)
.await
.unwrap();
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
let closed = assert_eval_events(&fixture, &response);
assert_eq!(closed.v, LINE + 500_000);
assert_eq!(closed.v_liq, LINE + 495_000);
assert_eq!(
fixture.pool_balance(fixture.base_asset).await,
pool_before + quantity
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.base_asset)
.await
.unwrap(),
parent_before
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.base_asset)
.await
.unwrap(),
platform_before
);
}
#[tokio::test]
async fn collateral_only_eval_completion_does_not_need_a_fresh_base_price() {
let mut params = turbo_tier_params();
params.max_price_age = 1_000_100;
let fixture = custom_fixture(Some(500), params).await;
gift(&fixture, fixture.collateral_asset, 500_000).await;
advance_eight_days(&fixture).await;
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
assert_eval_events(&fixture, &response);
}
#[tokio::test]
async fn eval_retains_all_proceeds_topups_and_capitalised_profit_in_the_pool() {
let mut fixture = eval_fixture(Some(500)).await;
fixture
.deployment
.pool
.methods()
.set_liquidator(Identity::Address(fixture.outsider.address()))
.call()
.await
.unwrap();
let pool_before = fixture.pool_balance(fixture.collateral_asset).await;
gift(&fixture, fixture.collateral_asset, 500_000).await;
fixture.fund_collateral(200_000).await;
let capitalise = fixture.pool_call(
fn_selector!(add_collateral(ContractId)),
100_000,
fixture.collateral_asset,
Some(call_data!(fixture.child_id)),
);
fixture.call_account(vec![capitalise], 0).await.unwrap();
assert_eq!(fixture.session().await.capitalised, 100_000);
assert_eq!(fixture.session().await.collateral, 300_000);
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let liquidator_before = fixture
.outsider
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
assert_eq!(assert_eval_events(&fixture, &response).v, LINE + 700_000);
assert_eq!(
fixture.pool_balance(fixture.collateral_asset).await,
pool_before + 700_000 - TURBO_DEPOSIT
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
parent_before
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
platform_before + u128::from(TURBO_DEPOSIT)
);
assert_eq!(
fixture
.outsider
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
liquidator_before
);
assert_eq!(fixture.account_balance(fixture.collateral_asset).await, 0);
assert!(fixture.session_opt().await.is_none());
}
#[tokio::test]
async fn fees_reduce_the_net_value_used_to_check_the_profit_target() {
let mut fixture = eval_fixture(Some(100)).await;
fixture.fund_collateral(100_000).await;
let quantity = 10_000_000u64;
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, quantity)),
);
fixture.call_account(vec![borrow], 1).await.unwrap();
let repay = fixture.repay_base_from_collateral_call(fixture.base_asset, quantity);
fixture.call_account(vec![repay], 1).await.unwrap();
let fees = fixture.session().await.fees_accrued;
assert_eq!(fees, 2);
assert_eq!(fixture.debt(fixture.base_asset).await, 0);
assert_eq!(
fixture.session_value().await,
i128::from(LINE + 100_000 - fees)
);
assert_close_rejected(&fixture, "ProfitTargetNotReached").await;
gift(&fixture, fixture.collateral_asset, fees).await;
let response = fixture
.close_eval_session(&fixture.outsider, fixture.session().await.session_id)
.await
.unwrap();
assert_eval_events(&fixture, &response);
}
#[tokio::test]
async fn an_enabled_eval_policy_blocks_withdrawals_and_voluntary_close_until_disabled() {
let mut fixture = eval_fixture(Some(500)).await;
gift(&fixture, fixture.collateral_asset, 500_000).await;
gift(&fixture, fixture.base_asset, 20_000_000).await;
let version = fixture.session().await.tier_version;
for target in [Some(500), Some(0)] {
set_policy(&fixture, version, target).await;
for (asset, amount) in [
(fixture.collateral_asset, 100_000u64),
(fixture.base_asset, 10_000_000),
] {
let before = state(&fixture).await;
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(asset, amount)),
);
let error = fixture.call_account(vec![withdraw], 2).await.unwrap_err();
assert!(
error.to_string().contains("EvalWithdrawalsDisabled"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
}
let before = state(&fixture).await;
let error = fixture
.account
.methods()
.close_session(vec![])
.with_contracts(&[
&fixture.deployment.pool,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("EvalUserCloseDisabled"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
}
set_policy(&fixture, version, None).await;
for (asset, amount) in [
(fixture.collateral_asset, 100_000u64),
(fixture.base_asset, 10_000_000),
] {
let before = fixture.user.get_asset_balance(&asset).await.unwrap();
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(asset, amount)),
);
fixture.call_account(vec![withdraw], 2).await.unwrap();
assert_eq!(
fixture.user.get_asset_balance(&asset).await.unwrap(),
before + u128::from(amount)
);
}
let newer = publish(&fixture, None).await.value;
assert!(newer > version);
set_policy(&fixture, version, Some(500)).await;
assert_eq!(policy(&fixture, newer).await, None);
assert_eq!(fixture.session().await.tier_version, version);
assert_eq!(fixture.session_value().await, i128::from(LINE + 420_000));
let before = state(&fixture).await;
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, 1u64)),
);
let error = fixture.call_account(vec![withdraw], 2).await.unwrap_err();
assert!(
error.to_string().contains("EvalWithdrawalsDisabled"),
"{error:#}"
);
let error = fixture
.account
.methods()
.close_session(vec![])
.with_contracts(&[
&fixture.deployment.pool,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("EvalUserCloseDisabled"),
"{error:#}"
);
assert_eq!(state(&fixture).await, before);
assert_close_rejected(&fixture, "ProfitTargetNotReached").await;
set_policy(&fixture, version, None).await;
let response = fixture.close_session().await;
assert_eq!(
fixture.closed_event(&response).reason,
SettlementReason::UserClose
);
assert!(fixture.session_opt().await.is_none());
}
#[tokio::test]
async fn eval_retention_skips_an_unused_payout_ratio_that_would_overflow() {
let gift_quantity = 10_000_000_000_000_000_000u64;
let mut fixture = PropFixture::new_with_base_balance(gift_quantity).await;
let mut params = turbo_tier_params();
params.liq_price_factor = 1;
fixture
.deployment
.pool
.methods()
.publish_tier_v4(
TURBO_TIER_ID,
params,
turbo_terms(DRAWDOWN_SCHEDULE),
vec![fixture.order_book.contract_id],
None,
Some(0),
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap();
open_turbo_session(&fixture).await;
fixture.fund_collateral(20_002).await;
let borrowed = 10_000_000u64;
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, borrowed)),
);
fixture.call_account(vec![borrow], 1).await.unwrap();
let repay = fixture.repay_base_from_collateral_call(fixture.base_asset, borrowed);
fixture.call_account(vec![repay], 1).await.unwrap();
assert_eq!(fixture.session().await.collateral, 2);
assert_eq!(fixture.session().await.fees_accrued, 2);
fixture
.rest_order(2_000_000, 1_000, fixture.base_asset, 1_000)
.await;
let draw = fixture.draw_call(2);
let net = fixture.repay_from_collateral_call(2);
let buy = fixture.book_call(2_000_000, 1_000, fixture.collateral_asset, 2);
let settle = fixture.settle_book_call();
fixture
.call_account(vec![draw, net, buy, settle], 2)
.await
.unwrap();
fixture.publish_base_price(1_001_000).await;
gift(&fixture, fixture.base_asset, gift_quantity).await;
let held = gift_quantity + borrowed + 1_000;
let session = fixture.session().await;
assert_eq!(session.collateral, 0);
assert_eq!(session.drawn_quote, 0);
assert_eq!(session.fees_accrued, 2);
assert_eq!(fixture.debt(fixture.base_asset).await, 0);
assert_eq!(fixture.account_balance(fixture.collateral_asset).await, 0);
assert_eq!(fixture.account_balance(fixture.base_asset).await, held);
assert_eq!(
fixture
.order_book
.order_book
.methods()
.get_total_balance_of(Identity::ContractId(fixture.child_id))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
(0, 0)
);
assert_eq!(fixture.session_value().await, i128::from(LINE + 10_008));
assert!(u128::from(held) * 2 > u128::from(u64::MAX));
let pool_before = fixture.pool_balance(fixture.base_asset).await;
let parent_before = fixture
.user
.get_asset_balance(&fixture.base_asset)
.await
.unwrap();
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.base_asset)
.await
.unwrap();
let response = fixture
.close_eval_session(&fixture.outsider, session.session_id)
.await
.unwrap();
let closed = assert_eval_events(&fixture, &response);
assert_eq!(closed.v, LINE + 10_008);
assert_eq!(closed.v_liq, LINE - 1);
assert_eq!(
fixture.pool_balance(fixture.base_asset).await,
pool_before + held
);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.base_asset)
.await
.unwrap(),
parent_before
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.base_asset)
.await
.unwrap(),
platform_before
);
assert!(fixture.session_opt().await.is_none());
}
#[tokio::test]
async fn eval_close_respects_pause_and_cannot_replay_into_a_replacement_session() {
let fixture = eval_fixture(Some(0)).await;
let session = fixture.session().await.session_id;
fixture
.deployment
.pool
.methods()
.pause()
.call()
.await
.unwrap();
assert_close_rejected(&fixture, "Paused").await;
fixture
.deployment
.pool
.methods()
.unpause()
.call()
.await
.unwrap();
let before = state(&fixture).await;
let error = fixture
.close_eval_session(&fixture.outsider, session + 1)
.await
.unwrap_err();
assert!(error.to_string().contains("SessionIdMismatch"), "{error:#}");
assert_eq!(state(&fixture).await, before);
fixture
.close_eval_session(&fixture.outsider, session)
.await
.unwrap();
let after = state(&fixture).await;
let error = fixture
.close_eval_session(&fixture.outsider, session)
.await
.unwrap_err();
assert!(error.to_string().contains("NoActiveSession"), "{error:#}");
assert_eq!(state(&fixture).await, after);
let reopened = start_turbo_session(&fixture).await;
assert!(reopened.session_id > session);
let before = state(&fixture).await;
let error = fixture
.close_eval_session(&fixture.outsider, session)
.await
.unwrap_err();
assert!(error.to_string().contains("SessionIdMismatch"), "{error:#}");
assert_eq!(state(&fixture).await, before);
}
async fn exit_balances(fixture: &PropFixture) -> [[i128; 4]; 2] {
let mut balances = [[0; 4]; 2];
for (index, asset) in [fixture.base_asset, fixture.collateral_asset]
.into_iter()
.enumerate()
{
balances[index][0] = i128::from(fixture.pool_balance(asset).await);
for (wallet_index, wallet) in
[&fixture.user, &fixture.deployer, &fixture.outsider]
.into_iter()
.enumerate()
{
balances[index][wallet_index + 1] =
i128::try_from(wallet.get_asset_balance(&asset).await.unwrap()).unwrap();
}
}
balances
}
struct ForcedOutcome {
closed: SessionClosed,
seized: SessionSeized,
tickets: Vec<(u64, u64, u64)>,
balance_changes: [[i128; 4]; 2],
pool_bad_debt: u64,
}
async fn forced_outcome(
fixture: &PropFixture,
response: CallResponse<()>,
before: [[i128; 4]; 2],
) -> ForcedOutcome {
let closed = fixture.closed_event(&response);
let mut seized = response.decode_logs_with_type::<SessionSeized>().unwrap();
assert_eq!(seized.len(), 1);
let seized = seized.pop().unwrap();
assert_eq!(
seized.liquidator,
Identity::Address(fixture.outsider.address())
);
let tickets = response
.decode_logs_with_type::<TicketSettled>()
.unwrap()
.into_iter()
.map(|ticket| (ticket.ticket, ticket.to_platform, ticket.absorbed_by_pool))
.collect();
let mut balance_changes = exit_balances(fixture).await;
for (asset, previous) in balance_changes.iter_mut().zip(before) {
for (balance, previous) in asset.iter_mut().zip(previous) {
*balance -= previous;
}
}
assert!(fixture.session_opt().await.is_none());
assert_eq!(fixture.account_balance(fixture.base_asset).await, 0);
assert_eq!(fixture.account_balance(fixture.collateral_asset).await, 0);
ForcedOutcome {
closed,
seized,
tickets,
balance_changes,
pool_bad_debt: fixture.bad_debt().await,
}
}
fn assert_only_parent_allocation_is_retained(
control: &ForcedOutcome,
eval: &ForcedOutcome,
) {
assert!(control.closed.user_net > 0);
assert!(!control.closed.payout_parent.is_empty());
let mut expected = control.closed.clone();
expected.account = eval.closed.account;
expected.session_id = eval.closed.session_id;
expected.timestamp = eval.closed.timestamp.clone();
expected.user_net = 0;
expected.payout_parent.clear();
assert_eq!(eval.closed, expected);
let mut expected = control.seized.clone();
expected.account = eval.seized.account;
expected.session_id = eval.seized.session_id;
expected.timestamp = eval.seized.timestamp.clone();
expected.liquidator = eval.seized.liquidator;
assert_eq!(eval.seized, expected);
assert_eq!(eval.tickets, control.tickets);
assert_eq!(eval.pool_bad_debt, control.pool_bad_debt);
for (
[
control_pool,
control_parent,
control_platform,
control_liquidator,
],
[eval_pool, eval_parent, eval_platform, eval_liquidator],
) in control
.balance_changes
.into_iter()
.zip(eval.balance_changes)
{
assert_eq!(eval_parent, 0);
assert_eq!(eval_platform, control_platform);
assert_eq!(eval_liquidator, control_liquidator);
assert_eq!(eval_pool, control_pool + control_parent);
}
}
#[tokio::test]
async fn manual_and_signed_eval_forced_exits_keep_parent_cash_without_changing_quote_debt()
{
for signed in [false, true] {
let mut outcomes = Vec::new();
for enabled in [false, true] {
let mut fixture = eval_fixture(Some(0)).await;
let session = fixture.session().await;
if !enabled {
set_policy(&fixture, session.tier_version, None).await;
}
let newer = publish(&fixture, None).await.value;
assert!(newer > session.tier_version);
fixture
.deployment
.pool
.methods()
.set_liquidator(Identity::Address(fixture.outsider.address()))
.call()
.await
.unwrap();
fixture.fund_collateral(100_000).await;
gift(&fixture, fixture.base_asset, 150_000_000).await;
fixture
.rest_order(2_000_000, 100_000_000, fixture.base_asset, 100_000_000)
.await;
let draw = fixture.draw_call(200_000);
let buy = fixture.book_call(
2_000_000,
100_000_000,
fixture.collateral_asset,
200_000,
);
let settle = fixture.settle_book_call();
fixture
.call_account(vec![draw, buy, settle], 2)
.await
.unwrap();
assert_eq!(fixture.session().await.collateral, 100_000);
assert_eq!(fixture.session().await.drawn_quote, 200_000);
assert_eq!(fixture.account_balance(fixture.collateral_asset).await, 0);
assert_eq!(
fixture.account_balance(fixture.base_asset).await,
250_000_000
);
assert!(!fixture.is_liquidatable().await);
let before = exit_balances(&fixture).await;
let response = if signed {
fixture
.deployment
.pool
.methods()
.set_expiry_cosigner(fixture.deployer.address())
.call()
.await
.unwrap();
let signature = fixture
.force_close_cosignature(session.session_id, &fixture.deployer)
.await;
fixture
.force_close_session_signed(
&fixture.outsider,
session.session_id,
vec![],
signature,
)
.await
.unwrap()
} else {
fixture
.force_close_session(&fixture.deployer, vec![])
.await
.unwrap()
};
let outcome = forced_outcome(&fixture, response, before).await;
assert_eq!(outcome.closed.reason, SettlementReason::PermissionedClosure);
assert_eq!(
outcome.seized.debt,
vec![(fixture.collateral_asset, 200_000)]
);
assert!(
outcome
.seized
.transferred
.iter()
.all(|(asset, _)| *asset == fixture.base_asset)
);
assert_eq!(outcome.tickets, vec![(TURBO_DEPOSIT, TURBO_DEPOSIT, 0)]);
outcomes.push(outcome);
}
assert_only_parent_allocation_is_retained(&outcomes[0], &outcomes[1]);
}
}
#[tokio::test]
async fn eval_forced_base_debt_keeps_liquidator_repayment_and_platform_profit_share() {
let mut outcomes = Vec::new();
for target in [None, Some(500)] {
let mut fixture = PropFixture::new().await;
fixture
.deployment
.pool
.methods()
.set_eval_profit_target(TIER_ID, 1, target)
.call()
.await
.unwrap();
fixture
.deployment
.pool
.methods()
.set_liquidator(Identity::Address(fixture.outsider.address()))
.call()
.await
.unwrap();
let quantity = 10_000_000u64;
gift(&fixture, fixture.base_asset, quantity).await;
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, quantity)),
);
fixture.call_account(vec![borrow], 1).await.unwrap();
let before = exit_balances(&fixture).await;
let response = fixture
.force_close_session(&fixture.deployer, vec![])
.await
.unwrap();
let outcome = forced_outcome(&fixture, response, before).await;
assert_eq!(
outcome.seized.holdings,
vec![(fixture.base_asset, 2 * quantity)]
);
assert_eq!(outcome.seized.debt, vec![(fixture.base_asset, quantity)]);
assert_eq!(
outcome.seized.transferred,
vec![(fixture.base_asset, quantity)]
);
assert!(outcome.closed.platform_total > 0);
assert!(outcome.closed.cancelled_debt.is_empty());
outcomes.push(outcome);
}
assert_only_parent_allocation_is_retained(&outcomes[0], &outcomes[1]);
}
#[tokio::test]
async fn eval_expiry_retains_parent_quote_base_and_topups_but_keeps_ticket_accounting() {
let mut outcomes = Vec::new();
for target in [None, Some(500)] {
let fixture = eval_fixture(target).await;
let session = fixture.session().await;
let newer = publish(&fixture, None).await.value;
assert!(newer > session.tier_version);
fixture
.deployment
.pool
.methods()
.set_liquidator(Identity::Address(fixture.outsider.address()))
.call()
.await
.unwrap();
fixture
.deployment
.pool
.methods()
.set_expiry_cosigner(fixture.deployer.address())
.call()
.await
.unwrap();
fixture.fund_collateral(100_000).await;
gift(&fixture, fixture.collateral_asset, 200_000).await;
gift(&fixture, fixture.base_asset, 100_000_000).await;
advance_past_expiry(&fixture).await;
let before = exit_balances(&fixture).await;
let response = try_expire(&fixture).await.unwrap();
let outcome = forced_outcome(&fixture, response, before).await;
assert_eq!(outcome.closed.reason, SettlementReason::Expiry);
assert!(outcome.seized.debt.is_empty());
assert!(outcome.seized.transferred.is_empty());
assert_eq!(outcome.tickets, vec![(TURBO_DEPOSIT, TURBO_DEPOSIT, 0)]);
outcomes.push(outcome);
}
assert_only_parent_allocation_is_retained(&outcomes[0], &outcomes[1]);
}
#[tokio::test]
async fn eval_liquidation_retains_residual_parent_collateral_without_changing_seizure() {
let mut outcomes = Vec::new();
for target in [None, Some(0)] {
let mut fixture = PropFixture::new().await;
fixture
.deployment
.pool
.methods()
.set_eval_profit_target(TIER_ID, 1, target)
.call()
.await
.unwrap();
fixture
.deployment
.pool
.methods()
.set_liquidator(Identity::Address(fixture.outsider.address()))
.call()
.await
.unwrap();
let quantity = 1_000_000_000u64;
fixture
.rest_order(2_000_000, quantity, fixture.collateral_asset, 2_000_000)
.await;
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, quantity)),
);
let sell = fixture.book_call(2_000_000, quantity, fixture.base_asset, quantity);
let settle = fixture.settle_book_call();
fixture
.call_account(vec![borrow, sell, settle], 2)
.await
.unwrap();
fixture.publish_base_price(3_800_000_000_000_000_000).await;
assert!(fixture.is_liquidatable().await);
let before = exit_balances(&fixture).await;
let response = fixture.liquidate().await;
let outcome = forced_outcome(&fixture, response, before).await;
assert_eq!(outcome.closed.reason, SettlementReason::Liquidation);
assert_eq!(outcome.closed.bad_debt, 0);
assert_eq!(
outcome.closed.cancelled_debt,
vec![(fixture.base_asset, quantity)]
);
assert_eq!(outcome.seized.debt, vec![(fixture.base_asset, quantity)]);
assert!(
outcome
.seized
.transferred
.iter()
.any(|(asset, amount)| *asset == fixture.collateral_asset && *amount > 0)
);
outcomes.push(outcome);
}
assert_only_parent_allocation_is_retained(&outcomes[0], &outcomes[1]);
}