use common_data::asset::Asset; use common_data::vault_config::VaultConfig; use near_plugins::{ access_control, access_control_any, pause, AccessControlRole, AccessControllable, Pausable, Upgradable, }; use near_sdk::{ borsh::{BorshDeserialize, BorshSerialize}, collections::Vector, env, json_types::Base64VecU8, near, AccountId, BorshStorageKey, CryptoHash, Gas, NearToken, PanicOnDefault, Promise, }; use serde::{Deserialize, Serialize}; use serde_json::json; const SMART_CONTRACT_INIT_GAS: Gas = Gas::from_tgas(100); #[derive(BorshSerialize, BorshStorageKey)] #[borsh(crate = "near_sdk::borsh")] enum StorageKey { Children, } #[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)] #[serde(crate = "near_sdk::serde")] pub enum Role { DAO, PauseManager, UnpauseManager, UpgradableCodeStager, UpgradableCodeDeployer, OpManager, } #[near(serializers = [borsh])] pub struct ContractData { latest_code_hash: CryptoHash, children: Vector, } #[near(serializers = [borsh])] pub enum VersionedContractData { Current(ContractData), } #[near(contract_state)] #[derive(Pausable, Upgradable, PanicOnDefault)] #[access_control(role_type(Role))] #[pausable(pause_roles(Role::PauseManager), unpause_roles(Role::UnpauseManager))] #[upgradable(access_control_roles( code_stagers(Role::UpgradableCodeStager, Role::DAO), code_deployers(Role::UpgradableCodeDeployer, Role::DAO), duration_initializers(Role::DAO), duration_update_stagers(Role::DAO), duration_update_appliers(Role::DAO), ))] pub struct Contract { data: VersionedContractData, } #[near] impl Contract { #[init] pub fn new(owner_id: Option, latest_code_hash: Base64VecU8) -> Self { let latest_code_hash: Vec = latest_code_hash.into(); let mut contract = Self { data: VersionedContractData::Current(ContractData { latest_code_hash: latest_code_hash.try_into().unwrap(), children: Vector::new(StorageKey::Children), }), }; let owner_id = owner_id.unwrap_or(env::predecessor_account_id()); contract.acl_init_super_admin(env::predecessor_account_id()); contract.acl_add_super_admin(owner_id.clone()); contract.acl_grant_role(Role::DAO.into(), owner_id.clone()); contract.acl_grant_role(Role::PauseManager.into(), owner_id.clone()); contract.acl_grant_role(Role::UnpauseManager.into(), owner_id.clone()); contract.acl_grant_role(Role::OpManager.into(), owner_id.clone()); contract } #[access_control_any(roles(Role::DAO))] pub fn set_latest_code_hash(&mut self, latest_code_hash: Base64VecU8) { let latest_code_hash: Vec = latest_code_hash.into(); self.data_mut().latest_code_hash = latest_code_hash.try_into().unwrap(); } pub fn children_count(&self) -> u64 { self.data().children.len() } pub fn get_children_paged( &self, from_index: Option, limit: Option, ) -> Vec { let from_index = from_index.unwrap_or(0); let limit = limit.unwrap_or(usize::MAX as u64) as usize; let size = self.data().children.len(); if from_index >= size { return vec![]; } let to_index = std::cmp::min(from_index + limit as u64, size) as usize; (from_index as usize..to_index) .map(|index| self.data().children.get(index as u64).unwrap()) .collect() } #[access_control_any(roles(Role::OpManager, Role::DAO))] #[payable] #[pause] pub fn create( &mut self, child_id: AccountId, asset: AccountId, base_asset: Asset, base_token_decimals: u8, extra_decimals: u8, token_symbol: String, token_name: String, ) -> Promise { let accepted_assets = vec![base_asset.clone()]; let vault_config = VaultConfig::default(); let owner = near_sdk::env::predecessor_account_id(); self.create_with_config( child_id, asset, base_asset, base_token_decimals, extra_decimals, token_symbol, token_name, accepted_assets, vault_config, Some(owner), ) } #[access_control_any(roles(Role::OpManager, Role::DAO))] #[payable] #[pause] pub fn create_with_config( &mut self, child_id: AccountId, asset: AccountId, base_asset: Asset, base_token_decimals: u8, extra_decimals: u8, token_symbol: String, token_name: String, accepted_assets: Vec, vault_config: VaultConfig, owner: Option, ) -> Promise { let owner = owner.unwrap_or(env::predecessor_account_id()); assert!( accepted_assets.contains(&base_asset), "Base asset must be included in accepted_assets" ); let suffix = format!(".{}", env::current_account_id()); assert!( child_id.as_str().ends_with(&suffix), "Child ID must end with '{}'", suffix ); vault_config.validate(); assert!( self.data().children.iter().all(|id| id != child_id), "Child ID '{}' is already in use", child_id ); Promise::new(child_id.clone()) .create_account() .transfer(env::attached_deposit()) .use_global_contract(self.data().latest_code_hash) .function_call( "new_with_config".to_string(), json!({ "asset": asset, "base_asset": base_asset, "base_token_decimals": base_token_decimals, "extra_decimals": extra_decimals, "token_symbol": token_symbol, "token_name": token_name, "accepted_assets": accepted_assets, "vault_config": vault_config, "owner": owner, }) .to_string() .as_bytes() .to_vec(), NearToken::from_near(0), SMART_CONTRACT_INIT_GAS, ) .then( Promise::new(env::current_account_id()).function_call( "on_create_callback".to_string(), json!({ "child_id": child_id, }) .to_string() .as_bytes() .to_vec(), NearToken::from_near(0), Gas::from_tgas(20), ), ) } #[private] pub fn on_create_callback(&mut self, child_id: AccountId) { assert_eq!( env::promise_results_count(), 1, "Expected exactly one promise result." ); match env::promise_result(0) { near_sdk::PromiseResult::Successful(_) => { self.data_mut().children.push(&child_id); } near_sdk::PromiseResult::Failed => { env::log_str("Child contract creation failed."); } } env::log_str(&format!("Child contract '{}' created.", child_id)); } } impl Contract { pub fn data(&self) -> &ContractData { match &self.data { VersionedContractData::Current(data) => data, } } pub fn data_mut(&mut self) -> &mut ContractData { match &mut self.data { VersionedContractData::Current(data) => data, } } }