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