use super::*;
const FEED_PAUSER_ROLE: u64 = 1;
#[tokio::test]
async fn a_session_may_top_up_itself_but_never_another_session() {
let mut fixture = PropFixture::new().await;
let parent = Identity::Address(fixture.user.address());
let sibling = fixture
.deployment
.deploy_account(&fixture.user, parent, 41)
.await
.unwrap();
let sibling_id = sibling.contract_id();
let sibling_account = PropAccountContract::new(sibling_id, fixture.user.clone());
let expiry = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
+ 86_400;
sibling_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();
sibling_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();
fixture
.deployer
.force_transfer_to_contract(
sibling_id,
500_000,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let fund_other = CallContractArg {
contract_id: fixture.deployment.pool_id,
function_selector: Bytes(fn_selector!(add_collateral(ContractId))),
call_params: CallParams {
coins: 500_000,
asset_id: fixture.collateral_asset,
gas: 30_000_000,
},
call_data: Some(Bytes(call_data!(fixture.child_id))),
};
let (user_signature, cosigner_signature, calls) =
fixture.signed_calls_for(sibling_id, vec![fund_other]).await;
let error = sibling_account
.methods()
.call_contracts(user_signature, cosigner_signature, calls)
.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::EstimateMinimum)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("FundOtherAccount"),
"one session may not capitalise another: {error:#}"
);
assert_eq!(fixture.session().await.collateral, COLLATERAL);
fixture.fund_collateral(500_000).await;
assert_eq!(fixture.session().await.collateral, COLLATERAL + 500_000);
assert_eq!(
fixture.session().await.capitalised,
0,
"an outside rescue is not the user's own profit and bears no share"
);
}
#[tokio::test]
async fn the_tier_catalogue_refuses_a_version_it_never_published() {
let fixture = PropFixture::new().await;
let pool = &fixture.deployment.pool;
assert_eq!(
pool.methods()
.tier_books(TIER_ID, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
vec![fixture.order_book.contract_id]
);
let assets = pool
.methods()
.tier_assets(TIER_ID, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert!(assets.contains(&fixture.base_asset));
assert!(assets.contains(&fixture.collateral_asset));
for (name, error) in [
(
"tier_assets",
pool.methods()
.tier_assets(TIER_ID, 99)
.simulate(Execution::state_read_only())
.await
.unwrap_err(),
),
(
"tier_books",
pool.methods()
.tier_books(TIER_ID, 99)
.simulate(Execution::state_read_only())
.await
.unwrap_err(),
),
(
"tier_assets on an unknown tier",
pool.methods()
.tier_assets(4_242, 1)
.simulate(Execution::state_read_only())
.await
.unwrap_err(),
),
] {
assert!(
error.to_string().contains("InvalidTierVersion"),
"{name} must refuse rather than answer empty: {error:#}"
);
}
let roles = pool
.methods()
.get_roles(Identity::Address(fixture.deployer.address()))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(roles & 1, 1, "the deployer holds DEFAULT_ADMIN_ROLE");
assert_eq!(
pool.methods()
.get_roles(Identity::Address(fixture.outsider.address()))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
0
);
}
#[tokio::test]
async fn a_tier_may_admit_a_market_before_its_publisher_is_live() {
let fixture = PropFixture::new().await;
let quiet_asset = AssetId::new([0xc1; 32]);
let quiet_book = OrderBookDeploy::deploy(
&fixture.deployer,
quiet_asset,
fixture.collateral_asset,
&OrderBookDeployConfig::default(),
)
.await
.unwrap();
fixture
.deployment
.price_feed
.methods()
.set_asset_decimals(quiet_asset, 9)
.call()
.await
.unwrap();
fixture
.deployment
.pool
.methods()
.add_books(TIER_ID, vec![quiet_book.contract_id])
.with_contract_ids(&[quiet_book.contract_id, fixture.deployment.price_feed_id])
.call()
.await
.unwrap();
let assets = fixture
.deployment
.pool
.methods()
.session_assets(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert!(
assets.contains(&quiet_asset),
"the live session's walk now covers an asset nobody quotes"
);
let lines = fixture
.deployment
.pool
.methods()
.get_session_lines(fixture.child_id)
.with_contract_ids(&[
fixture.child_id,
fixture.order_book.contract_id,
quiet_book.contract_id,
fixture.deployment.price_feed_id,
])
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(lines.threshold, U256::from(8_250_000u64));
let response = fixture
.account
.methods()
.close_session(Vec::<crate::prop::PropOrderBookCleanup>::new())
.with_contracts(&[
&fixture.deployment.pool,
&fixture.deployment.oracle,
&fixture.order_book.order_book,
&quiet_book.order_book,
&fixture.deployment.price_feed,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
let closed = fixture.closed_event(&response);
assert_eq!(closed.reason, SettlementReason::UserClose);
assert_eq!(closed.user_net, COLLATERAL);
}
#[tokio::test]
async fn a_retracted_price_is_refused_rather_than_marked_at_zero() {
let mut fixture = PropFixture::new().await;
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, 100_000_000u64)),
);
fixture.call_account(vec![borrow], 1).await.unwrap();
assert!(!fixture.is_liquidatable().await);
fixture
.deployment
.price_feed
.methods()
.grant_role(
FEED_PAUSER_ROLE,
Identity::Address(fixture.deployer.address()),
)
.call()
.await
.unwrap();
fixture
.deployment
.price_feed
.methods()
.invalidate_offchain_price(fixture.base_asset)
.call()
.await
.unwrap();
let error = fixture
.deployment
.pool
.methods()
.get_session_lines(fixture.child_id)
.with_contract_ids(&[
fixture.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("PriceMissing"),
"a retracted price is an absence, not a mark of zero: {error:#}"
);
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.liquidate(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
.unwrap_err();
assert!(
error.to_string().contains("PriceMissing"),
"a retraction must not hand the keeper a free seizure: {error:#}"
);
let repay =
fixture.pool_call(fn_selector!(repay()), 100_000_000, fixture.base_asset, None);
fixture.call_account(vec![repay], 0).await.unwrap();
assert_eq!(fixture.debt(fixture.base_asset).await, 0);
fixture.close_session().await;
}