terra/token_bridge: transfer native

Change-Id: I53fb27b467c96474f2980d495232ac955187775a
This commit is contained in:
Reisen 2021-10-04 11:52:11 +00:00 committed by David Paryente
parent 6076f9acc4
commit a9e98247bc
4 changed files with 429 additions and 35 deletions

View File

@ -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 }
),
];
}

View File

@ -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<S: Storage, A: Api, Q: Querier>(
}
HandleMsg::InitiateTransfer {
asset,
amount,
recipient_chain,
recipient,
fee,
@ -172,20 +174,76 @@ pub fn handle<S: Storage, A: Api, Q: Querier>(
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
) -> StdResult<HandleResponse> {
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<Uint128>| {
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
data: AssetInfo,
) -> StdResult<HandleResponse> {
let mut messages: Vec<CosmosMsg> = 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<Uint128>| {
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
@ -281,7 +339,29 @@ fn handle_attest_meta<S: Storage, A: Api, Q: Querier>(
fn handle_create_asset_meta<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
asset_address: &HumanAddr,
asset_info: AssetInfo,
nonce: u32,
) -> StdResult<HandleResponse> {
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
asset_address: HumanAddr,
nonce: u32,
) -> StdResult<HandleResponse> {
let cfg = config_read(&deps.storage).load()?;
@ -291,7 +371,7 @@ fn handle_create_asset_meta<S: Storage, A: Api, Q: Querier>(
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<S: Storage, A: Api, Q: Querier>(
})
}
/// 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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
denom: String,
nonce: u32,
) -> StdResult<HandleResponse> {
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
@ -395,7 +532,7 @@ fn handle_governance_payload<S: Storage, A: Api, Q: Querier>(
fn handle_register_chain<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
_env: Env,
data: &Vec<u8>,
) -> StdResult<HandleResponse> {
let RegisterChain {
@ -432,6 +569,23 @@ fn handle_complete_transfer<S: Storage, A: Api, Q: Querier>(
) -> StdResult<HandleResponse> {
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
emitter_chain: u16,
emitter_address: Vec<u8>,
data: &Vec<u8>,
) -> StdResult<HandleResponse> {
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<S: Storage, A: Api, Q: Querier>(
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<S: Storage, A: Api, Q: Querier>(
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<S: Storage, A: Api, Q: Querier>(
}
}
fn handle_complete_transfer_token_native<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
emitter_chain: u16,
emitter_address: Vec<u8>,
data: &Vec<u8>,
) -> StdResult<HandleResponse> {
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
asset: Asset,
recipient_chain: u16,
recipient: Vec<u8>,
fee: Uint128,
nonce: u32,
) -> StdResult<HandleResponse> {
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<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
asset: HumanAddr,
@ -574,11 +837,9 @@ fn handle_initiate_transfer<S: Storage, A: Api, Q: Querier>(
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<S: Storage, A: Api, Q: Querier>(
})
}
fn handle_initiate_transfer_native_token<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
denom: String,
amount: Uint128,
recipient_chain: u16,
recipient: Vec<u8>,
fee: Uint128,
nonce: u32,
) -> StdResult<HandleResponse> {
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<Uint128>| {
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<CosmosMsg> = vec![];
let asset_chain: u16 = CHAIN_ID;
let mut asset_address: Vec<u8> = 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<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
msg: QueryMsg,
@ -734,6 +1079,16 @@ fn build_asset_id(chain: u16, address: &[u8]) -> Vec<u8> {
hasher.finalize().to_vec()
}
// Produce a 20 byte asset "address" from a native terra denom.
fn build_native_id(denom: &str) -> Vec<u8> {
let mut asset_address: Vec<u8> = 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::{

View File

@ -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,
},
}

View File

@ -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<S: Storage>(storage: &S) -> ReadonlySingleton<S, ConfigInfo>
singleton_read(storage, CONFIG_KEY)
}
pub fn bridge_deposit<S: Storage>(storage: &mut S) -> Bucket<S, Uint128> {
bucket(BRIDGE_DEPOSITS, storage)
}
pub fn bridge_deposit_read<S: Storage>(storage: &S) -> ReadonlyBucket<S, Uint128> {
bucket_read(BRIDGE_DEPOSITS, storage)
}
pub fn bridge_contracts<S: Storage>(storage: &mut S) -> Bucket<S, Vec<u8>> {
bucket(BRIDGE_CONTRACTS, storage)
}