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