#![cfg(feature = "integration-tests")]
use fuel_core_types::fuel_types::AssetId;
use fuels::{
accounts::ViewOnlyAccount,
prelude::{
AssetConfig as CoinAssetConfig,
ChainConfig,
Provider,
Wallet,
WalletsConfig,
launch_custom_provider_and_get_wallets,
},
programs::calls::Execution,
types::{
ContractId,
Identity,
},
};
use o2_api_types::domain::book::{
AssetConfig,
MarketIdAssets,
OrderBookConfig,
};
use o2_deploy::{
DeployParams,
MarginConfig,
MarginPriceFeedConfig,
MarginSupportedAsset,
MarginTierConfig,
MarketsConfigOutput,
MarketsConfigPartial,
deploy,
};
use o2_tools::prop::{
PriceFeedContract,
PriceFeedProxyContract,
PropMarginPoolContract,
};
fn btc_asset() -> AssetId {
AssetId::new([0xBB; 32])
}
fn usdt_asset() -> AssetId {
AssetId::new([0xCC; 32])
}
fn default_book_config() -> OrderBookConfig {
let base = AssetConfig {
symbol: "BTC".into(),
asset: btc_asset(),
decimals: 9,
min_precision: 0,
max_precision: 9,
};
let quote = AssetConfig {
symbol: "USDT".into(),
asset: usdt_asset(),
decimals: 6,
min_precision: 0,
max_precision: 6,
};
let ids = MarketIdAssets {
base_asset: btc_asset(),
quote_asset: usdt_asset(),
};
OrderBookConfig {
contract_id: None,
blob_id: None,
market_id: ids.market_id(),
taker_fee: 30,
maker_fee: 10,
min_order: 10_000,
dust: 1_000,
price_window: 20,
allow_fractional_price: false,
base,
quote,
}
}
fn tier() -> MarginTierConfig {
MarginTierConfig {
tier_id: 111,
line: 2_500_000_000_000,
leverage: 20,
duration: 21_600,
maintenance_bps: 250,
open_buffer_bps: 375,
liq_price_factor: 9_900,
prolong_fee: [
"34".to_string(),
"61".to_string(),
"204".to_string(),
"680".to_string(),
],
max_credit_line_bps: 20_000,
max_price_age: 300,
open_fee: "34".to_string(),
profit_share_bps: 0,
price_band_bps: 85,
markets: vec!["BTC/USDT".into()],
}
}
fn margin_config(publisher: Identity) -> MarginConfig {
MarginConfig {
price_feed_id: None,
price_feed: MarginPriceFeedConfig {
id: None,
max_offchain_age_seconds: Some(3_600),
publishers: vec![format!("{}", identity_config_string(publisher))],
remove_publishers: vec![],
supported_assets: vec![
MarginSupportedAsset {
asset: fuels::types::AssetId::new(*btc_asset()),
decimals: 9,
},
MarginSupportedAsset {
asset: fuels::types::AssetId::new(*usdt_asset()),
decimals: 6,
},
],
},
margin_pool_id: None,
platform_payout: None,
collateral_asset: Some(fuels::types::AssetId::new(*usdt_asset())),
collateral_decimals: Some(6),
max_tier_books: None,
base_repay_fee_ppm: None,
tiers: vec![tier()],
}
}
fn identity_config_string(identity: Identity) -> String {
match identity {
Identity::Address(address) => format!("address:0x{}", hex::encode(*address)),
Identity::ContractId(id) => format!("contract:0x{}", hex::encode(*id)),
}
}
async fn setup_wallet() -> Wallet {
setup_wallets().await.0
}
async fn setup_wallets() -> (Wallet, Wallet) {
let initial_balance = 100_000_000_000u64;
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new_multiple_assets(
2,
vec![
CoinAssetConfig {
id: fuels::types::AssetId::default(),
num_coins: 8,
coin_amount: initial_balance,
},
CoinAssetConfig {
id: fuels::types::AssetId::new(*btc_asset()),
num_coins: 4,
coin_amount: initial_balance,
},
CoinAssetConfig {
id: fuels::types::AssetId::new(*usdt_asset()),
num_coins: 4,
coin_amount: initial_balance,
},
],
),
None,
Some(ChainConfig::local_testnet()),
)
.await
.unwrap();
let second = wallets.pop().unwrap();
let first = wallets.pop().unwrap();
(first, second)
}
fn params(config: MarketsConfigPartial) -> DeployParams {
DeployParams {
deploy_config: config,
output: None,
deploy_whitelist: true,
deploy_blacklist: false,
upgrade_bytecode: false,
new_proxy_owner: None,
new_contract_owner: None,
trial_cosigner: None,
trial_creator: None,
margin_cosigner: None,
margin_liquidator: None,
margin_tier_only: false,
revoke_orderbook_maintainers: Vec::new(),
new_orderbook_maintainers: Vec::new(),
}
}
fn resume_config(out: &MarketsConfigOutput) -> MarketsConfigPartial {
MarketsConfigPartial {
starting_height: out.starting_height,
trade_account_registry_id: Some(out.trade_account_registry_id),
order_book_registry_id: Some(out.order_book_registry_id),
trade_account_oracle_id: Some(out.trade_account_oracle_id),
trial_trade_account_oracle_id: Some(out.trial_trade_account_oracle_id),
order_book_whitelist_id: out.order_book_whitelist_id,
order_book_blacklist_id: out.order_book_blacklist_id,
fast_bridge_asset_registry_proxy_id: out.fast_bridge_asset_registry_proxy_id,
pairs: out.pairs.clone(),
margin: out.margin.clone(),
}
}
async fn height(provider: &Provider) -> u32 {
provider.latest_block_height().await.unwrap()
}
#[tokio::test]
async fn scenario_1_fresh_feed_with_decimals_and_no_prices() {
let wallet = setup_wallet().await;
let provider = wallet.try_provider().unwrap().clone();
let deployer = Identity::Address(wallet.address());
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let out = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("fresh margin deploy");
let feed_id = out.price_feed_id.expect("a feed id");
let pool_id = out.margin_pool_id.expect("a pool id");
let feed = PriceFeedContract::new(feed_id, wallet.clone());
let pool = PropMarginPoolContract::new(pool_id, wallet.clone());
for (asset, decimals) in [
(fuels::types::AssetId::new(*btc_asset()), 9u8),
(fuels::types::AssetId::new(*usdt_asset()), 6u8),
] {
let live = feed
.methods()
.get_asset_decimals(asset)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(live, Some(decimals), "feed decimals for {asset}");
let priced = feed
.methods()
.has_price(asset)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert!(
!priced,
"no price must ever have been published for {asset}"
);
}
for identity in [deployer, publisher] {
let holds = feed
.methods()
.has_role(o2_tools::prop_deploy::PRICE_SUBMITTER_ROLE, identity)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert!(holds, "{identity:?} must hold the submitter role");
}
let age = feed
.methods()
.get_max_offchain_age()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(age, 3_600);
let live_feed = pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(live_feed, feed_id, "the pool must value against this feed");
let version = pool
.methods()
.current_tier_version(111)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(version, Some(1), "tier 111 must be live at version 1");
let chain_now = provider
.latest_block_time()
.await
.unwrap()
.unwrap()
.timestamp() as u64;
feed.methods()
.publish_prices(vec![
o2_tools::prop::PriceInput {
asset: fuels::types::AssetId::new(*usdt_asset()),
bid: 1_000_000_000_000_000_000u64.into(),
ask: 1_000_000_000_000_000_000u64.into(),
timestamp: chain_now - 1,
},
o2_tools::prop::PriceInput {
asset: fuels::types::AssetId::new(*btc_asset()),
bid: 2_000_000_000_000_000_000u64.into(),
ask: 2_000_000_000_000_000_000u64.into(),
timestamp: chain_now - 1,
},
])
.call()
.await
.expect("the deploy key must be able to publish");
for asset in [
fuels::types::AssetId::new(*btc_asset()),
fuels::types::AssetId::new(*usdt_asset()),
] {
assert!(
feed.methods()
.has_price(asset)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
}
println!("SCENARIO 1 OK: feed {feed_id}, pool {pool_id}");
}
#[tokio::test]
async fn scenario_2_second_run_sends_nothing() {
let wallet = setup_wallet().await;
let provider = wallet.try_provider().unwrap().clone();
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let before = height(&provider).await;
let second = deploy(wallet.clone(), params(resume_config(&first)))
.await
.expect("second run");
let after = height(&provider).await;
assert_eq!(
first.price_feed_id, second.price_feed_id,
"the feed address must not move between runs"
);
assert_eq!(first.margin_pool_id, second.margin_pool_id);
assert_eq!(first.margin_oracle_id, second.margin_oracle_id);
println!(
"SCENARIO 2: second run produced {} block(s)",
after - before
);
assert_eq!(
after - before,
0,
"a settled deploy must send no transaction on re-run"
);
}
#[tokio::test]
async fn scenario_3_upgrade_bytecode_keeps_the_feed_address() {
let wallet = setup_wallet().await;
let _provider = wallet.try_provider().unwrap().clone();
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let feed_id = first.price_feed_id.unwrap();
let feed_proxy = PriceFeedProxyContract::new(feed_id, wallet.clone());
let target_before = feed_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
let mut upgrade = params(resume_config(&first));
upgrade.upgrade_bytecode = true;
let second = deploy(wallet.clone(), upgrade).await.expect("upgrade run");
let target_after = feed_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(
first.price_feed_id, second.price_feed_id,
"the feed proxy address must survive an upgrade"
);
assert_eq!(first.margin_pool_id, second.margin_pool_id, "pool survives");
assert_eq!(
first.margin_oracle_id, second.margin_oracle_id,
"oracle survives"
);
assert_eq!(
first.trade_account_registry_id, second.trade_account_registry_id,
"registry survives"
);
assert_eq!(
target_before, target_after,
"an upgrade to the SAME build must not retarget the feed"
);
assert!(target_after.is_some(), "the feed must have a live target");
println!("SCENARIO 3 OK: feed {feed_id} target {target_after:?}");
}
#[tokio::test]
async fn scenario_4_tier_only_reconciles_the_feed_but_not_the_system() {
let wallet = setup_wallet().await;
let _provider = wallet.try_provider().unwrap().clone();
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let late_publisher = Identity::Address(fuels::types::Address::new([0x22; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let feed_id = first.price_feed_id.unwrap();
let feed = PriceFeedContract::new(feed_id, wallet.clone());
let mut config = resume_config(&first);
let margin = config.margin.as_mut().unwrap();
margin.price_feed.max_offchain_age_seconds = Some(7_200);
margin
.price_feed
.publishers
.push(identity_config_string(late_publisher));
margin
.price_feed
.publishers
.retain(|entry| entry != &identity_config_string(publisher));
margin.price_feed.remove_publishers = vec![identity_config_string(publisher)];
let mut tier_only = params(config);
tier_only.margin_tier_only = true;
let second = deploy(wallet.clone(), tier_only)
.await
.expect("tier-only run");
assert_eq!(first.price_feed_id, second.price_feed_id);
assert_eq!(first.margin_pool_id, second.margin_pool_id);
assert_eq!(
first.margin_oracle_id, second.margin_oracle_id,
"tier-only must still report the live oracle"
);
let age = feed
.methods()
.get_max_offchain_age()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(age, 7_200, "tier-only must honour the age bound");
let added = feed
.methods()
.has_role(o2_tools::prop_deploy::PRICE_SUBMITTER_ROLE, late_publisher)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
let removed = feed
.methods()
.has_role(o2_tools::prop_deploy::PRICE_SUBMITTER_ROLE, publisher)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert!(added, "tier-only must grant a newly configured publisher");
assert!(!removed, "tier-only must revoke a removed publisher");
println!("SCENARIO 4 OK");
}
#[tokio::test]
async fn scenario_5_feed_swap() {
let wallet = setup_wallet().await;
let _provider = wallet.try_provider().unwrap().clone();
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let pool_id = first.margin_pool_id.unwrap();
let pool = PropMarginPoolContract::new(pool_id, wallet.clone());
let feed_a = first.price_feed_id.unwrap();
let feed_b = deploy_second_feed(&wallet).await;
assert_ne!(feed_a, feed_b);
let mut config = resume_config(&first);
config.margin.as_mut().unwrap().price_feed.id = Some(feed_b);
config
.margin
.as_mut()
.unwrap()
.price_feed
.supported_assets
.clear();
let mut swap = params(config.clone());
swap.margin_tier_only = true;
let err = deploy(wallet.clone(), swap)
.await
.expect_err("a feed that knows nothing must not be adopted");
println!("SCENARIO 5 (a) error chain: {err:?}");
let still = pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(
still, feed_a,
"a failed swap must leave the pool where it was"
);
let mut config = resume_config(&first);
config.margin.as_mut().unwrap().price_feed.id = Some(feed_b);
let mut swap = params(config);
swap.margin_tier_only = true;
deploy(wallet.clone(), swap)
.await
.expect("the swap must land");
let moved = pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(moved, feed_b, "the pool must have moved to feed B");
println!("SCENARIO 5 OK: {feed_a} -> {feed_b}");
}
async fn deploy_second_feed(wallet: &Wallet) -> ContractId {
use fuels::{
programs::contract::{
Contract,
LoadConfiguration,
},
types::Salt,
};
use o2_tools::prop::{
PRICE_FEED_BYTECODE,
PRICE_FEED_STORAGE,
PriceFeedContractConfigurables,
State,
};
let owner = Identity::Address(wallet.address());
let slots: Vec<fuels::tx::StorageSlot> =
serde_json::from_slice(PRICE_FEED_STORAGE).unwrap();
let configurables = PriceFeedContractConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(owner))
.unwrap();
let contract =
Contract::regular(PRICE_FEED_BYTECODE.to_vec(), Salt::new([0x77; 32]), slots)
.with_configurables(configurables);
let _ = LoadConfiguration::default();
let feed_id = contract
.deploy(wallet, fuels::types::transaction::TxPolicies::default())
.await
.unwrap()
.contract_id;
let feed = PriceFeedContract::new(feed_id, wallet.clone());
feed.methods().initialize().call().await.unwrap();
feed_id
}
#[tokio::test]
async fn scenario_7a_feed_address_is_stable_across_config_changes() {
let wallet = setup_wallet().await;
let books = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: None,
..Default::default()
}),
)
.await
.expect("books only");
let derive = |mutate: fn(&mut MarginConfig)| {
let mut config = resume_config(&books);
let mut margin =
margin_config(Identity::Address(fuels::types::Address::new([0x11; 32])));
margin.price_feed.publishers.clear();
margin.price_feed.supported_assets.clear();
margin.price_feed.max_offchain_age_seconds = None;
margin.tiers.clear();
mutate(&mut margin);
config.margin = Some(margin);
config
};
let base = deploy(wallet.clone(), params(derive(|_| {})))
.await
.unwrap()
.price_feed_id;
let decimals = deploy(
wallet.clone(),
params(derive(|m| m.collateral_decimals = Some(9))),
)
.await
.unwrap()
.price_feed_id;
let books_cap = deploy(
wallet.clone(),
params(derive(|m| m.max_tier_books = Some(40))),
)
.await
.unwrap()
.price_feed_id;
let fee = deploy(
wallet.clone(),
params(derive(|m| m.base_repay_fee_ppm = Some(250))),
)
.await
.unwrap()
.price_feed_id;
let payout = deploy(
wallet.clone(),
params(derive(|m| {
m.platform_payout =
Some("address:0x3333333333333333333333333333333333333333333333333333333333333333".into())
})),
)
.await
.unwrap()
.price_feed_id;
println!(
"SCENARIO 7a base={base:?} decimals={decimals:?} books={books_cap:?} fee={fee:?} payout={payout:?}"
);
assert_eq!(base, decimals, "collateral_decimals must not fork the feed");
assert_eq!(base, books_cap, "max_tier_books must not fork the feed");
assert_eq!(base, fee, "base_repay_fee_ppm must not fork the feed");
assert_eq!(base, payout, "platform_payout must not fork the feed");
println!("SCENARIO 7a OK");
}
#[tokio::test]
async fn scenario_7b_a_feed_owned_elsewhere() {
let (wallet, stranger) = setup_wallets().await;
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let foreign = deploy_second_feed(&stranger).await;
let mut config = resume_config(&first);
let margin = config.margin.as_mut().unwrap();
margin.price_feed.id = Some(foreign);
margin.price_feed.max_offchain_age_seconds = Some(999);
let mut run = params(config);
run.margin_tier_only = true;
let err = deploy(wallet.clone(), run)
.await
.expect_err("writing to a feed we do not own must fail");
println!("SCENARIO 7b error: {err:?}");
}
#[tokio::test]
async fn scenario_7c_upgrade_against_an_unproxied_feed() {
let wallet = setup_wallet().await;
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let direct = deploy_second_feed(&wallet).await;
let mut config = resume_config(&first);
config.margin.as_mut().unwrap().price_feed.id = Some(direct);
let mut run = params(config);
run.upgrade_bytecode = true;
match deploy(wallet.clone(), run).await {
Ok(_) => println!("SCENARIO 7c: upgrade against an unproxied feed SUCCEEDED"),
Err(err) => println!("SCENARIO 7c error: {err:?}"),
}
}
#[tokio::test]
async fn scenario_7d_tier_only_with_upgrade_bytecode() {
let wallet = setup_wallet().await;
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let feed_id = first.price_feed_id.unwrap();
let feed_proxy = PriceFeedProxyContract::new(feed_id, wallet.clone());
let before = feed_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
let mut run = params(resume_config(&first));
run.margin_tier_only = true;
run.upgrade_bytecode = true;
let out = deploy(wallet.clone(), run).await;
let after = feed_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
println!(
"SCENARIO 7d: result {:?}, target {before:?} -> {after:?}",
out.map(|o| o.price_feed_id)
);
}
#[tokio::test]
async fn scenario_7e_legacy_config_against_a_live_deployment() {
let wallet = setup_wallet().await;
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin_config(publisher)),
..Default::default()
}),
)
.await
.expect("first run");
let feed_a = first.price_feed_id.unwrap();
let pool_id = first.margin_pool_id.unwrap();
let mut config = resume_config(&first);
let margin = config.margin.as_mut().unwrap();
margin.price_feed = MarginPriceFeedConfig::default();
let out = deploy(wallet.clone(), params(config)).await;
match &out {
Ok(out) => println!(
"SCENARIO 7e: run SUCCEEDED, feed {:?} (was {feed_a})",
out.price_feed_id
),
Err(err) => println!("SCENARIO 7e: run FAILED: {err:?}"),
}
let pool = PropMarginPoolContract::new(pool_id, wallet.clone());
let live = pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
println!("SCENARIO 7e: the pool now values against {live} (was {feed_a})");
}
#[tokio::test]
async fn scenario_7f_devnet_shape_legacy_feed_id_dropped() {
let wallet = setup_wallet().await;
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let external = deploy_second_feed(&wallet).await;
let feed = PriceFeedContract::new(external, wallet.clone());
for (asset, decimals) in [
(fuels::types::AssetId::new(*btc_asset()), 9u8),
(fuels::types::AssetId::new(*usdt_asset()), 6u8),
] {
feed.methods()
.set_asset_decimals(asset, decimals)
.call()
.await
.unwrap();
}
let mut margin = margin_config(publisher);
margin.price_feed.id = Some(external);
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin),
..Default::default()
}),
)
.await
.expect("first run against an external feed");
assert_eq!(first.price_feed_id, Some(external));
let pool_id = first.margin_pool_id.unwrap();
let mut config = resume_config(&first);
config.margin.as_mut().unwrap().price_feed = MarginPriceFeedConfig::default();
let out = deploy(wallet.clone(), params(config)).await;
match &out {
Ok(out) => println!(
"SCENARIO 7f: run SUCCEEDED and resolved feed {:?} (live was {external})",
out.price_feed_id
),
Err(err) => {
println!("SCENARIO 7f: run FAILED after deploying a new feed: {err:?}")
}
}
let pool = PropMarginPoolContract::new(pool_id, wallet.clone());
let live = pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
println!("SCENARIO 7f: the pool now values against {live} (was {external})");
}
#[tokio::test]
async fn scenario_7g_silent_feed_swap_when_assets_are_declared() {
let wallet = setup_wallet().await;
let publisher = Identity::Address(fuels::types::Address::new([0x11; 32]));
let external = deploy_second_feed(&wallet).await;
let feed = PriceFeedContract::new(external, wallet.clone());
for (asset, decimals) in [
(fuels::types::AssetId::new(*btc_asset()), 9u8),
(fuels::types::AssetId::new(*usdt_asset()), 6u8),
] {
feed.methods()
.set_asset_decimals(asset, decimals)
.call()
.await
.unwrap();
}
let mut margin = margin_config(publisher);
margin.price_feed.id = Some(external);
let first = deploy(
wallet.clone(),
params(MarketsConfigPartial {
starting_height: 0,
pairs: vec![default_book_config()],
margin: Some(margin),
..Default::default()
}),
)
.await
.expect("first run against an external feed");
let pool_id = first.margin_pool_id.unwrap();
let mut config = resume_config(&first);
config.margin.as_mut().unwrap().price_feed.id = None;
let out = deploy(wallet.clone(), params(config))
.await
.expect("the run must be observed, not asserted");
let pool = PropMarginPoolContract::new(pool_id, wallet.clone());
let live = pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
println!(
"SCENARIO 7g: resolved {:?}; the pool now values against {live} (was {external})",
out.price_feed_id
);
assert_ne!(
live, external,
"AUDIT: the pool was moved off its live feed"
);
}