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