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