use super::*;
use crate::prop::AutoProlongChanged;
fn arm_call(fixture: &PropFixture, period: Option<ProlongPeriod>) -> CallContractArg {
fixture.pool_call(
fn_selector!(set_auto_prolong(Option<ProlongPeriod>)),
0,
AssetId::default(),
Some(call_data!(period)),
)
}
fn prolong_period_call(
fixture: &PropFixture,
period: ProlongPeriod,
times: u64,
) -> CallContractArg {
fixture.pool_call(
fn_selector!(prolong_session(ProlongPeriod, u64)),
0,
AssetId::default(),
Some(call_data!(period, times)),
)
}
async fn discount_every_premium(fixture: &PropFixture, bps: u64) {
fixture
.deployment
.pool
.methods()
.grant_role(
DISCOUNT_MANAGER_ROLE,
Identity::Address(fixture.deployer.address()),
)
.call()
.await
.unwrap();
fixture
.deployment
.pool
.methods()
.set_user_discount(
Identity::Address(fixture.user.address()),
None,
Some(UserDiscount {
discount_bps: bps,
scope: DiscountScope::AllPremiums,
}),
)
.call()
.await
.unwrap();
}
async fn approach_deadline(fixture: &PropFixture, seconds: u64) {
let session = fixture.session().await;
let provider = fixture.user.try_provider().unwrap().clone();
let latest = provider.latest_block_time().await.unwrap().unwrap();
let target = session.expires_at - seconds;
let jump = target.saturating_sub(latest.timestamp() as u64);
provider
.produce_blocks(
1,
Some(
latest
.checked_add_signed(chrono::TimeDelta::seconds(jump as i64))
.unwrap(),
),
)
.await
.unwrap();
}
#[tokio::test]
async fn an_auto_extension_can_be_armed_only_on_a_short_period_the_tier_sells() {
let mut fixture = PropFixture::new().await;
fixture
.call_account(
vec![prolong_period_call(&fixture, ProlongPeriod::Week, 1)],
1,
)
.await
.unwrap();
for period in [ProlongPeriod::Week, ProlongPeriod::Month] {
let error = fixture
.call_account(vec![arm_call(&fixture, Some(period.clone()))], 1)
.await
.unwrap_err();
assert!(
error.to_string().contains("AutoProlongPeriodInvalid"),
"{period:?} is too long to hand to a keeper: {error:#}"
);
}
let error = fixture
.call_account(vec![arm_call(&fixture, Some(ProlongPeriod::SixHours))], 1)
.await
.unwrap_err();
assert!(
error.to_string().contains("PeriodNotOffered"),
"a term the tier does not sell cannot be renewed: {error:#}"
);
let response = fixture
.call_account(vec![arm_call(&fixture, Some(ProlongPeriod::Day))], 1)
.await
.unwrap();
let armed = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<AutoProlongChanged>(&response.tx_status.receipts)
.unwrap();
assert_eq!(armed.len(), 1);
assert_eq!(armed[0].period, Some(ProlongPeriod::Day));
assert_eq!(armed[0].account, fixture.child_id);
assert!(armed[0].timestamp.unix != 0);
let prolonged = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionProlonged>(&response.tx_status.receipts)
.unwrap();
assert!(
prolonged.is_empty(),
"arming outside the window charges nothing"
);
assert_eq!(
fixture.session().await.auto_prolong,
Some(ProlongPeriod::Day)
);
let response = fixture
.call_account(vec![arm_call(&fixture, None)], 1)
.await
.unwrap();
let armed = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<AutoProlongChanged>(&response.tx_status.receipts)
.unwrap();
assert_eq!(armed[0].period, None);
assert_eq!(fixture.session().await.auto_prolong, None);
}
#[tokio::test]
async fn arming_inside_the_window_extends_and_pays_in_the_same_call() {
let mut fixture = PropFixture::new().await;
approach_deadline(&fixture, 30).await;
let before = fixture.session().await;
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let response = fixture
.call_account(vec![arm_call(&fixture, Some(ProlongPeriod::Day))], 1)
.await
.unwrap();
let armed = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<AutoProlongChanged>(&response.tx_status.receipts)
.unwrap();
assert_eq!(armed.len(), 1);
assert_eq!(
armed[0].expires_at, before.expires_at,
"the arming event reports the deadline it found, not the one it bought"
);
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].trigger, ProlongTrigger::Auto);
assert_eq!(prolonged[0].times, 1);
assert_eq!(prolonged[0].seconds, 86_400);
assert_eq!(prolonged[0].fee, 6_000);
assert_eq!(prolonged[0].new_expires_at, before.expires_at + 86_400);
let after = fixture.session().await;
assert_eq!(after.expires_at, before.expires_at + 86_400);
assert_eq!(after.fees_accrued, 6_000);
assert_eq!(
after.credit_line,
before.credit_line - 6_000,
"the fee shrinks the line one for one"
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
6_000,
"and it leaves for the platform inside the same call"
);
}
#[tokio::test]
async fn a_keeper_extends_an_armed_session_and_only_once_it_is_due() {
let mut fixture = PropFixture::new().await;
let keeper = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone());
let error = keeper
.methods()
.auto_prolong_session(fixture.child_id)
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AutoProlongDisabled"),
"renewal is opt-in: {error:#}"
);
fixture
.call_account(
vec![prolong_period_call(&fixture, ProlongPeriod::Week, 1)],
1,
)
.await
.unwrap();
fixture
.call_account(vec![arm_call(&fixture, Some(ProlongPeriod::Day))], 1)
.await
.unwrap();
let error = keeper
.methods()
.auto_prolong_session(fixture.child_id)
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AutoProlongNotDue"),
"an armed session is not a standing invitation: {error:#}"
);
approach_deadline(&fixture, 30).await;
let before = fixture.session().await;
let response = keeper
.methods()
.auto_prolong_session(fixture.child_id)
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
let prolonged = response
.decode_logs_with_type::<SessionProlonged>()
.unwrap();
assert_eq!(prolonged.len(), 1);
assert_eq!(prolonged[0].trigger, ProlongTrigger::Auto);
assert_eq!(prolonged[0].new_expires_at, before.expires_at + 86_400);
let error = keeper
.methods()
.auto_prolong_session(fixture.child_id)
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AutoProlongNotDue"),
"one call per window, or the fee is a faucet for griefers: {error:#}"
);
}
#[tokio::test]
async fn prolonging_charges_times_the_price_and_buys_times_the_term() {
let mut fixture = PropFixture::new().await;
let before = fixture.session().await;
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let error = fixture
.call_account(
vec![prolong_period_call(&fixture, ProlongPeriod::Day, 0)],
1,
)
.await
.unwrap_err();
assert!(
error.to_string().contains("TimesIsZero"),
"buying nothing is refused rather than logged: {error:#}"
);
let error = fixture
.call_account(
vec![prolong_period_call(&fixture, ProlongPeriod::SixHours, 1)],
1,
)
.await
.unwrap_err();
assert!(
error.to_string().contains("PeriodNotOffered"),
"a free term is an entry perk, not a renewal price: {error:#}"
);
let response = fixture
.call_account(
vec![prolong_period_call(&fixture, ProlongPeriod::Day, 3)],
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].trigger, ProlongTrigger::User);
assert_eq!(prolonged[0].times, 3);
assert_eq!(prolonged[0].seconds, 3 * 86_400);
assert_eq!(prolonged[0].fee, 3 * 6_000);
assert_eq!(prolonged[0].fees_accrued, 18_000);
assert_eq!(prolonged[0].new_expires_at, before.expires_at + 3 * 86_400);
assert_eq!(
prolonged[0].new_credit_line,
before.credit_line - 18_000,
"the line prices off the collateral NET of every fee charged"
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
18_000
);
let mut expected = 18_000u64;
for _ in 0..3 {
fixture
.call_account(
vec![prolong_period_call(&fixture, ProlongPeriod::Day, 1)],
1,
)
.await
.unwrap();
expected += 6_000;
}
let after = fixture.session().await;
assert_eq!(after.fees_accrued, expected);
assert_eq!(after.expires_at, before.expires_at + 6 * 86_400);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
expected as u128
);
let error = fixture
.call_account(
vec![prolong_period_call(
&fixture,
ProlongPeriod::Month,
8_000_000_000_000,
)],
1,
)
.await
.unwrap_err();
assert!(
error.to_string().contains("ProlongAboveCollateral"),
"the bound answers before the arithmetic can overflow: {error:#}"
);
let error = fixture
.call_account(
vec![prolong_period_call(&fixture, ProlongPeriod::Month, 3)],
1,
)
.await
.unwrap_err();
assert!(
error.to_string().contains("ProlongAboveCollateral"),
"the platform is never paid out of another session's collateral: {error:#}"
);
}
async fn overshoot_deadline(fixture: &PropFixture, past: u64) {
let session = fixture.session().await;
let provider = fixture.user.try_provider().unwrap().clone();
let latest = provider.latest_block_time().await.unwrap().unwrap();
let target = session.expires_at + past;
let jump = target.saturating_sub(latest.timestamp() as u64);
provider
.produce_blocks(
1,
Some(
latest
.checked_add_signed(chrono::TimeDelta::seconds(jump as i64))
.unwrap(),
),
)
.await
.unwrap();
}
async fn 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?)
}
#[tokio::test]
async fn an_armed_and_funded_session_is_renewed_at_expiry_rather_than_absorbed() {
let mut fixture = PropFixture::new().await;
fixture
.call_account(vec![arm_call(&fixture, Some(ProlongPeriod::Day))], 1)
.await
.unwrap();
let before = fixture.session().await;
assert_eq!(before.auto_prolong, Some(ProlongPeriod::Day));
assert_eq!(before.fees_accrued, 0);
let platform_before = fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
overshoot_deadline(&fixture, 1).await;
let response = expire(&fixture).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].trigger, ProlongTrigger::Auto);
assert_eq!(prolonged[0].times, 1);
assert_eq!(prolonged[0].seconds, 86_400);
assert_eq!(prolonged[0].fee, 6_000);
let closed = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionClosed>(&response.tx_status.receipts)
.unwrap();
assert!(
closed.is_empty(),
"the expiry call renewed the session; nothing was settled"
);
let seized = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionSeized>(&response.tx_status.receipts)
.unwrap();
assert!(seized.is_empty(), "and no basket was routed anywhere");
let after = fixture.session().await;
assert_eq!(
after.session_id, before.session_id,
"the same session lives on"
);
assert_eq!(after.fees_accrued, 6_000);
assert_eq!(after.collateral, before.collateral);
assert_eq!(after.auto_prolong, Some(ProlongPeriod::Day));
assert_eq!(
after.expires_at,
before.expires_at + 86_400,
"one period, measured from the deadline it replaces"
);
assert_eq!(
prolonged[0].new_expires_at, after.expires_at,
"the event carries the deadline the queue must re-key to"
);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- platform_before,
6_000,
"and the renewal is paid for like any other"
);
}
#[tokio::test]
async fn the_renewal_at_expiry_leaves_the_users_resting_orders_alone() {
let mut fixture = PropFixture::new().await;
let draw = fixture.draw_call(1_000_000);
let rest = fixture.book_call(
1_000_000,
1_000_000_000,
fixture.collateral_asset,
1_000_000,
);
fixture.call_account(vec![draw, rest], 1).await.unwrap();
fixture
.call_account(vec![arm_call(&fixture, Some(ProlongPeriod::Day))], 1)
.await
.unwrap();
let locked = fixture
.order_book
.order_book
.methods()
.get_total_balance_of(Identity::ContractId(fixture.child_id))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_ne!(locked, (0, 0), "the account has something resting");
overshoot_deadline(&fixture, 1).await;
let orders = fixture
.order_book
.order_book
.methods()
.get_orders_at_price(crate::order_book_deploy::Side::Buy, 1_000_000)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
let cleanup = crate::prop::PropOrderBookCleanup {
order_book: fixture.order_book.contract_id,
order_ids: orders
.into_iter()
.filter(|o| o.trader_id == Identity::ContractId(fixture.child_id))
.map(|o| o.order_id)
.collect(),
};
assert_eq!(
cleanup.order_ids.len(),
1,
"the keeper is handed a real order"
);
fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.expire_session(fixture.child_id, vec![cleanup])
.with_contracts(&[
&fixture.account,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&fixture.deployment.price_feed,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
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,
locked,
"a renewed session keeps its book exactly as it was, even when the \
caller handed the pool an order to cancel"
);
assert_eq!(
fixture.session().await.drawn_quote,
1_000_000,
"and its draw"
);
}
#[tokio::test]
async fn a_late_renewal_buys_every_elapsed_period_and_is_billed_for_all_of_them() {
let mut fixture = PropFixture::new().await;
fixture
.call_account(vec![arm_call(&fixture, Some(ProlongPeriod::Day))], 1)
.await
.unwrap();
let before = fixture.session().await;
overshoot_deadline(&fixture, 3 * 86_400).await;
let response = expire(&fixture).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].times, 4);
assert_eq!(prolonged[0].seconds, 4 * 86_400);
assert_eq!(prolonged[0].fee, 24_000, "four periods at the tier's price");
let chain_now = fixture
.user
.try_provider()
.unwrap()
.latest_block_time()
.await
.unwrap()
.unwrap()
.timestamp() as u64;
let after = fixture.session().await;
assert_eq!(
after.expires_at,
before.expires_at + 4 * 86_400,
"the term extends the OLD deadline, not the moment the keeper woke up"
);
assert!(
after.expires_at > chain_now,
"and still clears now, or the seat could be charged again at once: {} vs {chain_now}",
after.expires_at
);
assert!(
after.expires_at <= chain_now + 86_400,
"with only the unelapsed remainder left to run"
);
assert_eq!(after.fees_accrued, 24_000);
let error = expire(&fixture).await.unwrap_err();
assert!(
error.to_string().contains("NotExpired"),
"a renewed session is live, so nobody can charge it again: {error:#}"
);
assert_eq!(fixture.session().await.fees_accrued, 24_000);
}
#[tokio::test]
async fn a_session_that_cannot_cover_the_gap_pays_for_what_it_can_and_is_absorbed() {
let mut fixture = PropFixture::new().await;
fixture
.call_account(
vec![
arm_call(&fixture, Some(ProlongPeriod::Day)),
prolong_period_call(&fixture, ProlongPeriod::Day, 330),
],
1,
)
.await
.unwrap();
let starved = fixture.session().await;
assert_eq!(starved.fees_accrued, 1_980_000);
overshoot_deadline(&fixture, 10 * 86_400).await;
let response = expire(&fixture).await.unwrap();
let prolonged = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionProlonged>(&response.tx_status.receipts)
.unwrap();
assert_eq!(prolonged.len(), 1, "the partial charge is a real charge");
assert_eq!(prolonged[0].times, 3);
assert_eq!(prolonged[0].fee, 18_000);
let closed = fixture.closed_event(&response);
assert_eq!(
closed.reason,
SettlementReason::Expiry,
"charged for what it could, then absorbed in the same call"
);
assert_eq!(
closed.fees_accrued, 1_998_000,
"the three periods it could pay for are on the settlement"
);
assert_eq!(
closed.user_net, 2_000,
"and only the unbillable remainder comes back"
);
assert!(
fixture
.deployment
.pool
.methods()
.get_session(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none(),
"the seat is gone all the same"
);
}
#[tokio::test]
async fn an_armed_session_that_cannot_pay_the_renewal_is_absorbed_as_before() {
let mut fixture = PropFixture::new().await;
fixture
.call_account(
vec![
arm_call(&fixture, Some(ProlongPeriod::Day)),
prolong_period_call(&fixture, ProlongPeriod::Day, 333),
],
1,
)
.await
.unwrap();
let starved = fixture.session().await;
assert_eq!(starved.fees_accrued, 1_998_000);
assert_eq!(starved.auto_prolong, Some(ProlongPeriod::Day));
overshoot_deadline(&fixture, 1).await;
let response = expire(&fixture).await.unwrap();
let closed = fixture.closed_event(&response);
assert_eq!(closed.reason, SettlementReason::Expiry);
assert_eq!(closed.fees_accrued, 1_998_000);
assert_eq!(
closed.user_net, 2_000,
"what the collateral had left after the premiums it did pay"
);
assert!(
fixture
.deployment
.pool
.methods()
.get_session(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none(),
"the seat is gone, as it would have been before this change"
);
}
#[tokio::test]
async fn a_discounted_session_is_renewed_on_the_count_its_discount_buys() {
let mut fixture = PropFixture::new().await;
fixture
.call_account(
vec![
arm_call(&fixture, Some(ProlongPeriod::Day)),
prolong_period_call(&fixture, ProlongPeriod::Day, 330),
],
1,
)
.await
.unwrap();
let before = fixture.session().await;
assert_eq!(before.fees_accrued, 1_980_000);
discount_every_premium(&fixture, 5_000).await;
overshoot_deadline(&fixture, 4 * 86_400).await;
let response = expire(&fixture).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].times, 5,
"five periods — the count list price would have refused"
);
assert_eq!(
prolonged[0].fee, 15_000,
"and billed at the discount that allowed them"
);
let closed = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionClosed>(&response.tx_status.receipts)
.unwrap();
assert!(
closed.is_empty(),
"the seat survives: judging this at list price would have absorbed it"
);
let after = fixture.session().await;
assert_eq!(after.expires_at, before.expires_at + 5 * 86_400);
assert_eq!(after.fees_accrued, 1_995_000);
}
#[tokio::test]
async fn a_wholly_forgiven_premium_renews_a_session_that_could_not_pay_a_cent() {
let mut fixture = PropFixture::new().await;
fixture
.call_account(
vec![
arm_call(&fixture, Some(ProlongPeriod::Day)),
prolong_period_call(&fixture, ProlongPeriod::Day, 333),
],
1,
)
.await
.unwrap();
let before = fixture.session().await;
assert_eq!(before.fees_accrued, 1_998_000);
discount_every_premium(&fixture, 10_000).await;
overshoot_deadline(&fixture, 3 * 86_400).await;
let response = expire(&fixture).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].times, 4,
"the whole gap, because none of it costs anything"
);
assert_eq!(prolonged[0].fee, 0);
let after = fixture.session().await;
assert_eq!(after.expires_at, before.expires_at + 4 * 86_400);
assert_eq!(
after.fees_accrued, 1_998_000,
"a free renewal accrues nothing, so the line does not move either"
);
assert_eq!(after.credit_line, before.credit_line);
}
#[tokio::test]
async fn a_renewal_the_pool_cannot_fund_absorbs_instead_of_reverting() {
let mut fixture = PropFixture::new().await;
let (child_id, account, collateral) = starved_pool_session(&mut fixture, 1_000).await;
assert_eq!(
fixture.pool_balance(fixture.collateral_asset).await,
1_000,
"the pool now holds less than the 6_000 renewal fee"
);
overshoot_child_deadline(&fixture, child_id, 1).await;
let response = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.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();
let closed = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionClosed>(&response.tx_status.receipts)
.unwrap();
assert_eq!(
closed.len(),
1,
"the session was absorbed, not left stranded"
);
assert_eq!(closed[0].reason, SettlementReason::Expiry);
assert_eq!(closed[0].bad_debt, 0);
assert_eq!(closed[0].user_net, collateral);
let prolonged = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionProlonged>(&response.tx_status.receipts)
.unwrap();
assert!(
prolonged.is_empty(),
"a pool that cannot pay the fee must not have renewed"
);
assert!(
fixture
.deployment
.pool
.methods()
.get_session(child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
}
#[tokio::test]
async fn the_pools_float_caps_the_partial_charge_before_the_collateral_does() {
let mut fixture = PropFixture::new().await;
let (child_id, account, _collateral) =
starved_pool_session(&mut fixture, 13_000).await;
overshoot_child_deadline(&fixture, child_id, 4 * 86_400).await;
let response = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.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();
let prolonged = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionProlonged>(&response.tx_status.receipts)
.unwrap();
assert_eq!(prolonged.len(), 1, "what the float could fund was charged");
assert_eq!(
prolonged[0].times, 2,
"two periods — the collateral would have carried all five"
);
assert_eq!(prolonged[0].fee, 12_000);
let closed = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionClosed>(&response.tx_status.receipts)
.unwrap();
assert_eq!(closed.len(), 1, "and the rest of the gap it could not fund");
assert_eq!(closed[0].reason, SettlementReason::Expiry);
assert_eq!(closed[0].bad_debt, 0);
assert!(
fixture
.deployment
.pool
.methods()
.get_session(child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
}
async fn starved_pool_session(
fixture: &mut PropFixture,
float_left: u64,
) -> (ContractId, PropAccountContract<Wallet>, u64) {
let wide = TierParams {
line: 200_000_000,
leverage: 5,
duration: 64_800,
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: u64::MAX,
open_fee: 0,
profit_share_bps: 1_000,
price_band_bps: 1_000,
};
let tier_id = 31;
fixture
.deployment
.pool
.methods()
.publish_tier_version(tier_id, wide, vec![fixture.order_book.contract_id])
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap();
let parent = Identity::Address(fixture.user.address());
let child = fixture
.deployment
.deploy_account(&fixture.user, parent, 31)
.await
.unwrap();
let child_id = child.contract_id();
let account = PropAccountContract::new(child_id, fixture.user.clone());
let key_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: key_expiry },
contract_ids: vec![],
})
.with_contract_ids(&[fixture.deployment.oracle_id])
.call()
.await
.unwrap();
let collateral = 40_000_000u64;
account
.methods()
.start_session(tier_id, collateral, ProlongPeriod::SixHours)
.call_params(CallParameters::new(
collateral,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contract_ids(&[
fixture.deployment.oracle_id,
fixture.deployment.pool_id,
fixture.deployment.registry_id,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
let float = fixture.pool_balance(fixture.collateral_asset).await;
let draw = float - float_left;
let calls = vec![
fixture.pool_call(
fn_selector!(set_auto_prolong(Option<ProlongPeriod>)),
0,
AssetId::default(),
Some(call_data!(Some(ProlongPeriod::Day))),
),
fixture.pool_call(
fn_selector!(draw(u64)),
0,
AssetId::default(),
Some(call_data!(draw)),
),
];
let signed = fixture.signed_calls_for(child_id, calls).await;
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(2))
.call()
.await
.unwrap();
assert_eq!(
fixture.pool_balance(fixture.collateral_asset).await,
float_left,
"the draw left the pool exactly the float the caller asked for"
);
(child_id, account, collateral)
}
async fn overshoot_child_deadline(fixture: &PropFixture, child: ContractId, past: u64) {
let session = fixture
.deployment
.pool
.methods()
.get_session(child)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.unwrap();
let provider = fixture.user.try_provider().unwrap().clone();
let latest = provider.latest_block_time().await.unwrap().unwrap();
let jump = (session.expires_at + past).saturating_sub(latest.timestamp() as u64);
provider
.produce_blocks(
1,
Some(
latest
.checked_add_signed(chrono::TimeDelta::seconds(jump as i64))
.unwrap(),
),
)
.await
.unwrap();
}