diff --git a/sdk/js/src/token_bridge/transfer.ts b/sdk/js/src/token_bridge/transfer.ts index 79007e27..fab5640a 100644 --- a/sdk/js/src/token_bridge/transfer.ts +++ b/sdk/js/src/token_bridge/transfer.ts @@ -90,6 +90,7 @@ export async function transferFromTerra( recipientAddress: Uint8Array ) { const nonce = Math.round(Math.random() * 100000); + const isNativeAsset = ["uluna"].includes(tokenAddress); return [ new MsgExecuteContract( walletAddress, @@ -105,21 +106,49 @@ export async function transferFromTerra( }, { uluna: 10000 } ), - new MsgExecuteContract( - walletAddress, - tokenBridgeAddress, - { - initiate_transfer: { - asset: tokenAddress, - amount: amount, - recipient_chain: recipientChain, - recipient: Buffer.from(recipientAddress).toString("base64"), - fee: "0", - nonce: nonce, - }, - }, - { uluna: 10000 } - ), + isNativeAsset + ? new MsgExecuteContract( + walletAddress, + tokenBridgeAddress, + { + initiate_transfer: { + asset: { + amount: amount, + info: { + native_token: { + denom: tokenAddress, + }, + }, + }, + recipient_chain: recipientChain, + recipient: Buffer.from(recipientAddress).toString("base64"), + fee: "0", + nonce: nonce, + }, + }, + { uluna: 10000, [tokenAddress]: amount } + ) + : new MsgExecuteContract( + walletAddress, + tokenBridgeAddress, + { + initiate_transfer: { + asset: { + amount: amount, + info: { + token: { + contract_addr: tokenAddress, + }, + }, + }, + recipient_chain: recipientChain, + recipient: Buffer.from(recipientAddress).toString("base64"), + fee: "0", + nonce: nonce, + }, + }, + { uluna: 10000 } + ), ]; } diff --git a/terra/contracts/token-bridge/src/contract.rs b/terra/contracts/token-bridge/src/contract.rs index de3f7b15..52ffdaeb 100644 --- a/terra/contracts/token-bridge/src/contract.rs +++ b/terra/contracts/token-bridge/src/contract.rs @@ -1,8 +1,10 @@ use crate::msg::WrappedRegistryResponse; use cosmwasm_std::{ + coin, log, to_binary, Api, + BankMsg, Binary, CanonicalAddr, Coin, @@ -33,6 +35,7 @@ use crate::{ bridge_contracts_read, config, config_read, + bridge_deposit, receive_native, send_native, wrapped_asset, @@ -163,7 +166,6 @@ pub fn handle( } HandleMsg::InitiateTransfer { asset, - amount, recipient_chain, recipient, fee, @@ -172,20 +174,76 @@ pub fn handle( deps, env, asset, - amount, recipient_chain, recipient.as_slice().to_vec(), fee, nonce, ), + HandleMsg::DepositTokens => deposit_tokens(deps, env), + HandleMsg::WithdrawTokens { asset } => withdraw_tokens(deps, env, asset), HandleMsg::SubmitVaa { data } => submit_vaa(deps, env, &data), HandleMsg::CreateAssetMeta { - asset_address, + asset_info, nonce, - } => handle_create_asset_meta(deps, env, &asset_address, nonce), + } => handle_create_asset_meta(deps, env, asset_info, nonce), } } +fn deposit_tokens( + deps: &mut Extern, + env: Env, +) -> StdResult { + for fund in env.message.sent_funds { + let deposit_key = format!("{}:{}", env.message.sender, fund.denom); + bridge_deposit(&mut deps.storage).update(deposit_key.as_bytes(), |amount: Option| { + match amount { + Some(v) => Ok(v + fund.amount), + None => Ok(fund.amount) + } + })?; + } + + Ok(HandleResponse { + messages: vec![], + log: vec![ + log("action", "deposit_tokens"), + ], + data: None, + }) +} + +fn withdraw_tokens( + deps: &mut Extern, + env: Env, + data: AssetInfo, +) -> StdResult { + let mut messages: Vec = vec![]; + if let AssetInfo::NativeToken { denom } = data { + let deposit_key = format!("{}:{}", env.message.sender, denom); + bridge_deposit(&mut deps.storage).update(deposit_key.as_bytes(), |current: Option| { + match current { + Some(v) => { + messages.push(CosmosMsg::Bank(BankMsg::Send { + from_address: env.contract.address.clone(), + to_address: env.message.sender.clone(), + amount: vec![coin(v.u128(), &denom)], + })); + Ok(Uint128(0)) + } + None => Err(StdError::generic_err("no deposit found to withdraw")) + } + })?; + } + + Ok(HandleResponse { + messages: vec![], + log: vec![ + log("action", "withdraw_tokens"), + ], + data: None, + }) +} + /// Handle wrapped asset registration messages fn handle_register_asset( deps: &mut Extern, @@ -281,7 +339,29 @@ fn handle_attest_meta( fn handle_create_asset_meta( deps: &mut Extern, env: Env, - asset_address: &HumanAddr, + asset_info: AssetInfo, + nonce: u32, +) -> StdResult { + match asset_info { + AssetInfo::Token { contract_addr } => handle_create_asset_meta_token( + deps, + env, + contract_addr, + nonce, + ), + AssetInfo::NativeToken { ref denom } => handle_create_asset_meta_native_token( + deps, + env, + denom.clone(), + nonce, + ) + } +} + +fn handle_create_asset_meta_token( + deps: &mut Extern, + env: Env, + asset_address: HumanAddr, nonce: u32, ) -> StdResult { let cfg = config_read(&deps.storage).load()?; @@ -291,7 +371,7 @@ fn handle_create_asset_meta( msg: to_binary(&TokenQuery::TokenInfo {})?, }); - let asset_canonical = deps.api.canonical_address(asset_address)?; + let asset_canonical = deps.api.canonical_address(&asset_address)?; let token_info: TokenInfoResponse = deps.querier.query(&request)?; let meta: AssetMeta = AssetMeta { @@ -327,6 +407,63 @@ fn handle_create_asset_meta( }) } +/// All ISO-4217 currency codes are 3 letters, so we can safely slice anything that is not ULUNA. +/// https://www.xe.com/iso4217.php +fn format_native_denom_symbol(denom: &str) -> String { + if denom == "uluna" { + return "LUNA".to_string(); + } + // UUSD -> US -> UST + denom.to_uppercase()[1..3].to_string() + "T" +} + +fn handle_create_asset_meta_native_token( + deps: &mut Extern, + env: Env, + denom: String, + nonce: u32, +) -> StdResult { + let cfg = config_read(&deps.storage).load()?; + + let mut asset_id = extend_address_to_32(&build_native_id(&denom).into()); + asset_id[0] = 1; + + let symbol = format_native_denom_symbol(&denom); + + let meta: AssetMeta = AssetMeta { + token_chain: CHAIN_ID, + token_address: asset_id.clone(), + decimals: 6, + symbol: extend_string_to_32(&symbol)?, + name: extend_string_to_32(&symbol)?, + }; + + let token_bridge_message = TokenBridgeMessage { + action: Action::ATTEST_META, + payload: meta.serialize().to_vec(), + }; + + Ok(HandleResponse { + messages: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeHandleMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + // forward coins sent to this message + send: coins_after_tax(deps, env.message.sent_funds.clone())?, + })], + log: vec![ + log("meta.token_chain", CHAIN_ID), + log("meta.symbol", symbol), + log("meta.asset_id", hex::encode(asset_id)), + log("meta.nonce", nonce), + log("meta.block_time", env.block.time), + ], + data: None, + }) +} + fn submit_vaa( deps: &mut Extern, env: Env, @@ -395,7 +532,7 @@ fn handle_governance_payload( fn handle_register_chain( deps: &mut Extern, - env: Env, + _env: Env, data: &Vec, ) -> StdResult { let RegisterChain { @@ -432,6 +569,23 @@ fn handle_complete_transfer( ) -> StdResult { let transfer_info = TransferInfo::deserialize(&data)?; + // All terra token addresses are 20 bytes, and so start with 12 0's, if the address begins with + // a 1 we can identify it as a fully native token. + match transfer_info.token_address.as_slice()[0] { + 1 => handle_complete_transfer_token_native(deps, env, emitter_chain, emitter_address, data), + _ => handle_complete_transfer_token(deps, env, emitter_chain, emitter_address, data), + } +} + +fn handle_complete_transfer_token( + deps: &mut Extern, + env: Env, + emitter_chain: u16, + emitter_address: Vec, + data: &Vec, +) -> StdResult { + let transfer_info = TransferInfo::deserialize(&data)?; + let expected_contract = bridge_contracts_read(&deps.storage).load(&emitter_chain.to_be_bytes())?; @@ -439,15 +593,14 @@ fn handle_complete_transfer( if expected_contract != emitter_address { return Err(StdError::unauthorized()); } - if transfer_info.recipient_chain != CHAIN_ID { return Err(StdError::generic_err( "this transfer is not directed at this chain", )); } - let token_chain = transfer_info.token_chain; let target_address = (&transfer_info.recipient.as_slice()).get_address(0); + let token_chain = transfer_info.token_chain; let (not_supported_amount, mut amount) = transfer_info.amount; let (not_supported_fee, mut fee) = transfer_info.fee; @@ -551,7 +704,7 @@ fn handle_complete_transfer( Ok(HandleResponse { messages, log: vec![ - log("action", "complete_transfer_native"), + log("action", "complete_transfer_token"), log("recipient", recipient), log("contract", contract_addr), log("amount", amount), @@ -561,7 +714,117 @@ fn handle_complete_transfer( } } +fn handle_complete_transfer_token_native( + deps: &mut Extern, + env: Env, + emitter_chain: u16, + emitter_address: Vec, + data: &Vec, +) -> StdResult { + let transfer_info = TransferInfo::deserialize(&data)?; + + let expected_contract = + bridge_contracts_read(&deps.storage).load(&emitter_chain.to_be_bytes())?; + + // must be sent by a registered token bridge contract + if expected_contract != emitter_address { + return Err(StdError::unauthorized()); + } + if transfer_info.recipient_chain != CHAIN_ID { + return Err(StdError::generic_err( + "this transfer is not directed at this chain", + )); + } + + let target_address = (&transfer_info.recipient.as_slice()).get_address(0); + + let (not_supported_amount, mut amount) = transfer_info.amount; + let (not_supported_fee, fee) = transfer_info.fee; + + amount = amount.checked_sub(fee).unwrap(); + + // Check high 128 bit of amount value to be empty + if not_supported_amount != 0 || not_supported_fee != 0 { + return ContractError::AmountTooHigh.std_err(); + } + + // Wipe the native byte marker and extract the serialized denom. + let mut token_address = transfer_info.token_address.clone(); + let token_address = token_address.as_mut_slice(); + token_address[0] = 0; + + let mut denom = token_address.to_vec(); + denom.retain(|&c| c != 0); + let denom = String::from_utf8(denom).unwrap(); + + // note -- here the amount is the amount the recipient will receive; + // amount + fee is the total sent + let recipient = deps.api.human_address(&target_address)?; + let token_address = (&*token_address).get_address(0); + receive_native(&mut deps.storage, &token_address, Uint128(amount + fee))?; + + let mut messages = vec![CosmosMsg::Bank(BankMsg::Send { + from_address: env.contract.address.clone(), + to_address: recipient.clone(), + amount: vec![coin(amount, &denom)], + })]; + + if fee != 0 { + messages.push(CosmosMsg::Bank(BankMsg::Send { + from_address: env.contract.address.clone(), + to_address: recipient.clone(), + amount: vec![coin(fee, &denom)], + })); + } + + Ok(HandleResponse { + messages, + log: vec![ + log("action", "complete_transfer_token_native"), + log("recipient", recipient), + log("denom", denom), + log("amount", amount), + ], + data: None, + }) +} + fn handle_initiate_transfer( + deps: &mut Extern, + env: Env, + asset: Asset, + recipient_chain: u16, + recipient: Vec, + fee: Uint128, + nonce: u32, +) -> StdResult { + match asset.info { + AssetInfo::Token { contract_addr } => handle_initiate_transfer_token( + deps, + env, + contract_addr, + asset.amount, + recipient_chain, + recipient, + fee, + nonce, + ), + AssetInfo::NativeToken { ref denom } => { + handle_initiate_transfer_native_token( + deps, + env, + denom.clone(), + asset.amount, + recipient_chain, + recipient, + fee, + nonce, + ) + } + } +} + +fn handle_initiate_transfer_token( deps: &mut Extern, env: Env, asset: HumanAddr, @@ -574,11 +837,9 @@ fn handle_initiate_transfer( if recipient_chain == CHAIN_ID { return ContractError::SameSourceAndTarget.std_err(); } - if amount.is_zero() { return ContractError::AmountTooLow.std_err(); } - if fee > amount { return Err(StdError::generic_err("fee greater than sent amount")); } @@ -700,6 +961,90 @@ fn handle_initiate_transfer( }) } +fn handle_initiate_transfer_native_token( + deps: &mut Extern, + env: Env, + denom: String, + amount: Uint128, + recipient_chain: u16, + recipient: Vec, + fee: Uint128, + nonce: u32, +) -> StdResult { + if recipient_chain == CHAIN_ID { + return ContractError::SameSourceAndTarget.std_err(); + } + if amount.is_zero() { + return ContractError::AmountTooLow.std_err(); + } + if fee > amount { + return Err(StdError::generic_err("fee greater than sent amount")); + } + + let deposit_key = format!("{}:{}", env.message.sender, denom); + bridge_deposit(&mut deps.storage).update(deposit_key.as_bytes(), |current: Option| { + match current { + Some(v) => Ok((v - amount)?), + None => Err(StdError::generic_err("no deposit found to transfer")) + } + })?; + + let cfg: ConfigInfo = config_read(&deps.storage).load()?; + let mut messages: Vec = vec![]; + + let asset_chain: u16 = CHAIN_ID; + let mut asset_address: Vec = build_native_id(&denom); + + send_native(&mut deps.storage, &asset_address[..].into(), amount)?; + + // Mark the first byte of the address to distinguish it as native. + asset_address = extend_address_to_32(&asset_address.into()); + asset_address[0] = 1; + + let transfer_info = TransferInfo { + token_chain: asset_chain, + token_address: asset_address.to_vec(), + amount: (0, amount.u128()), + recipient_chain, + recipient: recipient.clone(), + fee: (0, fee.u128()), + }; + + let token_bridge_message = TokenBridgeMessage { + action: Action::TRANSFER, + payload: transfer_info.serialize(), + }; + + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeHandleMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + send: coins_after_tax(deps, env.message.sent_funds.clone())?, + })); + + Ok(HandleResponse { + messages, + log: vec![ + log("transfer.token_chain", asset_chain), + log("transfer.token", hex::encode(asset_address)), + log( + "transfer.sender", + hex::encode(extend_address_to_32( + &deps.api.canonical_address(&env.message.sender)?, + )), + ), + log("transfer.recipient_chain", recipient_chain), + log("transfer.recipient", hex::encode(recipient)), + log("transfer.amount", amount), + log("transfer.nonce", nonce), + log("transfer.block_time", env.block.time), + ], + data: None, + }) +} + pub fn query( deps: &Extern, msg: QueryMsg, @@ -734,6 +1079,16 @@ fn build_asset_id(chain: u16, address: &[u8]) -> Vec { hasher.finalize().to_vec() } +// Produce a 20 byte asset "address" from a native terra denom. +fn build_native_id(denom: &str) -> Vec { + let mut asset_address: Vec = denom.clone().as_bytes().to_vec(); + asset_address.reverse(); + asset_address.extend(vec![0u8; 20 - denom.len()]); + asset_address.reverse(); + assert_eq!(asset_address.len(), 20); + asset_address +} + #[cfg(test)] mod tests { use cosmwasm_std::{ diff --git a/terra/contracts/token-bridge/src/msg.rs b/terra/contracts/token-bridge/src/msg.rs index bbb7c2a4..6bb7fff8 100644 --- a/terra/contracts/token-bridge/src/msg.rs +++ b/terra/contracts/token-bridge/src/msg.rs @@ -1,9 +1,6 @@ -use cosmwasm_std::{ - Binary, - HumanAddr, - Uint128, -}; +use cosmwasm_std::{Binary, HumanAddr, Uint128}; use schemars::JsonSchema; +use terraswap::asset::{Asset, AssetInfo}; use serde::{ Deserialize, Serialize, @@ -26,9 +23,13 @@ pub enum HandleMsg { asset_id: Binary, }, + DepositTokens, + WithdrawTokens { + asset: AssetInfo, + }, + InitiateTransfer { - asset: HumanAddr, - amount: Uint128, + asset: Asset, recipient_chain: u16, recipient: Binary, fee: Uint128, @@ -40,7 +41,7 @@ pub enum HandleMsg { }, CreateAssetMeta { - asset_address: HumanAddr, + asset_info: AssetInfo, nonce: u32, }, } diff --git a/terra/contracts/token-bridge/src/state.rs b/terra/contracts/token-bridge/src/state.rs index 492ecc3b..bbab0ee5 100644 --- a/terra/contracts/token-bridge/src/state.rs +++ b/terra/contracts/token-bridge/src/state.rs @@ -29,6 +29,7 @@ pub static CONFIG_KEY: &[u8] = b"config"; pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset"; pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address"; pub static BRIDGE_CONTRACTS: &[u8] = b"bridge_contracts"; +pub static BRIDGE_DEPOSITS: &[u8] = b"bridge_deposits"; pub static NATIVE_COUNTER: &[u8] = b"native_counter"; // Guardian set information @@ -50,6 +51,14 @@ pub fn config_read(storage: &S) -> ReadonlySingleton singleton_read(storage, CONFIG_KEY) } +pub fn bridge_deposit(storage: &mut S) -> Bucket { + bucket(BRIDGE_DEPOSITS, storage) +} + +pub fn bridge_deposit_read(storage: &S) -> ReadonlyBucket { + bucket_read(BRIDGE_DEPOSITS, storage) +} + pub fn bridge_contracts(storage: &mut S) -> Bucket> { bucket(BRIDGE_CONTRACTS, storage) }