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