use super::*;
use crate::{
call_data,
fn_selector,
order_book_deploy::{
OrderArgs,
OrderBookConfigurables,
OrderBookDeploy,
OrderBookDeployConfig,
OrderType,
},
parallel_nonce::{
build_parallel_nonce,
generate_parallel_session_signing_payload,
},
prop::{
AbsorbedFundsTransferred,
CallContractArg,
CallParams,
MarginContractCallEvent,
MarginPoolAssetWithdrawn,
MarginPoolLiquidatorChanged,
MarginWithdrawn,
ParallelMultiCallContractArgs,
ParallelSessionArgs,
PriceInput,
ProlongPeriod,
ProlongTrigger,
PropAccountContract,
PropAccountOracleProxyContract,
PropAccountProxyContract,
Secp256k1,
SessionClosed,
SessionOpened,
SessionProlonged,
SettlementReason,
Signature,
State,
TierParams,
TierVersionPublished,
Time,
},
trial_trade_account::generate_trial_trade_approval_signing_payload,
};
use fuels::{
programs::responses::CallResponse,
test_helpers::{
AssetConfig,
ChainConfig,
DbType,
NodeConfig,
WalletsConfig,
launch_custom_provider_and_get_wallets,
},
types::U256,
};
use futures::TryStreamExt;
use std::time::{
SystemTime,
UNIX_EPOCH,
};
const TIER_ID: u64 = 1;
const COLLATERAL: u64 = 2_000_000;
const LINE: u64 = 10_000_000;
struct PropFixture {
_node_db: Option<tempfile::TempDir>,
collateral_asset: AssetId,
base_asset: AssetId,
deployer: Wallet,
user: Wallet,
outsider: Wallet,
deployment: PropDeployment<Wallet>,
order_book: OrderBookDeploy<Wallet>,
child_id: ContractId,
account: PropAccountContract<Wallet>,
nonce_position: u8,
}
impl PropFixture {
async fn new() -> Self {
Self::new_with_node_config(None, None).await
}
async fn new_with_historical_storage() -> Self {
let node_db = tempfile::tempdir().unwrap();
let database_type = DbType::RocksDb(Some(node_db.path().to_path_buf()));
Self::new_with_node_config(
Some(NodeConfig {
database_type,
historical_execution: true,
..NodeConfig::default()
}),
Some(node_db),
)
.await
}
async fn new_with_node_config(
node_config: Option<NodeConfig>,
node_db: Option<tempfile::TempDir>,
) -> Self {
let collateral_asset = AssetId::new([0x31; 32]);
let base_asset = AssetId::new([0x32; 32]);
let initial_balance = 100_000_000_000u64;
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new_multiple_assets(
3,
vec![
AssetConfig {
id: AssetId::default(),
num_coins: 4,
coin_amount: initial_balance,
},
AssetConfig {
id: collateral_asset,
num_coins: 4,
coin_amount: initial_balance,
},
AssetConfig {
id: base_asset,
num_coins: 4,
coin_amount: initial_balance,
},
],
),
node_config,
Some(ChainConfig::local_testnet()),
)
.await
.unwrap();
let outsider = wallets.pop().unwrap();
let user = wallets.pop().unwrap();
let deployer = wallets.pop().unwrap();
let deployment =
PropDeployment::deploy(&deployer, &PropDeployConfig::new(collateral_asset))
.await
.unwrap();
let order_book_configurables = OrderBookConfigurables::default()
.with_MAKER_FEE(0u64.into())
.unwrap()
.with_TAKER_FEE(0u64.into())
.unwrap()
.with_MIN_ORDER(1)
.unwrap()
.with_DUST(0)
.unwrap();
let order_book = OrderBookDeploy::deploy(
&deployer,
base_asset,
collateral_asset,
&OrderBookDeployConfig::with_configurables(order_book_configurables),
)
.await
.unwrap();
deployment
.price_feed
.methods()
.set_asset_decimals(collateral_asset, 6)
.call()
.await
.unwrap();
deployment
.price_feed
.methods()
.set_asset_decimals(base_asset, 9)
.call()
.await
.unwrap();
deployment
.price_feed
.methods()
.publish_prices(vec![
PriceInput {
asset: collateral_asset,
bid: 1_000_000_000_000_000_000u64.into(),
ask: 1_000_000_000_000_000_000u64.into(),
timestamp: 0,
},
PriceInput {
asset: base_asset,
bid: 2_000_000_000_000_000_000u64.into(),
ask: 2_000_000_000_000_000_000u64.into(),
timestamp: 0,
},
])
.call()
.await
.unwrap();
let tier_params = TierParams {
line: LINE,
leverage: 5,
duration: 86_400,
maintenance_bps: 250,
open_buffer_bps: 375,
liq_price_factor: 9_900,
prolong_fee_bps: [6, 21, 76, 738],
max_credit_line_bps: 20_000,
max_price_age: u64::MAX,
open_fee_bps: 0,
profit_share_bps: 1_000,
price_band_bps: 1_000,
};
let tier_response = deployment
.pool
.methods()
.publish_tier_version(
TIER_ID,
tier_params.clone(),
vec![order_book.contract_id],
)
.with_contract_ids(&[order_book.contract_id, deployment.price_feed_id])
.call()
.await
.unwrap();
let tier_events = tier_response
.decode_logs_with_type::<TierVersionPublished>()
.unwrap();
assert_eq!(tier_events.len(), 1);
assert_eq!(tier_events[0].params, tier_params);
assert_eq!(tier_events[0].threshold_floor, 8_250_000);
assert_eq!(
tier_events[0].auto_prolong_periods,
vec![ProlongPeriod::SixHours, ProlongPeriod::Day]
);
assert!(tier_events[0].timestamp.unix != 0);
for (asset, amount) in
[(collateral_asset, 50_000_000), (base_asset, 10_000_000_000)]
{
deployment
.pool
.methods()
.fund_inventory()
.call_params(CallParameters::new(amount, asset, u64::MAX))
.unwrap()
.call()
.await
.unwrap();
}
let parent = Identity::Address(user.address());
let child = deployment.deploy_account(&user, parent, 0).await.unwrap();
let child_id = child.contract_id();
user.force_transfer_to_contract(
child_id,
COLLATERAL,
collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let account = PropAccountContract::new(child_id, user.clone());
let opened_response = account
.methods()
.start_session(TIER_ID, COLLATERAL)
.with_contract_ids(&[
deployment.oracle_id,
deployment.pool_id,
deployment.registry_id,
])
.call()
.await
.unwrap();
let opened_events = deployment
.pool
.log_decoder()
.decode_logs_with_type::<SessionOpened>(&opened_response.tx_status.receipts)
.unwrap();
assert_eq!(opened_events.len(), 1);
assert_eq!(opened_events[0].account, child_id);
assert_eq!(opened_events[0].parent, parent);
assert_eq!(
opened_events[0].expires_at,
opened_events[0].started_at + 86_400
);
assert_eq!(opened_events[0].open_fee, 0);
assert_eq!(opened_events[0].fees_accrued, 0);
assert!(opened_events[0].timestamp.unix != 0);
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(user.address()),
expiry: Time { unix: expiry },
contract_ids: vec![],
})
.with_contract_ids(&[deployment.oracle_id])
.call()
.await
.unwrap();
Self {
_node_db: node_db,
collateral_asset,
base_asset,
deployer,
user,
outsider,
deployment,
order_book,
child_id,
account,
nonce_position: 0,
}
}
fn pool_call(
&self,
function_selector: Vec<u8>,
coins: u64,
asset_id: AssetId,
call_data: Option<Vec<u8>>,
) -> CallContractArg {
CallContractArg {
contract_id: self.deployment.pool_id,
function_selector: Bytes(function_selector),
call_params: CallParams {
coins,
asset_id,
gas: 10_000_000,
},
call_data: call_data.map(Bytes),
}
}
async fn signed_calls(
&mut self,
call_contract_args: Vec<CallContractArg>,
) -> (Signature, Signature, ParallelMultiCallContractArgs) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let nonce = build_parallel_nonce(0, now + 3_600, 0, self.nonce_position);
self.nonce_position += 1;
let calls = ParallelMultiCallContractArgs {
nonce,
call_contract_args,
};
let user_message = generate_parallel_session_signing_payload(
calls.nonce,
calls.call_contract_args.clone(),
);
let chain_id = self
.user
.provider()
.consensus_parameters()
.await
.unwrap()
.chain_id();
let cosigner_message = generate_trial_trade_approval_signing_payload(
*chain_id,
self.child_id,
calls.nonce,
calls.call_contract_args.clone(),
);
let user_signature = self.user.signer().sign(user_message).await.unwrap();
let cosigner_signature =
self.deployer.signer().sign(cosigner_message).await.unwrap();
(
Signature::Secp256k1(Secp256k1 {
bits: *user_signature,
}),
Signature::Secp256k1(Secp256k1 {
bits: *cosigner_signature,
}),
calls,
)
}
async fn call_account(
&mut self,
calls: Vec<CallContractArg>,
variable_outputs: usize,
) -> Result<CallResponse<()>> {
let (user_signature, cosigner_signature, calls) = self.signed_calls(calls).await;
Ok(self
.account
.methods()
.call_contracts(user_signature, cosigner_signature, calls)
.with_contracts(&[
&self.deployment.oracle,
&self.deployment.pool,
&self.deployment.price_feed,
&self.deployment.registry,
&self.order_book.order_book,
])
.with_variable_output_policy(VariableOutputPolicy::Exactly(variable_outputs))
.call()
.await?)
}
async fn session(&self) -> crate::prop::SessionView {
self.deployment
.pool
.methods()
.get_session(self.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.unwrap()
}
async fn pool_balance(&self, asset: AssetId) -> u64 {
self.deployer
.try_provider()
.unwrap()
.get_contract_asset_balance(&self.deployment.pool_id, &asset)
.await
.unwrap()
}
}
async fn expire_fixture_session(fixture: &PropFixture) {
let session = fixture.session().await;
let provider = fixture.outsider.try_provider().unwrap();
let latest = provider
.latest_block_time()
.await
.unwrap()
.expect("local chain has a latest block time");
let seconds_until_expired =
session.expires_at.saturating_sub(latest.timestamp() as u64) + 1;
let expired_time = latest
.checked_add_signed(chrono::TimeDelta::seconds(seconds_until_expired as i64))
.expect("test expiry timestamp is representable");
provider
.produce_blocks(1, Some(expired_time))
.await
.unwrap();
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
.unwrap();
}
async fn occupied_pool_storage_slots(fixture: &PropFixture) -> usize {
fixture
.deployer
.try_provider()
.unwrap()
.client()
.contract_storage_slots(&fixture.deployment.pool_id)
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap()
.len()
}
#[test]
fn settlement_reason_wire_order_matches_backend_abi() {
fn discriminant(reason: SettlementReason) -> u64 {
match fuels::core::traits::Tokenizable::into_token(reason) {
fuels::types::Token::Enum(selector) => selector.0,
token => panic!("expected enum token, got {token:?}"),
}
}
assert_eq!(discriminant(SettlementReason::UserClose), 0);
assert_eq!(discriminant(SettlementReason::Expiry), 1);
assert_eq!(discriminant(SettlementReason::Liquidation), 2);
}
#[tokio::test]
async fn admin_controls_liquidator_and_can_withdraw_any_pool_asset() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let fixture = PropFixture::new().await;
let deployer = Identity::Address(fixture.deployer.address());
let liquidator = Identity::Address(fixture.outsider.address());
assert_eq!(
fixture
.deployment
.pool
.methods()
.liquidator()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployer
);
let outsider_pool = fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone());
assert!(
outsider_pool
.methods()
.set_liquidator(liquidator)
.call()
.await
.is_err()
);
for invalid in [
Identity::Address(Address::zeroed()),
Identity::ContractId(ContractId::zeroed()),
] {
assert!(
fixture
.deployment
.pool
.methods()
.set_liquidator(invalid)
.call()
.await
.is_err()
);
}
let response = fixture
.deployment
.pool
.methods()
.set_liquidator(liquidator)
.call()
.await
.unwrap();
let events = response
.decode_logs_with_type::<MarginPoolLiquidatorChanged>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].old_liquidator, deployer);
assert_eq!(events[0].new_liquidator, liquidator);
assert!(events[0].timestamp.unix != 0);
assert_eq!(
fixture
.deployment
.pool
.methods()
.liquidator()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
liquidator
);
assert!(
outsider_pool
.methods()
.admin_withdraw(fixture.base_asset, 1)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.is_err()
);
assert!(
fixture
.deployment
.pool
.methods()
.admin_withdraw(fixture.base_asset, 0)
.call()
.await
.is_err()
);
let pool_before = fixture.pool_balance(fixture.base_asset).await;
assert!(
fixture
.deployment
.pool
.methods()
.admin_withdraw(fixture.base_asset, pool_before + 1)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.is_err()
);
let admin_before = fixture
.deployer
.get_asset_balance(&fixture.base_asset)
.await
.unwrap();
let partial = 1_000;
let response = fixture
.deployment
.pool
.methods()
.admin_withdraw(fixture.base_asset, partial)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
let events = response
.decode_logs_with_type::<MarginPoolAssetWithdrawn>()
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].admin, deployer);
assert_eq!(events[0].asset_id, fixture.base_asset);
assert_eq!(events[0].amount, partial);
assert_eq!(events[0].remaining_balance, pool_before - partial);
assert!(events[0].timestamp.unix != 0);
assert_eq!(
fixture
.deployer
.get_asset_balance(&fixture.base_asset)
.await
.unwrap(),
admin_before + u128::from(partial)
);
let response = fixture
.deployment
.pool
.methods()
.admin_withdraw(fixture.base_asset, u64::MAX)
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
let events = response
.decode_logs_with_type::<MarginPoolAssetWithdrawn>()
.unwrap();
assert_eq!(events[0].amount, pool_before - partial);
assert_eq!(events[0].remaining_balance, 0);
assert_eq!(fixture.pool_balance(fixture.base_asset).await, 0);
assert!(
fixture
.deployment
.pool
.methods()
.admin_withdraw(fixture.base_asset, u64::MAX)
.call()
.await
.is_err()
);
}
#[tokio::test]
async fn live_withdrawal_emits_canonical_event() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let mut fixture = PropFixture::new().await;
let amount = 100_000;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
amount,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let withdraw = fixture.pool_call(
fn_selector!(withdraw(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.collateral_asset, amount)),
);
let response = fixture.call_account(vec![withdraw], 2).await.unwrap();
let events = fixture
.deployment
.pool
.log_decoder()
.decode_logs_with_type::<MarginWithdrawn>(&response.tx_status.receipts)
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].account, fixture.child_id);
assert_eq!(events[0].session_id, 1);
assert_eq!(events[0].asset_id, fixture.collateral_asset);
assert_eq!(events[0].amount, amount);
assert_eq!(events[0].from_account, amount);
assert_eq!(events[0].from_pool, 0);
assert_eq!(events[0].from_capitalised, 0);
assert_eq!(events[0].share_qty, 10_000);
assert_eq!(events[0].new_collateral, COLLATERAL);
assert_eq!(events[0].new_credit_line, LINE);
assert_eq!(events[0].capitalised_total, 0);
assert!(events[0].timestamp.unix != 0);
}
#[tokio::test]
async fn outside_funders_can_rescue_debt_but_self_funding_still_requires_a_clean_slate() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let mut fixture = PropFixture::new().await;
let lines = 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()
.value;
assert_eq!(lines.positive, U256::from(LINE));
assert_eq!(lines.negative, U256::zero());
assert_eq!(lines.threshold, U256::from(8_250_000u64));
assert_eq!(lines.freeze, U256::from(8_375_000u64));
let liquidation_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,
])
.call()
.await
.unwrap_err();
assert!(
liquidation_error.to_string().contains("NotLiquidatable"),
"permissionless liquidation should reach the objective health gate: {liquidation_error:#}"
);
let draw_amount = 1_000_000;
let draw = fixture.pool_call(
fn_selector!(draw(u64)),
0,
AssetId::default(),
Some(call_data!(draw_amount)),
);
fixture.call_account(vec![draw], 1).await.unwrap();
let rescue_amount = 500_000;
fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.add_collateral(fixture.child_id)
.call_params(CallParameters::new(
rescue_amount,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.call()
.await
.unwrap();
let rescued = fixture.session().await;
assert_eq!(rescued.drawn_quote, draw_amount);
assert_eq!(rescued.collateral, COLLATERAL + rescue_amount);
assert_eq!(rescued.credit_line, LINE + rescue_amount);
assert_eq!(rescued.capitalised, 0);
let self_amount = 100_000;
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
self_amount,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let self_top_up = fixture.pool_call(
fn_selector!(add_collateral(ContractId)),
self_amount,
fixture.collateral_asset,
Some(call_data!(fixture.child_id)),
);
let error = fixture
.call_account(vec![self_top_up], 0)
.await
.unwrap_err();
assert!(
error.to_string().contains("QuoteOutstanding"),
"unexpected self-funding error: {error:#}"
);
let return_quote = fixture.pool_call(
fn_selector!(return_quote()),
draw_amount,
fixture.collateral_asset,
None,
);
fixture.call_account(vec![return_quote], 0).await.unwrap();
let self_top_up = fixture.pool_call(
fn_selector!(add_collateral(ContractId)),
self_amount,
fixture.collateral_asset,
Some(call_data!(fixture.child_id)),
);
fixture.call_account(vec![self_top_up], 0).await.unwrap();
let capitalised = fixture.session().await;
assert_eq!(capitalised.drawn_quote, 0);
assert_eq!(capitalised.capitalised, self_amount);
let borrow_amount = 1_000_000_000u64;
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, borrow_amount)),
);
fixture.call_account(vec![borrow], 1).await.unwrap();
fixture
.deployment
.pool
.clone()
.with_account(fixture.outsider.clone())
.methods()
.add_collateral(fixture.child_id)
.call_params(CallParameters::new(
rescue_amount,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.call()
.await
.unwrap();
let rescued_debt = fixture.session().await;
assert_eq!(rescued_debt.capitalised, self_amount);
fixture
.user
.force_transfer_to_contract(
fixture.child_id,
self_amount,
fixture.collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let self_top_up = fixture.pool_call(
fn_selector!(add_collateral(ContractId)),
self_amount,
fixture.collateral_asset,
Some(call_data!(fixture.child_id)),
);
let error = fixture
.call_account(vec![self_top_up], 0)
.await
.unwrap_err();
assert!(
error.to_string().contains("DebtsOutstanding"),
"unexpected self-funding error: {error:#}"
);
assert_oracle_proxy_is_upgradeable(&fixture).await;
assert_new_pool_defaults_do_not_require_a_create_manifest(&fixture).await;
}
#[tokio::test]
async fn outsider_can_expire_an_elapsed_session() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let fixture = PropFixture::new().await;
let session = fixture.session().await;
let provider = fixture.outsider.try_provider().unwrap();
let latest = provider
.latest_block_time()
.await
.unwrap()
.expect("local chain has a latest block time");
let seconds_until_expired =
session.expires_at.saturating_sub(latest.timestamp() as u64) + 1;
let expired_time = latest
.checked_add_signed(chrono::TimeDelta::seconds(seconds_until_expired as i64))
.expect("test expiry timestamp is representable");
provider
.produce_blocks(1, Some(expired_time))
.await
.unwrap();
let response = 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::Exactly(1))
.call()
.await
.unwrap();
let events = response.decode_logs_with_type::<SessionClosed>().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].reason, SettlementReason::Expiry);
assert!(events[0].timestamp.unix > session.expires_at);
assert!(
!fixture
.deployment
.pool
.methods()
.has_session(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
#[tokio::test]
async fn expiry_routes_in_kind_debt_repayment_to_the_liquidator() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let mut fixture = PropFixture::new().await;
let liquidator = Identity::Address(fixture.outsider.address());
fixture
.deployment
.pool
.methods()
.set_liquidator(liquidator)
.call()
.await
.unwrap();
assert!(
!fixture
.deployment
.pool
.methods()
.has_debts(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let borrowed = 1_000_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();
assert!(
fixture
.deployment
.pool
.methods()
.has_debts(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
assert_eq!(
fixture
.deployment
.pool
.methods()
.get_debt(fixture.child_id, fixture.base_asset)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
borrowed
);
let pool_after_borrow = fixture.pool_balance(fixture.base_asset).await;
let liquidator_before = fixture
.outsider
.get_asset_balance(&fixture.base_asset)
.await
.unwrap();
let session = fixture.session().await;
let provider = fixture.outsider.try_provider().unwrap();
let latest = provider
.latest_block_time()
.await
.unwrap()
.expect("local chain has a latest block time");
let seconds_until_expired =
session.expires_at.saturating_sub(latest.timestamp() as u64) + 1;
let expired_time = latest
.checked_add_signed(chrono::TimeDelta::seconds(seconds_until_expired as i64))
.expect("test expiry timestamp is representable");
provider
.produce_blocks(1, Some(expired_time))
.await
.unwrap();
let response = 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::Exactly(2))
.call()
.await
.unwrap();
let absorbed = response
.decode_logs_with_type::<AbsorbedFundsTransferred>()
.unwrap();
assert_eq!(absorbed.len(), 1);
assert_eq!(absorbed[0].account, fixture.child_id);
assert_eq!(absorbed[0].session_id, session.session_id);
assert_eq!(absorbed[0].reason, SettlementReason::Expiry);
assert_eq!(absorbed[0].liquidator, liquidator);
assert_eq!(absorbed[0].assets, vec![(fixture.base_asset, borrowed)]);
assert!(absorbed[0].timestamp.unix > session.expires_at);
assert_eq!(
fixture.pool_balance(fixture.base_asset).await,
pool_after_borrow
);
assert_eq!(
fixture
.outsider
.get_asset_balance(&fixture.base_asset)
.await
.unwrap(),
liquidator_before + u128::from(borrowed)
);
assert!(
!fixture
.deployment
.pool
.methods()
.has_session(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
assert_eq!(
fixture
.deployment
.pool
.methods()
.get_debt(fixture.child_id, fixture.base_asset)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
0
);
assert!(
!fixture
.deployment
.pool
.methods()
.has_debts(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
#[tokio::test]
async fn settlement_clears_session_scoped_accounting_storage() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let clean_fixture = PropFixture::new_with_historical_storage().await;
expire_fixture_session(&clean_fixture).await;
let clean_close_slots = occupied_pool_storage_slots(&clean_fixture).await;
let mut debt_fixture = PropFixture::new_with_historical_storage().await;
let borrowed = 1_000_000_000u64;
let borrow = debt_fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(debt_fixture.base_asset, borrowed)),
);
debt_fixture.call_account(vec![borrow], 1).await.unwrap();
expire_fixture_session(&debt_fixture).await;
let debt_close_slots = occupied_pool_storage_slots(&debt_fixture).await;
assert_eq!(
debt_close_slots, clean_close_slots,
"settlement left debt, debt-asset index, or received-asset storage behind",
);
}
#[tokio::test]
async fn liquidation_routes_the_basket_after_the_accounts_short_trade() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let mut fixture = PropFixture::new().await;
let liquidator = Identity::Address(fixture.outsider.address());
fixture
.deployment
.pool
.methods()
.set_liquidator(liquidator)
.call()
.await
.unwrap();
let quantity = 1_000_000_000u64;
let sale_proceeds = 2_000_000u64;
fixture
.order_book
.order_book
.methods()
.create_order(OrderArgs {
price: sale_proceeds,
quantity,
order_type: OrderType::Spot,
})
.call_params(CallParameters::new(
sale_proceeds,
fixture.collateral_asset,
u64::MAX,
))
.unwrap()
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
let borrow = fixture.pool_call(
fn_selector!(borrow(AssetId, u64)),
0,
AssetId::default(),
Some(call_data!(fixture.base_asset, quantity)),
);
let sell = CallContractArg {
contract_id: fixture.order_book.contract_id,
function_selector: Bytes(fn_selector!(create_order(OrderArgs))),
call_params: CallParams {
coins: quantity,
asset_id: fixture.base_asset,
gas: 10_000_000,
},
call_data: Some(Bytes(call_data!(OrderArgs {
price: sale_proceeds,
quantity,
order_type: OrderType::Spot,
}))),
};
let settle_book = CallContractArg {
contract_id: fixture.order_book.contract_id,
function_selector: Bytes(fn_selector!(settle_balance(Identity))),
call_params: CallParams {
coins: 0,
asset_id: AssetId::default(),
gas: 10_000_000,
},
call_data: Some(Bytes(call_data!(Identity::ContractId(fixture.child_id,)))),
};
fixture
.call_account(vec![borrow, sell, settle_book], 2)
.await
.unwrap();
let adverse_price = 10_000_000_000_000_000_000u128;
fixture
.deployment
.price_feed
.methods()
.publish_prices(vec![PriceInput {
asset: fixture.base_asset,
bid: adverse_price,
ask: adverse_price,
timestamp: 1,
}])
.call()
.await
.unwrap();
let lines = 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()
.value;
assert!(lines.positive <= lines.negative + lines.threshold);
let pool_cash_before = fixture.pool_balance(fixture.collateral_asset).await;
let liquidator_before = fixture
.outsider
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
let response = 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::Exactly(1))
.call()
.await
.unwrap();
let absorbed = response
.decode_logs_with_type::<AbsorbedFundsTransferred>()
.unwrap();
let absorbed_cash = COLLATERAL + sale_proceeds;
assert_eq!(absorbed.len(), 1);
assert_eq!(absorbed[0].account, fixture.child_id);
assert_eq!(absorbed[0].reason, SettlementReason::Liquidation);
assert_eq!(absorbed[0].liquidator, liquidator);
assert_eq!(
absorbed[0].assets,
vec![(fixture.collateral_asset, absorbed_cash)]
);
assert!(absorbed[0].timestamp.unix != 0);
assert_eq!(
fixture
.outsider
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap(),
liquidator_before + u128::from(absorbed_cash)
);
assert_eq!(
fixture.pool_balance(fixture.collateral_asset).await,
pool_cash_before - COLLATERAL
);
let closed = response.decode_logs_with_type::<SessionClosed>().unwrap();
assert_eq!(closed.len(), 1);
assert_eq!(closed[0].reason, SettlementReason::Liquidation);
assert!(closed[0].profit_is_negative);
assert!(closed[0].bad_debt != 0);
assert_eq!(
closed[0].cancelled_debt,
vec![(fixture.base_asset, quantity)]
);
assert!(closed[0].payout_parent.is_empty());
assert!(closed[0].payout_platform.is_empty());
assert_eq!(
fixture
.deployment
.pool
.methods()
.get_bad_debt()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
closed[0].bad_debt
);
assert!(
!fixture
.deployment
.pool
.methods()
.has_session(fixture.child_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
#[tokio::test]
async fn prolongation_fee_can_move_a_session_through_its_health_lines() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let mut fixture = PropFixture::new().await;
let before = fixture.session().await;
let times = 300u64;
let expected_fee = 1_800_000u64;
let expected_extension = 21_600u64 * times;
let prolong = fixture.pool_call(
fn_selector!(prolong_session(ProlongPeriod, u64)),
0,
AssetId::default(),
Some(call_data!(ProlongPeriod::SixHours, times)),
);
let response = fixture.call_account(vec![prolong], 0).await.unwrap();
let events = response
.decode_logs_with_type::<SessionProlonged>()
.unwrap();
let call_events = response
.decode_logs_with_type::<MarginContractCallEvent>()
.unwrap();
assert_eq!(call_events.len(), 1);
assert_eq!(
call_events[0].authority,
Identity::Address(fixture.user.address())
);
assert_eq!(call_events[0].called_contract, fixture.deployment.pool_id);
assert!(call_events[0].timestamp.unix != 0);
assert_eq!(events.len(), 1);
assert_eq!(events[0].period, ProlongPeriod::SixHours);
assert_eq!(events[0].times, times);
assert_eq!(events[0].seconds, expected_extension);
assert_eq!(events[0].trigger, ProlongTrigger::User);
assert_eq!(events[0].fee, expected_fee);
assert_eq!(events[0].fees_accrued, expected_fee);
let after = fixture.session().await;
assert_eq!(after.expires_at, before.expires_at + expected_extension);
assert_eq!(after.fees_accrued, expected_fee);
let lines = 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()
.value;
assert!(lines.positive <= lines.negative + lines.freeze);
assert!(lines.positive <= lines.negative + lines.threshold);
}
async fn assert_oracle_proxy_is_upgradeable(fixture: &PropFixture) {
let expected_owner =
State::Initialized(Identity::Address(fixture.deployer.address()));
assert_eq!(
fixture
.deployment
.oracle_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(ContractId::from(fixture.deployment.oracle_blob_id))
);
assert_eq!(
fixture
.deployment
.oracle_proxy
.methods()
.proxy_owner()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
expected_owner
);
assert!(
fixture
.deployment
.oracle_proxy
.methods()
.initialize_proxy()
.call()
.await
.is_err()
);
assert!(
fixture
.deployment
.oracle
.methods()
.initialize()
.call()
.await
.is_err()
);
let outsider_proxy = PropAccountOracleProxyContract::new(
fixture.deployment.oracle_id,
fixture.outsider.clone(),
);
assert!(
outsider_proxy
.methods()
.set_proxy_target(ContractId::from(fixture.deployment.account_blob_id,))
.call()
.await
.is_err()
);
assert!(
fixture
.deployment
.oracle_proxy
.methods()
.set_proxy_target(ContractId::zeroed())
.call()
.await
.is_err()
);
fixture
.deployment
.oracle_proxy
.methods()
.set_proxy_target(ContractId::from(fixture.deployment.account_blob_id))
.call()
.await
.unwrap();
assert_eq!(
fixture
.deployment
.oracle_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(ContractId::from(fixture.deployment.account_blob_id))
);
fixture
.deployment
.oracle_proxy
.methods()
.set_proxy_target(ContractId::from(fixture.deployment.oracle_blob_id))
.call()
.await
.unwrap();
assert_eq!(
fixture
.deployment
.oracle
.methods()
.get_prop_account_impl()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(ContractId::from(fixture.deployment.account_blob_id))
);
assert_eq!(
fixture
.deployment
.oracle
.methods()
.get_prop_margin_pool()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(fixture.deployment.pool_id)
);
assert_eq!(
PropAccountProxyContract::new(fixture.child_id, fixture.user.clone(),)
.methods()
.oracle()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
fixture.deployment.oracle_id
);
}
async fn assert_new_pool_defaults_do_not_require_a_create_manifest(
fixture: &PropFixture,
) {
let proxy_configurables = PropMarginPoolProxyContractConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(Identity::Address(
fixture.deployer.address(),
)))
.unwrap()
.with_INITIAL_TARGET(ContractId::from(fixture.deployment.pool_blob_id))
.unwrap();
let proxy_contract = regular_contract(
PROP_MARGIN_POOL_PROXY_BYTECODE,
PROP_MARGIN_POOL_PROXY_STORAGE,
Salt::from([0x77; 32]),
)
.unwrap()
.with_configurables(proxy_configurables);
let (proxy_id, is_new) = deploy_regular(&fixture.deployer, proxy_contract)
.await
.unwrap();
assert!(is_new);
let proxy = PropMarginPoolProxyContract::new(proxy_id, fixture.deployer.clone());
proxy.methods().initialize_proxy().call().await.unwrap();
let pool = PropMarginPoolContract::new(proxy_id, fixture.deployer.clone());
assert!(
!pool
.methods()
.is_initialized()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
assert_eq!(
pool.methods()
.get_bad_debt()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
0
);
}