use super::*;
use crate::prop::{
MarginPoolPlatformPayoutChanged,
MarginPoolPriceFeedChanged,
MarginPoolRegistryChanged,
TierBooksAdded,
TurboTerms,
};
async fn fixture_tier(fixture: &PropFixture) -> TierParams {
fixture
.deployment
.pool
.methods()
.get_tier(TIER_ID, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.unwrap()
}
async fn extra_book(
fixture: &PropFixture,
base: AssetId,
decimals: u8,
) -> OrderBookDeploy<Wallet> {
let book = OrderBookDeploy::deploy(
&fixture.deployer,
base,
fixture.collateral_asset,
&OrderBookDeployConfig::default(),
)
.await
.unwrap();
fixture
.deployment
.price_feed
.methods()
.set_asset_decimals(base, decimals)
.call()
.await
.unwrap();
fixture
.deployment
.price_feed
.methods()
.publish_prices(vec![PriceInput {
asset: base,
bid: 3_000_000_000_000_000_000u64.into(),
ask: 3_000_000_000_000_000_000u64.into(),
timestamp: fixture.next_publish_time(),
}])
.call()
.await
.unwrap();
book
}
async fn publish_tier(
fixture: &PropFixture,
tier_id: u64,
params: TierParams,
) -> Result<()> {
fixture
.deployment
.pool
.methods()
.publish_tier_v2(
tier_id,
params,
publication_terms(),
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await?;
Ok(())
}
fn publication_terms() -> TurboTerms {
TurboTerms {
max_loss_bps: [2_000; 4],
rollover_profit_bps: 0,
max_rollovers: 0,
}
}
async fn prepaid_tier(fixture: &PropFixture) -> TierParams {
TierParams {
prolong_fee: [0; 4],
profit_share_bps: 0,
..fixture_tier(fixture).await
}
}
#[tokio::test]
async fn a_tier_the_pool_could_not_price_is_refused_at_publication() {
let fixture = PropFixture::new().await;
let base = prepaid_tier(&fixture).await;
publish_tier(&fixture, 8, base.clone()).await.unwrap();
let cases: Vec<(TierParams, &str, &str)> = vec![
(
TierParams {
line: 0,
..base.clone()
},
"InvalidTierParams",
"a tier that lends nothing",
),
(
TierParams {
maintenance_bps: 10_001,
open_buffer_bps: 10_002,
..base.clone()
},
"InvalidTierParams",
"a maintenance margin past the whole line",
),
(
TierParams {
open_buffer_bps: 10_001,
..base.clone()
},
"InvalidTurboTerms",
"a freeze buffer past the whole line",
),
(
TierParams {
liq_price_factor: 0,
..base.clone()
},
"InvalidTierParams",
"a zero haircut factor divides by zero on the debt leg",
),
(
TierParams {
liq_price_factor: 10_001,
..base.clone()
},
"InvalidTierParams",
"a haircut that PAYS the seizure",
),
(
TierParams {
max_credit_line_bps: 10_000,
..base.clone()
},
"InvalidTierParams",
"a cap at the line leaves added collateral buying nothing",
),
(
TierParams {
profit_share_bps: 10_001,
..base.clone()
},
"InvalidTurboTerms",
"a share past the whole profit",
),
(
TierParams {
price_band_bps: 10_001,
..base.clone()
},
"InvalidTierParams",
"an admission band wider than the price",
),
(
TierParams {
leverage: 0,
..base.clone()
},
"InvalidLeverage",
"leverage divides the line",
),
(
TierParams {
leverage: base.line + 1,
..base.clone()
},
"InvalidLeverage",
"leverage past the line makes the entry ticket zero",
),
(
TierParams {
open_buffer_bps: base.maintenance_bps,
..base.clone()
},
"BufferBelowMaintenance",
"the freeze line must sit ABOVE the liquidation line",
),
(
TierParams {
leverage: 1_000,
max_credit_line_bps: 10_100,
..base.clone()
},
"BufferAboveCap",
"a session cannot be born above its own cap",
),
];
for (params, expected, why) in cases {
let error = publish_tier(&fixture, 9, params).await.unwrap_err();
assert!(
error.to_string().contains(expected),
"{why}: expected {expected}, got {error:#}"
);
}
let error = fixture
.deployment
.pool
.methods()
.publish_tier_v2(9, base.clone(), publication_terms(), vec![])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidBook"),
"a tier needs at least one book: {error:#}"
);
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.publish_tier_v2(
9,
base.clone(),
publication_terms(),
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("NotAuthorized")
|| error.to_string().contains("AccessControl"),
"only the admin publishes tiers: {error:#}"
);
assert!(
fixture
.deployment
.pool
.methods()
.current_tier_version(9)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
}
#[tokio::test]
async fn a_tier_admits_only_fresh_books_the_feed_can_scale() {
let fixture = PropFixture::new().await;
let error = fixture
.deployment
.pool
.methods()
.add_books(77, vec![fixture.order_book.contract_id])
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidTier"),
"books belong to a published tier: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.add_books(TIER_ID, vec![ContractId::zeroed()])
.with_contract_ids(&[fixture.deployment.price_feed_id])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidBook"),
"the zero contract is not a book: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.add_books(TIER_ID, vec![fixture.order_book.contract_id])
.with_contract_ids(&[
fixture.order_book.contract_id,
fixture.deployment.price_feed_id,
])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidBook"),
"a book joins a tier once: {error:#}"
);
let foreign_quote = AssetId::new([0x51; 32]);
let foreign = OrderBookDeploy::deploy(
&fixture.deployer,
AssetId::new([0x52; 32]),
foreign_quote,
&OrderBookDeployConfig::default(),
)
.await
.unwrap();
let error = fixture
.deployment
.pool
.methods()
.add_books(TIER_ID, vec![foreign.contract_id])
.with_contract_ids(&[foreign.contract_id, fixture.deployment.price_feed_id])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidBook"),
"every tier book settles in the collateral asset: {error:#}"
);
let unscaled = OrderBookDeploy::deploy(
&fixture.deployer,
AssetId::new([0x53; 32]),
fixture.collateral_asset,
&OrderBookDeployConfig::default(),
)
.await
.unwrap();
let error = fixture
.deployment
.pool
.methods()
.add_books(TIER_ID, vec![unscaled.contract_id])
.with_contract_ids(&[unscaled.contract_id, fixture.deployment.price_feed_id])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("AssetNotSupported"),
"the feed must know the asset's scale before the tier admits it: {error:#}"
);
let good = extra_book(&fixture, AssetId::new([0x54; 32]), 8).await;
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.add_books(TIER_ID, vec![good.contract_id])
.with_contract_ids(&[good.contract_id, fixture.deployment.price_feed_id])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("NotAuthorized")
|| error.to_string().contains("AccessControl"),
"only the admin widens a tier: {error:#}"
);
}
#[tokio::test]
async fn a_book_added_mid_session_becomes_tradable_inside_it() {
let fixture = PropFixture::new().await;
let second_base = AssetId::new([0x61; 32]);
let second = extra_book(&fixture, second_base, 9).await;
let before = fixture
.deployment
.pool
.methods()
.session_books(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(before, vec![fixture.order_book.contract_id]);
assert!(
!fixture
.deployment
.pool
.methods()
.is_call_allowed(fixture.child_id, second.contract_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let response = fixture
.deployment
.pool
.methods()
.add_books(TIER_ID, vec![second.contract_id])
.with_contract_ids(&[second.contract_id, fixture.deployment.price_feed_id])
.call()
.await
.unwrap();
let added = response.decode_logs_with_type::<TierBooksAdded>().unwrap();
assert_eq!(added.len(), 1);
assert_eq!(added[0].tier_id, TIER_ID);
assert_eq!(
added[0].version, 1,
"appended to the live version, not a new one"
);
assert_eq!(added[0].books, vec![second.contract_id]);
assert_eq!(
added[0].new_assets,
vec![second_base],
"the collateral asset was already in the tier, so only the base is new"
);
let after = fixture
.deployment
.pool
.methods()
.session_books(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(after.len(), 2);
assert!(after.contains(&second.contract_id));
let assets = fixture
.deployment
.pool
.methods()
.session_assets(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert!(assets.contains(&second_base));
assert!(
fixture
.deployment
.pool
.methods()
.is_call_allowed(fixture.child_id, second.contract_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
"and the account may now route orders to it"
);
let lines = fixture
.deployment
.pool
.methods()
.get_session_lines(fixture.child_id)
.with_contract_ids(&[
fixture.child_id,
fixture.order_book.contract_id,
second.contract_id,
fixture.deployment.price_feed_id,
])
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(lines.threshold, U256::from(8_250_000u64));
}
#[tokio::test]
async fn a_live_session_keeps_the_tier_version_it_pinned() {
let fixture = PropFixture::new().await;
let original = fixture_tier(&fixture).await;
assert_eq!(fixture.session().await.tier_version, 1);
let successor = TierParams {
maintenance_bps: 5_000,
open_buffer_bps: 6_000,
profit_share_bps: 0,
prolong_fee: [0; 4],
..original.clone()
};
publish_tier(&fixture, TIER_ID, successor).await.unwrap();
assert_eq!(
fixture
.deployment
.pool
.methods()
.current_tier_version(TIER_ID)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(2)
);
let session = fixture.session().await;
assert_eq!(session.tier_version, 1, "the live session did not move");
assert_eq!(
fixture
.deployment
.pool
.methods()
.get_tier(TIER_ID, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(original),
"version 1 is immutable once published"
);
let lines = fixture.lines().await;
assert_eq!(lines.threshold, U256::from(8_250_000u64));
}
#[tokio::test]
async fn a_pause_stops_new_risk_and_leaves_every_exit_open() {
let mut fixture = PropFixture::new().await;
let draw = fixture.draw_call(1_000_000);
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![draw, borrow], 2).await.unwrap();
fixture
.deployment
.pool
.methods()
.pause()
.call()
.await
.unwrap();
assert!(
fixture
.deployment
.pool
.methods()
.is_paused()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let blocked: Vec<(&str, CallContractArg, usize)> = vec![
("draw", fixture.draw_call(1), 1),
(
"borrow",
fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, 1u64)),
),
1,
),
(
"withdraw",
fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, 1u64)),
),
2,
),
("prolong_session", fixture.prolong_call(1), 1),
(
"repay_base_from_collateral",
fixture.repay_base_from_collateral_call(fixture.base_asset, 1),
1,
),
];
for (name, call, outputs) in blocked {
let error = fixture.call_account(vec![call], outputs).await.unwrap_err();
assert!(
error.to_string().contains("Paused"),
"{name} must stop while the pool is paused: {error:#}"
);
}
let error = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.add_collateral(fixture.child_id)
.call_params(CallParameters::new(
1_000,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("Paused"),
"add_collateral is gated with the rest: {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("Paused"),
"a paused pool carries its underwater sessions: {error:#}"
);
let repay_in_kind =
fixture.pool_call(fn_selector!(repay()), 100_000_000, fixture.base_asset, None);
fixture.call_account(vec![repay_in_kind], 0).await.unwrap();
assert_eq!(fixture.debt(fixture.base_asset).await, 0);
let net = fixture.repay_from_collateral_call(400_000);
fixture.call_account(vec![net], 0).await.unwrap();
assert_eq!(fixture.session().await.drawn_quote, 600_000);
let give_back = fixture.pool_call(
fn_selector!(return_quote()),
600_000,
fixture.collateral_asset,
None,
);
fixture.call_account(vec![give_back], 0).await.unwrap();
assert_eq!(fixture.session().await.drawn_quote, 0);
fixture.close_session().await;
assert!(
fixture
.deployment
.pool
.methods()
.get_session(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
fixture
.deployment
.pool
.methods()
.unpause()
.call()
.await
.unwrap();
assert!(
!fixture
.deployment
.pool
.methods()
.is_paused()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
#[tokio::test]
async fn an_elapsed_deadline_adds_no_risk_and_still_lets_the_user_walk_out() {
let mut fixture = PropFixture::new().await;
let draw = fixture.draw_call(1_000_000);
fixture.call_account(vec![draw], 1).await.unwrap();
let session = fixture.session().await;
let provider = fixture.user.try_provider().unwrap().clone();
let latest = provider.latest_block_time().await.unwrap().unwrap();
let jump = session.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 chain_now = provider
.latest_block_time()
.await
.unwrap()
.unwrap()
.timestamp() as u64;
fixture
.account
.methods()
.set_session(ParallelSessionArgs {
nonce: U256::one(),
session_id: Identity::Address(fixture.user.address()),
expiry: Time {
unix: chain_now + 86_400,
},
contract_ids: vec![],
})
.with_contract_ids(&[fixture.deployment.oracle_id])
.call()
.await
.unwrap();
let blocked: Vec<(&str, CallContractArg, usize, &str)> = vec![
("draw", fixture.draw_call(1), 1, "SessionEnded"),
(
"borrow",
fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, 1u64)),
),
1,
"SessionEnded",
),
(
"withdraw",
fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, 1u64)),
),
2,
"SessionEnded",
),
(
"prolong_session",
fixture.prolong_call(1),
1,
"ProlongAfterExpiry",
),
];
for (name, call, outputs, expected) in blocked {
let error = fixture.call_account(vec![call], outputs).await.unwrap_err();
assert!(
error.to_string().contains(expected),
"{name} past the deadline must raise {expected}: {error:#}"
);
}
let arm = fixture.pool_call(
fn_selector!(set_auto_prolong(Option<ProlongPeriod>)),
0,
AssetId::default(),
Some(call_data!(Some(ProlongPeriod::Day))),
);
let error = fixture.call_account(vec![arm], 0).await.unwrap_err();
assert!(
error.to_string().contains("SessionEnded"),
"an elapsed session cannot arm: {error:#}"
);
let give_back = fixture.pool_call(
fn_selector!(return_quote()),
1_000_000,
fixture.collateral_asset,
None,
);
fixture.call_account(vec![give_back], 0).await.unwrap();
let response = fixture.close_session().await;
let closed = fixture.closed_event(&response);
assert_eq!(closed.reason, SettlementReason::UserClose);
assert_eq!(closed.bad_debt, 0);
}
#[tokio::test]
async fn only_the_admin_moves_the_pools_three_pointers() {
let fixture = PropFixture::new().await;
let intruder = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone());
let stranger = ContractId::new([0x71; 32]);
for (name, error) in [
(
"set_registry",
intruder
.methods()
.set_registry(stranger)
.call()
.await
.unwrap_err(),
),
(
"set_platform_payout",
intruder
.methods()
.set_platform_payout(Identity::Address(fixture.outsider.address()))
.call()
.await
.unwrap_err(),
),
] {
assert!(
error.to_string().contains("NotAuthorized")
|| error.to_string().contains("AccessControl"),
"{name} is the admin's alone: {error:#}"
);
}
let error = fixture
.deployment
.pool
.methods()
.set_registry(ContractId::zeroed())
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidRegistry"),
"a zero registry would admit every caller: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.set_price_feed(ContractId::zeroed())
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidPriceFeed"),
"a zero feed would strand every marked path: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.set_platform_payout(Identity::Address(Address::zeroed()))
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidPlatformPayout"),
"a zero payout would burn every fee: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.set_platform_payout(Identity::ContractId(ContractId::zeroed()))
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("InvalidPlatformPayout"),
"in either identity flavour: {error:#}"
);
let old_registry = fixture.deployment.registry_id;
let response = fixture
.deployment
.pool
.methods()
.set_registry(stranger)
.call()
.await
.unwrap();
let events = response
.decode_logs_with_type::<MarginPoolRegistryChanged>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].old_registry, old_registry);
assert_eq!(events[0].new_registry, stranger);
assert!(events[0].timestamp.unix != 0);
fixture
.deployment
.pool
.methods()
.set_registry(old_registry)
.call()
.await
.unwrap();
let new_payout = Identity::Address(fixture.outsider.address());
let response = fixture
.deployment
.pool
.methods()
.set_platform_payout(new_payout)
.call()
.await
.unwrap();
let events = response
.decode_logs_with_type::<MarginPoolPlatformPayoutChanged>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].new_platform_payout, new_payout);
assert_eq!(
fixture
.deployment
.pool
.methods()
.platform_payout()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
new_payout
);
let feed = fixture.deployment.price_feed_id;
let response = fixture
.deployment
.pool
.methods()
.set_price_feed(feed)
.with_contract_ids(&[feed])
.call()
.await
.unwrap();
let events = response
.decode_logs_with_type::<MarginPoolPriceFeedChanged>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].old_price_feed, feed);
assert_eq!(events[0].new_price_feed, feed);
}
#[tokio::test]
async fn the_pool_initialises_once_and_its_admin_right_moves_whole() {
let fixture = PropFixture::new().await;
const DEFAULT_ADMIN_ROLE: u64 = 0;
let pool = &fixture.deployment.pool;
assert!(
pool.methods()
.is_initialized()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let error = pool.methods().initialize().call().await.unwrap_err();
assert!(
error.to_string().contains("AlreadyInitialized"),
"a second initialise would roll governance back to the blob: {error:#}"
);
assert_eq!(
pool.methods()
.registry()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
fixture.deployment.registry_id
);
let outsider = Identity::Address(fixture.outsider.address());
let deployer = Identity::Address(fixture.deployer.address());
assert!(
!pool
.methods()
.has_role(DEFAULT_ADMIN_ROLE, outsider)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
pool.methods()
.grant_role(DEFAULT_ADMIN_ROLE, outsider)
.call()
.await
.unwrap();
pool.methods()
.revoke_role(DEFAULT_ADMIN_ROLE, deployer)
.call()
.await
.unwrap();
let error = pool
.methods()
.set_liquidator(outsider)
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("NotAuthorized")
|| error.to_string().contains("AccessControl"),
"the outgoing admin keeps nothing: {error:#}"
);
let new_admin = pool.clone().with_account(fixture.outsider.clone());
new_admin
.methods()
.set_liquidator(outsider)
.call()
.await
.unwrap();
assert_eq!(
pool.methods()
.liquidator()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
outsider
);
new_admin
.methods()
.grant_role(DEFAULT_ADMIN_ROLE, deployer)
.call()
.await
.unwrap();
assert!(
pool.methods()
.has_role(DEFAULT_ADMIN_ROLE, deployer)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
#[tokio::test]
async fn a_feed_that_moves_an_admitted_assets_scale_is_refused_everywhere() {
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.lines().await.positive > U256::from(0u64));
fixture
.deployment
.price_feed
.methods()
.set_asset_decimals(fixture.base_asset, 8)
.call()
.await
.unwrap();
let feed = fixture.deployment.price_feed_id;
let error = fixture
.deployment
.pool
.methods()
.set_price_feed(feed)
.with_contract_ids(&[feed])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("PriceDecimalsChanged"),
"a replacement feed must agree on every admitted scale: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.get_session_lines(fixture.child_id)
.with_contract_ids(&[fixture.child_id, fixture.order_book.contract_id, feed])
.simulate(Execution::state_read_only())
.await
.unwrap_err();
assert!(
error.to_string().contains("PriceDecimalsChanged"),
"the live mark is refused too: {error:#}"
);
let error = fixture
.deployment
.pool
.methods()
.publish_tier_v2(
12,
prepaid_tier(&fixture).await,
publication_terms(),
vec![fixture.order_book.contract_id],
)
.with_contract_ids(&[fixture.order_book.contract_id, feed])
.call()
.await
.unwrap_err();
assert!(
error.to_string().contains("PriceDecimalsChanged"),
"the pinned scale is the pool's, not the tier's: {error:#}"
);
fixture
.deployment
.price_feed
.methods()
.set_asset_decimals(fixture.base_asset, 9)
.call()
.await
.unwrap();
assert!(fixture.lines().await.positive > U256::from(0u64));
}
#[tokio::test]
async fn swapping_the_registry_freezes_new_seats_and_touches_no_live_one() {
let mut fixture = PropFixture::new().await;
let parent = Identity::Address(fixture.user.address());
let sibling = fixture
.deployment
.deploy_account(&fixture.user, parent, 31)
.await
.unwrap();
let sibling_id = sibling.contract_id();
let sibling_account = PropAccountContract::new(sibling_id, fixture.user.clone());
let real_registry = fixture.deployment.registry_id;
fixture
.deployment
.pool
.methods()
.set_registry(fixture.order_book.contract_id)
.call()
.await
.unwrap();
assert!(
sibling_account
.methods()
.start_session(TIER_ID, COLLATERAL, ProlongPeriod::SixHours)
.call_params(CallParameters::new(
COLLATERAL,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.order_book.order_book,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.is_err(),
"a pool that cannot ask the registry must not open a session"
);
fixture.fund_collateral(500_000).await;
assert_eq!(fixture.session().await.collateral, COLLATERAL + 500_000);
fixture
.call_account(vec![fixture.prolong_call(1)], 1)
.await
.unwrap();
assert_eq!(fixture.session().await.fees_accrued, 6_000);
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, 100_000u64)),
);
fixture.call_account(vec![withdraw], 2).await.unwrap();
fixture
.call_account(vec![fixture.draw_call(1_000_000)], 1)
.await
.unwrap();
let net = fixture.repay_from_collateral_call(1_000_000);
fixture.call_account(vec![net], 0).await.unwrap();
assert_eq!(fixture.session().await.drawn_quote, 0);
let closed = fixture.closed_event(&fixture.close_session().await);
assert_eq!(closed.reason, SettlementReason::UserClose);
assert_eq!(closed.bad_debt, 0);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- parent_before,
(100_000 + closed.user_net) as u128,
"the parent is paid the same whatever the pool's registry pointer says"
);
fixture
.deployment
.pool
.methods()
.set_registry(real_registry)
.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_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.deployment.registry,
])
.with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
.call()
.await
.unwrap();
assert!(
fixture
.deployment
.pool
.methods()
.get_session(sibling_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_some()
);
}