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