use near_contract_standards::fungible_token::metadata::{ FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC, }; use near_contract_standards::fungible_token::receiver::ext_ft_receiver; use near_contract_standards::fungible_token::resolver::ext_ft_resolver; use near_contract_standards::fungible_token::{ FungibleToken, FungibleTokenCore, FungibleTokenResolver, }; use near_contract_standards::storage_management::{ StorageBalance, StorageBalanceBounds, StorageManagement, }; use near_sdk::borsh::{self, BorshSerialize}; use near_sdk::collections::LazyOption; use near_sdk::json_types::{Base64VecU8, U128}; use near_sdk::{ assert_one_yocto, env, log, near, require, AccountId, BorshStorageKey, Gas, NearToken, PanicOnDefault, PromiseOrValue, }; mod migrate; #[derive(PanicOnDefault)] #[near(contract_state)] pub struct Contract { controller: AccountId, bridge_id: AccountId, token: FungibleToken, metadata: LazyOption, } const GAS_FOR_RESOLVE_TRANSFER: Gas = Gas::from_tgas(5); const GAS_FOR_FT_TRANSFER_CALL: Gas = Gas::from_tgas(30); const OUTER_UPGRADE_GAS: Gas = Gas::from_tgas(15); const NO_DEPOSIT: NearToken = NearToken::from_yoctonear(0); const WITHDRAW_RELAYER_ADDRESS: &[u8] = b"WITHDRAW_RELAYER_ADDRESS"; const WITHDRAW_MEMO_PREFIX: &str = "WITHDRAW_TO:"; #[derive(BorshSerialize, BorshStorageKey)] #[borsh(crate = "near_sdk::borsh")] enum StorageKey { FungibleToken, Metadata, } #[near(serializers = [json])] pub struct PostAction { pub receiver_id: AccountId, pub amount: U128, pub memo: Option, pub msg: String, pub gas: Option, } #[near] impl Contract { #[init] pub fn new( controller: AccountId, bridge_id: AccountId, name: String, symbol: String, icon: Option, decimals: u8, ) -> Self { require!(!env::state_exists(), "Already initialized"); let mut contract = Self { controller, bridge_id, token: FungibleToken::new(StorageKey::FungibleToken), metadata: LazyOption::new( StorageKey::Metadata, Some(&FungibleTokenMetadata { spec: FT_METADATA_SPEC.to_string(), name, symbol, icon, reference: None, reference_hash: None, decimals, }), ), }; contract .token .internal_register_account(&contract.bridge_id); contract } #[payable] pub fn set_controller(&mut self, controller: AccountId) { assert_one_yocto(); self.assert_controller(); self.controller = controller; } #[payable] pub fn safe_mint( &mut self, account_id: AccountId, amount: U128, msg: Option, ) -> PromiseOrValue { self.assert_bridge(); require!( account_id != self.bridge_id, "safe_mint: account_id must not be the bridge" ); self.token.internal_deposit(&self.bridge_id, amount.into()); if self.token.accounts.get(&account_id).is_none() { return PromiseOrValue::Value(U128(0)); } if let Some(msg) = msg { self.ft_transfer_call(account_id, amount, None, msg) } else { self.ft_transfer(account_id, amount, None); PromiseOrValue::Value(amount) } } pub fn mint( &mut self, mint_account_id: AccountId, mint_amount: U128, protocol_fee: U128, relayer_account_id: AccountId, relayer_fee: U128, post_actions: Option>, ) { self.assert_bridge(); self.mint_inner(&mint_account_id, mint_amount); if protocol_fee.0 > 0 { self.mint_inner(&self.bridge_id.clone(), protocol_fee); } if relayer_fee.0 > 0 { self.mint_inner(&relayer_account_id, relayer_fee); } if let Some(post_actions) = post_actions { Self::ext(env::current_account_id()) .handle_post_actions(mint_account_id, post_actions) .detach(); } } pub fn burn( &mut self, burn_account_id: AccountId, burn_amount: U128, relayer_account_id: AccountId, relayer_fee: U128, ) { self.assert_bridge(); self.token .internal_withdraw(&self.bridge_id, burn_amount.into()); if relayer_fee.0 > 0 { if self.token.accounts.get(&relayer_account_id).is_none() { self.token.internal_register_account(&relayer_account_id); } self.token.internal_transfer( &self.bridge_id, &relayer_account_id, relayer_fee.into(), None, ); } near_contract_standards::fungible_token::events::FtBurn { owner_id: &burn_account_id, amount: burn_amount, memo: None, } .emit(); } } #[near] impl FungibleTokenCore for Contract { #[payable] fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { // Legacy bridging flow used by Near Intents if receiver_id == env::current_account_id() && memo .as_ref() .is_some_and(|m| m.starts_with(WITHDRAW_MEMO_PREFIX)) { if let Some(withdraw_relayer) = Self::read_withdraw_relayer_address() { return self.token.ft_transfer(withdraw_relayer, amount, memo); } } self.token.ft_transfer(receiver_id, amount, memo); } #[payable] fn ft_transfer_call( &mut self, receiver_id: AccountId, amount: U128, memo: Option, msg: String, ) -> PromiseOrValue { self.token.ft_transfer_call(receiver_id, amount, memo, msg) } fn ft_total_supply(&self) -> U128 { self.token.ft_total_supply() } fn ft_balance_of(&self, account_id: AccountId) -> U128 { self.token.ft_balance_of(account_id) } } #[near] impl FungibleTokenResolver for Contract { #[private] fn ft_resolve_transfer( &mut self, sender_id: AccountId, receiver_id: AccountId, amount: U128, ) -> U128 { let (used_amount, burned_amount) = self.token .internal_ft_resolve_transfer(&sender_id, receiver_id, amount); if burned_amount > 0 { log!("Account @{} burned {}", sender_id, burned_amount); } used_amount.into() } } #[near] impl StorageManagement for Contract { #[payable] fn storage_deposit( &mut self, account_id: Option, registration_only: Option, ) -> StorageBalance { self.token.storage_deposit(account_id, registration_only) } #[payable] fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { self.token.storage_withdraw(amount) } #[payable] fn storage_unregister(&mut self, force: Option) -> bool { #[allow(unused_variables)] if let Some((account_id, balance)) = self.token.internal_storage_unregister(force) { log!("Closed @{} with {}", account_id, balance); true } else { false } } fn storage_balance_bounds(&self) -> StorageBalanceBounds { self.token.storage_balance_bounds() } fn storage_balance_of(&self, account_id: AccountId) -> Option { self.token.storage_balance_of(account_id) } } #[near] impl FungibleTokenMetadataProvider for Contract { fn ft_metadata(&self) -> FungibleTokenMetadata { self.metadata.get().unwrap() } } #[near] impl Contract { #[payable] pub fn set_metadata( &mut self, name: Option, symbol: Option, reference: Option, reference_hash: Option, decimals: Option, icon: Option, ) { assert_one_yocto(); self.assert_controller(); let mut metadata = self.ft_metadata(); if let Some(name) = name { metadata.name = name; } if let Some(symbol) = symbol { metadata.symbol = symbol; } if let Some(reference) = reference { metadata.reference = Some(reference); } if let Some(reference_hash) = reference_hash { metadata.reference_hash = Some(reference_hash); } if let Some(decimals) = decimals { // Decimals can't be changed if it's already set. if metadata.decimals == 0 { metadata.decimals = decimals; } } if let Some(icon) = icon { metadata.icon = Some(icon); } self.metadata.set(&metadata); } /// # Panics /// /// This function will panic if serialization fails. pub fn set_withdraw_relayer_address(&mut self, relayer: &AccountId) { self.assert_controller(); env::storage_write(WITHDRAW_RELAYER_ADDRESS, &borsh::to_vec(relayer).unwrap()); } } impl Contract { fn assert_bridge(&self) { require!(self.bridge_id == env::predecessor_account_id(), "Not Allow"); } fn assert_controller(&self) { let caller = env::predecessor_account_id(); require!(caller == self.controller, "ERR_MISSING_PERMISSION"); } fn mint_inner(&mut self, account_id: &AccountId, amount: U128) { if self.token.accounts.get(account_id).is_none() { self.token.internal_register_account(account_id); } self.token.internal_deposit(account_id, amount.into()); near_contract_standards::fungible_token::events::FtMint { owner_id: account_id, amount, memo: None, } .emit(); } fn read_withdraw_relayer_address() -> Option { env::storage_read(WITHDRAW_RELAYER_ADDRESS).and_then(|data| borsh::from_slice(&data).ok()) } } #[near] impl Contract { #[private] pub fn handle_post_actions(&mut self, sender_id: AccountId, post_actions: Vec) { for post_action in post_actions { let PostAction { receiver_id, amount, memo, msg, gas, } = post_action; if let Some(gas) = gas { Self::ext(env::current_account_id()) .with_static_gas(gas) .handle_post_action(sender_id.clone(), receiver_id, amount, memo, msg) .detach(); } else { Self::ext(env::current_account_id()) .handle_post_action(sender_id.clone(), receiver_id, amount, memo, msg) .detach(); } } } #[private] pub fn handle_post_action( &mut self, sender_id: AccountId, receiver_id: AccountId, amount: U128, memo: Option, msg: String, ) { require!( env::prepaid_gas() > GAS_FOR_FT_TRANSFER_CALL, "More gas is required" ); require!( receiver_id != self.bridge_id, "handle_post_action: receiver_id must not be the bridge" ); let amount = amount.into(); self.token .internal_transfer(&sender_id, &receiver_id, amount, memo); let receiver_gas = env::prepaid_gas() .checked_sub(GAS_FOR_FT_TRANSFER_CALL) .unwrap_or_else(|| env::panic_str("Prepaid gas overflow")); // Initiating receiver's call and the callback ext_ft_receiver::ext(receiver_id.clone()) .with_static_gas(receiver_gas) .ft_on_transfer(sender_id.clone(), amount.into(), msg) .then( ext_ft_resolver::ext(env::current_account_id()) .with_static_gas(GAS_FOR_RESOLVE_TRANSFER) .ft_resolve_transfer(sender_id, receiver_id, amount.into()), ) .detach(); } }