1#![allow(clippy::needless_pass_by_value)]
2
3mod state;
4
5use std::ops::{Deref, DerefMut};
6
7use near_sdk::{
8 assert_one_yocto, env,
9 json_types::{Base58CryptoHash, Base64VecU8},
10 near, require, AccountId, CryptoHash, Gas, NearToken, PanicOnDefault, Promise, PromiseOrValue,
11};
12use near_sdk_contract_tools::{owner::Owner, Owner};
13use templar_common::{
14 contract::list,
15 registry::{Deployment, RegistryEntryView, VersionAvailability, VersionInfo, VersionSource},
16 self_ext,
17 upgrade::{UpgradeSource, MIGRATE_METHOD},
18 versioned_state::{impl_versioned_state, StateVersion, VersionedState},
19};
20
21type State = state::V1;
22
23#[derive(Debug, Clone)]
24#[near(serializers = [json, borsh])]
25pub enum VersionEntry {
26 Code {
27 hash: CryptoHash,
28 code: Option<Vec<u8>>,
29 },
30 GlobalHash(CryptoHash),
31}
32
33impl VersionEntry {
34 pub fn code_hash(&self) -> CryptoHash {
35 match self {
36 Self::Code { hash, .. } | Self::GlobalHash(hash) => *hash,
37 }
38 }
39
40 fn availability(&self) -> VersionAvailability {
41 match self {
42 Self::Code {
43 code: Some(code), ..
44 } => VersionAvailability::Stored {
45 code_len: u32::try_from(code.len()).unwrap_or(u32::MAX),
47 },
48 Self::Code { code: None, .. } => VersionAvailability::Removed,
49 Self::GlobalHash(_) => VersionAvailability::Global,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
55#[near(serializers = [borsh])]
56pub enum RegistryEntry {
57 Reserved,
58 Deployed(Deployment),
59}
60
61#[derive(PanicOnDefault, Owner)]
62#[near(contract_state)]
63pub struct Contract {
64 pub state: VersionedState<State>,
65}
66
67impl_versioned_state!(Contract, State, crate::state::Migration);
70
71impl Deref for Contract {
74 type Target = State;
75
76 fn deref(&self) -> &Self::Target {
77 &self.state
78 }
79}
80
81impl DerefMut for Contract {
82 fn deref_mut(&mut self) -> &mut Self::Target {
83 &mut self.state
84 }
85}
86
87#[near]
88impl Contract {
89 pub const GAS_FOR_MIGRATE: Gas = Gas::from_tgas(250);
96
97 pub const MAX_CODE_CHUNK_LEN: u32 = 128 * 1024;
102
103 #[init]
104 pub fn new() -> Self {
105 let mut self_ = Self {
106 state: State::new(()),
107 };
108
109 self_.init(&env::predecessor_account_id());
110
111 self_
112 }
113
114 #[payable]
120 pub fn upgrade(&mut self, code: UpgradeSource, migrate_args: Base64VecU8) -> Promise {
121 assert_one_yocto();
122 self.assert_owner();
123
124 require!(!code.is_empty_code(), "Upgrade code must not be empty");
125
126 near_sdk::log!("Upgrading registry to {:?}", code.summary());
127
128 code.deploy_and_migrate(MIGRATE_METHOD, migrate_args, Self::GAS_FOR_MIGRATE)
129 }
130
131 pub fn list_versions(&self, count: Option<u32>, offset: Option<u32>) -> Vec<&String> {
132 list(self.versions.keys(), offset, count)
133 }
134
135 pub fn get_version_code_hash(&self, version_key: String) -> Option<Base58CryptoHash> {
136 self.versions
137 .get(&version_key)
138 .map(VersionEntry::code_hash)
139 .map(Into::into)
140 }
141
142 pub fn get_version(&self, version_key: String) -> Option<VersionInfo> {
148 self.versions.get(&version_key).map(|entry| VersionInfo {
149 code_hash: entry.code_hash().into(),
150 availability: entry.availability(),
151 })
152 }
153
154 pub fn get_registry_entry(&self, account_id: AccountId) -> Option<RegistryEntryView> {
159 self.registry.get(&account_id).map(|entry| match entry {
160 RegistryEntry::Reserved => RegistryEntryView::Reserved,
161 RegistryEntry::Deployed(deployment) => RegistryEntryView::Deployed(deployment.clone()),
162 })
163 }
164
165 #[result_serializer(borsh)]
174 pub fn get_version_code_chunk(
175 &self,
176 version_key: String,
177 offset: u32,
178 len: u32,
179 ) -> Option<Vec<u8>> {
180 require!(
181 len <= Self::MAX_CODE_CHUNK_LEN,
182 format!("len exceeds maximum of {}", Self::MAX_CODE_CHUNK_LEN),
183 );
184
185 let code = match self.versions.get(&version_key)? {
186 VersionEntry::Code { code, .. } => code.as_ref()?,
187 VersionEntry::GlobalHash(_) => return None,
188 };
189
190 let start = (offset as usize).min(code.len());
191 let end = start.saturating_add(len as usize).min(code.len());
192
193 Some(code[start..end].to_vec())
194 }
195
196 pub fn list_deployments(&self, count: Option<u32>, offset: Option<u32>) -> Vec<&AccountId> {
197 list(
198 self.registry
199 .iter()
200 .filter(|(_, e)| matches!(e, RegistryEntry::Deployed(_)))
201 .map(|(a, _)| a),
202 offset,
203 count,
204 )
205 }
206
207 pub fn get_deployment(&self, account_id: AccountId) -> Option<&Deployment> {
208 self.registry.get(&account_id).and_then(|e| match e {
209 RegistryEntry::Reserved => None,
210 RegistryEntry::Deployed(deployment) => Some(deployment),
211 })
212 }
213
214 fn scratch_account(prefix: &str) -> Promise {
220 let account_id: AccountId = format!("{prefix}.{}", env::current_account_id())
221 .parse()
222 .unwrap_or_else(|_| {
223 templar_common::panic_with_message("Failed to construct scratch account ID.")
224 });
225
226 Promise::new(account_id)
227 .create_account()
228 .transfer(env::attached_deposit())
229 }
230
231 #[payable]
232 pub fn add_version(
233 &mut self,
234 #[serializer(borsh)] version_key: String,
235 #[serializer(borsh)] source: VersionSource,
236 ) -> PromiseOrValue<()> {
237 self.assert_owner();
238 require!(
239 !self.versions.contains_key(&version_key),
240 "Version key already exists",
241 );
242
243 match source {
244 VersionSource::Stored(code) => {
245 assert_one_yocto();
246 let version_entry = VersionEntry::Code {
247 hash: env::sha256_array(&code.0),
248 code: Some(code.0),
249 };
250 self.versions.insert(version_key, version_entry);
251 PromiseOrValue::Value(())
252 }
253 VersionSource::PublishGlobal(code) => {
254 require!(
255 !env::attached_deposit().is_zero(),
256 "Deposit required to pay for global contract deployment",
257 );
258 let version_entry = VersionEntry::GlobalHash(env::sha256_array(&code.0));
259 self.versions.insert(version_key.clone(), version_entry);
260 PromiseOrValue::Promise(
261 Self::scratch_account("deploy")
262 .deploy_global_contract(code.0)
263 .delete_account(env::current_account_id())
264 .then(self_ext!(Gas::from_tgas(6)).add_version_01_finalize(version_key)),
265 )
266 }
267 VersionSource::ExistingGlobal(hash) => {
272 assert_one_yocto();
276 self.versions
277 .insert(version_key.clone(), VersionEntry::GlobalHash(hash.into()));
278 PromiseOrValue::Promise(
279 Self::scratch_account("probe")
280 .use_global_contract(hash)
281 .delete_account(env::current_account_id())
282 .then(self_ext!(Gas::from_tgas(6)).add_version_01_finalize(version_key)),
283 )
284 }
285 }
286 }
287
288 #[private]
289 pub fn add_version_01_finalize(&mut self, version_key: String) -> PromiseOrValue<()> {
290 let result = env::promise_result_checked(0, 0x1000);
291 if result.is_ok() {
292 PromiseOrValue::Value(())
293 } else {
294 self.versions.remove(&version_key);
295 PromiseOrValue::Promise(
296 self_ext!(Gas::from_tgas(1)).fail("Failed to deploy global contract".to_string()),
297 )
298 }
299 }
300
301 #[payable]
302 pub fn remove_version(&mut self, version_key: String) {
303 assert_one_yocto();
304 self.assert_owner();
305
306 self.versions.entry(version_key).and_modify(|e| match e {
307 VersionEntry::Code { code, .. } => {
308 *code = None;
309 }
310 VersionEntry::GlobalHash(_) => {
311 templar_common::panic_with_message("Global contract cannot be removed")
312 }
313 });
314 }
315
316 #[payable]
317 pub fn deploy(
318 &mut self,
319 name: String,
320 version_key: String,
321 init_args: Base64VecU8,
322 full_access_keys: Option<Vec<near_sdk::PublicKey>>,
323 ) -> Promise {
324 require!(!name.is_empty(), "Name must not be empty");
325 self.assert_owner();
326
327 let state = &mut *self.state;
330
331 let Some(version) = state.versions.get(&version_key) else {
332 templar_common::panic_with_message("Version key does not exist");
333 };
334
335 let attached_deposit = env::attached_deposit();
336
337 let current_account_id = env::current_account_id();
338 let market_id = format!("{name}.{current_account_id}");
339
340 let market_id: AccountId = market_id.parse().unwrap_or_else(|_| {
341 templar_common::panic_with_message("New market ID is not a valid account ID")
342 });
343
344 require!(
345 market_id.is_sub_account_of(¤t_account_id),
346 "Market ID cannot be created",
347 );
348
349 require!(
350 !state.registry.contains_key(&market_id),
351 "Market ID collision",
352 );
353
354 state
355 .registry
356 .insert(market_id.clone(), RegistryEntry::Reserved);
357
358 near_sdk::log!("Deploying market to {market_id}");
359
360 let mut promise = Promise::new(market_id.clone())
361 .create_account()
362 .transfer(env::attached_deposit());
363
364 match version {
365 VersionEntry::Code { code, .. } => {
366 let code = code.as_ref().unwrap_or_else(|| {
367 templar_common::panic_with_message("Version code has been deleted")
368 });
369
370 let minimum_deposit = env::storage_byte_cost().saturating_mul(code.len() as u128);
371
372 require!(
373 attached_deposit >= minimum_deposit,
374 format!("Insufficient deposit to pay for storage (minimum: {minimum_deposit})"),
375 );
376
377 promise = promise.deploy_contract(code.clone());
378 }
379 VersionEntry::GlobalHash(hash) => promise = promise.use_global_contract(*hash),
380 }
381
382 for key in full_access_keys.unwrap_or_default() {
383 near_sdk::log!(
384 "WARNING: Deploying market with full-access key {}",
385 String::from(&key),
386 );
387 promise = promise.add_full_access_key(key);
388 }
389
390 promise
391 .function_call_weight(
392 "new".to_string(),
393 init_args.0,
394 NearToken::from_near(0),
395 Gas::from_tgas(2),
396 near_sdk::GasWeight(20),
397 )
398 .then(
399 Self::ext(env::current_account_id())
400 .with_unused_gas_weight(1)
401 .with_static_gas(Gas::from_tgas(2))
402 .deploy_01_finalize(
403 market_id,
404 Deployment {
405 version_key,
406 code_hash: version.code_hash().into(),
407 block_height: env::block_height().into(),
408 },
409 ),
410 )
411 }
412
413 #[private]
414 pub fn deploy_01_finalize(
415 &mut self,
416 market_id: AccountId,
417 deployment: Deployment,
418 ) -> PromiseOrValue<AccountId> {
419 let successful = env::promise_result_checked(0, 0x1000).is_ok();
420
421 if successful {
422 self.registry
423 .insert(market_id.clone(), RegistryEntry::Deployed(deployment));
424
425 PromiseOrValue::Value(market_id)
426 } else {
427 self.registry.remove(&market_id);
428
429 PromiseOrValue::Promise(
430 Self::ext(env::current_account_id()).fail("Market deployment failed".to_string()),
431 )
432 }
433 }
434
435 #[private]
436 pub fn fail(&self, message: String) {
437 templar_common::panic_with_message(&message);
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use near_sdk::{
444 mock::MockAction,
445 test_utils::{get_created_receipts, VMContextBuilder},
446 testing_env,
447 };
448 use rstest::rstest;
449
450 use super::*;
451
452 const STORED: &str = "market@1.5.0";
453 const REMOVED: &str = "market@1.0.0";
454 const GLOBAL: &str = "oracle@0.4.1";
455
456 fn contract() -> Contract {
457 testing_env!(VMContextBuilder::new().build());
458 let mut contract = Contract::new();
459 contract.versions.insert(
460 STORED.to_string(),
461 VersionEntry::Code {
462 hash: [1u8; 32],
463 code: Some(vec![0xau8; 300]),
464 },
465 );
466 contract.versions.insert(
467 REMOVED.to_string(),
468 VersionEntry::Code {
469 hash: [2u8; 32],
470 code: None,
471 },
472 );
473 contract
474 .versions
475 .insert(GLOBAL.to_string(), VersionEntry::GlobalHash([3u8; 32]));
476 contract
477 }
478
479 #[rstest]
480 #[case(STORED, Some(VersionAvailability::Stored { code_len: 300 }))]
481 #[case(REMOVED, Some(VersionAvailability::Removed))]
482 #[case(GLOBAL, Some(VersionAvailability::Global))]
483 #[case("nothing@0.0.0", None)]
484 fn get_version_separates_all_four_states(
485 #[case] key: &str,
486 #[case] expected: Option<VersionAvailability>,
487 ) {
488 let info = contract().get_version(key.to_string());
489 assert_eq!(info.map(|info| info.availability), expected);
490 }
491
492 #[test]
495 fn get_registry_entry_reports_reserved_that_get_deployment_hides() {
496 let mut contract = contract();
497 let reserved: AccountId = "reserved.registry.near".parse().unwrap();
498 let deployed: AccountId = "deployed.registry.near".parse().unwrap();
499 let deployment = Deployment {
500 version_key: STORED.to_string(),
501 code_hash: [1u8; 32].into(),
502 block_height: 1.into(),
503 };
504 contract
505 .registry
506 .insert(reserved.clone(), RegistryEntry::Reserved);
507 contract.registry.insert(
508 deployed.clone(),
509 RegistryEntry::Deployed(deployment.clone()),
510 );
511
512 assert_eq!(contract.get_deployment(reserved.clone()), None);
513 assert_eq!(
514 contract.get_registry_entry(reserved),
515 Some(RegistryEntryView::Reserved),
516 );
517 assert_eq!(
518 contract.get_registry_entry(deployed),
519 Some(RegistryEntryView::Deployed(deployment)),
520 );
521 assert_eq!(
522 contract.get_registry_entry("free.registry.near".parse().unwrap()),
523 None,
524 );
525 }
526
527 #[rstest]
528 #[case::whole(0, 300, 300)]
529 #[case::prefix(0, 10, 10)]
530 #[case::tail(290, 64, 10)]
531 #[case::past_the_end_reads_empty(300, 64, 0)]
532 #[case::far_past_the_end_reads_empty(9_999, 64, 0)]
533 fn code_chunk_clamps_to_the_blob(
534 #[case] offset: u32,
535 #[case] len: u32,
536 #[case] expected: usize,
537 ) {
538 let chunk = contract()
539 .get_version_code_chunk(STORED.to_string(), offset, len)
540 .expect("a stored version yields bytes");
541 assert_eq!(chunk.len(), expected);
542 assert!(chunk.iter().all(|byte| *byte == 0xau8));
543 }
544
545 #[test]
547 fn code_chunks_reassemble_exactly() {
548 let contract = contract();
549 let mut reassembled = Vec::new();
550 let mut offset = 0;
551 loop {
552 let chunk = contract
553 .get_version_code_chunk(STORED.to_string(), offset, 128)
554 .expect("a stored version yields bytes");
555 if chunk.is_empty() {
556 break;
557 }
558 offset += u32::try_from(chunk.len()).unwrap();
559 reassembled.extend(chunk);
560 }
561 assert_eq!(reassembled, vec![0xau8; 300]);
562 }
563
564 #[rstest]
567 #[case(GLOBAL)]
568 #[case(REMOVED)]
569 #[case("nothing@0.0.0")]
570 fn code_chunk_is_absent_without_stored_code(#[case] key: &str) {
571 assert_eq!(
572 contract().get_version_code_chunk(key.to_string(), 0, 64),
573 None,
574 );
575 }
576
577 #[test]
578 #[should_panic(expected = "len exceeds maximum")]
579 fn code_chunk_refuses_an_oversized_read() {
580 contract().get_version_code_chunk(STORED.to_string(), 0, Contract::MAX_CODE_CHUNK_LEN + 1);
581 }
582
583 #[test]
586 fn upgrade_batches_deploy_then_migrate_into_one_self_receipt() {
587 testing_env!(VMContextBuilder::new()
588 .current_account_id("registry.near".parse().unwrap())
589 .predecessor_account_id("registry.near".parse().unwrap())
590 .attached_deposit(NearToken::from_yoctonear(1))
591 .build());
592 let mut contract = Contract::new();
593 let code = vec![0xde, 0xad, 0xbe, 0xef];
594 let migrate_args = br#"{"from_version":"pre_global_contracts"}"#.to_vec();
595
596 contract
597 .upgrade(
598 UpgradeSource::Code(Base64VecU8(code.clone())),
599 Base64VecU8(migrate_args.clone()),
600 )
601 .detach();
602
603 let receipts = get_created_receipts();
604 assert_eq!(receipts.len(), 1, "the upgrade must not fan out");
605 let receipt = &receipts[0];
606 assert_eq!(receipt.receiver_id.as_str(), "registry.near");
607 assert_eq!(receipt.actions.len(), 2);
608
609 let receipt_index = match &receipt.actions[0] {
610 MockAction::DeployContract {
611 receipt_index,
612 code: deployed,
613 } => {
614 assert_eq!(deployed, &code);
615 *receipt_index
616 }
617 action => panic!("expected the deploy first, got {action:?}"),
618 };
619 match &receipt.actions[1] {
620 MockAction::FunctionCallWeight {
621 receipt_index: migrate_index,
622 method_name,
623 args,
624 prepaid_gas,
625 ..
626 } => {
627 assert_eq!(
628 *migrate_index, receipt_index,
629 "migrate must ride the deploy"
630 );
631 assert_eq!(method_name, b"migrate");
632 assert_eq!(args, &migrate_args);
633 assert_eq!(*prepaid_gas, Contract::GAS_FOR_MIGRATE);
634 }
635 action => panic!("expected the migrate second, got {action:?}"),
636 }
637 }
638
639 #[test]
640 #[should_panic(expected = "Requires attached deposit of exactly 1 yoctoNEAR")]
641 fn upgrade_requires_one_yocto() {
642 testing_env!(VMContextBuilder::new()
643 .current_account_id("registry.near".parse().unwrap())
644 .predecessor_account_id("registry.near".parse().unwrap())
645 .build());
646 let mut contract = Contract::new();
647 contract
648 .upgrade(
649 UpgradeSource::Code(Base64VecU8(vec![1, 2, 3])),
650 Base64VecU8(Vec::new()),
651 )
652 .detach();
653 }
654
655 #[test]
656 #[should_panic(expected = "Upgrade code must not be empty")]
657 fn upgrade_refuses_an_empty_blob() {
658 testing_env!(VMContextBuilder::new()
659 .current_account_id("registry.near".parse().unwrap())
660 .predecessor_account_id("registry.near".parse().unwrap())
661 .attached_deposit(NearToken::from_yoctonear(1))
662 .build());
663 let mut contract = Contract::new();
664 contract
665 .upgrade(
666 UpgradeSource::Code(Base64VecU8(Vec::new())),
667 Base64VecU8(Vec::new()),
668 )
669 .detach();
670 }
671}
672
673#[cfg(target_arch = "wasm32")]
674mod custom_getrandom {
675 #![allow(clippy::no_mangle_with_rust_abi)]
676
677 use getrandom::{register_custom_getrandom, Error};
678 use near_sdk::env;
679
680 register_custom_getrandom!(custom_getrandom);
681
682 #[allow(clippy::unnecessary_wraps)]
683 pub fn custom_getrandom(buf: &mut [u8]) -> Result<(), Error> {
684 buf.copy_from_slice(&env::random_seed_array());
685 Ok(())
686 }
687}