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