use super::*;
async fn seed_variant(
fixture: &PropFixture,
tier_id: u64,
params: TierParams,
) -> TierParams {
seed_legacy_tier(
&fixture.deployment,
tier_id,
params.clone(),
vec![fixture.order_book.contract_id],
)
.await
.unwrap();
params
}
fn variant(open_fee: u64, max_price_age: u64) -> TierParams {
TierParams {
line: LINE,
leverage: 5,
duration: 86_400,
maintenance_bps: 250,
open_buffer_bps: 375,
liq_price_factor: 9_900,
prolong_fee: [0, 6_000, 76_000, 738_000],
max_credit_line_bps: 20_000,
max_price_age,
open_fee,
profit_share_bps: 1_000,
price_band_bps: 1_000,
}
}
async fn armed_sibling(
fixture: &PropFixture,
index: u64,
) -> (ContractId, PropAccountContract<Wallet>) {
let parent = Identity::Address(fixture.user.address());
let child = fixture
.deployment
.deploy_account(&fixture.user, parent, index)
.await
.unwrap();
let child_id = child.contract_id();
let account = PropAccountContract::new(child_id, fixture.user.clone());
let expiry = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
+ 86_400;
account
.methods()
.set_session(ParallelSessionArgs {
nonce: U256::zero(),
session_id: Identity::Address(fixture.user.address()),
expiry: Time { unix: expiry },
contract_ids: vec![],
})
.with_contract_ids(&[fixture.deployment.oracle_id])
.call()
.await
.unwrap();
(child_id, account)
}
async fn open_on(
fixture: &PropFixture,
account: &PropAccountContract<Wallet>,
tier_id: u64,
amount: u64,
) -> Result<CallResponse<()>> {
Ok(account
.methods()
.start_session(tier_id, amount, ProlongPeriod::SixHours)
.call_params(CallParameters::new(
amount,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.deployment.registry,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await?)
}
#[tokio::test]
async fn an_entry_the_premium_would_leave_frozen_is_refused_at_the_door() {
let fixture = PropFixture::new().await;
let tier_id = 21;
seed_variant(&fixture, tier_id, variant(1_700_000, u64::MAX)).await;
let (child_id, account) = armed_sibling(&fixture, 21).await;
let error = open_on(&fixture, &account, tier_id, 1_999_999)
.await
.unwrap_err();
assert!(
error.to_string().contains("CollateralTooSmall"),
"the entry ticket is `line / leverage`: {error:#}"
);
let error = open_on(&fixture, &account, tier_id, 2_000_000)
.await
.unwrap_err();
assert!(
error.to_string().contains("SessionUnusableAtOpen"),
"a session must clear the freeze line at birth, not merely survive: {error:#}"
);
assert!(
fixture
.deployment
.pool
.methods()
.current_tier_version(4_242)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
assert!(
open_on(&fixture, &account, 4_242, 2_100_000).await.is_err(),
"there is no default tier"
);
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let response = open_on(&fixture, &account, tier_id, 2_100_000)
.await
.unwrap();
let opened = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionOpened>(&response.tx_status.receipts)
.unwrap();
assert_eq!(opened.len(), 1);
assert_eq!(opened[0].open_fee, 1_700_000);
assert_eq!(opened[0].fees_accrued, 1_700_000);
assert_eq!(
opened[0].collateral, 2_100_000,
"the collateral stays GROSS; the fee is recorded, not deducted"
);
assert_eq!(
opened[0].credit_line,
8_000_000 + (2_100_000 - 1_700_000),
"the line prices off the collateral net of the premium"
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
1_700_000,
"and the premium leaves for the platform inside the opening call"
);
let lines = fixture
.deployment
.pool
.methods()
.get_session_lines(child_id)
.with_contract_ids(&[
child_id,
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert!(lines.positive - lines.negative >= lines.freeze);
}
#[tokio::test]
async fn a_stale_feed_stops_the_marked_paths_and_leaves_the_cash_exit_open() {
let mut fixture = PropFixture::new().await;
let tier_id = 22;
seed_variant(&fixture, tier_id, variant(0, 3_600)).await;
let (child_id, account) = armed_sibling(&fixture, 22).await;
open_on(&fixture, &account, tier_id, COLLATERAL)
.await
.unwrap();
let quantity = 500_000_000u64;
let borrow = CallContractArg {
contract_id: fixture.deployment.pool_id,
function_selector: Bytes(fn_selector!(borrow(AssetId, u64))),
call_params: CallParams {
coins: 0,
asset_id: AssetId::default(),
gas: 30_000_000,
},
call_data: Some(Bytes(call_data!(fixture.base_asset, quantity))),
};
let submit = |fixture: &PropFixture,
account: &PropAccountContract<Wallet>,
signed: (Signature, Signature, ParallelMultiCallContractArgs),
outputs: usize| {
account
.methods()
.call_contracts(signed.0, signed.1, signed.2)
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.deployment.price_feed,
&fixture.deployment.registry,
&fixture.order_book.order_book,
])
.with_variable_output_policy(VariableOutputPolicy::Exactly(outputs))
};
let signed = fixture
.signed_calls_for(child_id, vec![borrow.clone()])
.await;
let error = submit(&fixture, &account, signed, 1)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("PriceStale"),
"the loan cap refuses to price a debt off a stale mark: {error:#}"
);
let chain_now = fixture
.user
.try_provider()
.unwrap()
.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_000u64.into(),
ask: 2_000_000_000_000_000_000u64.into(),
timestamp: chain_now,
}])
.call()
.await
.unwrap();
let signed = fixture.signed_calls_for(child_id, vec![borrow]).await;
submit(&fixture, &account, signed, 1).call().await.unwrap();
assert_eq!(
fixture
.deployment
.pool
.methods()
.get_debt(child_id, fixture.base_asset)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
quantity
);
let provider = fixture.user.try_provider().unwrap().clone();
let latest = provider.latest_block_time().await.unwrap().unwrap();
provider
.produce_blocks(
1,
Some(
latest
.checked_add_signed(chrono::TimeDelta::seconds(7_200))
.unwrap(),
),
)
.await
.unwrap();
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.liquidate(child_id, vec![])
.with_contracts(&[
&account,
&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("PriceStale"),
"a seizure needs a live mark: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.force_close_session(child_id, vec![])
.with_contracts(&[
&account,
&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("PriceStale"),
"a permissioned closure also needs a live mark: {error:#}"
);
assert_eq!(
fixture
.deployer
.try_provider()
.unwrap()
.get_contract_asset_balance(&child_id, &fixture.base_asset)
.await
.unwrap(),
quantity
);
assert!(
fixture
.deployment
.pool
.methods()
.has_session(child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let error = fixture
.deployment
.pool
.methods()
.get_session_lines(child_id)
.with_contract_ids(&[
child_id,
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.simulate(Execution::state_read_only())
.await
.unwrap_err();
assert!(
error.to_string().contains("PriceStale"),
"the health read fails closed rather than serving an old number: {error:#}"
);
let convert = CallContractArg {
contract_id: fixture.deployment.pool_id,
function_selector: Bytes(fn_selector!(repay_base_from_collateral(AssetId, u64))),
call_params: CallParams {
coins: 0,
asset_id: AssetId::default(),
gas: 30_000_000,
},
call_data: Some(Bytes(call_data!(fixture.base_asset, quantity))),
};
let signed = fixture.signed_calls_for(child_id, vec![convert]).await;
let error = submit(&fixture, &account, signed, 1)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("PriceStale"),
"the marked conversion stops with the rest: {error:#}"
);
let repay = CallContractArg {
contract_id: fixture.deployment.pool_id,
function_selector: Bytes(fn_selector!(repay())),
call_params: CallParams {
coins: quantity,
asset_id: fixture.base_asset,
gas: 30_000_000,
},
call_data: None,
};
let signed = fixture.signed_calls_for(child_id, vec![repay]).await;
submit(&fixture, &account, signed, 0).call().await.unwrap();
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
account
.methods()
.close_session(Vec::<crate::prop::PropOrderBookCleanup>::new())
.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();
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- parent_before,
COLLATERAL as u128,
"a quiet feed must never hold a debt-free user inside a session"
);
}
#[tokio::test]
async fn a_quiet_feed_strands_a_session_that_still_holds_the_asset() {
let fixture = PropFixture::new().await;
let tier_id = 23;
seed_variant(&fixture, tier_id, variant(0, 3_600)).await;
let (child_id, account) = armed_sibling(&fixture, 23).await;
open_on(&fixture, &account, tier_id, COLLATERAL)
.await
.unwrap();
assert_eq!(
fixture
.deployer
.try_provider()
.unwrap()
.get_contract_asset_balance(&child_id, &fixture.base_asset)
.await
.unwrap(),
0
);
fixture
.deployer
.force_transfer_to_contract(
child_id,
1,
fixture.base_asset,
TxPolicies::default(),
)
.await
.unwrap();
let error = account
.methods()
.close_session(Vec::<crate::prop::PropOrderBookCleanup>::new())
.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("PriceStale"),
"a held position is valued even on a voluntary close: {error:#}"
);
let keeper = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone());
let error = keeper
.methods()
.liquidate(child_id, vec![])
.with_contracts(&[
&account,
&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("PriceStale"),
"the keeper stops on the same read: {error:#}"
);
let error = keeper
.methods()
.expire_session(child_id, vec![])
.with_contracts(&[
&account,
&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("NotExpired"),
"the deadline is checked before the walk: {error:#}"
);
let provider = fixture.user.try_provider().unwrap().clone();
let expires_at = fixture
.deployment
.pool
.methods()
.get_session(child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.unwrap()
.expires_at;
let latest = provider.latest_block_time().await.unwrap().unwrap();
let jump = expires_at.saturating_sub(latest.timestamp() as u64) + 1;
provider
.produce_blocks(
1,
Some(
latest
.checked_add_signed(chrono::TimeDelta::seconds(jump as i64))
.unwrap(),
),
)
.await
.unwrap();
let error = keeper
.methods()
.expire_session(child_id, vec![])
.with_contracts(&[
&account,
&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("PriceStale"),
"past the deadline it stops on the mark too, so nobody can free it: {error:#}"
);
let chain_now = fixture
.user
.try_provider()
.unwrap()
.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_000u64.into(),
ask: 2_000_000_000_000_000_000u64.into(),
timestamp: chain_now,
}])
.call()
.await
.unwrap();
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
keeper
.methods()
.expire_session(child_id, vec![])
.with_contracts(&[
&account,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
assert!(
fixture
.deployment
.pool
.methods()
.get_session(child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
assert!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
> parent_before,
"and the collateral comes home once a publisher returns"
);
}