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