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);
}