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