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