diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index 942171172..c55d67562 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -102,6 +102,7 @@ pub struct LedgerInfo { pub sequence_number: u32, pub timestamp: u64, pub network_passphrase: Vec, + pub base_reserve: u32, } #[derive(Clone, Default)] @@ -809,6 +810,120 @@ impl Host { self.record_contract_event(ContractEventType::System, topics, data)?; Ok(Status::OK.into()) } + + pub(crate) fn transfer_account_balance( + &self, + account_id: Object, + amount: i64, + ) -> Result<(), HostError> { + use xdr::{AccountEntryExt, AccountEntryExtensionV1Ext, LedgerKeyAccount}; + + self.with_current_frame(|frame| match frame { + Frame::Token(id, _) => Ok(()), + _ => Err(self.err_general("only native token can transfer classic balance")), + })?; + + let lk = LedgerKey::Account(LedgerKeyAccount { + account_id: AccountId(PublicKey::PublicKeyTypeEd25519(self.to_u256(account_id)?)), + }); + self.visit_storage(|storage| { + let mut le = storage.get(&lk)?; + let ae = match &mut le.data { + LedgerEntryData::Account(ae) => Ok(ae), + _ => Err(self.err_general("not account")), + }?; + if ae.balance < 0 { + return Err(self.err_general("balance is negative")); + } + + let base_reserve = self.with_ledger_info(|li| Ok(li.base_reserve))? as i64; + let (min_balance, max_balance) = if let AccountEntryExt::V1(ext1) = &ae.ext { + let net_entries = if let AccountEntryExtensionV1Ext::V2(ext2) = &ext1.ext { + 2i64 + (ae.num_sub_entries as i64) + (ext2.num_sponsored as i64) + - (ext2.num_sponsoring as i64) + } else { + 2i64 + ae.num_sub_entries as i64 + }; + let min_balance = net_entries * base_reserve + ext1.liabilities.selling; + let max_balance = i64::MAX - ext1.liabilities.buying; + (min_balance, max_balance) + } else { + let net_entries = 2i64 + (ae.num_sub_entries as i64); + let min_balance = net_entries * base_reserve; + let max_balance = i64::MAX; + (min_balance, max_balance) + }; + + let new_balance = if amount <= 0 { + ae.balance + amount + } else if ae.balance <= i64::MAX - amount { + ae.balance + amount + } else { + return Err(self.err_general("balance overflowed")); + }; + if new_balance >= min_balance && new_balance <= max_balance { + ae.balance = new_balance; + storage.put(&lk, &le) + } else { + Err(self.err_general("invalid balance")) + } + }) + } + + pub(crate) fn transfer_trustline_balance( + &self, + account_id: Object, + asset_code: Object, + issuer: Object, + amount: i64, + ) -> Result<(), HostError> { + use xdr::{TrustLineEntryExt, TrustLineFlags}; + + self.with_current_frame(|frame| match frame { + Frame::Token(id, _) => Ok(()), + _ => Err(self.err_general("only native token can transfer classic balance")), + })?; + + let lk = self.to_trustline_key(account_id, asset_code, issuer)?; + self.visit_storage(|storage| { + let mut le = storage.get(&lk)?; + let tl = match &mut le.data { + LedgerEntryData::Trustline(tl) => Ok(tl), + _ => Err(self.err_general("not trustline")), + }?; + if tl.balance < 0 { + return Err(self.err_general("balance is negative")); + } + if tl.flags & (TrustLineFlags::AuthorizedFlag as u32) == 0 { + return Err(self.err_general("not authorized")); + } + + let base_reserve = self.with_ledger_info(|li| Ok(li.base_reserve))? as i64; + let (min_balance, max_balance) = if let TrustLineEntryExt::V1(ext1) = &tl.ext { + let min_balance = ext1.liabilities.selling; + let max_balance = i64::MAX - ext1.liabilities.buying; + (min_balance, max_balance) + } else { + let min_balance = 0; + let max_balance = i64::MAX; + (min_balance, max_balance) + }; + + let new_balance = if amount <= 0 { + tl.balance + amount + } else if tl.balance <= i64::MAX - amount { + tl.balance + amount + } else { + return Err(self.err_general("balance overflowed")); + }; + if new_balance >= min_balance && new_balance <= max_balance { + tl.balance = new_balance; + storage.put(&lk, &le) + } else { + Err(self.err_general("invalid balance")) + } + }) + } } // Notes on metering: these are called from the guest and thus charged on the VM instructions. diff --git a/soroban-env-host/src/host/data_helper.rs b/soroban-env-host/src/host/data_helper.rs index 1ae16a695..7d9237911 100644 --- a/soroban-env-host/src/host/data_helper.rs +++ b/soroban-env-host/src/host/data_helper.rs @@ -1,12 +1,11 @@ use crate::budget::CostType; use crate::xdr::{ - ContractDataEntry, HashIdPreimage, HashIdPreimageContractId, HashIdPreimageEd25519ContractId, - LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, LedgerKeyAccount, - LedgerKeyContractData, ScContractCode, ScHostStorageErrorCode, ScHostValErrorCode, ScObject, - ScStatic, ScVal, + AccountEntry, AccountId, ContractDataEntry, Hash, HashIdPreimage, HashIdPreimageContractId, + HashIdPreimageEd25519ContractId, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, + LedgerKeyAccount, LedgerKeyContractData, LedgerKeyTrustLine, PublicKey, ScContractCode, + ScHostStorageErrorCode, ScHostValErrorCode, ScObject, ScStatic, ScVal, Uint256, WriteXdr, }; use crate::{Host, HostError, Object}; -use soroban_env_common::xdr::{AccountEntry, AccountId, Hash, PublicKey, Uint256, WriteXdr}; impl Host { // Notes on metering: free @@ -110,4 +109,32 @@ impl Host { }); self.visit_storage(|storage| storage.has(&acc)) } + + pub fn to_trustline_key( + &self, + account_id: Object, + asset_code: Object, + issuer: Object, + ) -> Result { + use crate::xdr::{AlphaNum12, AlphaNum4, AssetCode12, AssetCode4, TrustLineAsset}; + let asset = self.visit_obj(asset_code, |b: &Vec| { + if b.len() > 0 && b.len() <= 4 { + Ok(TrustLineAsset::CreditAlphanum4(AlphaNum4 { + asset_code: AssetCode4(b.as_slice().try_into().unwrap()), + issuer: AccountId(PublicKey::PublicKeyTypeEd25519(self.to_u256(issuer)?)), + })) + } else if b.len() > 0 && b.len() <= 12 { + Ok(TrustLineAsset::CreditAlphanum12(AlphaNum12 { + asset_code: AssetCode12(b.as_slice().try_into().unwrap()), + issuer: AccountId(PublicKey::PublicKeyTypeEd25519(self.to_u256(issuer)?)), + })) + } else { + Err(self.err_general("invalid asset code")) + } + })?; + Ok(LedgerKey::Trustline(LedgerKeyTrustLine { + account_id: AccountId(PublicKey::PublicKeyTypeEd25519(self.to_u256(account_id)?)), + asset, + })) + } } diff --git a/soroban-env-host/src/native_contract/base_types.rs b/soroban-env-host/src/native_contract/base_types.rs index 6fa69c534..461862cf1 100644 --- a/soroban-env-host/src/native_contract/base_types.rs +++ b/soroban-env-host/src/native_contract/base_types.rs @@ -123,8 +123,35 @@ impl TryIntoVal for Bytes { } impl From for Object { - fn from(vec: Bytes) -> Self { - vec.0.val + fn from(b: Bytes) -> Self { + b.0.val + } +} + +impl From> for Bytes { + fn from(b: BytesN) -> Self { + Self(b.0) + } +} + +impl Bytes { + pub fn push(&mut self, x: u8) -> Result<(), HostError> { + let x32: u32 = x.into(); + self.0 = self + .0 + .env + .bytes_push(self.0.val, x32.into())? + .in_env(&self.0.env); + Ok(()) + } + + pub fn append(&mut self, other: Bytes) -> Result<(), HostError> { + self.0 = self + .0 + .env + .bytes_append(self.0.val, other.0.val)? + .in_env(&self.0.env); + Ok(()) } } diff --git a/soroban-env-host/src/native_contract/token/balance.rs b/soroban-env-host/src/native_contract/token/balance.rs index 8abd0e888..8d4cbda0a 100644 --- a/soroban-env-host/src/native_contract/token/balance.rs +++ b/soroban-env-host/src/native_contract/token/balance.rs @@ -1,7 +1,8 @@ use crate::host::Host; -use crate::native_contract::base_types::BigInt; +use crate::native_contract::base_types::{BigInt, BytesN}; use crate::native_contract::token::error::Error; -use crate::native_contract::token::public_types::Identifier; +use crate::native_contract::token::metadata::read_metadata; +use crate::native_contract::token::public_types::{Identifier, Metadata}; use crate::native_contract::token::storage_types::DataKey; use core::cmp::Ordering; use soroban_env_common::{CheckedEnv, TryIntoVal}; @@ -57,3 +58,23 @@ pub fn write_state(e: &Host, id: Identifier, is_frozen: bool) -> Result<(), Erro e.put_contract_data(key.try_into_val(e)?, is_frozen.into())?; Ok(()) } + +pub fn transfer_classic_balance(e: &Host, to_key: BytesN<32>, amount: i64) -> Result<(), Error> { + match read_metadata(e)? { + Metadata::Token(_) => return Err(Error::ContractError), + Metadata::Native => e.transfer_account_balance(to_key.into(), amount)?, + Metadata::AlphaNum4(asset) => e.transfer_trustline_balance( + to_key.into(), + asset.asset_code.into(), + asset.issuer.into(), + amount, + )?, + Metadata::AlphaNum12(asset) => e.transfer_trustline_balance( + to_key.into(), + asset.asset_code.into(), + asset.issuer.into(), + amount, + )?, + }; + Ok(()) +} diff --git a/soroban-env-host/src/native_contract/token/contract.rs b/soroban-env-host/src/native_contract/token/contract.rs index ea18a8815..29c4a9327 100644 --- a/soroban-env-host/src/native_contract/token/contract.rs +++ b/soroban-env-host/src/native_contract/token/contract.rs @@ -3,26 +3,20 @@ use crate::native_contract::base_types::{BigInt, Bytes, Vec}; use crate::native_contract::token::admin::{check_admin, has_administrator, write_administrator}; use crate::native_contract::token::allowance::{read_allowance, spend_allowance, write_allowance}; use crate::native_contract::token::balance::{ - read_balance, read_state, receive_balance, spend_balance, write_state, + read_balance, read_state, receive_balance, spend_balance, transfer_classic_balance, write_state, }; use crate::native_contract::token::cryptography::check_auth; use crate::native_contract::token::error::Error; use crate::native_contract::token::metadata::{ - read_decimal, read_name, read_symbol, write_decimal, write_name, write_symbol, + read_decimal, read_name, read_symbol, write_metadata, }; use crate::native_contract::token::nonce::read_nonce; -use crate::native_contract::token::public_types::{Identifier, Signature}; +use crate::native_contract::token::public_types::{Identifier, Metadata, Signature}; use soroban_env_common::{Symbol, TryIntoVal}; use soroban_native_sdk_macros::contractimpl; pub trait TokenTrait { - fn initialize( - e: &Host, - admin: Identifier, - decimal: u32, - name: Bytes, - symbol: Bytes, - ) -> Result<(), Error>; + fn initialize(e: &Host, admin: Identifier, metadata: Metadata) -> Result<(), Error>; fn nonce(e: &Host, id: Identifier) -> Result; @@ -89,27 +83,22 @@ pub trait TokenTrait { fn name(e: &Host) -> Result; fn symbol(e: &Host) -> Result; + + fn to_smart(e: &Host, id: Signature, nonce: BigInt, amount: i64) -> Result<(), Error>; + + fn to_classic(e: &Host, id: Signature, nonce: BigInt, amount: i64) -> Result<(), Error>; } pub struct Token; #[contractimpl] impl TokenTrait for Token { - fn initialize( - e: &Host, - admin: Identifier, - decimal: u32, - name: Bytes, - symbol: Bytes, - ) -> Result<(), Error> { + fn initialize(e: &Host, admin: Identifier, metadata: Metadata) -> Result<(), Error> { if has_administrator(&e)? { return Err(Error::ContractError); } write_administrator(&e, admin)?; - - write_decimal(&e, u8::try_from(decimal).map_err(|_| Error::ContractError)?)?; - write_name(&e, name)?; - write_symbol(&e, symbol)?; + write_metadata(&e, metadata)?; Ok(()) } @@ -273,4 +262,48 @@ impl TokenTrait for Token { fn symbol(e: &Host) -> Result { read_symbol(&e) } + + fn to_smart(e: &Host, id: Signature, nonce: BigInt, amount: i64) -> Result<(), Error> { + if amount < 0 { + return Err(Error::ContractError); + } + + let id_key = match &id { + Signature::Account(acc) => Ok(acc.account_id.clone()), + _ => Err(Error::ContractError), + }?; + let mut args = Vec::new(e)?; + args.push(amount.clone())?; + check_auth(&e, id, nonce, Symbol::from_str("to_smart"), args)?; + + transfer_classic_balance(e, id_key.clone(), amount)?; + receive_balance( + &e, + Identifier::Account(id_key), + BigInt::from_u64(&e, amount.try_into().map_err(|_| Error::ContractError)?)?, + )?; + Ok(()) + } + + fn to_classic(e: &Host, id: Signature, nonce: BigInt, amount: i64) -> Result<(), Error> { + if amount < 0 { + return Err(Error::ContractError); + } + + let id_key = match &id { + Signature::Account(acc) => Ok(acc.account_id.clone()), + _ => Err(Error::ContractError), + }?; + let mut args = Vec::new(e)?; + args.push(amount.clone())?; + check_auth(&e, id, nonce, Symbol::from_str("to_classic"), args)?; + + transfer_classic_balance(e, id_key.clone(), -amount)?; + spend_balance( + &e, + Identifier::Account(id_key), + BigInt::from_u64(&e, amount.try_into().map_err(|_| Error::ContractError)?)?, + )?; + Ok(()) + } } diff --git a/soroban-env-host/src/native_contract/token/metadata.rs b/soroban-env-host/src/native_contract/token/metadata.rs index 474de5461..3469ba66c 100644 --- a/soroban-env-host/src/native_contract/token/metadata.rs +++ b/soroban-env-host/src/native_contract/token/metadata.rs @@ -1,41 +1,53 @@ use crate::host::Host; use crate::native_contract::base_types::Bytes; use crate::native_contract::token::error::Error; +use crate::native_contract::token::public_types::Metadata; use crate::native_contract::token::storage_types::DataKey; -use soroban_env_common::{CheckedEnv, TryIntoVal}; +use soroban_env_common::{CheckedEnv, EnvBase, TryFromVal, TryIntoVal}; -pub fn read_decimal(e: &Host) -> Result { - let key = DataKey::Decimals; - let rv = e.get_contract_data(key.try_into_val(e)?)?; - Ok(rv.try_into()?) -} - -pub fn write_decimal(e: &Host, d: u8) -> Result<(), Error> { - let key = DataKey::Decimals; - e.put_contract_data(key.try_into_val(e)?, u32::from(d).into())?; +pub fn write_metadata(e: &Host, metadata: Metadata) -> Result<(), Error> { + let key = DataKey::Metadata; + e.put_contract_data(key.try_into_val(e)?, metadata.try_into_val(e)?)?; Ok(()) } -pub fn read_name(e: &Host) -> Result { - let key = DataKey::Name; +pub fn read_metadata(e: &Host) -> Result { + let key = DataKey::Metadata; let rv = e.get_contract_data(key.try_into_val(e)?)?; Ok(rv.try_into_val(e)?) } -pub fn write_name(e: &Host, d: Bytes) -> Result<(), Error> { - let key = DataKey::Name; - e.put_contract_data(key.try_into_val(e)?, d.try_into_val(e)?)?; - Ok(()) +pub fn read_name(e: &Host) -> Result { + match read_metadata(e)? { + Metadata::Token(token) => Ok(token.name), + Metadata::Native => Ok(Bytes::try_from_val(e, e.bytes_new_from_slice(b"native"))?), + Metadata::AlphaNum4(asset) => { + let mut res: Bytes = asset.asset_code.into(); + res.push(b':')?; + res.append(asset.issuer.into())?; + Ok(res) + } + Metadata::AlphaNum12(asset) => { + let mut res: Bytes = asset.asset_code.into(); + res.push(b':')?; + res.append(asset.issuer.into())?; + Ok(res) + } + } } pub fn read_symbol(e: &Host) -> Result { - let key = DataKey::Symbol; - let rv = e.get_contract_data(key.try_into_val(e)?)?; - Ok(rv.try_into_val(e)?) + match read_metadata(e)? { + Metadata::Token(token) => Ok(token.symbol), + Metadata::Native => Ok(Bytes::try_from_val(e, e.bytes_new_from_slice(b"native"))?), + Metadata::AlphaNum4(asset) => Ok(asset.asset_code.into()), + Metadata::AlphaNum12(asset) => Ok(asset.asset_code.into()), + } } -pub fn write_symbol(e: &Host, d: Bytes) -> Result<(), Error> { - let key = DataKey::Symbol; - e.put_contract_data(key.try_into_val(e)?, d.try_into_val(e)?)?; - Ok(()) +pub fn read_decimal(e: &Host) -> Result { + match read_metadata(e)? { + Metadata::Token(token) => Ok(token.decimals), + Metadata::Native | Metadata::AlphaNum4(_) | Metadata::AlphaNum12(_) => Ok(7), + } } diff --git a/soroban-env-host/src/native_contract/token/public_types.rs b/soroban-env-host/src/native_contract/token/public_types.rs index bf3be6ae0..57a5c4bdf 100644 --- a/soroban-env-host/src/native_contract/token/public_types.rs +++ b/soroban-env-host/src/native_contract/token/public_types.rs @@ -60,3 +60,34 @@ pub struct SignaturePayloadV0 { pub enum SignaturePayload { V0(SignaturePayloadV0), } + +#[derive(Clone)] +#[contracttype] +pub struct TokenMetadata { + pub name: Bytes, + pub symbol: Bytes, + pub decimals: u32, +} + +#[derive(Clone)] +#[contracttype] +pub struct AlphaNum4Metadata { + pub asset_code: BytesN<4>, + pub issuer: BytesN<32>, +} + +#[derive(Clone)] +#[contracttype] +pub struct AlphaNum12Metadata { + pub asset_code: BytesN<12>, + pub issuer: BytesN<32>, +} + +#[derive(Clone)] +#[contracttype] +pub enum Metadata { + Token(TokenMetadata), + Native, + AlphaNum4(AlphaNum4Metadata), + AlphaNum12(AlphaNum12Metadata), +} diff --git a/soroban-env-host/src/native_contract/token/storage_types.rs b/soroban-env-host/src/native_contract/token/storage_types.rs index f4c6b457f..b402a3b97 100644 --- a/soroban-env-host/src/native_contract/token/storage_types.rs +++ b/soroban-env-host/src/native_contract/token/storage_types.rs @@ -16,7 +16,5 @@ pub enum DataKey { Nonce(Identifier), State(Identifier), Admin, - Decimals, - Name, - Symbol, + Metadata, }