use super::*;
use crate::prop::TurboTerms;
use fuels::programs::calls::CallHandler;
mod mock {
use fuels::prelude::abigen;
abigen!(Contract(
name = "PropMarginPoolMockContract",
abi = "artifacts/test_artifacts/prop-margin-pool-mock-abi.json"
));
}
const UNIT: u64 = 1_000_000_000;
const TIER_ID: u64 = 13;
const CLOSE_LINE: u64 = 2_500_000_000_000;
const OPEN_FEE: u64 = 12_500_000_000; const TICKET: u64 = CLOSE_LINE / 40;
const WBTC_DEBT: u64 = 3_145;
const WBTC_PRICE: u64 = 80_000 * UNIT;
const DEBT_COST: u64 = WBTC_DEBT * WBTC_PRICE / UNIT;
const NET_PROFIT: u64 = 7 * UNIT;
const MAKER_QUANTITY: u64 = 100_000;
const CLOSE_TIER: TierParams = TierParams {
line: CLOSE_LINE,
leverage: 40,
duration: 604_800,
maintenance_bps: 100,
open_buffer_bps: 101,
liq_price_factor: 9_900,
prolong_fee: [0, 0, 0, 0],
max_credit_line_bps: 20_000,
max_price_age: u64::MAX,
open_fee: OPEN_FEE,
profit_share_bps: 0,
price_band_bps: 85,
};
const TURBO_TERMS: TurboTerms = TurboTerms {
max_loss_bps: [150, 200, 250, 300],
rollover_profit_bps: 800,
max_rollovers: 3,
};
async fn close_fixture(min_order: u64) -> PropFixture {
let collateral_asset = AssetId::new([0x31; 32]); let base_asset = AssetId::new([0x32; 32]); let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new_multiple_assets(
3,
[AssetId::default(), collateral_asset, base_asset]
.into_iter()
.map(|id| AssetConfig {
id,
num_coins: 4,
coin_amount: 1_000_000 * UNIT,
})
.collect(),
),
None,
Some(ChainConfig::local_testnet()),
)
.await
.unwrap();
let outsider = wallets.pop().unwrap();
let user = wallets.pop().unwrap();
let deployer = wallets.pop().unwrap();
let mut config = PropDeployConfig::new(collateral_asset);
config.collateral_decimals = 9;
let deployment = PropDeployment::deploy(&deployer, &config).await.unwrap();
let order_book = OrderBookDeploy::deploy(
&deployer,
base_asset,
collateral_asset,
&OrderBookDeployConfig::with_configurables(
OrderBookConfigurables::default()
.with_BASE_DECIMALS(UNIT)
.unwrap()
.with_QUOTE_DECIMALS(UNIT)
.unwrap()
.with_MIN_ORDER(min_order)
.unwrap()
.with_MAKER_FEE(0u64.into())
.unwrap()
.with_TAKER_FEE(0u64.into())
.unwrap()
.with_DUST(0)
.unwrap(),
),
)
.await
.unwrap();
for asset in [collateral_asset, base_asset] {
deployment
.price_feed
.methods()
.set_asset_decimals(asset, 9)
.call()
.await
.unwrap();
}
let now = deployer
.try_provider()
.unwrap()
.latest_block_time()
.await
.unwrap()
.unwrap()
.timestamp() as u64;
let price_scale = 1_000_000_000_000_000_000u128;
deployment
.price_feed
.methods()
.publish_prices(vec![
PriceInput {
asset: collateral_asset,
bid: price_scale,
ask: price_scale,
timestamp: now,
},
PriceInput {
asset: base_asset,
bid: 80_000 * price_scale,
ask: 80_000 * price_scale,
timestamp: now,
},
])
.call()
.await
.unwrap();
deployment
.pool
.methods()
.publish_tier_v2(
TIER_ID,
CLOSE_TIER,
TURBO_TERMS,
vec![order_book.contract_id],
)
.with_contracts(&[&order_book.order_book, &deployment.price_feed])
.call()
.await
.unwrap();
deployer
.force_transfer_to_contract(
deployment.pool_id,
2 * CLOSE_LINE,
collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let child = deployment
.deploy_account(&user, Identity::Address(user.address()), 0)
.await
.unwrap();
let child_id = child.contract_id();
let account = PropAccountContract::new(child_id, user.clone());
account
.methods()
.start_session(TIER_ID, TICKET + OPEN_FEE, ProlongPeriod::Week)
.call_params(CallParameters::new(
TICKET + OPEN_FEE,
collateral_asset,
u64::MAX,
))
.unwrap()
.with_contracts(&[&deployment.pool, &deployment.oracle, &deployment.registry])
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
account
.methods()
.set_session(ParallelSessionArgs {
nonce: U256::zero(),
session_id: Identity::Address(user.address()),
expiry: Time { unix: now + 86_400 },
contract_ids: vec![],
})
.with_contract_ids(&[deployment.oracle_id])
.call()
.await
.unwrap();
let mut fixture = PropFixture {
_node_db: None,
collateral_asset,
base_asset,
deployer,
user,
outsider,
deployment,
order_book,
child_id,
account,
nonce_position: 0,
publish_clock: std::sync::atomic::AtomicU64::new(now),
};
fixture
.call_account(vec![fixture.draw_call(CLOSE_LINE)], 1)
.await
.unwrap();
fixture
.deployer
.force_transfer_to_contract(
child_id,
NET_PROFIT + DEBT_COST,
collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let mock_blob_id = upload_implementation(
&fixture.deployer,
include_bytes!("../artifacts/test_artifacts/prop-margin-pool-mock.bin"),
include_bytes!(
"../artifacts/test_artifacts/prop-margin-pool-mock-storage_slots.json"
),
mock::PropMarginPoolMockContractConfigurables::default()
.with_COLLATERAL_ASSET(collateral_asset)
.unwrap()
.with_COLLATERAL_DECIMALS(9)
.unwrap(),
&config,
)
.await
.unwrap();
let mock_pool = mock::PropMarginPoolMockContract::new(
fixture.deployment.pool_id,
fixture.deployer.clone(),
);
let proxy = &fixture.deployment.pool_proxy;
CallHandler::new_multi_call(fixture.deployer.clone())
.add_call(proxy.methods().set_proxy_target(mock_blob_id.into()))
.add_call(
mock_pool
.methods()
.set_account_debt(child_id, base_asset, WBTC_DEBT),
)
.add_call(
proxy
.methods()
.set_proxy_target(fixture.deployment.pool_blob_id.into()),
)
.call::<((), (), ())>()
.await
.unwrap();
fixture
.rest_order(WBTC_PRICE, MAKER_QUANTITY, base_asset, MAKER_QUANTITY)
.await;
assert_eq!(fixture.debt(base_asset).await, WBTC_DEBT);
assert_eq!(
fixture.session_value().await,
i128::from(CLOSE_LINE + NET_PROFIT)
);
assert_eq!(DEBT_COST, 251_600_000); assert_eq!(WBTC_DEBT * WBTC_PRICE % UNIT, 0);
assert!(DEBT_COST < UNIT);
fixture
}
async fn try_close(fixture: &mut PropFixture) -> Result<()> {
let buyback = CallContractArg {
contract_id: fixture.order_book.contract_id,
function_selector: Bytes(fn_selector!(create_order(OrderArgs))),
call_params: CallParams {
coins: DEBT_COST,
asset_id: fixture.collateral_asset,
gas: 10_000_000,
},
call_data: Some(Bytes(call_data!(OrderArgs {
price: WBTC_PRICE,
quantity: WBTC_DEBT,
order_type: OrderType::Market,
}))),
};
let repay_base =
fixture.pool_call(fn_selector!(repay()), WBTC_DEBT, fixture.base_asset, None);
let return_quote = fixture.pool_call(
fn_selector!(return_quote()),
CLOSE_LINE,
fixture.collateral_asset,
None,
);
let (user_signature, cosigner_signature, calls) = fixture
.signed_calls(vec![
buyback,
fixture.settle_book_call(),
repay_base,
return_quote,
])
.await;
let prepare = fixture
.account
.methods()
.call_contracts(user_signature, cosigner_signature, calls)
.with_contracts(&[
&fixture.deployment.oracle,
&fixture.deployment.pool,
&fixture.order_book.order_book,
]);
let close = fixture
.account
.methods()
.close_session(vec![])
.with_contracts(&[
&fixture.deployment.pool,
&fixture.deployment.oracle,
&fixture.deployment.price_feed,
&fixture.order_book.order_book,
]);
CallHandler::new_multi_call(fixture.user.clone())
.add_call(prepare)
.add_call(close)
.with_variable_output_policy(VariableOutputPolicy::Exactly(2))
.call::<((), ())>()
.await?;
Ok(())
}
async fn assert_can_close(min_order: u64) {
let mut fixture = close_fixture(min_order).await;
let parent_before = fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap();
if let Err(error) = try_close(&mut fixture).await {
assert!(
error.to_string().contains("InvalidInputAmount"),
"unexpected close failure: {error:#}"
);
assert_eq!(fixture.debt(fixture.base_asset).await, WBTC_DEBT);
assert_eq!(
fixture.account_balance(fixture.collateral_asset).await,
CLOSE_LINE + NET_PROFIT + DEBT_COST
);
panic!("The $0.2516 debt buyback must not block closure: {error:#}");
}
assert!(fixture.session_opt().await.is_none());
assert_eq!(fixture.debt(fixture.base_asset).await, 0);
assert_eq!(fixture.account_balance(fixture.base_asset).await, 0);
assert_eq!(fixture.account_balance(fixture.collateral_asset).await, 0);
assert_eq!(
fixture
.user
.get_asset_balance(&fixture.collateral_asset)
.await
.unwrap()
- parent_before,
u128::from(NET_PROFIT)
);
}
#[tokio::test]
async fn turbo_close_with_sub_minimum_wbtc_debt_should_succeed() {
assert_can_close(UNIT).await;
}
#[tokio::test]
async fn turbo_close_with_sub_minimum_wbtc_debt_lower_minimum_control() {
assert_can_close(1).await;
}