Skip to main content

o2_tools/
prop_deploy.rs

1//! Deployment helpers for the independent prop-account contract family.
2//!
3//! Prop accounts consult a stable account-oracle proxy at runtime. The oracle,
4//! registry and margin pool each retain independent SRC-14 upgrade authority.
5
6use crate::{
7    blob_loader,
8    prop::{
9        PROP_ACCOUNT_BYTECODE,
10        PROP_ACCOUNT_ORACLE_BYTECODE,
11        PROP_ACCOUNT_ORACLE_PROXY_BYTECODE,
12        PROP_ACCOUNT_ORACLE_PROXY_STORAGE,
13        PROP_ACCOUNT_ORACLE_STORAGE,
14        PROP_ACCOUNT_PROXY_BYTECODE,
15        PROP_ACCOUNT_PROXY_STORAGE,
16        PROP_ACCOUNT_STORAGE,
17        PROP_MARGIN_POOL_BYTECODE,
18        PROP_MARGIN_POOL_PROXY_BYTECODE,
19        PROP_MARGIN_POOL_PROXY_STORAGE,
20        PROP_MARGIN_POOL_STORAGE,
21        PROP_PRICE_FEED_MOCK_BYTECODE,
22        PROP_PRICE_FEED_MOCK_STORAGE,
23        PropAccountContract,
24        PropAccountOracleContract,
25        PropAccountOracleContractConfigurables,
26        PropAccountOracleProxyContract,
27        PropAccountOracleProxyContractConfigurables,
28        PropAccountProxyContract,
29        PropAccountProxyContractConfigurables,
30        PropMarginPoolContract,
31        PropMarginPoolContractConfigurables,
32        PropMarginPoolProxyContract,
33        PropMarginPoolProxyContractConfigurables,
34        PropPriceFeedMockContract,
35        PropPriceFeedMockContractConfigurables,
36        State,
37    },
38    trade_account_registry::{
39        State as TradeAccountRegistryState,
40        TRADE_ACCOUNT_REGISTER_BYTECODE,
41        TRADE_ACCOUNT_REGISTER_PROXY_BYTECODE,
42        TRADE_ACCOUNT_REGISTER_PROXY_STORAGE,
43        TRADE_ACCOUNT_REGISTER_STORAGE,
44        TradeAccountRegistry,
45        TradeAccountRegistryConfigurables,
46        TradeAccountRegistryDeployConfig,
47        TradeAccountRegistryManager,
48        TradeAccountRegistryProxy,
49        TradeAccountRegistryProxyConfigurables,
50    },
51};
52use anyhow::{
53    Context,
54    Result,
55    ensure,
56};
57use fuels::{
58    core::{
59        Configurable,
60        Configurables,
61    },
62    prelude::*,
63    programs::contract::Regular,
64    tx::StorageSlot,
65    types::{
66        Address,
67        AssetId,
68        ContractId,
69        Identity,
70        transaction_builders::Blob,
71    },
72};
73
74/// An already-deployed shared trade-account registry to reuse for
75/// prop-account registration instead of deploying a dedicated one. The
76/// registry's implementation is UPGRADED in place (new blob, same proxy) to
77/// carry the prop configurables, so the oracle ids it was deployed with must
78/// be repeated — the fresh implementation blob embeds them again.
79#[derive(Clone, Debug)]
80pub struct ExistingRegistry {
81    /// The registry proxy id every account resolution goes through.
82    pub registry_id: ContractId,
83    /// The trade-account oracle the registry validates against.
84    pub trade_account_oracle_id: ContractId,
85    /// The trial-trade-account oracle the registry validates against.
86    pub trial_trade_account_oracle_id: ContractId,
87}
88
89/// Deployment-time settings for the prop-account system.
90#[derive(Clone, Debug)]
91pub struct PropDeployConfig {
92    /// Asset used for collateral and quote accounting by the pool.
93    pub collateral_asset: AssetId,
94    /// Decimal precision of `collateral_asset`.
95    pub collateral_decimals: u8,
96    /// Maximum number of order books in one tier version.
97    pub max_tier_books: u64,
98    /// Owner of the oracle implementation, feed, registry implementation and pool.
99    /// Defaults to the deployer.
100    pub owner: Option<Identity>,
101    /// Owner of the oracle, registry and pool SRC-14 proxies. Defaults to the deployer.
102    pub proxy_owner: Option<Identity>,
103    /// Cosigner exposed by every prop account. Defaults to the deployer.
104    pub cosigner: Option<Address>,
105    /// Recipient of the platform share. Defaults to the deployer.
106    pub platform_payout: Option<Identity>,
107    /// Recipient of assets retained during forced settlement. Defaults to the deployer.
108    pub liquidator: Option<Identity>,
109    /// Deterministic salt used for all contracts in this deployment.
110    pub salt: Salt,
111    /// Maximum implementation words placed into each loader data blob.
112    pub max_words_per_blob: usize,
113    /// Reuse (and upgrade) an existing shared trade-account registry
114    /// instead of deploying a dedicated one.
115    pub existing_registry: Option<ExistingRegistry>,
116    /// Reuse an already-deployed price feed instead of deploying the mock
117    /// feed. Production passes its proxied feed here; the mock is only fit
118    /// for test and development environments.
119    pub existing_price_feed: Option<ContractId>,
120}
121
122impl PropDeployConfig {
123    pub fn new(collateral_asset: AssetId) -> Self {
124        Self {
125            collateral_asset,
126            collateral_decimals: 6,
127            max_tier_books: 80,
128            owner: None,
129            proxy_owner: None,
130            cosigner: None,
131            platform_payout: None,
132            liquidator: None,
133            salt: Salt::default(),
134            max_words_per_blob: 10_000,
135            existing_registry: None,
136            existing_price_feed: None,
137        }
138    }
139}
140
141/// Handles and implementation blob IDs produced by a complete deployment.
142#[derive(Clone)]
143pub struct PropDeployment<W> {
144    /// Oracle implementation ABI bound to its stable SRC-14 proxy.
145    pub oracle: PropAccountOracleContract<W>,
146    pub oracle_proxy: PropAccountOracleProxyContract<W>,
147    pub oracle_id: ContractId,
148    pub oracle_blob_id: BlobId,
149    /// Direct mock feed. It implements the same ABI expected from production.
150    pub price_feed: PropPriceFeedMockContract<W>,
151    pub price_feed_id: ContractId,
152    /// Shared trade-account registry implementation ABI bound to its SRC-14 proxy.
153    pub registry: TradeAccountRegistry<W>,
154    pub registry_proxy: TradeAccountRegistryProxy<W>,
155    pub registry_id: ContractId,
156    pub registry_blob_id: BlobId,
157    /// Margin-pool implementation ABI bound to its SRC-14 proxy.
158    pub pool: PropMarginPoolContract<W>,
159    pub pool_proxy: PropMarginPoolProxyContract<W>,
160    pub pool_id: ContractId,
161    pub pool_blob_id: BlobId,
162    /// Runtime account implementation selected through the oracle proxy.
163    pub account_blob_id: BlobId,
164    /// Raw configurable account-proxy bytecode consumed by the registry.
165    pub account_proxy_blob_id: BlobId,
166    /// Fixed salt used with parent/index configurables for deterministic children.
167    pub account_salt: Salt,
168    pub deployer_wallet: W,
169}
170
171impl<W> PropDeployment<W>
172where
173    W: Account + Clone,
174{
175    /// Deploy the full system in dependency order.
176    ///
177    /// The registry is deployed before the pool so its ID can be embedded in
178    /// the pool implementation configurables. The pool ID is then stored in
179    /// the account oracle through its stable proxy.
180    pub async fn deploy(deployer_wallet: &W, config: &PropDeployConfig) -> Result<Self> {
181        ensure!(
182            config.collateral_asset != AssetId::zeroed(),
183            "prop collateral asset cannot be zero"
184        );
185        ensure!(
186            config.collateral_decimals <= 18,
187            "prop collateral decimals cannot exceed 18"
188        );
189        ensure!(
190            config.max_tier_books != 0,
191            "prop max tier books cannot be zero"
192        );
193        ensure!(
194            config.max_words_per_blob != 0,
195            "prop loader blob size cannot be zero"
196        );
197
198        let deployer = Identity::Address(deployer_wallet.address());
199        let owner = config.owner.unwrap_or(deployer);
200        let proxy_owner = config.proxy_owner.unwrap_or(deployer);
201        // The pool's `initialize` refuses a zero cosigner or liquidator, so a
202        // fresh deploy has to name SOMETHING - the deployer is the only
203        // sensible bootstrap. In production both arrive as explicit deploy
204        // parameters; falling back here quietly would make the deploy key the
205        // residue payee, or would leave the backend holding a key that does
206        // not match the oracle, so say so out loud instead.
207        let cosigner = config.cosigner.unwrap_or_else(|| {
208            tracing::warn!(
209                "prop deploy: no cosigner given - defaulting to the deployer. \
210                 The backend must run MARGIN_COSIGNER_KEY for this address or \
211                 margin stays inert."
212            );
213            deployer_wallet.address()
214        });
215        let platform_payout = config.platform_payout.unwrap_or(deployer);
216        let liquidator = config.liquidator.unwrap_or_else(|| {
217            tracing::warn!(
218                "prop deploy: no liquidator given - defaulting to the deployer, \
219                 which will receive the residue of every forced exit."
220            );
221            deployer
222        });
223
224        let account_blob_id = upload_implementation(
225            deployer_wallet,
226            PROP_ACCOUNT_BYTECODE,
227            PROP_ACCOUNT_STORAGE,
228            Configurables::default(),
229            config,
230        )
231        .await
232        .context("deploy prop-account implementation blob")?;
233
234        // This must remain raw bytecode: the registry BLDd-loads it and patches
235        // the account-specific configurables before checking the child root.
236        let account_proxy_blob_id = blob_loader::upload_loader_blobs(
237            deployer_wallet,
238            vec![],
239            Blob::new(PROP_ACCOUNT_PROXY_BYTECODE.to_vec()),
240        )
241        .await
242        .context("deploy raw prop-account proxy blob")?;
243
244        // The final oracle implementation embeds the pool ID, while the pool
245        // transitively embeds the stable oracle proxy ID. Point the proxy at a
246        // default-config bootstrap blob first to break that deployment cycle.
247        let oracle_bootstrap_blob_id = upload_implementation(
248            deployer_wallet,
249            PROP_ACCOUNT_ORACLE_BYTECODE,
250            PROP_ACCOUNT_ORACLE_STORAGE,
251            Configurables::default(),
252            config,
253        )
254        .await
255        .context("deploy prop-account oracle implementation blob")?;
256        let oracle_proxy_configurables =
257            PropAccountOracleProxyContractConfigurables::default()
258                .with_INITIAL_OWNER(State::Initialized(proxy_owner))?
259                .with_INITIAL_TARGET(ContractId::from(oracle_bootstrap_blob_id))?;
260        let oracle_proxy_contract = regular_contract_with_implementation_storage(
261            PROP_ACCOUNT_ORACLE_PROXY_BYTECODE,
262            PROP_ACCOUNT_ORACLE_PROXY_STORAGE,
263            PROP_ACCOUNT_ORACLE_STORAGE,
264            config.salt,
265        )?
266        .with_configurables(oracle_proxy_configurables);
267        let (oracle_id, _) = deploy_regular(deployer_wallet, oracle_proxy_contract)
268            .await
269            .context("deploy prop-account oracle proxy")?;
270        let oracle_proxy =
271            PropAccountOracleProxyContract::new(oracle_id, deployer_wallet.clone());
272        let oracle = PropAccountOracleContract::new(oracle_id, deployer_wallet.clone());
273        // State-checked rather than is-new-gated: a run that crashed between
274        // the Create and this call resumes here instead of stranding an
275        // uninitialized proxy forever.
276        let oracle_proxy_target = oracle_proxy
277            .methods()
278            .proxy_target()
279            .simulate(Execution::state_read_only())
280            .await
281            .context("read prop-account oracle proxy target")?
282            .value;
283        if oracle_proxy_target.is_none() {
284            oracle_proxy
285                .methods()
286                .initialize_proxy()
287                .call()
288                .await
289                .context("initialize prop-account oracle proxy")?;
290        }
291
292        let (price_feed, price_feed_id) = match config.existing_price_feed {
293            Some(price_feed_id) => (
294                PropPriceFeedMockContract::new(price_feed_id, deployer_wallet.clone()),
295                price_feed_id,
296            ),
297            None => {
298                let feed_configurables =
299                    PropPriceFeedMockContractConfigurables::default()
300                        .with_INITIAL_OWNER(owner)?;
301                let feed_contract = regular_contract(
302                    PROP_PRICE_FEED_MOCK_BYTECODE,
303                    PROP_PRICE_FEED_MOCK_STORAGE,
304                    config.salt,
305                )?
306                .with_configurables(feed_configurables);
307                let (price_feed_id, _) = deploy_regular(deployer_wallet, feed_contract)
308                    .await
309                    .context("deploy prop price-feed mock")?;
310                let price_feed = PropPriceFeedMockContract::new(
311                    price_feed_id,
312                    deployer_wallet.clone(),
313                );
314                let feed_is_uninitialized = price_feed
315                    .methods()
316                    .owner()
317                    .simulate(Execution::state_read_only())
318                    .await
319                    .context("read prop price-feed mock initialization state")?
320                    .value
321                    == State::Uninitialized;
322                if feed_is_uninitialized {
323                    price_feed
324                        .methods()
325                        .initialize()
326                        .call()
327                        .await
328                        .context("initialize prop price-feed mock")?;
329                }
330                (price_feed, price_feed_id)
331            }
332        };
333
334        let [oracle_offset, parent_offset, index_offset] = prop_account_proxy_offsets()?;
335        let prop_registry_configurables = |base: TradeAccountRegistryConfigurables| {
336            Ok::<_, anyhow::Error>(
337                base.with_PROP_ACCOUNT_ORACLE_CONTRACT_ID(oracle_id)?
338                    .with_DEFAULT_PROP_ACCOUNT_PROXY(ContractId::from(
339                        account_proxy_blob_id,
340                    ))?
341                    .with_PROP_ORACLE_CONFIG_OFFSET(oracle_offset)?
342                    .with_PROP_PARENT_CONFIG_OFFSET(parent_offset)?
343                    .with_PROP_INDEX_CONFIG_OFFSET(index_offset)?,
344            )
345        };
346        let (registry_id, registry_blob_id) = match &config.existing_registry {
347            Some(existing) => {
348                // Upgrade the shared registry's implementation in place:
349                // same proxy, a fresh blob repeating the oracle ids it was
350                // deployed with plus the prop configurables above. Storage
351                // (registered accounts, ownership) rides through untouched.
352                let manager = TradeAccountRegistryManager::new(
353                    deployer_wallet.clone(),
354                    existing.registry_id,
355                );
356                let deploy_config = TradeAccountRegistryDeployConfig {
357                    registry_config: prop_registry_configurables(
358                        TradeAccountRegistryConfigurables::default(),
359                    )?,
360                    ..Default::default()
361                };
362                // Blob uploads are exists-checked; the retarget only fires
363                // when the proxy does not already point at the upgraded
364                // implementation, so a re-run is a no-op.
365                let registry_blob_id = TradeAccountRegistryManager::deploy_register_blob(
366                    deployer_wallet,
367                    existing.trade_account_oracle_id,
368                    existing.trial_trade_account_oracle_id,
369                    &deploy_config,
370                )
371                .await
372                .context("build upgraded shared trade-account registry blob")?;
373                let current_target = manager
374                    .registry_proxy
375                    .methods()
376                    .proxy_target()
377                    .simulate(Execution::state_read_only())
378                    .await
379                    .context("read shared trade-account registry proxy target")?
380                    .value;
381                if current_target != Some(ContractId::from(registry_blob_id)) {
382                    manager
383                        .registry_proxy
384                        .methods()
385                        .set_proxy_target(ContractId::from(registry_blob_id))
386                        .call()
387                        .await
388                        .context("upgrade shared trade-account registry for prop")?;
389                }
390                (existing.registry_id, registry_blob_id)
391            }
392            None => {
393                let registry_configurables = prop_registry_configurables(
394                    TradeAccountRegistryConfigurables::default()
395                        .with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(
396                            owner,
397                        ))?
398                        .with_INITIAL_TRIAL_TRADE_ACCOUNT_CREATOR(owner)?,
399                )?;
400                let registry_blob_id = upload_implementation(
401                    deployer_wallet,
402                    TRADE_ACCOUNT_REGISTER_BYTECODE,
403                    TRADE_ACCOUNT_REGISTER_STORAGE,
404                    registry_configurables,
405                    config,
406                )
407                .await
408                .context("deploy shared trade-account registry implementation blob")?;
409
410                let registry_proxy_configurables =
411                    TradeAccountRegistryProxyConfigurables::default()
412                        .with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(
413                            proxy_owner,
414                        ))?
415                        .with_INITIAL_TARGET(ContractId::from(registry_blob_id))?;
416                let registry_proxy_contract = regular_contract(
417                    TRADE_ACCOUNT_REGISTER_PROXY_BYTECODE,
418                    TRADE_ACCOUNT_REGISTER_PROXY_STORAGE,
419                    config.salt,
420                )?
421                .with_configurables(registry_proxy_configurables);
422                let (registry_id, registry_proxy_is_new) =
423                    deploy_regular(deployer_wallet, registry_proxy_contract)
424                        .await
425                        .context("deploy shared trade-account registry proxy")?;
426                let registry_proxy =
427                    TradeAccountRegistryProxy::new(registry_id, deployer_wallet.clone());
428                let registry =
429                    TradeAccountRegistry::new(registry_id, deployer_wallet.clone());
430                if registry_proxy_is_new {
431                    registry_proxy
432                        .methods()
433                        .initialize_proxy()
434                        .call()
435                        .await
436                        .context("initialize shared trade-account registry proxy")?;
437                    registry
438                        .methods()
439                        .initialize()
440                        .call()
441                        .await
442                        .context("initialize shared trade-account registry")?;
443                }
444                (registry_id, registry_blob_id)
445            }
446        };
447        let registry_proxy =
448            TradeAccountRegistryProxy::new(registry_id, deployer_wallet.clone());
449        let registry = TradeAccountRegistry::new(registry_id, deployer_wallet.clone());
450
451        let pool_configurables = PropMarginPoolContractConfigurables::default()
452            .with_INITIAL_ADMIN(owner)?
453            .with_INITIAL_REGISTRY(registry_id)?
454            .with_INITIAL_PRICE_FEED(price_feed_id)?
455            .with_INITIAL_PLATFORM_PAYOUT(platform_payout)?
456            .with_INITIAL_LIQUIDATOR(liquidator)?
457            .with_COLLATERAL_ASSET(config.collateral_asset)?
458            .with_COLLATERAL_DECIMALS(config.collateral_decimals)?
459            .with_MAX_TIER_BOOKS(config.max_tier_books)?;
460        let pool_blob_id = upload_implementation(
461            deployer_wallet,
462            PROP_MARGIN_POOL_BYTECODE,
463            PROP_MARGIN_POOL_STORAGE,
464            pool_configurables,
465            config,
466        )
467        .await
468        .context("deploy prop margin-pool implementation blob")?;
469
470        // The proxy's ADDRESS must not depend on the implementation's
471        // configurables, or changing a runtime value - the liquidator, the
472        // payout party, the feed - would derive a DIFFERENT proxy and fork the
473        // pool instead of upgrading it. Point it at a default-config bootstrap
474        // blob (the same trick the oracle below uses to break its own cycle),
475        // then retarget to the configured implementation.
476        let pool_bootstrap_blob_id = upload_implementation(
477            deployer_wallet,
478            PROP_MARGIN_POOL_BYTECODE,
479            PROP_MARGIN_POOL_STORAGE,
480            Configurables::default(),
481            config,
482        )
483        .await
484        .context("deploy prop margin-pool bootstrap implementation blob")?;
485        let pool_proxy_configurables =
486            PropMarginPoolProxyContractConfigurables::default()
487                .with_INITIAL_OWNER(State::Initialized(proxy_owner))?
488                .with_INITIAL_TARGET(ContractId::from(pool_bootstrap_blob_id))?;
489        let pool_proxy_contract = regular_contract_with_implementation_storage(
490            PROP_MARGIN_POOL_PROXY_BYTECODE,
491            PROP_MARGIN_POOL_PROXY_STORAGE,
492            PROP_MARGIN_POOL_STORAGE,
493            config.salt,
494        )?
495        .with_configurables(pool_proxy_configurables);
496        let (pool_id, _) = deploy_regular(deployer_wallet, pool_proxy_contract)
497            .await
498            .context("deploy prop margin-pool proxy")?;
499        let pool_proxy =
500            PropMarginPoolProxyContract::new(pool_id, deployer_wallet.clone());
501        let pool = PropMarginPoolContract::new(pool_id, deployer_wallet.clone());
502        let pool_proxy_target = pool_proxy
503            .methods()
504            .proxy_target()
505            .simulate(Execution::state_read_only())
506            .await
507            .context("read prop margin-pool proxy target")?
508            .value;
509        if pool_proxy_target.is_none() {
510            pool_proxy
511                .methods()
512                .initialize_proxy()
513                .call()
514                .await
515                .context("initialize prop margin-pool proxy")?;
516        }
517        // The upgrade itself: new configurables mean a new blob, and the
518        // stable proxy is pointed at it. A run that changes nothing computes
519        // the same blob id and retargets nothing.
520        let pool_target = pool_proxy
521            .methods()
522            .proxy_target()
523            .simulate(Execution::state_read_only())
524            .await
525            .context("read configured prop margin-pool proxy target")?
526            .value;
527        if pool_target != Some(ContractId::from(pool_blob_id)) {
528            pool_proxy
529                .methods()
530                .set_proxy_target(ContractId::from(pool_blob_id))
531                .call()
532                .await
533                .context("activate configured prop margin-pool implementation")?;
534        }
535
536        let oracle_configurables = PropAccountOracleContractConfigurables::default()
537            .with_INITIAL_OWNER(owner)?
538            .with_INITIAL_PROP_ACCOUNT_IMPL(ContractId::from(account_blob_id))?
539            .with_INITIAL_COSIGNER(cosigner)?
540            .with_INITIAL_PROP_MARGIN_POOL(pool_id)?;
541        let oracle_blob_id = upload_implementation(
542            deployer_wallet,
543            PROP_ACCOUNT_ORACLE_BYTECODE,
544            PROP_ACCOUNT_ORACLE_STORAGE,
545            oracle_configurables,
546            config,
547        )
548        .await
549        .context("deploy configured prop-account oracle implementation blob")?;
550        let oracle_target = oracle_proxy
551            .methods()
552            .proxy_target()
553            .simulate(Execution::state_read_only())
554            .await
555            .context("read prop-account oracle proxy target")?
556            .value;
557        if oracle_target != Some(ContractId::from(oracle_blob_id)) {
558            oracle_proxy
559                .methods()
560                .set_proxy_target(ContractId::from(oracle_blob_id))
561                .call()
562                .await
563                .context("activate configured prop-account oracle implementation")?;
564        }
565
566        let oracle_is_uninitialized = oracle
567            .methods()
568            .owner()
569            .simulate(Execution::state_read_only())
570            .await
571            .context("read prop-account oracle initialization state")?
572            .value
573            == State::Uninitialized;
574        if oracle_is_uninitialized {
575            oracle
576                .methods()
577                .initialize()
578                .call()
579                .await
580                .context("initialize prop-account oracle")?;
581        }
582
583        // State-checked: a resumed run initializes iff the pool has not
584        // been initialized yet, whether or not this run created the proxy.
585        let deployed_collateral = pool
586            .methods()
587            .collateral_asset()
588            .simulate(Execution::state_read_only())
589            .await
590            .context("read deployed prop margin-pool collateral")?
591            .value;
592        ensure!(
593            deployed_collateral == config.collateral_asset,
594            "prop margin-pool collateral configurable mismatch"
595        );
596        ensure!(
597            pool.methods()
598                .collateral_decimals()
599                .simulate(Execution::state_read_only())
600                .await
601                .context("read deployed prop margin-pool collateral decimals")?
602                .value
603                == config.collateral_decimals,
604            "prop margin-pool decimals configurable mismatch"
605        );
606        ensure!(
607            pool.methods()
608                .max_tier_books()
609                .simulate(Execution::state_read_only())
610                .await
611                .context("read deployed prop margin-pool book limit")?
612                .value
613                == config.max_tier_books,
614            "prop margin-pool book-limit configurable mismatch"
615        );
616        let pool_is_initialized = pool
617            .methods()
618            .is_initialized()
619            .simulate(Execution::state_read_only())
620            .await
621            .context("read deployed prop margin-pool initialization state")?
622            .value;
623        if !pool_is_initialized {
624            pool.methods()
625                .initialize()
626                .call()
627                .await
628                .context("initialize prop margin pool")?;
629        }
630
631        Ok(Self {
632            oracle,
633            oracle_proxy,
634            oracle_id,
635            oracle_blob_id,
636            price_feed,
637            price_feed_id,
638            registry,
639            registry_proxy,
640            registry_id,
641            registry_blob_id,
642            pool,
643            pool_proxy,
644            pool_id,
645            pool_blob_id,
646            account_blob_id,
647            account_proxy_blob_id,
648            account_salt: config.salt,
649            deployer_wallet: deployer_wallet.clone(),
650        })
651    }
652
653    /// Deploy and register one deterministic prop-account child.
654    ///
655    /// `parent_caller` submits the registration transaction and must resolve
656    /// to `parent`; the registry enforces this relationship on chain.
657    pub async fn deploy_account(
658        &self,
659        parent_caller: &W,
660        parent: Identity,
661        index: u64,
662    ) -> Result<PropAccountProxyContract<W>> {
663        ensure!(
664            parent != Identity::Address(Address::zeroed()),
665            "prop-account parent cannot be zero"
666        );
667
668        let configurables = PropAccountProxyContractConfigurables::default()
669            .with_ORACLE_CONTRACT_ID(self.oracle_id)?
670            .with_PARENT(parent)?
671            .with_INDEX(index)?;
672        let child_contract = regular_contract(
673            PROP_ACCOUNT_PROXY_BYTECODE,
674            PROP_ACCOUNT_PROXY_STORAGE,
675            self.account_salt,
676        )?
677        .with_configurables(configurables);
678        let (child_id, _) = deploy_regular(&self.deployer_wallet, child_contract)
679            .await
680            .context("deploy prop-account child")?;
681
682        let child = PropAccountProxyContract::new(child_id, self.deployer_wallet.clone());
683        if !self
684            .registry
685            .methods()
686            .prop_is_valid(child_id)
687            .simulate(Execution::state_read_only())
688            .await?
689            .value
690        {
691            TradeAccountRegistry::new(self.registry_id, parent_caller.clone())
692                .methods()
693                .prop_register_contract(child_id, parent, index)
694                .with_contract_ids(&[self.oracle_id, child_id, self.pool_id])
695                .call()
696                .await
697                .context("register prop-account child")?;
698        }
699
700        Ok(child)
701    }
702
703    /// Bind the implementation ABI to a deployed child proxy.
704    pub fn account(&self, account_id: ContractId) -> PropAccountContract<W> {
705        PropAccountContract::new(account_id, self.deployer_wallet.clone())
706    }
707
708    /// DRY-RUN: compute every deterministic id [`Self::deploy`] would use
709    /// and report, read-only, what already exists on the live chain versus
710    /// what a real run would create or change. Sends NO transactions.
711    pub async fn verify(
712        deployer_wallet: &W,
713        config: &PropDeployConfig,
714    ) -> Result<PropDeployReport> {
715        let provider = deployer_wallet.try_provider()?;
716        let deployer = Identity::Address(deployer_wallet.address());
717        let owner = config.owner.unwrap_or(deployer);
718        let proxy_owner = config.proxy_owner.unwrap_or(deployer);
719        // The pool's `initialize` refuses a zero cosigner or liquidator, so a
720        // fresh deploy has to name SOMETHING - the deployer is the only
721        // sensible bootstrap. In production both arrive as explicit deploy
722        // parameters; falling back here quietly would make the deploy key the
723        // residue payee, or would leave the backend holding a key that does
724        // not match the oracle, so say so out loud instead.
725        let cosigner = config.cosigner.unwrap_or_else(|| {
726            tracing::warn!(
727                "prop deploy: no cosigner given - defaulting to the deployer. \
728                 The backend must run MARGIN_COSIGNER_KEY for this address or \
729                 margin stays inert."
730            );
731            deployer_wallet.address()
732        });
733        let platform_payout = config.platform_payout.unwrap_or(deployer);
734        let liquidator = config.liquidator.unwrap_or_else(|| {
735            tracing::warn!(
736                "prop deploy: no liquidator given - defaulting to the deployer, \
737                 which will receive the residue of every forced exit."
738            );
739            deployer
740        });
741
742        let mut report = PropDeployReport::default();
743        let mut note =
744            |name: &'static str, id: ContractId, exists: bool, action: String| {
745                tracing::info!("[prop verify] {name}: {id} — {action}");
746                report.components.push(PropComponentStatus {
747                    name,
748                    id,
749                    exists,
750                    action,
751                });
752            };
753        let blob_action = |exists: bool| {
754            if exists {
755                "present".to_string()
756            } else {
757                "would upload blob".to_string()
758            }
759        };
760
761        // 1. Implementation blobs whose ids depend only on bytecode + salt.
762        let account_blob_id = loader_blob_id(
763            PROP_ACCOUNT_BYTECODE,
764            PROP_ACCOUNT_STORAGE,
765            Configurables::default(),
766            config,
767        )?;
768        let account_blob_exists = provider.blob_exists(account_blob_id).await?;
769        note(
770            "prop-account implementation blob",
771            ContractId::from(account_blob_id),
772            account_blob_exists,
773            blob_action(account_blob_exists),
774        );
775
776        let account_proxy_blob_id = Blob::new(PROP_ACCOUNT_PROXY_BYTECODE.to_vec()).id();
777        let account_proxy_blob_exists =
778            provider.blob_exists(account_proxy_blob_id).await?;
779        note(
780            "prop-account raw proxy blob",
781            ContractId::from(account_proxy_blob_id),
782            account_proxy_blob_exists,
783            blob_action(account_proxy_blob_exists),
784        );
785
786        // 2. The oracle proxy (id keyed by the BOOTSTRAP implementation).
787        let oracle_bootstrap_blob_id = loader_blob_id(
788            PROP_ACCOUNT_ORACLE_BYTECODE,
789            PROP_ACCOUNT_ORACLE_STORAGE,
790            Configurables::default(),
791            config,
792        )?;
793        let oracle_proxy_configurables =
794            PropAccountOracleProxyContractConfigurables::default()
795                .with_INITIAL_OWNER(State::Initialized(proxy_owner))?
796                .with_INITIAL_TARGET(ContractId::from(oracle_bootstrap_blob_id))?;
797        let oracle_id = regular_contract_with_implementation_storage(
798            PROP_ACCOUNT_ORACLE_PROXY_BYTECODE,
799            PROP_ACCOUNT_ORACLE_PROXY_STORAGE,
800            PROP_ACCOUNT_ORACLE_STORAGE,
801            config.salt,
802        )?
803        .with_configurables(oracle_proxy_configurables)
804        .contract_id();
805        let oracle_exists = provider.contract_exists(&oracle_id).await?;
806        report.oracle_id = oracle_id;
807        note(
808            "prop-account oracle proxy",
809            oracle_id,
810            oracle_exists,
811            if oracle_exists {
812                "present".to_string()
813            } else {
814                "would deploy + initialize".to_string()
815            },
816        );
817
818        // 3. The price feed: reused as-is or the deterministic mock.
819        let price_feed_id = match config.existing_price_feed {
820            Some(price_feed_id) => {
821                let exists = provider.contract_exists(&price_feed_id).await?;
822                note(
823                    "price feed (existing)",
824                    price_feed_id,
825                    exists,
826                    if exists {
827                        "present".to_string()
828                    } else {
829                        "MISSING — configured feed is not on chain".to_string()
830                    },
831                );
832                price_feed_id
833            }
834            None => {
835                let feed_configurables =
836                    PropPriceFeedMockContractConfigurables::default()
837                        .with_INITIAL_OWNER(owner)?;
838                let feed_id = regular_contract(
839                    PROP_PRICE_FEED_MOCK_BYTECODE,
840                    PROP_PRICE_FEED_MOCK_STORAGE,
841                    config.salt,
842                )?
843                .with_configurables(feed_configurables)
844                .contract_id();
845                let exists = provider.contract_exists(&feed_id).await?;
846                note(
847                    "price feed (mock)",
848                    feed_id,
849                    exists,
850                    if exists {
851                        "present".to_string()
852                    } else {
853                        "would deploy + initialize".to_string()
854                    },
855                );
856                feed_id
857            }
858        };
859        report.price_feed_id = price_feed_id;
860
861        // 4. The registry: an in-place upgrade of the existing one, or a
862        //    fresh dedicated deploy.
863        let [oracle_offset, parent_offset, index_offset] = prop_account_proxy_offsets()?;
864        let prop_registry_config = TradeAccountRegistryConfigurables::default()
865            .with_PROP_ACCOUNT_ORACLE_CONTRACT_ID(oracle_id)?
866            .with_DEFAULT_PROP_ACCOUNT_PROXY(ContractId::from(account_proxy_blob_id))?
867            .with_PROP_ORACLE_CONFIG_OFFSET(oracle_offset)?
868            .with_PROP_PARENT_CONFIG_OFFSET(parent_offset)?
869            .with_PROP_INDEX_CONFIG_OFFSET(index_offset)?;
870        let registry_id = match &config.existing_registry {
871            Some(existing) => {
872                let deploy_config = TradeAccountRegistryDeployConfig {
873                    registry_config: prop_registry_config,
874                    ..Default::default()
875                };
876                let proxy_blob = TradeAccountRegistryManager::register_proxy_blob(
877                    deployer_wallet,
878                    &deploy_config,
879                )
880                .await?;
881                let trial_proxy_blob =
882                    TradeAccountRegistryManager::register_trial_proxy_blob(
883                        deployer_wallet,
884                        &deploy_config,
885                    )
886                    .await?;
887                let upgraded_blob = TradeAccountRegistryManager::register_blob(
888                    deployer_wallet,
889                    existing.trade_account_oracle_id,
890                    existing.trial_trade_account_oracle_id,
891                    proxy_blob.id,
892                    trial_proxy_blob.id,
893                    &deploy_config,
894                )
895                .await?;
896                let manager = TradeAccountRegistryManager::new(
897                    deployer_wallet.clone(),
898                    existing.registry_id,
899                );
900                let current_target = manager
901                    .registry_proxy
902                    .methods()
903                    .proxy_target()
904                    .simulate(Execution::state_read_only())
905                    .await
906                    .context("read shared trade-account registry proxy target")?
907                    .value;
908                let upgraded = current_target == Some(ContractId::from(upgraded_blob.id));
909                note(
910                    "shared trade-account registry (in-place upgrade)",
911                    existing.registry_id,
912                    true,
913                    if upgraded {
914                        "implementation current — no-op".to_string()
915                    } else {
916                        format!(
917                            "would retarget proxy {} -> {}",
918                            current_target
919                                .map(|target| target.to_string())
920                                .unwrap_or_else(|| "unset".to_string()),
921                            ContractId::from(upgraded_blob.id),
922                        )
923                    },
924                );
925                existing.registry_id
926            }
927            None => {
928                let registry_configurables = prop_registry_config
929                    .with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(owner))?
930                    .with_INITIAL_TRIAL_TRADE_ACCOUNT_CREATOR(owner)?;
931                let registry_blob_id = loader_blob_id(
932                    TRADE_ACCOUNT_REGISTER_BYTECODE,
933                    TRADE_ACCOUNT_REGISTER_STORAGE,
934                    registry_configurables,
935                    config,
936                )?;
937                let registry_proxy_configurables =
938                    TradeAccountRegistryProxyConfigurables::default()
939                        .with_INITIAL_OWNER(TradeAccountRegistryState::Initialized(
940                            proxy_owner,
941                        ))?
942                        .with_INITIAL_TARGET(ContractId::from(registry_blob_id))?;
943                let registry_id = regular_contract(
944                    TRADE_ACCOUNT_REGISTER_PROXY_BYTECODE,
945                    TRADE_ACCOUNT_REGISTER_PROXY_STORAGE,
946                    config.salt,
947                )?
948                .with_configurables(registry_proxy_configurables)
949                .contract_id();
950                let exists = provider.contract_exists(&registry_id).await?;
951                note(
952                    "dedicated trade-account registry",
953                    registry_id,
954                    exists,
955                    if exists {
956                        "present".to_string()
957                    } else {
958                        "would deploy + initialize".to_string()
959                    },
960                );
961                registry_id
962            }
963        };
964        report.registry_id = registry_id;
965
966        // 5. The pool (implementation embeds registry + feed + payout).
967        let pool_configurables = PropMarginPoolContractConfigurables::default()
968            .with_INITIAL_ADMIN(owner)?
969            .with_INITIAL_REGISTRY(registry_id)?
970            .with_INITIAL_PRICE_FEED(price_feed_id)?
971            .with_INITIAL_PLATFORM_PAYOUT(platform_payout)?
972            .with_INITIAL_LIQUIDATOR(liquidator)?
973            .with_COLLATERAL_ASSET(config.collateral_asset)?
974            .with_COLLATERAL_DECIMALS(config.collateral_decimals)?
975            .with_MAX_TIER_BOOKS(config.max_tier_books)?;
976        let pool_blob_id = loader_blob_id(
977            PROP_MARGIN_POOL_BYTECODE,
978            PROP_MARGIN_POOL_STORAGE,
979            pool_configurables,
980            config,
981        )?;
982        // Mirrors the real run: the proxy is derived from a default-config
983        // bootstrap blob, so the predicted id is stable no matter which
984        // runtime values this rehearsal carries.
985        let pool_bootstrap_blob_id = loader_blob_id(
986            PROP_MARGIN_POOL_BYTECODE,
987            PROP_MARGIN_POOL_STORAGE,
988            Configurables::default(),
989            config,
990        )?;
991        let pool_proxy_configurables =
992            PropMarginPoolProxyContractConfigurables::default()
993                .with_INITIAL_OWNER(State::Initialized(proxy_owner))?
994                .with_INITIAL_TARGET(ContractId::from(pool_bootstrap_blob_id))?;
995        let pool_id = regular_contract_with_implementation_storage(
996            PROP_MARGIN_POOL_PROXY_BYTECODE,
997            PROP_MARGIN_POOL_PROXY_STORAGE,
998            PROP_MARGIN_POOL_STORAGE,
999            config.salt,
1000        )?
1001        .with_configurables(pool_proxy_configurables)
1002        .contract_id();
1003        report.pool_id = pool_id;
1004        let pool_exists = provider.contract_exists(&pool_id).await?;
1005        let pool_action = if !pool_exists {
1006            "would deploy + initialize".to_string()
1007        } else {
1008            let pool = PropMarginPoolContract::new(pool_id, deployer_wallet.clone());
1009            let initialized = pool
1010                .methods()
1011                .is_initialized()
1012                .simulate(Execution::state_read_only())
1013                .await
1014                .context("read prop margin-pool initialization state")?
1015                .value;
1016            // The proxy address is stable, so the only thing a live pool can
1017            // owe is a new IMPLEMENTATION - report that separately from
1018            // initialization, because it is the upgrade the operator came for.
1019            let target =
1020                PropMarginPoolProxyContract::new(pool_id, deployer_wallet.clone())
1021                    .methods()
1022                    .proxy_target()
1023                    .simulate(Execution::state_read_only())
1024                    .await
1025                    .context("read prop margin-pool proxy target")?
1026                    .value;
1027            // `up_to_date` below matches these strings EXACTLY, so a pool
1028            // that owes nothing must read exactly like any other settled
1029            // component. Only an owed upgrade earns extra prose - and it
1030            // correctly makes the report read "not up to date".
1031            let owes_upgrade = target != Some(ContractId::from(pool_blob_id));
1032            match (initialized, owes_upgrade) {
1033                (true, false) => "present + initialized".to_string(),
1034                (true, true) => {
1035                    "present + initialized — WOULD UPGRADE implementation".to_string()
1036                }
1037                (false, false) => "present — would initialize".to_string(),
1038                (false, true) => {
1039                    "present — would initialize + WOULD UPGRADE implementation"
1040                        .to_string()
1041                }
1042            }
1043        };
1044        note("prop margin pool proxy", pool_id, pool_exists, pool_action);
1045
1046        // 6. The CONFIGURED oracle implementation (embeds the pool id).
1047        let oracle_configurables = PropAccountOracleContractConfigurables::default()
1048            .with_INITIAL_OWNER(owner)?
1049            .with_INITIAL_PROP_ACCOUNT_IMPL(ContractId::from(account_blob_id))?
1050            .with_INITIAL_COSIGNER(cosigner)?
1051            .with_INITIAL_PROP_MARGIN_POOL(pool_id)?;
1052        let oracle_blob_id = loader_blob_id(
1053            PROP_ACCOUNT_ORACLE_BYTECODE,
1054            PROP_ACCOUNT_ORACLE_STORAGE,
1055            oracle_configurables,
1056            config,
1057        )?;
1058        let oracle_impl_action = if !oracle_exists {
1059            "would upload + retarget after oracle proxy deploy".to_string()
1060        } else {
1061            let current_target =
1062                PropAccountOracleProxyContract::new(oracle_id, deployer_wallet.clone())
1063                    .methods()
1064                    .proxy_target()
1065                    .simulate(Execution::state_read_only())
1066                    .await
1067                    .context("read prop-account oracle proxy target")?
1068                    .value;
1069            if current_target == Some(ContractId::from(oracle_blob_id)) {
1070                "current".to_string()
1071            } else {
1072                format!(
1073                    "would retarget oracle proxy {} -> {}",
1074                    current_target
1075                        .map(|target| target.to_string())
1076                        .unwrap_or_else(|| "unset".to_string()),
1077                    ContractId::from(oracle_blob_id),
1078                )
1079            }
1080        };
1081        note(
1082            "prop-account oracle implementation",
1083            ContractId::from(oracle_blob_id),
1084            oracle_exists,
1085            oracle_impl_action,
1086        );
1087
1088        report.up_to_date = report.components.iter().all(|component| {
1089            component.action == "present"
1090                || component.action == "present + initialized"
1091                || component.action == "current"
1092                || component.action == "implementation current — no-op"
1093        });
1094        Ok(report)
1095    }
1096}
1097
1098/// One line of a [`PropDeployReport`].
1099#[derive(Clone, Debug)]
1100pub struct PropComponentStatus {
1101    pub name: &'static str,
1102    pub id: ContractId,
1103    pub exists: bool,
1104    /// What a real run would do: `present` / `current` /
1105    /// `would deploy + initialize` / `would retarget ...` / ...
1106    pub action: String,
1107}
1108
1109/// The read-only result of [`PropDeployment::verify`].
1110#[derive(Clone, Debug, Default)]
1111pub struct PropDeployReport {
1112    pub components: Vec<PropComponentStatus>,
1113    pub oracle_id: ContractId,
1114    pub price_feed_id: ContractId,
1115    pub registry_id: ContractId,
1116    pub pool_id: ContractId,
1117    /// True when a real run would send no transaction at all.
1118    pub up_to_date: bool,
1119}
1120
1121fn prop_account_proxy_offsets() -> Result<[u64; 3]> {
1122    fn only_offset(configurables: Configurables, name: &str) -> Result<u64> {
1123        let offsets: Vec<Configurable> = configurables.offsets_with_data;
1124        ensure!(
1125            offsets.len() == 1,
1126            "expected one {name} configurable, got {}",
1127            offsets.len()
1128        );
1129        Ok(offsets[0].offset)
1130    }
1131
1132    let oracle: Configurables = PropAccountProxyContractConfigurables::default()
1133        .with_ORACLE_CONTRACT_ID(ContractId::zeroed())?
1134        .into();
1135    let parent: Configurables = PropAccountProxyContractConfigurables::default()
1136        .with_PARENT(Identity::Address(Address::zeroed()))?
1137        .into();
1138    let index: Configurables = PropAccountProxyContractConfigurables::default()
1139        .with_INDEX(0)?
1140        .into();
1141
1142    Ok([
1143        only_offset(oracle, "oracle")?,
1144        only_offset(parent, "parent")?,
1145        only_offset(index, "index")?,
1146    ])
1147}
1148
1149fn storage_slots(bytes: &[u8]) -> Result<Vec<StorageSlot>> {
1150    serde_json::from_slice(bytes).context("decode contract storage slots")
1151}
1152
1153/// The deterministic loader-blob id an [`upload_implementation`] of this
1154/// bytecode would produce — computed locally, nothing touches the chain.
1155fn loader_blob_id(
1156    bytecode: &[u8],
1157    storage: &[u8],
1158    configurables: impl Into<Configurables>,
1159    config: &PropDeployConfig,
1160) -> Result<BlobId> {
1161    let (_, loader_blob) = blob_loader::build_loader_blobs(
1162        bytecode.to_vec(),
1163        config.salt,
1164        storage_slots(storage)?,
1165        configurables,
1166        config.max_words_per_blob,
1167    )?;
1168    Ok(loader_blob.id())
1169}
1170
1171fn regular_contract(
1172    bytecode: &[u8],
1173    storage: &[u8],
1174    salt: Salt,
1175) -> Result<Contract<Regular>> {
1176    Ok(Contract::regular(
1177        bytecode.to_vec(),
1178        salt,
1179        storage_slots(storage)?,
1180    ))
1181}
1182
1183fn regular_contract_with_implementation_storage(
1184    bytecode: &[u8],
1185    proxy_storage: &[u8],
1186    implementation_storage: &[u8],
1187    salt: Salt,
1188) -> Result<Contract<Regular>> {
1189    let mut slots = storage_slots(proxy_storage)?;
1190    for implementation_slot in storage_slots(implementation_storage)? {
1191        ensure!(
1192            !slots
1193                .iter()
1194                .any(|proxy_slot| proxy_slot.key() == implementation_slot.key()),
1195            "proxy and implementation storage slots collide"
1196        );
1197        slots.push(implementation_slot);
1198    }
1199    Ok(Contract::regular(bytecode.to_vec(), salt, slots))
1200}
1201
1202async fn upload_implementation<W>(
1203    deployer_wallet: &W,
1204    bytecode: &[u8],
1205    storage: &[u8],
1206    configurables: impl Into<Configurables>,
1207    config: &PropDeployConfig,
1208) -> Result<BlobId>
1209where
1210    W: Account,
1211{
1212    let (data_blobs, loader_blob) = blob_loader::build_loader_blobs(
1213        bytecode.to_vec(),
1214        config.salt,
1215        storage_slots(storage)?,
1216        configurables,
1217        config.max_words_per_blob,
1218    )?;
1219    blob_loader::upload_loader_blobs(deployer_wallet, data_blobs, loader_blob).await
1220}
1221
1222async fn deploy_regular<W>(
1223    deployer_wallet: &W,
1224    contract: Contract<Regular>,
1225) -> Result<(ContractId, bool)>
1226where
1227    W: Account,
1228{
1229    let contract_id = contract.contract_id();
1230    let is_new = !deployer_wallet
1231        .try_provider()?
1232        .contract_exists(&contract_id)
1233        .await?;
1234    if is_new {
1235        contract
1236            .deploy(deployer_wallet, TxPolicies::default())
1237            .await?;
1238    }
1239    Ok((contract_id, is_new))
1240}
1241
1242#[cfg(test)]
1243static PROP_DEPLOY_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1244
1245#[cfg(test)]
1246mod tests {
1247    use super::*;
1248    use crate::{
1249        order_book_deploy::{
1250            OrderArgs,
1251            OrderBookConfigurables,
1252            OrderBookDeploy,
1253            OrderBookDeployConfig,
1254            OrderType,
1255        },
1256        prop::{
1257            AbsorbedFundsTransferred,
1258            InventoryFunded,
1259            MarginPoolPauseChanged,
1260            MarginPoolPlatformPayoutChanged,
1261            MarginPoolPriceFeedChanged,
1262            MarginPoolRegistryChanged,
1263            MarginTradeAccountWithdrawn,
1264            PriceInput,
1265            PropOrderBookCleanup,
1266            SessionClosed,
1267            SettlementReason,
1268            TierParams,
1269        },
1270    };
1271    use fuels::test_helpers::{
1272        AssetConfig,
1273        WalletsConfig,
1274        launch_custom_provider_and_get_wallets,
1275    };
1276    use std::time::{
1277        SystemTime,
1278        UNIX_EPOCH,
1279    };
1280
1281    fn assert_recent_timestamp(timestamp: u64) {
1282        let now = SystemTime::now()
1283            .duration_since(UNIX_EPOCH)
1284            .expect("system time predates the Unix epoch")
1285            .as_secs();
1286        assert!(
1287            timestamp.abs_diff(now) <= 1,
1288            "event timestamp {timestamp} is not close to {now}"
1289        );
1290    }
1291
1292    #[tokio::test]
1293    async fn deploys_independent_prop_system_and_registers_child() {
1294        let _test_guard = PROP_DEPLOY_TEST_LOCK.lock().await;
1295        let collateral_asset = AssetId::new([1; 32]);
1296        let base_asset = AssetId::new([2; 32]);
1297        let initial_balance = 10_000_000_000u64;
1298        let mut wallets = launch_custom_provider_and_get_wallets(
1299            WalletsConfig::new_multiple_assets(
1300                2,
1301                vec![
1302                    AssetConfig {
1303                        id: AssetId::default(),
1304                        num_coins: 2,
1305                        coin_amount: initial_balance,
1306                    },
1307                    AssetConfig {
1308                        id: collateral_asset,
1309                        num_coins: 2,
1310                        coin_amount: initial_balance,
1311                    },
1312                    AssetConfig {
1313                        id: base_asset,
1314                        num_coins: 2,
1315                        coin_amount: initial_balance,
1316                    },
1317                ],
1318            ),
1319            None,
1320            Some(::fuels::test_helpers::ChainConfig::local_testnet()),
1321        )
1322        .await
1323        .unwrap();
1324        let user = wallets.pop().unwrap();
1325        let deployer = wallets.pop().unwrap();
1326
1327        let deployment =
1328            PropDeployment::deploy(&deployer, &PropDeployConfig::new(collateral_asset))
1329                .await
1330                .unwrap();
1331
1332        let oracle_target = deployment
1333            .oracle
1334            .methods()
1335            .get_prop_account_impl()
1336            .simulate(Execution::state_read_only())
1337            .await
1338            .unwrap()
1339            .value;
1340        assert_eq!(
1341            oracle_target,
1342            Some(ContractId::from(deployment.account_blob_id))
1343        );
1344        assert_eq!(
1345            deployment
1346                .oracle
1347                .methods()
1348                .get_prop_margin_pool()
1349                .simulate(Execution::state_read_only())
1350                .await
1351                .unwrap()
1352                .value,
1353            Some(deployment.pool_id)
1354        );
1355        assert_eq!(
1356            deployment
1357                .registry
1358                .methods()
1359                .get_prop_oracle_id()
1360                .simulate(Execution::state_read_only())
1361                .await
1362                .unwrap()
1363                .value,
1364            deployment.oracle_id
1365        );
1366        assert_eq!(
1367            deployment
1368                .registry
1369                .methods()
1370                .get_prop_pool_id()
1371                .with_contract_ids(&[deployment.oracle_id])
1372                .simulate(Execution::state_read_only())
1373                .await
1374                .unwrap()
1375                .value,
1376            deployment.pool_id
1377        );
1378        assert_eq!(
1379            deployment
1380                .pool
1381                .methods()
1382                .registry()
1383                .simulate(Execution::state_read_only())
1384                .await
1385                .unwrap()
1386                .value,
1387            deployment.registry_id
1388        );
1389        assert_eq!(
1390            deployment
1391                .pool
1392                .methods()
1393                .price_feed()
1394                .simulate(Execution::state_read_only())
1395                .await
1396                .unwrap()
1397                .value,
1398            deployment.price_feed_id
1399        );
1400        let deployer_identity = Identity::Address(deployer.address());
1401
1402        let inventory_amount = 1_234;
1403        let inventory_update = deployment
1404            .pool
1405            .methods()
1406            .fund_inventory()
1407            .call_params(CallParameters::new(
1408                inventory_amount,
1409                collateral_asset,
1410                u64::MAX,
1411            ))
1412            .unwrap()
1413            .call()
1414            .await
1415            .unwrap();
1416        let inventory_events = inventory_update
1417            .decode_logs_with_type::<InventoryFunded>()
1418            .unwrap();
1419        assert_recent_timestamp(inventory_events[0].timestamp.unix);
1420        assert_eq!(
1421            inventory_events,
1422            vec![InventoryFunded {
1423                asset_id: collateral_asset,
1424                amount: inventory_amount,
1425                new_inventory: inventory_amount,
1426                timestamp: inventory_events[0].timestamp.clone(),
1427            }]
1428        );
1429
1430        let registry_update = deployment
1431            .pool
1432            .methods()
1433            .set_registry(deployment.oracle_id)
1434            .call()
1435            .await
1436            .unwrap();
1437        let registry_events = registry_update
1438            .decode_logs_with_type::<MarginPoolRegistryChanged>()
1439            .unwrap();
1440        assert_recent_timestamp(registry_events[0].timestamp.unix);
1441        assert_eq!(
1442            registry_events,
1443            vec![MarginPoolRegistryChanged {
1444                old_registry: deployment.registry_id,
1445                new_registry: deployment.oracle_id,
1446                timestamp: registry_events[0].timestamp.clone(),
1447            }]
1448        );
1449        assert_eq!(
1450            deployment
1451                .pool
1452                .methods()
1453                .registry()
1454                .simulate(Execution::state_read_only())
1455                .await
1456                .unwrap()
1457                .value,
1458            deployment.oracle_id
1459        );
1460        deployment
1461            .pool
1462            .methods()
1463            .set_registry(deployment.registry_id)
1464            .call()
1465            .await
1466            .unwrap();
1467
1468        let price_feed_update = deployment
1469            .pool
1470            .methods()
1471            .set_price_feed(deployment.oracle_id)
1472            .call()
1473            .await
1474            .unwrap();
1475        let price_feed_events = price_feed_update
1476            .decode_logs_with_type::<MarginPoolPriceFeedChanged>()
1477            .unwrap();
1478        assert_recent_timestamp(price_feed_events[0].timestamp.unix);
1479        assert_eq!(
1480            price_feed_events,
1481            vec![MarginPoolPriceFeedChanged {
1482                old_price_feed: deployment.price_feed_id,
1483                new_price_feed: deployment.oracle_id,
1484                timestamp: price_feed_events[0].timestamp.clone(),
1485            }]
1486        );
1487        assert_eq!(
1488            deployment
1489                .pool
1490                .methods()
1491                .price_feed()
1492                .simulate(Execution::state_read_only())
1493                .await
1494                .unwrap()
1495                .value,
1496            deployment.oracle_id
1497        );
1498        deployment
1499            .pool
1500            .methods()
1501            .set_price_feed(deployment.price_feed_id)
1502            .call()
1503            .await
1504            .unwrap();
1505
1506        let platform_payout = Identity::Address(user.address());
1507        let payout_update = deployment
1508            .pool
1509            .methods()
1510            .set_platform_payout(platform_payout)
1511            .call()
1512            .await
1513            .unwrap();
1514        let payout_events = payout_update
1515            .decode_logs_with_type::<MarginPoolPlatformPayoutChanged>()
1516            .unwrap();
1517        assert_recent_timestamp(payout_events[0].timestamp.unix);
1518        assert_eq!(
1519            payout_events,
1520            vec![MarginPoolPlatformPayoutChanged {
1521                old_platform_payout: deployer_identity,
1522                new_platform_payout: platform_payout,
1523                timestamp: payout_events[0].timestamp.clone(),
1524            }]
1525        );
1526        deployment
1527            .pool
1528            .methods()
1529            .set_platform_payout(deployer_identity)
1530            .call()
1531            .await
1532            .unwrap();
1533
1534        let pause_response = deployment.pool.methods().pause().call().await.unwrap();
1535        let pause_events = pause_response
1536            .decode_logs_with_type::<MarginPoolPauseChanged>()
1537            .unwrap();
1538        assert_recent_timestamp(pause_events[0].timestamp.unix);
1539        assert_eq!(pause_events.len(), 1);
1540        assert!(pause_events[0].paused);
1541
1542        let unpause_response = deployment.pool.methods().unpause().call().await.unwrap();
1543        let unpause_events = unpause_response
1544            .decode_logs_with_type::<MarginPoolPauseChanged>()
1545            .unwrap();
1546        assert_recent_timestamp(unpause_events[0].timestamp.unix);
1547        assert_eq!(unpause_events.len(), 1);
1548        assert!(!unpause_events[0].paused);
1549
1550        let user_pool = PropMarginPoolContract::new(deployment.pool_id, user.clone());
1551        assert!(
1552            user_pool
1553                .methods()
1554                .set_registry(deployment.oracle_id)
1555                .call()
1556                .await
1557                .is_err()
1558        );
1559
1560        assert!(
1561            deployment
1562                .pool
1563                .methods()
1564                .has_role(0, deployer_identity)
1565                .simulate(Execution::state_read_only())
1566                .await
1567                .unwrap()
1568                .value
1569        );
1570        let order_book_configurables = OrderBookConfigurables::default()
1571            .with_MAKER_FEE(0u64.into())
1572            .unwrap()
1573            .with_TAKER_FEE(0u64.into())
1574            .unwrap()
1575            .with_MIN_ORDER(1)
1576            .unwrap()
1577            .with_DUST(0)
1578            .unwrap();
1579        let order_book_config =
1580            OrderBookDeployConfig::with_configurables(order_book_configurables);
1581        let order_book = OrderBookDeploy::deploy(
1582            &deployer,
1583            base_asset,
1584            collateral_asset,
1585            &order_book_config,
1586        )
1587        .await
1588        .unwrap();
1589        let mut second_order_book_config = order_book_config.clone();
1590        second_order_book_config.salt = Salt::from([1u8; 32]);
1591        let second_order_book = OrderBookDeploy::deploy(
1592            &deployer,
1593            base_asset,
1594            collateral_asset,
1595            &second_order_book_config,
1596        )
1597        .await
1598        .unwrap();
1599
1600        deployment
1601            .price_feed
1602            .methods()
1603            .set_asset_decimals(collateral_asset, 6)
1604            .call()
1605            .await
1606            .unwrap();
1607        deployment
1608            .price_feed
1609            .methods()
1610            .set_asset_decimals(base_asset, 9)
1611            .call()
1612            .await
1613            .unwrap();
1614        deployment
1615            .price_feed
1616            .methods()
1617            .publish_prices(vec![
1618                PriceInput {
1619                    asset: collateral_asset,
1620                    bid: 1_000_000_000_000_000_000u64.into(),
1621                    ask: 1_000_000_000_000_000_000u64.into(),
1622                    timestamp: 0,
1623                },
1624                PriceInput {
1625                    asset: base_asset,
1626                    bid: 2_000_000_000_000_000_000u64.into(),
1627                    ask: 2_000_000_000_000_000_000u64.into(),
1628                    timestamp: 0,
1629                },
1630            ])
1631            .call()
1632            .await
1633            .unwrap();
1634        assert!(
1635            deployment
1636                .price_feed
1637                .methods()
1638                .has_price(collateral_asset)
1639                .simulate(Execution::state_read_only())
1640                .await
1641                .unwrap()
1642                .value
1643        );
1644
1645        let tier_params = TierParams {
1646            line: 10_000_000,
1647            leverage: 5,
1648            duration: 86_400,
1649            maintenance_bps: 250,
1650            open_buffer_bps: 375,
1651            liq_price_factor: 9_900,
1652            prolong_fee_bps: [6, 21, 76, 738],
1653            max_credit_line_bps: 20_000,
1654            max_price_age: 60,
1655            open_fee_bps: 0,
1656            profit_share_bps: 1_000,
1657            price_band_bps: 1_000,
1658        };
1659        assert!(
1660            deployment
1661                .pool
1662                .methods()
1663                .publish_tier_version(
1664                    1,
1665                    TierParams {
1666                        leverage: 0,
1667                        ..tier_params.clone()
1668                    },
1669                    vec![order_book.contract_id],
1670                )
1671                .call()
1672                .await
1673                .is_err()
1674        );
1675        assert!(
1676            deployment
1677                .pool
1678                .methods()
1679                .publish_tier_version(
1680                    1,
1681                    TierParams {
1682                        open_fee_bps: 2_000,
1683                        ..tier_params.clone()
1684                    },
1685                    vec![order_book.contract_id],
1686                )
1687                .call()
1688                .await
1689                .is_err()
1690        );
1691
1692        deployment
1693            .pool
1694            .methods()
1695            .publish_tier_version(
1696                1,
1697                tier_params,
1698                vec![order_book.contract_id, second_order_book.contract_id],
1699            )
1700            .with_contract_ids(&[
1701                order_book.contract_id,
1702                second_order_book.contract_id,
1703                deployment.price_feed_id,
1704            ])
1705            .call()
1706            .await
1707            .unwrap();
1708
1709        let tier = deployment
1710            .pool
1711            .methods()
1712            .get_tier(1, 1)
1713            .simulate(Execution::state_read_only())
1714            .await
1715            .unwrap()
1716            .value
1717            .expect("published tier version");
1718        assert_eq!(tier.line, 10_000_000);
1719        assert_eq!(
1720            deployment
1721                .pool
1722                .methods()
1723                .current_tier_version(1)
1724                .simulate(Execution::state_read_only())
1725                .await
1726                .unwrap()
1727                .value,
1728            Some(1)
1729        );
1730        assert_eq!(
1731            deployment
1732                .pool
1733                .methods()
1734                .tier_books(1, 1)
1735                .simulate(Execution::state_read_only())
1736                .await
1737                .unwrap()
1738                .value,
1739            vec![order_book.contract_id, second_order_book.contract_id]
1740        );
1741        let tier_assets = deployment
1742            .pool
1743            .methods()
1744            .tier_assets(1, 1)
1745            .simulate(Execution::state_read_only())
1746            .await
1747            .unwrap()
1748            .value;
1749        assert_eq!(tier_assets.len(), 2);
1750        assert!(tier_assets.contains(&base_asset));
1751        assert!(tier_assets.contains(&collateral_asset));
1752        assert!(
1753            deployment
1754                .pool
1755                .methods()
1756                .get_tier(1, 2)
1757                .simulate(Execution::state_read_only())
1758                .await
1759                .unwrap()
1760                .value
1761                .is_none()
1762        );
1763        assert!(
1764            deployment
1765                .pool
1766                .methods()
1767                .tier_books(1, 2)
1768                .simulate(Execution::state_read_only())
1769                .await
1770                .is_err()
1771        );
1772        assert!(
1773            deployment
1774                .pool
1775                .methods()
1776                .tier_assets(1, 2)
1777                .simulate(Execution::state_read_only())
1778                .await
1779                .is_err()
1780        );
1781
1782        deployment
1783            .price_feed
1784            .methods()
1785            .set_asset_decimals(base_asset, 8)
1786            .call()
1787            .await
1788            .unwrap();
1789        deployment
1790            .price_feed
1791            .methods()
1792            .publish_prices(vec![PriceInput {
1793                asset: base_asset,
1794                bid: 2_000_000_000_000_000_000u64.into(),
1795                ask: 2_000_000_000_000_000_000u64.into(),
1796                timestamp: 1,
1797            }])
1798            .call()
1799            .await
1800            .unwrap();
1801        assert!(
1802            deployment
1803                .pool
1804                .methods()
1805                .set_price_feed(deployment.price_feed_id)
1806                .with_contracts(&[&deployment.price_feed])
1807                .call()
1808                .await
1809                .is_err()
1810        );
1811        deployment
1812            .price_feed
1813            .methods()
1814            .set_asset_decimals(base_asset, 9)
1815            .call()
1816            .await
1817            .unwrap();
1818        deployment
1819            .price_feed
1820            .methods()
1821            .publish_prices(vec![PriceInput {
1822                asset: base_asset,
1823                bid: 2_000_000_000_000_000_000u64.into(),
1824                ask: 2_000_000_000_000_000_000u64.into(),
1825                timestamp: 2,
1826            }])
1827            .call()
1828            .await
1829            .unwrap();
1830
1831        let parent = Identity::Address(user.address());
1832        let registration_error = deployment
1833            .deploy_account(&deployer, parent, 7)
1834            .await
1835            .expect_err("a caller other than the configured parent must be rejected");
1836        assert!(
1837            format!("{registration_error:?}").contains("NotParent"),
1838            "unexpected registration error: {registration_error:?}"
1839        );
1840        let child = deployment.deploy_account(&user, parent, 7).await.unwrap();
1841        assert!(
1842            deployment
1843                .registry
1844                .methods()
1845                .prop_is_valid(child.contract_id())
1846                .simulate(Execution::state_read_only())
1847                .await
1848                .unwrap()
1849                .value
1850        );
1851        assert_eq!(
1852            child
1853                .methods()
1854                .parent()
1855                .simulate(Execution::state_read_only())
1856                .await
1857                .unwrap()
1858                .value,
1859            parent
1860        );
1861        assert_eq!(
1862            child
1863                .methods()
1864                .index()
1865                .simulate(Execution::state_read_only())
1866                .await
1867                .unwrap()
1868                .value,
1869            7
1870        );
1871        assert_eq!(
1872            child
1873                .methods()
1874                .pool()
1875                .with_contract_ids(&[deployment.oracle_id])
1876                .simulate(Execution::state_read_only())
1877                .await
1878                .unwrap()
1879                .value,
1880            deployment.pool_id
1881        );
1882        assert_eq!(
1883            child
1884                .methods()
1885                .oracle()
1886                .simulate(Execution::state_read_only())
1887                .await
1888                .unwrap()
1889                .value,
1890            deployment.oracle_id
1891        );
1892
1893        let collateral = 2_000_000;
1894        user.force_transfer_to_contract(
1895            child.contract_id(),
1896            collateral,
1897            collateral_asset,
1898            TxPolicies::default(),
1899        )
1900        .await
1901        .unwrap();
1902        let account = PropAccountContract::new(child.contract_id(), user.clone());
1903        account
1904            .methods()
1905            .start_session(1, collateral)
1906            .with_contract_ids(&[
1907                deployment.oracle_id,
1908                deployment.pool_id,
1909                deployment.registry_id,
1910            ])
1911            .call()
1912            .await
1913            .unwrap();
1914
1915        assert!(
1916            deployment
1917                .pool
1918                .methods()
1919                .is_call_allowed(child.contract_id(), order_book.contract_id)
1920                .simulate(Execution::state_read_only())
1921                .await
1922                .unwrap()
1923                .value
1924        );
1925        assert!(
1926            deployment
1927                .pool
1928                .methods()
1929                .is_call_allowed(child.contract_id(), second_order_book.contract_id)
1930                .simulate(Execution::state_read_only())
1931                .await
1932                .unwrap()
1933                .value
1934        );
1935        assert!(
1936            !deployment
1937                .pool
1938                .methods()
1939                .is_call_allowed(child.contract_id(), ContractId::new([0x99; 32]))
1940                .simulate(Execution::state_read_only())
1941                .await
1942                .unwrap()
1943                .value
1944        );
1945
1946        let session = deployment
1947            .pool
1948            .methods()
1949            .get_session(child.contract_id())
1950            .simulate(Execution::state_read_only())
1951            .await
1952            .unwrap()
1953            .value
1954            .expect("prop session opened");
1955        assert_eq!(session.session_id, 1);
1956        assert_eq!(session.credit_line, 10_000_000);
1957        assert_eq!(session.collateral, collateral);
1958
1959        assert!(
1960            deployment
1961                .pool
1962                .methods()
1963                .min_sellable(child.contract_id(), base_asset)
1964                .with_contracts(&[&order_book.order_book, &second_order_book.order_book,])
1965                .simulate(Execution::state_read_only())
1966                .await
1967                .is_err()
1968        );
1969
1970        let best_bid = 2_000_000;
1971        let bid_quantity = 1_000_000_000;
1972        order_book
1973            .order_book
1974            .methods()
1975            .create_order(OrderArgs {
1976                price: best_bid,
1977                quantity: bid_quantity,
1978                order_type: OrderType::Spot,
1979            })
1980            .call_params(CallParameters::new(
1981                bid_quantity * best_bid / 1_000_000_000,
1982                collateral_asset,
1983                u64::MAX,
1984            ))
1985            .unwrap()
1986            .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
1987            .call()
1988            .await
1989            .unwrap();
1990        assert_eq!(
1991            order_book
1992                .order_book
1993                .methods()
1994                .get_base_decimals()
1995                .simulate(Execution::state_read_only())
1996                .await
1997                .unwrap()
1998                .value,
1999            1_000_000_000
2000        );
2001        assert_eq!(
2002            deployment
2003                .pool
2004                .methods()
2005                .min_sellable(child.contract_id(), base_asset)
2006                .with_contracts(&[&order_book.order_book, &second_order_book.order_book,])
2007                .simulate(Execution::state_read_only())
2008                .await
2009                .unwrap()
2010                .value,
2011            Some(500)
2012        );
2013
2014        let close = account
2015            .methods()
2016            .close_session(Vec::<PropOrderBookCleanup>::new())
2017            .with_contracts(&[
2018                &deployment.oracle,
2019                &deployment.pool,
2020                &order_book.order_book,
2021                &second_order_book.order_book,
2022            ])
2023            .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
2024            .call()
2025            .await
2026            .unwrap();
2027        let close_events = close.decode_logs_with_type::<SessionClosed>().unwrap();
2028        assert!(
2029            close
2030                .decode_logs_with_type::<AbsorbedFundsTransferred>()
2031                .unwrap()
2032                .is_empty()
2033        );
2034        assert_recent_timestamp(close_events[0].timestamp.unix);
2035        assert_eq!(
2036            close_events,
2037            vec![SessionClosed {
2038                account: child.contract_id(),
2039                session_id: 1,
2040                reason: SettlementReason::UserClose,
2041                v: 10_000_000,
2042                v_is_negative: false,
2043                v_liq: 10_000_000,
2044                v_liq_is_negative: false,
2045                profit_abs: 0,
2046                profit_is_negative: false,
2047                fees_accrued: 0,
2048                platform_total: 0,
2049                user_net: collateral,
2050                bad_debt: 0,
2051                cancelled_debt: vec![],
2052                payout_parent: vec![(collateral_asset, collateral)],
2053                payout_platform: vec![],
2054                timestamp: close_events[0].timestamp.clone(),
2055            }]
2056        );
2057        assert!(
2058            !deployment
2059                .pool
2060                .methods()
2061                .has_session(child.contract_id())
2062                .simulate(Execution::state_read_only())
2063                .await
2064                .unwrap()
2065                .value
2066        );
2067
2068        let excess_collateral = 25_000_000;
2069        user.force_transfer_to_contract(
2070            child.contract_id(),
2071            excess_collateral,
2072            collateral_asset,
2073            TxPolicies::default(),
2074        )
2075        .await
2076        .unwrap();
2077        account
2078            .methods()
2079            .start_session(1, excess_collateral)
2080            .with_contract_ids(&[
2081                deployment.oracle_id,
2082                deployment.pool_id,
2083                deployment.registry_id,
2084            ])
2085            .call()
2086            .await
2087            .unwrap();
2088        let excess_session = deployment
2089            .pool
2090            .methods()
2091            .get_session(child.contract_id())
2092            .simulate(Execution::state_read_only())
2093            .await
2094            .unwrap()
2095            .value
2096            .expect("excess-collateral session opened");
2097        assert_eq!(excess_session.session_id, 2);
2098        assert_eq!(excess_session.collateral, excess_collateral);
2099        assert_eq!(excess_session.credit_line, 20_000_000);
2100
2101        let excess_close = account
2102            .methods()
2103            .close_session(Vec::<PropOrderBookCleanup>::new())
2104            .with_contracts(&[
2105                &deployment.oracle,
2106                &deployment.pool,
2107                &order_book.order_book,
2108                &second_order_book.order_book,
2109            ])
2110            .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
2111            .call()
2112            .await
2113            .unwrap();
2114        let excess_close_events = excess_close
2115            .decode_logs_with_type::<SessionClosed>()
2116            .unwrap();
2117        assert_recent_timestamp(excess_close_events[0].timestamp.unix);
2118        assert_eq!(
2119            excess_close_events,
2120            vec![SessionClosed {
2121                account: child.contract_id(),
2122                session_id: 2,
2123                reason: SettlementReason::UserClose,
2124                v: 33_000_000,
2125                v_is_negative: false,
2126                v_liq: 33_000_000,
2127                v_liq_is_negative: false,
2128                profit_abs: 0,
2129                profit_is_negative: false,
2130                fees_accrued: 0,
2131                platform_total: 0,
2132                user_net: excess_collateral,
2133                bad_debt: 0,
2134                cancelled_debt: vec![],
2135                payout_parent: vec![(collateral_asset, excess_collateral)],
2136                payout_platform: vec![],
2137                timestamp: excess_close_events[0].timestamp.clone(),
2138            }]
2139        );
2140
2141        let withdrawal = 1_000;
2142        user.force_transfer_to_contract(
2143            child.contract_id(),
2144            withdrawal,
2145            collateral_asset,
2146            TxPolicies::default(),
2147        )
2148        .await
2149        .unwrap();
2150        let balance_before = user.get_asset_balance(&collateral_asset).await.unwrap();
2151        let withdraw_response = account
2152            .methods()
2153            .withdraw(collateral_asset, withdrawal)
2154            .with_contract_ids(&[deployment.oracle_id, deployment.pool_id])
2155            .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
2156            .call()
2157            .await
2158            .unwrap();
2159        let withdraw_events = withdraw_response
2160            .decode_logs_with_type::<MarginTradeAccountWithdrawn>()
2161            .unwrap();
2162        assert_recent_timestamp(withdraw_events[0].timestamp.unix);
2163        assert_eq!(
2164            withdraw_events,
2165            vec![MarginTradeAccountWithdrawn {
2166                account: child.contract_id(),
2167                parent,
2168                asset_id: collateral_asset,
2169                amount: withdrawal,
2170                timestamp: withdraw_events[0].timestamp.clone(),
2171            }]
2172        );
2173        assert_eq!(
2174            user.get_asset_balance(&collateral_asset).await.unwrap(),
2175            balance_before + withdrawal as u128
2176        );
2177    }
2178}
2179
2180#[cfg(test)]
2181#[path = "prop_deploy_tests.rs"]
2182mod integration_tests;