use crate::{
blob_loader,
prop::{
PROP_ACCOUNT_BYTECODE,
PROP_ACCOUNT_ORACLE_BYTECODE,
PROP_ACCOUNT_ORACLE_PROXY_BYTECODE,
PROP_ACCOUNT_ORACLE_PROXY_STORAGE,
PROP_ACCOUNT_ORACLE_STORAGE,
PROP_ACCOUNT_PROXY_BYTECODE,
PROP_ACCOUNT_PROXY_STORAGE,
PROP_ACCOUNT_STORAGE,
PROP_MARGIN_POOL_BYTECODE,
PROP_MARGIN_POOL_PROXY_BYTECODE,
PROP_MARGIN_POOL_PROXY_STORAGE,
PROP_MARGIN_POOL_STORAGE,
PROP_PRICE_FEED_MOCK_BYTECODE,
PROP_PRICE_FEED_MOCK_STORAGE,
PropAccountContract,
PropAccountOracleContract,
PropAccountOracleContractConfigurables,
PropAccountOracleProxyContract,
PropAccountOracleProxyContractConfigurables,
PropAccountProxyContract,
PropAccountProxyContractConfigurables,
PropMarginPoolContract,
PropMarginPoolContractConfigurables,
PropMarginPoolProxyContract,
PropMarginPoolProxyContractConfigurables,
PropPriceFeedMockContract,
PropPriceFeedMockContractConfigurables,
State,
},
trade_account_registry::{
State as TradeAccountRegistryState,
TRADE_ACCOUNT_REGISTER_BYTECODE,
TRADE_ACCOUNT_REGISTER_PROXY_BYTECODE,
TRADE_ACCOUNT_REGISTER_PROXY_STORAGE,
TRADE_ACCOUNT_REGISTER_STORAGE,
TradeAccountRegistry,
TradeAccountRegistryConfigurables,
TradeAccountRegistryDeployConfig,
TradeAccountRegistryManager,
TradeAccountRegistryProxy,
TradeAccountRegistryProxyConfigurables,
},
};
use anyhow::{
Context,
Result,
ensure,
};
use fuels::{
core::{
Configurable,
Configurables,
},
prelude::*,
programs::contract::Regular,
tx::StorageSlot,
types::{
Address,
AssetId,
ContractId,
Identity,
transaction_builders::Blob,
},
};
#[derive(Clone, Debug)]
pub struct ExistingRegistry {
pub registry_id: ContractId,
pub trade_account_oracle_id: ContractId,
pub trial_trade_account_oracle_id: ContractId,
}
#[derive(Clone, Debug)]
pub struct PropDeployConfig {
pub collateral_asset: AssetId,
pub collateral_decimals: u8,
pub max_tier_books: u64,
pub owner: Option<Identity>,
pub proxy_owner: Option<Identity>,
pub cosigner: Option<Address>,
pub platform_payout: Option<Identity>,
pub liquidator: Option<Identity>,
pub salt: Salt,
pub max_words_per_blob: usize,
pub existing_registry: Option<ExistingRegistry>,
pub existing_price_feed: Option<ContractId>,
}
impl PropDeployConfig {
pub fn new(collateral_asset: AssetId) -> Self {
Self {
collateral_asset,
collateral_decimals: 6,
max_tier_books: 80,
owner: None,
proxy_owner: None,
cosigner: None,
platform_payout: None,
liquidator: None,
salt: Salt::default(),
max_words_per_blob: 10_000,
existing_registry: None,
existing_price_feed: None,
}
}
}
#[derive(Clone)]
pub struct PropDeployment<W> {
pub oracle: PropAccountOracleContract<W>,
pub oracle_proxy: PropAccountOracleProxyContract<W>,
pub oracle_id: ContractId,
pub oracle_blob_id: BlobId,
pub price_feed: PropPriceFeedMockContract<W>,
pub price_feed_id: ContractId,
pub registry: TradeAccountRegistry<W>,
pub registry_proxy: TradeAccountRegistryProxy<W>,
pub registry_id: ContractId,
pub registry_blob_id: BlobId,
pub pool: PropMarginPoolContract<W>,
pub pool_proxy: PropMarginPoolProxyContract<W>,
pub pool_id: ContractId,
pub pool_blob_id: BlobId,
pub account_blob_id: BlobId,
pub account_proxy_blob_id: BlobId,
pub account_salt: Salt,
pub deployer_wallet: W,
}
impl<W> PropDeployment<W>
where
W: Account + Clone,
{
pub async fn deploy(deployer_wallet: &W, config: &PropDeployConfig) -> Result<Self> {
ensure!(
config.collateral_asset != AssetId::zeroed(),
"prop collateral asset cannot be zero"
);
ensure!(
config.collateral_decimals <= 18,
"prop collateral decimals cannot exceed 18"
);
ensure!(
config.max_tier_books != 0,
"prop max tier books cannot be zero"
);
ensure!(
config.max_words_per_blob != 0,
"prop loader blob size cannot be zero"
);
let deployer = Identity::Address(deployer_wallet.address());
let owner = config.owner.unwrap_or(deployer);
let proxy_owner = config.proxy_owner.unwrap_or(deployer);
let cosigner = config.cosigner.unwrap_or_else(|| {
tracing::warn!(
"prop deploy: no cosigner given - defaulting to the deployer. \
The backend must run MARGIN_COSIGNER_KEY for this address or \
margin stays inert."
);
deployer_wallet.address()
});
let platform_payout = config.platform_payout.unwrap_or(deployer);
let liquidator = config.liquidator.unwrap_or_else(|| {
tracing::warn!(
"prop deploy: no liquidator given - defaulting to the deployer, \
which will receive the residue of every forced exit."
);
deployer
});
let account_blob_id = upload_implementation(
deployer_wallet,
PROP_ACCOUNT_BYTECODE,
PROP_ACCOUNT_STORAGE,
Configurables::default(),
config,
)
.await
.context("deploy prop-account implementation blob")?;
let account_proxy_blob_id = blob_loader::upload_loader_blobs(
deployer_wallet,
vec![],
Blob::new(PROP_ACCOUNT_PROXY_BYTECODE.to_vec()),
)
.await
.context("deploy raw prop-account proxy blob")?;
let oracle_bootstrap_blob_id = upload_implementation(
deployer_wallet,
PROP_ACCOUNT_ORACLE_BYTECODE,
PROP_ACCOUNT_ORACLE_STORAGE,
Configurables::default(),
config,
)
.await
.context("deploy prop-account oracle implementation blob")?;
let oracle_proxy_configurables =
PropAccountOracleProxyContractConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(proxy_owner))?
.with_INITIAL_TARGET(ContractId::from(oracle_bootstrap_blob_id))?;
let oracle_proxy_contract = regular_contract_with_implementation_storage(
PROP_ACCOUNT_ORACLE_PROXY_BYTECODE,
PROP_ACCOUNT_ORACLE_PROXY_STORAGE,
PROP_ACCOUNT_ORACLE_STORAGE,
config.salt,
)?
.with_configurables(oracle_proxy_configurables);
let (oracle_id, _) = deploy_regular(deployer_wallet, oracle_proxy_contract)
.await
.context("deploy prop-account oracle proxy")?;
let oracle_proxy =
PropAccountOracleProxyContract::new(oracle_id, deployer_wallet.clone());
let oracle = PropAccountOracleContract::new(oracle_id, deployer_wallet.clone());
let oracle_proxy_target = oracle_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read prop-account oracle proxy target")?
.value;
if oracle_proxy_target.is_none() {
oracle_proxy
.methods()
.initialize_proxy()
.call()
.await
.context("initialize prop-account oracle proxy")?;
}
let (price_feed, price_feed_id) = match config.existing_price_feed {
Some(price_feed_id) => (
PropPriceFeedMockContract::new(price_feed_id, deployer_wallet.clone()),
price_feed_id,
),
None => {
let feed_configurables =
PropPriceFeedMockContractConfigurables::default()
.with_INITIAL_OWNER(owner)?;
let feed_contract = regular_contract(
PROP_PRICE_FEED_MOCK_BYTECODE,
PROP_PRICE_FEED_MOCK_STORAGE,
config.salt,
)?
.with_configurables(feed_configurables);
let (price_feed_id, _) = deploy_regular(deployer_wallet, feed_contract)
.await
.context("deploy prop price-feed mock")?;
let price_feed = PropPriceFeedMockContract::new(
price_feed_id,
deployer_wallet.clone(),
);
let feed_is_uninitialized = price_feed
.methods()
.owner()
.simulate(Execution::state_read_only())
.await
.context("read prop price-feed mock initialization state")?
.value
== State::Uninitialized;
if feed_is_uninitialized {
price_feed
.methods()
.initialize()
.call()
.await
.context("initialize prop price-feed mock")?;
}
(price_feed, price_feed_id)
}
};
let [oracle_offset, parent_offset, index_offset] = prop_account_proxy_offsets()?;
let prop_registry_configurables = |base: TradeAccountRegistryConfigurables| {
Ok::<_, anyhow::Error>(
base.with_PROP_ACCOUNT_ORACLE_CONTRACT_ID(oracle_id)?
.with_DEFAULT_PROP_ACCOUNT_PROXY(ContractId::from(
account_proxy_blob_id,
))?
.with_PROP_ORACLE_CONFIG_OFFSET(oracle_offset)?
.with_PROP_PARENT_CONFIG_OFFSET(parent_offset)?
.with_PROP_INDEX_CONFIG_OFFSET(index_offset)?,
)
};
let (registry_id, registry_blob_id) = match &config.existing_registry {
Some(existing) => {
let manager = TradeAccountRegistryManager::new(
deployer_wallet.clone(),
existing.registry_id,
);
let deploy_config = TradeAccountRegistryDeployConfig {
registry_config: prop_registry_configurables(
TradeAccountRegistryConfigurables::default(),
)?,
..Default::default()
};
let registry_blob_id = TradeAccountRegistryManager::deploy_register_blob(
deployer_wallet,
existing.trade_account_oracle_id,
existing.trial_trade_account_oracle_id,
&deploy_config,
)
.await
.context("build upgraded shared trade-account registry blob")?;
let current_target = manager
.registry_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read shared trade-account registry proxy target")?
.value;
if current_target != Some(ContractId::from(registry_blob_id)) {
manager
.registry_proxy
.methods()
.set_proxy_target(ContractId::from(registry_blob_id))
.call()
.await
.context("upgrade shared trade-account registry for prop")?;
}
(existing.registry_id, registry_blob_id)
}
None => {
let registry_configurables = prop_registry_configurables(
TradeAccountRegistryConfigurables::default()
.with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(
owner,
))?
.with_INITIAL_TRIAL_TRADE_ACCOUNT_CREATOR(owner)?,
)?;
let registry_blob_id = upload_implementation(
deployer_wallet,
TRADE_ACCOUNT_REGISTER_BYTECODE,
TRADE_ACCOUNT_REGISTER_STORAGE,
registry_configurables,
config,
)
.await
.context("deploy shared trade-account registry implementation blob")?;
let registry_proxy_configurables =
TradeAccountRegistryProxyConfigurables::default()
.with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(
proxy_owner,
))?
.with_INITIAL_TARGET(ContractId::from(registry_blob_id))?;
let registry_proxy_contract = regular_contract(
TRADE_ACCOUNT_REGISTER_PROXY_BYTECODE,
TRADE_ACCOUNT_REGISTER_PROXY_STORAGE,
config.salt,
)?
.with_configurables(registry_proxy_configurables);
let (registry_id, registry_proxy_is_new) =
deploy_regular(deployer_wallet, registry_proxy_contract)
.await
.context("deploy shared trade-account registry proxy")?;
let registry_proxy =
TradeAccountRegistryProxy::new(registry_id, deployer_wallet.clone());
let registry =
TradeAccountRegistry::new(registry_id, deployer_wallet.clone());
if registry_proxy_is_new {
registry_proxy
.methods()
.initialize_proxy()
.call()
.await
.context("initialize shared trade-account registry proxy")?;
registry
.methods()
.initialize()
.call()
.await
.context("initialize shared trade-account registry")?;
}
(registry_id, registry_blob_id)
}
};
let registry_proxy =
TradeAccountRegistryProxy::new(registry_id, deployer_wallet.clone());
let registry = TradeAccountRegistry::new(registry_id, deployer_wallet.clone());
let pool_configurables = PropMarginPoolContractConfigurables::default()
.with_INITIAL_ADMIN(owner)?
.with_INITIAL_REGISTRY(registry_id)?
.with_INITIAL_PRICE_FEED(price_feed_id)?
.with_INITIAL_PLATFORM_PAYOUT(platform_payout)?
.with_INITIAL_LIQUIDATOR(liquidator)?
.with_COLLATERAL_ASSET(config.collateral_asset)?
.with_COLLATERAL_DECIMALS(config.collateral_decimals)?
.with_MAX_TIER_BOOKS(config.max_tier_books)?;
let pool_blob_id = upload_implementation(
deployer_wallet,
PROP_MARGIN_POOL_BYTECODE,
PROP_MARGIN_POOL_STORAGE,
pool_configurables,
config,
)
.await
.context("deploy prop margin-pool implementation blob")?;
let pool_bootstrap_blob_id = upload_implementation(
deployer_wallet,
PROP_MARGIN_POOL_BYTECODE,
PROP_MARGIN_POOL_STORAGE,
Configurables::default(),
config,
)
.await
.context("deploy prop margin-pool bootstrap implementation blob")?;
let pool_proxy_configurables =
PropMarginPoolProxyContractConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(proxy_owner))?
.with_INITIAL_TARGET(ContractId::from(pool_bootstrap_blob_id))?;
let pool_proxy_contract = regular_contract_with_implementation_storage(
PROP_MARGIN_POOL_PROXY_BYTECODE,
PROP_MARGIN_POOL_PROXY_STORAGE,
PROP_MARGIN_POOL_STORAGE,
config.salt,
)?
.with_configurables(pool_proxy_configurables);
let (pool_id, _) = deploy_regular(deployer_wallet, pool_proxy_contract)
.await
.context("deploy prop margin-pool proxy")?;
let pool_proxy =
PropMarginPoolProxyContract::new(pool_id, deployer_wallet.clone());
let pool = PropMarginPoolContract::new(pool_id, deployer_wallet.clone());
let pool_proxy_target = pool_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read prop margin-pool proxy target")?
.value;
if pool_proxy_target.is_none() {
pool_proxy
.methods()
.initialize_proxy()
.call()
.await
.context("initialize prop margin-pool proxy")?;
}
let pool_target = pool_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read configured prop margin-pool proxy target")?
.value;
if pool_target != Some(ContractId::from(pool_blob_id)) {
pool_proxy
.methods()
.set_proxy_target(ContractId::from(pool_blob_id))
.call()
.await
.context("activate configured prop margin-pool implementation")?;
}
let oracle_configurables = PropAccountOracleContractConfigurables::default()
.with_INITIAL_OWNER(owner)?
.with_INITIAL_PROP_ACCOUNT_IMPL(ContractId::from(account_blob_id))?
.with_INITIAL_COSIGNER(cosigner)?
.with_INITIAL_PROP_MARGIN_POOL(pool_id)?;
let oracle_blob_id = upload_implementation(
deployer_wallet,
PROP_ACCOUNT_ORACLE_BYTECODE,
PROP_ACCOUNT_ORACLE_STORAGE,
oracle_configurables,
config,
)
.await
.context("deploy configured prop-account oracle implementation blob")?;
let oracle_target = oracle_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read prop-account oracle proxy target")?
.value;
if oracle_target != Some(ContractId::from(oracle_blob_id)) {
oracle_proxy
.methods()
.set_proxy_target(ContractId::from(oracle_blob_id))
.call()
.await
.context("activate configured prop-account oracle implementation")?;
}
let oracle_is_uninitialized = oracle
.methods()
.owner()
.simulate(Execution::state_read_only())
.await
.context("read prop-account oracle initialization state")?
.value
== State::Uninitialized;
if oracle_is_uninitialized {
oracle
.methods()
.initialize()
.call()
.await
.context("initialize prop-account oracle")?;
}
let deployed_collateral = pool
.methods()
.collateral_asset()
.simulate(Execution::state_read_only())
.await
.context("read deployed prop margin-pool collateral")?
.value;
ensure!(
deployed_collateral == config.collateral_asset,
"prop margin-pool collateral configurable mismatch"
);
ensure!(
pool.methods()
.collateral_decimals()
.simulate(Execution::state_read_only())
.await
.context("read deployed prop margin-pool collateral decimals")?
.value
== config.collateral_decimals,
"prop margin-pool decimals configurable mismatch"
);
ensure!(
pool.methods()
.max_tier_books()
.simulate(Execution::state_read_only())
.await
.context("read deployed prop margin-pool book limit")?
.value
== config.max_tier_books,
"prop margin-pool book-limit configurable mismatch"
);
let pool_is_initialized = pool
.methods()
.is_initialized()
.simulate(Execution::state_read_only())
.await
.context("read deployed prop margin-pool initialization state")?
.value;
if !pool_is_initialized {
pool.methods()
.initialize()
.call()
.await
.context("initialize prop margin pool")?;
}
Ok(Self {
oracle,
oracle_proxy,
oracle_id,
oracle_blob_id,
price_feed,
price_feed_id,
registry,
registry_proxy,
registry_id,
registry_blob_id,
pool,
pool_proxy,
pool_id,
pool_blob_id,
account_blob_id,
account_proxy_blob_id,
account_salt: config.salt,
deployer_wallet: deployer_wallet.clone(),
})
}
pub async fn deploy_account(
&self,
parent_caller: &W,
parent: Identity,
index: u64,
) -> Result<PropAccountProxyContract<W>> {
ensure!(
parent != Identity::Address(Address::zeroed()),
"prop-account parent cannot be zero"
);
let configurables = PropAccountProxyContractConfigurables::default()
.with_ORACLE_CONTRACT_ID(self.oracle_id)?
.with_PARENT(parent)?
.with_INDEX(index)?;
let child_contract = regular_contract(
PROP_ACCOUNT_PROXY_BYTECODE,
PROP_ACCOUNT_PROXY_STORAGE,
self.account_salt,
)?
.with_configurables(configurables);
let (child_id, _) = deploy_regular(&self.deployer_wallet, child_contract)
.await
.context("deploy prop-account child")?;
let child = PropAccountProxyContract::new(child_id, self.deployer_wallet.clone());
if !self
.registry
.methods()
.prop_is_valid(child_id)
.simulate(Execution::state_read_only())
.await?
.value
{
TradeAccountRegistry::new(self.registry_id, parent_caller.clone())
.methods()
.prop_register_contract(child_id, parent, index)
.with_contract_ids(&[self.oracle_id, child_id, self.pool_id])
.call()
.await
.context("register prop-account child")?;
}
Ok(child)
}
pub fn account(&self, account_id: ContractId) -> PropAccountContract<W> {
PropAccountContract::new(account_id, self.deployer_wallet.clone())
}
pub async fn verify(
deployer_wallet: &W,
config: &PropDeployConfig,
) -> Result<PropDeployReport> {
let provider = deployer_wallet.try_provider()?;
let deployer = Identity::Address(deployer_wallet.address());
let owner = config.owner.unwrap_or(deployer);
let proxy_owner = config.proxy_owner.unwrap_or(deployer);
let cosigner = config.cosigner.unwrap_or_else(|| {
tracing::warn!(
"prop deploy: no cosigner given - defaulting to the deployer. \
The backend must run MARGIN_COSIGNER_KEY for this address or \
margin stays inert."
);
deployer_wallet.address()
});
let platform_payout = config.platform_payout.unwrap_or(deployer);
let liquidator = config.liquidator.unwrap_or_else(|| {
tracing::warn!(
"prop deploy: no liquidator given - defaulting to the deployer, \
which will receive the residue of every forced exit."
);
deployer
});
let mut report = PropDeployReport::default();
let mut note =
|name: &'static str, id: ContractId, exists: bool, action: String| {
tracing::info!("[prop verify] {name}: {id} — {action}");
report.components.push(PropComponentStatus {
name,
id,
exists,
action,
});
};
let blob_action = |exists: bool| {
if exists {
"present".to_string()
} else {
"would upload blob".to_string()
}
};
let account_blob_id = loader_blob_id(
PROP_ACCOUNT_BYTECODE,
PROP_ACCOUNT_STORAGE,
Configurables::default(),
config,
)?;
let account_blob_exists = provider.blob_exists(account_blob_id).await?;
note(
"prop-account implementation blob",
ContractId::from(account_blob_id),
account_blob_exists,
blob_action(account_blob_exists),
);
let account_proxy_blob_id = Blob::new(PROP_ACCOUNT_PROXY_BYTECODE.to_vec()).id();
let account_proxy_blob_exists =
provider.blob_exists(account_proxy_blob_id).await?;
note(
"prop-account raw proxy blob",
ContractId::from(account_proxy_blob_id),
account_proxy_blob_exists,
blob_action(account_proxy_blob_exists),
);
let oracle_bootstrap_blob_id = loader_blob_id(
PROP_ACCOUNT_ORACLE_BYTECODE,
PROP_ACCOUNT_ORACLE_STORAGE,
Configurables::default(),
config,
)?;
let oracle_proxy_configurables =
PropAccountOracleProxyContractConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(proxy_owner))?
.with_INITIAL_TARGET(ContractId::from(oracle_bootstrap_blob_id))?;
let oracle_id = regular_contract_with_implementation_storage(
PROP_ACCOUNT_ORACLE_PROXY_BYTECODE,
PROP_ACCOUNT_ORACLE_PROXY_STORAGE,
PROP_ACCOUNT_ORACLE_STORAGE,
config.salt,
)?
.with_configurables(oracle_proxy_configurables)
.contract_id();
let oracle_exists = provider.contract_exists(&oracle_id).await?;
report.oracle_id = oracle_id;
note(
"prop-account oracle proxy",
oracle_id,
oracle_exists,
if oracle_exists {
"present".to_string()
} else {
"would deploy + initialize".to_string()
},
);
let price_feed_id = match config.existing_price_feed {
Some(price_feed_id) => {
let exists = provider.contract_exists(&price_feed_id).await?;
note(
"price feed (existing)",
price_feed_id,
exists,
if exists {
"present".to_string()
} else {
"MISSING — configured feed is not on chain".to_string()
},
);
price_feed_id
}
None => {
let feed_configurables =
PropPriceFeedMockContractConfigurables::default()
.with_INITIAL_OWNER(owner)?;
let feed_id = regular_contract(
PROP_PRICE_FEED_MOCK_BYTECODE,
PROP_PRICE_FEED_MOCK_STORAGE,
config.salt,
)?
.with_configurables(feed_configurables)
.contract_id();
let exists = provider.contract_exists(&feed_id).await?;
note(
"price feed (mock)",
feed_id,
exists,
if exists {
"present".to_string()
} else {
"would deploy + initialize".to_string()
},
);
feed_id
}
};
report.price_feed_id = price_feed_id;
let [oracle_offset, parent_offset, index_offset] = prop_account_proxy_offsets()?;
let prop_registry_config = TradeAccountRegistryConfigurables::default()
.with_PROP_ACCOUNT_ORACLE_CONTRACT_ID(oracle_id)?
.with_DEFAULT_PROP_ACCOUNT_PROXY(ContractId::from(account_proxy_blob_id))?
.with_PROP_ORACLE_CONFIG_OFFSET(oracle_offset)?
.with_PROP_PARENT_CONFIG_OFFSET(parent_offset)?
.with_PROP_INDEX_CONFIG_OFFSET(index_offset)?;
let registry_id = match &config.existing_registry {
Some(existing) => {
let deploy_config = TradeAccountRegistryDeployConfig {
registry_config: prop_registry_config,
..Default::default()
};
let proxy_blob = TradeAccountRegistryManager::register_proxy_blob(
deployer_wallet,
&deploy_config,
)
.await?;
let trial_proxy_blob =
TradeAccountRegistryManager::register_trial_proxy_blob(
deployer_wallet,
&deploy_config,
)
.await?;
let upgraded_blob = TradeAccountRegistryManager::register_blob(
deployer_wallet,
existing.trade_account_oracle_id,
existing.trial_trade_account_oracle_id,
proxy_blob.id,
trial_proxy_blob.id,
&deploy_config,
)
.await?;
let manager = TradeAccountRegistryManager::new(
deployer_wallet.clone(),
existing.registry_id,
);
let current_target = manager
.registry_proxy
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read shared trade-account registry proxy target")?
.value;
let upgraded = current_target == Some(ContractId::from(upgraded_blob.id));
note(
"shared trade-account registry (in-place upgrade)",
existing.registry_id,
true,
if upgraded {
"implementation current — no-op".to_string()
} else {
format!(
"would retarget proxy {} -> {}",
current_target
.map(|target| target.to_string())
.unwrap_or_else(|| "unset".to_string()),
ContractId::from(upgraded_blob.id),
)
},
);
existing.registry_id
}
None => {
let registry_configurables = prop_registry_config
.with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(owner))?
.with_INITIAL_TRIAL_TRADE_ACCOUNT_CREATOR(owner)?;
let registry_blob_id = loader_blob_id(
TRADE_ACCOUNT_REGISTER_BYTECODE,
TRADE_ACCOUNT_REGISTER_STORAGE,
registry_configurables,
config,
)?;
let registry_proxy_configurables =
TradeAccountRegistryProxyConfigurables::default()
.with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(
proxy_owner,
))?
.with_INITIAL_TARGET(ContractId::from(registry_blob_id))?;
let registry_id = regular_contract(
TRADE_ACCOUNT_REGISTER_PROXY_BYTECODE,
TRADE_ACCOUNT_REGISTER_PROXY_STORAGE,
config.salt,
)?
.with_configurables(registry_proxy_configurables)
.contract_id();
let exists = provider.contract_exists(®istry_id).await?;
note(
"dedicated trade-account registry",
registry_id,
exists,
if exists {
"present".to_string()
} else {
"would deploy + initialize".to_string()
},
);
registry_id
}
};
report.registry_id = registry_id;
let pool_configurables = PropMarginPoolContractConfigurables::default()
.with_INITIAL_ADMIN(owner)?
.with_INITIAL_REGISTRY(registry_id)?
.with_INITIAL_PRICE_FEED(price_feed_id)?
.with_INITIAL_PLATFORM_PAYOUT(platform_payout)?
.with_INITIAL_LIQUIDATOR(liquidator)?
.with_COLLATERAL_ASSET(config.collateral_asset)?
.with_COLLATERAL_DECIMALS(config.collateral_decimals)?
.with_MAX_TIER_BOOKS(config.max_tier_books)?;
let pool_blob_id = loader_blob_id(
PROP_MARGIN_POOL_BYTECODE,
PROP_MARGIN_POOL_STORAGE,
pool_configurables,
config,
)?;
let pool_bootstrap_blob_id = loader_blob_id(
PROP_MARGIN_POOL_BYTECODE,
PROP_MARGIN_POOL_STORAGE,
Configurables::default(),
config,
)?;
let pool_proxy_configurables =
PropMarginPoolProxyContractConfigurables::default()
.with_INITIAL_OWNER(State::Initialized(proxy_owner))?
.with_INITIAL_TARGET(ContractId::from(pool_bootstrap_blob_id))?;
let pool_id = regular_contract_with_implementation_storage(
PROP_MARGIN_POOL_PROXY_BYTECODE,
PROP_MARGIN_POOL_PROXY_STORAGE,
PROP_MARGIN_POOL_STORAGE,
config.salt,
)?
.with_configurables(pool_proxy_configurables)
.contract_id();
report.pool_id = pool_id;
let pool_exists = provider.contract_exists(&pool_id).await?;
let pool_action = if !pool_exists {
"would deploy + initialize".to_string()
} else {
let pool = PropMarginPoolContract::new(pool_id, deployer_wallet.clone());
let initialized = pool
.methods()
.is_initialized()
.simulate(Execution::state_read_only())
.await
.context("read prop margin-pool initialization state")?
.value;
let target =
PropMarginPoolProxyContract::new(pool_id, deployer_wallet.clone())
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read prop margin-pool proxy target")?
.value;
let owes_upgrade = target != Some(ContractId::from(pool_blob_id));
match (initialized, owes_upgrade) {
(true, false) => "present + initialized".to_string(),
(true, true) => {
"present + initialized — WOULD UPGRADE implementation".to_string()
}
(false, false) => "present — would initialize".to_string(),
(false, true) => {
"present — would initialize + WOULD UPGRADE implementation"
.to_string()
}
}
};
note("prop margin pool proxy", pool_id, pool_exists, pool_action);
let oracle_configurables = PropAccountOracleContractConfigurables::default()
.with_INITIAL_OWNER(owner)?
.with_INITIAL_PROP_ACCOUNT_IMPL(ContractId::from(account_blob_id))?
.with_INITIAL_COSIGNER(cosigner)?
.with_INITIAL_PROP_MARGIN_POOL(pool_id)?;
let oracle_blob_id = loader_blob_id(
PROP_ACCOUNT_ORACLE_BYTECODE,
PROP_ACCOUNT_ORACLE_STORAGE,
oracle_configurables,
config,
)?;
let oracle_impl_action = if !oracle_exists {
"would upload + retarget after oracle proxy deploy".to_string()
} else {
let current_target =
PropAccountOracleProxyContract::new(oracle_id, deployer_wallet.clone())
.methods()
.proxy_target()
.simulate(Execution::state_read_only())
.await
.context("read prop-account oracle proxy target")?
.value;
if current_target == Some(ContractId::from(oracle_blob_id)) {
"current".to_string()
} else {
format!(
"would retarget oracle proxy {} -> {}",
current_target
.map(|target| target.to_string())
.unwrap_or_else(|| "unset".to_string()),
ContractId::from(oracle_blob_id),
)
}
};
note(
"prop-account oracle implementation",
ContractId::from(oracle_blob_id),
oracle_exists,
oracle_impl_action,
);
report.up_to_date = report.components.iter().all(|component| {
component.action == "present"
|| component.action == "present + initialized"
|| component.action == "current"
|| component.action == "implementation current — no-op"
});
Ok(report)
}
}
#[derive(Clone, Debug)]
pub struct PropComponentStatus {
pub name: &'static str,
pub id: ContractId,
pub exists: bool,
pub action: String,
}
#[derive(Clone, Debug, Default)]
pub struct PropDeployReport {
pub components: Vec<PropComponentStatus>,
pub oracle_id: ContractId,
pub price_feed_id: ContractId,
pub registry_id: ContractId,
pub pool_id: ContractId,
pub up_to_date: bool,
}
fn prop_account_proxy_offsets() -> Result<[u64; 3]> {
fn only_offset(configurables: Configurables, name: &str) -> Result<u64> {
let offsets: Vec<Configurable> = configurables.offsets_with_data;
ensure!(
offsets.len() == 1,
"expected one {name} configurable, got {}",
offsets.len()
);
Ok(offsets[0].offset)
}
let oracle: Configurables = PropAccountProxyContractConfigurables::default()
.with_ORACLE_CONTRACT_ID(ContractId::zeroed())?
.into();
let parent: Configurables = PropAccountProxyContractConfigurables::default()
.with_PARENT(Identity::Address(Address::zeroed()))?
.into();
let index: Configurables = PropAccountProxyContractConfigurables::default()
.with_INDEX(0)?
.into();
Ok([
only_offset(oracle, "oracle")?,
only_offset(parent, "parent")?,
only_offset(index, "index")?,
])
}
fn storage_slots(bytes: &[u8]) -> Result<Vec<StorageSlot>> {
serde_json::from_slice(bytes).context("decode contract storage slots")
}
fn loader_blob_id(
bytecode: &[u8],
storage: &[u8],
configurables: impl Into<Configurables>,
config: &PropDeployConfig,
) -> Result<BlobId> {
let (_, loader_blob) = blob_loader::build_loader_blobs(
bytecode.to_vec(),
config.salt,
storage_slots(storage)?,
configurables,
config.max_words_per_blob,
)?;
Ok(loader_blob.id())
}
fn regular_contract(
bytecode: &[u8],
storage: &[u8],
salt: Salt,
) -> Result<Contract<Regular>> {
Ok(Contract::regular(
bytecode.to_vec(),
salt,
storage_slots(storage)?,
))
}
fn regular_contract_with_implementation_storage(
bytecode: &[u8],
proxy_storage: &[u8],
implementation_storage: &[u8],
salt: Salt,
) -> Result<Contract<Regular>> {
let mut slots = storage_slots(proxy_storage)?;
for implementation_slot in storage_slots(implementation_storage)? {
ensure!(
!slots
.iter()
.any(|proxy_slot| proxy_slot.key() == implementation_slot.key()),
"proxy and implementation storage slots collide"
);
slots.push(implementation_slot);
}
Ok(Contract::regular(bytecode.to_vec(), salt, slots))
}
async fn upload_implementation<W>(
deployer_wallet: &W,
bytecode: &[u8],
storage: &[u8],
configurables: impl Into<Configurables>,
config: &PropDeployConfig,
) -> Result<BlobId>
where
W: Account,
{
let (data_blobs, loader_blob) = blob_loader::build_loader_blobs(
bytecode.to_vec(),
config.salt,
storage_slots(storage)?,
configurables,
config.max_words_per_blob,
)?;
blob_loader::upload_loader_blobs(deployer_wallet, data_blobs, loader_blob).await
}
async fn deploy_regular<W>(
deployer_wallet: &W,
contract: Contract<Regular>,
) -> Result<(ContractId, bool)>
where
W: Account,
{
let contract_id = contract.contract_id();
let is_new = !deployer_wallet
.try_provider()?
.contract_exists(&contract_id)
.await?;
if is_new {
contract
.deploy(deployer_wallet, TxPolicies::default())
.await?;
}
Ok((contract_id, is_new))
}
#[cfg(test)]
static PROP_DEPLOY_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
#[cfg(test)]
mod tests {
use super::*;
use crate::{
order_book_deploy::{
OrderArgs,
OrderBookConfigurables,
OrderBookDeploy,
OrderBookDeployConfig,
OrderType,
},
prop::{
AbsorbedFundsTransferred,
InventoryFunded,
MarginPoolPauseChanged,
MarginPoolPlatformPayoutChanged,
MarginPoolPriceFeedChanged,
MarginPoolRegistryChanged,
MarginTradeAccountWithdrawn,
PriceInput,
PropOrderBookCleanup,
SessionClosed,
SettlementReason,
TierParams,
},
};
use fuels::test_helpers::{
AssetConfig,
WalletsConfig,
launch_custom_provider_and_get_wallets,
};
use std::time::{
SystemTime,
UNIX_EPOCH,
};
fn assert_recent_timestamp(timestamp: u64) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time predates the Unix epoch")
.as_secs();
assert!(
timestamp.abs_diff(now) <= 1,
"event timestamp {timestamp} is not close to {now}"
);
}
#[tokio::test]
async fn deploys_independent_prop_system_and_registers_child() {
let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
let collateral_asset = AssetId::new([1; 32]);
let base_asset = AssetId::new([2; 32]);
let initial_balance = 10_000_000_000u64;
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new_multiple_assets(
2,
vec![
AssetConfig {
id: AssetId::default(),
num_coins: 2,
coin_amount: initial_balance,
},
AssetConfig {
id: collateral_asset,
num_coins: 2,
coin_amount: initial_balance,
},
AssetConfig {
id: base_asset,
num_coins: 2,
coin_amount: initial_balance,
},
],
),
None,
Some(::fuels::test_helpers::ChainConfig::local_testnet()),
)
.await
.unwrap();
let user = wallets.pop().unwrap();
let deployer = wallets.pop().unwrap();
let deployment =
PropDeployment::deploy(&deployer, &PropDeployConfig::new(collateral_asset))
.await
.unwrap();
let oracle_target = deployment
.oracle
.methods()
.get_prop_account_impl()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(
oracle_target,
Some(ContractId::from(deployment.account_blob_id))
);
assert_eq!(
deployment
.oracle
.methods()
.get_prop_margin_pool()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(deployment.pool_id)
);
assert_eq!(
deployment
.registry
.methods()
.get_prop_oracle_id()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.oracle_id
);
assert_eq!(
deployment
.registry
.methods()
.get_prop_pool_id()
.with_contract_ids(&[deployment.oracle_id])
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.pool_id
);
assert_eq!(
deployment
.pool
.methods()
.registry()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.registry_id
);
assert_eq!(
deployment
.pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.price_feed_id
);
let deployer_identity = Identity::Address(deployer.address());
let inventory_amount = 1_234;
let inventory_update = deployment
.pool
.methods()
.fund_inventory()
.call_params(CallParameters::new(
inventory_amount,
collateral_asset,
u64::MAX,
))
.unwrap()
.call()
.await
.unwrap();
let inventory_events = inventory_update
.decode_logs_with_type::<InventoryFunded>()
.unwrap();
assert_recent_timestamp(inventory_events[0].timestamp.unix);
assert_eq!(
inventory_events,
vec![InventoryFunded {
asset_id: collateral_asset,
amount: inventory_amount,
new_inventory: inventory_amount,
timestamp: inventory_events[0].timestamp.clone(),
}]
);
let registry_update = deployment
.pool
.methods()
.set_registry(deployment.oracle_id)
.call()
.await
.unwrap();
let registry_events = registry_update
.decode_logs_with_type::<MarginPoolRegistryChanged>()
.unwrap();
assert_recent_timestamp(registry_events[0].timestamp.unix);
assert_eq!(
registry_events,
vec![MarginPoolRegistryChanged {
old_registry: deployment.registry_id,
new_registry: deployment.oracle_id,
timestamp: registry_events[0].timestamp.clone(),
}]
);
assert_eq!(
deployment
.pool
.methods()
.registry()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.oracle_id
);
deployment
.pool
.methods()
.set_registry(deployment.registry_id)
.call()
.await
.unwrap();
let price_feed_update = deployment
.pool
.methods()
.set_price_feed(deployment.oracle_id)
.call()
.await
.unwrap();
let price_feed_events = price_feed_update
.decode_logs_with_type::<MarginPoolPriceFeedChanged>()
.unwrap();
assert_recent_timestamp(price_feed_events[0].timestamp.unix);
assert_eq!(
price_feed_events,
vec![MarginPoolPriceFeedChanged {
old_price_feed: deployment.price_feed_id,
new_price_feed: deployment.oracle_id,
timestamp: price_feed_events[0].timestamp.clone(),
}]
);
assert_eq!(
deployment
.pool
.methods()
.price_feed()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.oracle_id
);
deployment
.pool
.methods()
.set_price_feed(deployment.price_feed_id)
.call()
.await
.unwrap();
let platform_payout = Identity::Address(user.address());
let payout_update = deployment
.pool
.methods()
.set_platform_payout(platform_payout)
.call()
.await
.unwrap();
let payout_events = payout_update
.decode_logs_with_type::<MarginPoolPlatformPayoutChanged>()
.unwrap();
assert_recent_timestamp(payout_events[0].timestamp.unix);
assert_eq!(
payout_events,
vec![MarginPoolPlatformPayoutChanged {
old_platform_payout: deployer_identity,
new_platform_payout: platform_payout,
timestamp: payout_events[0].timestamp.clone(),
}]
);
deployment
.pool
.methods()
.set_platform_payout(deployer_identity)
.call()
.await
.unwrap();
let pause_response = deployment.pool.methods().pause().call().await.unwrap();
let pause_events = pause_response
.decode_logs_with_type::<MarginPoolPauseChanged>()
.unwrap();
assert_recent_timestamp(pause_events[0].timestamp.unix);
assert_eq!(pause_events.len(), 1);
assert!(pause_events[0].paused);
let unpause_response = deployment.pool.methods().unpause().call().await.unwrap();
let unpause_events = unpause_response
.decode_logs_with_type::<MarginPoolPauseChanged>()
.unwrap();
assert_recent_timestamp(unpause_events[0].timestamp.unix);
assert_eq!(unpause_events.len(), 1);
assert!(!unpause_events[0].paused);
let user_pool = PropMarginPoolContract::new(deployment.pool_id, user.clone());
assert!(
user_pool
.methods()
.set_registry(deployment.oracle_id)
.call()
.await
.is_err()
);
assert!(
deployment
.pool
.methods()
.has_role(0, deployer_identity)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
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_config =
OrderBookDeployConfig::with_configurables(order_book_configurables);
let order_book = OrderBookDeploy::deploy(
&deployer,
base_asset,
collateral_asset,
&order_book_config,
)
.await
.unwrap();
let mut second_order_book_config = order_book_config.clone();
second_order_book_config.salt = Salt::from([1u8; 32]);
let second_order_book = OrderBookDeploy::deploy(
&deployer,
base_asset,
collateral_asset,
&second_order_book_config,
)
.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();
assert!(
deployment
.price_feed
.methods()
.has_price(collateral_asset)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let tier_params = TierParams {
line: 10_000_000,
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: 60,
open_fee_bps: 0,
profit_share_bps: 1_000,
price_band_bps: 1_000,
};
assert!(
deployment
.pool
.methods()
.publish_tier_version(
1,
TierParams {
leverage: 0,
..tier_params.clone()
},
vec![order_book.contract_id],
)
.call()
.await
.is_err()
);
assert!(
deployment
.pool
.methods()
.publish_tier_version(
1,
TierParams {
open_fee_bps: 2_000,
..tier_params.clone()
},
vec![order_book.contract_id],
)
.call()
.await
.is_err()
);
deployment
.pool
.methods()
.publish_tier_version(
1,
tier_params,
vec![order_book.contract_id, second_order_book.contract_id],
)
.with_contract_ids(&[
order_book.contract_id,
second_order_book.contract_id,
deployment.price_feed_id,
])
.call()
.await
.unwrap();
let tier = deployment
.pool
.methods()
.get_tier(1, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.expect("published tier version");
assert_eq!(tier.line, 10_000_000);
assert_eq!(
deployment
.pool
.methods()
.current_tier_version(1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(1)
);
assert_eq!(
deployment
.pool
.methods()
.tier_books(1, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
vec![order_book.contract_id, second_order_book.contract_id]
);
let tier_assets = deployment
.pool
.methods()
.tier_assets(1, 1)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value;
assert_eq!(tier_assets.len(), 2);
assert!(tier_assets.contains(&base_asset));
assert!(tier_assets.contains(&collateral_asset));
assert!(
deployment
.pool
.methods()
.get_tier(1, 2)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.is_none()
);
assert!(
deployment
.pool
.methods()
.tier_books(1, 2)
.simulate(Execution::state_read_only())
.await
.is_err()
);
assert!(
deployment
.pool
.methods()
.tier_assets(1, 2)
.simulate(Execution::state_read_only())
.await
.is_err()
);
deployment
.price_feed
.methods()
.set_asset_decimals(base_asset, 8)
.call()
.await
.unwrap();
deployment
.price_feed
.methods()
.publish_prices(vec![PriceInput {
asset: base_asset,
bid: 2_000_000_000_000_000_000u64.into(),
ask: 2_000_000_000_000_000_000u64.into(),
timestamp: 1,
}])
.call()
.await
.unwrap();
assert!(
deployment
.pool
.methods()
.set_price_feed(deployment.price_feed_id)
.with_contracts(&[&deployment.price_feed])
.call()
.await
.is_err()
);
deployment
.price_feed
.methods()
.set_asset_decimals(base_asset, 9)
.call()
.await
.unwrap();
deployment
.price_feed
.methods()
.publish_prices(vec![PriceInput {
asset: base_asset,
bid: 2_000_000_000_000_000_000u64.into(),
ask: 2_000_000_000_000_000_000u64.into(),
timestamp: 2,
}])
.call()
.await
.unwrap();
let parent = Identity::Address(user.address());
let registration_error = deployment
.deploy_account(&deployer, parent, 7)
.await
.expect_err("a caller other than the configured parent must be rejected");
assert!(
format!("{registration_error:?}").contains("NotParent"),
"unexpected registration error: {registration_error:?}"
);
let child = deployment.deploy_account(&user, parent, 7).await.unwrap();
assert!(
deployment
.registry
.methods()
.prop_is_valid(child.contract_id())
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
assert_eq!(
child
.methods()
.parent()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
parent
);
assert_eq!(
child
.methods()
.index()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
7
);
assert_eq!(
child
.methods()
.pool()
.with_contract_ids(&[deployment.oracle_id])
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.pool_id
);
assert_eq!(
child
.methods()
.oracle()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
deployment.oracle_id
);
let collateral = 2_000_000;
user.force_transfer_to_contract(
child.contract_id(),
collateral,
collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let account = PropAccountContract::new(child.contract_id(), user.clone());
account
.methods()
.start_session(1, collateral)
.with_contract_ids(&[
deployment.oracle_id,
deployment.pool_id,
deployment.registry_id,
])
.call()
.await
.unwrap();
assert!(
deployment
.pool
.methods()
.is_call_allowed(child.contract_id(), order_book.contract_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
assert!(
deployment
.pool
.methods()
.is_call_allowed(child.contract_id(), second_order_book.contract_id)
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
assert!(
!deployment
.pool
.methods()
.is_call_allowed(child.contract_id(), ContractId::new([0x99; 32]))
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let session = deployment
.pool
.methods()
.get_session(child.contract_id())
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.expect("prop session opened");
assert_eq!(session.session_id, 1);
assert_eq!(session.credit_line, 10_000_000);
assert_eq!(session.collateral, collateral);
assert!(
deployment
.pool
.methods()
.min_sellable(child.contract_id(), base_asset)
.with_contracts(&[&order_book.order_book, &second_order_book.order_book,])
.simulate(Execution::state_read_only())
.await
.is_err()
);
let best_bid = 2_000_000;
let bid_quantity = 1_000_000_000;
order_book
.order_book
.methods()
.create_order(OrderArgs {
price: best_bid,
quantity: bid_quantity,
order_type: OrderType::Spot,
})
.call_params(CallParameters::new(
bid_quantity * best_bid / 1_000_000_000,
collateral_asset,
u64::MAX,
))
.unwrap()
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
assert_eq!(
order_book
.order_book
.methods()
.get_base_decimals()
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
1_000_000_000
);
assert_eq!(
deployment
.pool
.methods()
.min_sellable(child.contract_id(), base_asset)
.with_contracts(&[&order_book.order_book, &second_order_book.order_book,])
.simulate(Execution::state_read_only())
.await
.unwrap()
.value,
Some(500)
);
let close = account
.methods()
.close_session(Vec::<PropOrderBookCleanup>::new())
.with_contracts(&[
&deployment.oracle,
&deployment.pool,
&order_book.order_book,
&second_order_book.order_book,
])
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
let close_events = close.decode_logs_with_type::<SessionClosed>().unwrap();
assert!(
close
.decode_logs_with_type::<AbsorbedFundsTransferred>()
.unwrap()
.is_empty()
);
assert_recent_timestamp(close_events[0].timestamp.unix);
assert_eq!(
close_events,
vec![SessionClosed {
account: child.contract_id(),
session_id: 1,
reason: SettlementReason::UserClose,
v: 10_000_000,
v_is_negative: false,
v_liq: 10_000_000,
v_liq_is_negative: false,
profit_abs: 0,
profit_is_negative: false,
fees_accrued: 0,
platform_total: 0,
user_net: collateral,
bad_debt: 0,
cancelled_debt: vec![],
payout_parent: vec![(collateral_asset, collateral)],
payout_platform: vec![],
timestamp: close_events[0].timestamp.clone(),
}]
);
assert!(
!deployment
.pool
.methods()
.has_session(child.contract_id())
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
);
let excess_collateral = 25_000_000;
user.force_transfer_to_contract(
child.contract_id(),
excess_collateral,
collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
account
.methods()
.start_session(1, excess_collateral)
.with_contract_ids(&[
deployment.oracle_id,
deployment.pool_id,
deployment.registry_id,
])
.call()
.await
.unwrap();
let excess_session = deployment
.pool
.methods()
.get_session(child.contract_id())
.simulate(Execution::state_read_only())
.await
.unwrap()
.value
.expect("excess-collateral session opened");
assert_eq!(excess_session.session_id, 2);
assert_eq!(excess_session.collateral, excess_collateral);
assert_eq!(excess_session.credit_line, 20_000_000);
let excess_close = account
.methods()
.close_session(Vec::<PropOrderBookCleanup>::new())
.with_contracts(&[
&deployment.oracle,
&deployment.pool,
&order_book.order_book,
&second_order_book.order_book,
])
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
let excess_close_events = excess_close
.decode_logs_with_type::<SessionClosed>()
.unwrap();
assert_recent_timestamp(excess_close_events[0].timestamp.unix);
assert_eq!(
excess_close_events,
vec![SessionClosed {
account: child.contract_id(),
session_id: 2,
reason: SettlementReason::UserClose,
v: 33_000_000,
v_is_negative: false,
v_liq: 33_000_000,
v_liq_is_negative: false,
profit_abs: 0,
profit_is_negative: false,
fees_accrued: 0,
platform_total: 0,
user_net: excess_collateral,
bad_debt: 0,
cancelled_debt: vec![],
payout_parent: vec![(collateral_asset, excess_collateral)],
payout_platform: vec![],
timestamp: excess_close_events[0].timestamp.clone(),
}]
);
let withdrawal = 1_000;
user.force_transfer_to_contract(
child.contract_id(),
withdrawal,
collateral_asset,
TxPolicies::default(),
)
.await
.unwrap();
let balance_before = user.get_asset_balance(&collateral_asset).await.unwrap();
let withdraw_response = account
.methods()
.withdraw(collateral_asset, withdrawal)
.with_contract_ids(&[deployment.oracle_id, deployment.pool_id])
.with_variable_output_policy(VariableOutputPolicy::Exactly(1))
.call()
.await
.unwrap();
let withdraw_events = withdraw_response
.decode_logs_with_type::<MarginTradeAccountWithdrawn>()
.unwrap();
assert_recent_timestamp(withdraw_events[0].timestamp.unix);
assert_eq!(
withdraw_events,
vec![MarginTradeAccountWithdrawn {
account: child.contract_id(),
parent,
asset_id: collateral_asset,
amount: withdrawal,
timestamp: withdraw_events[0].timestamp.clone(),
}]
);
assert_eq!(
user.get_asset_balance(&collateral_asset).await.unwrap(),
balance_before + withdrawal as u128
);
}
}
#[cfg(test)]
#[path = "prop_deploy_tests.rs"]
mod integration_tests;