use cw20::{BalanceResponse, TokenInfoResponse}; use cw20_base::msg::{ExecuteMsg as TokenMsg, QueryMsg as TokenQuery}; use cw20_wrapped::msg::{ ExecuteMsg as WrappedMsg, InitHook, InstantiateMsg as WrappedInit, QueryMsg as WrappedQuery, WrappedAssetInfoResponse, }; use sha3::{Digest, Keccak256}; use std::{ cmp::{max, min}, str::FromStr, }; use terraswap::asset::{Asset, AssetInfo}; use terra_cosmwasm::TerraQuerier; use wormhole::{ byte_utils::{ extend_address_to_32, extend_address_to_32_array, extend_string_to_32, get_string_from_32, ByteUtils, }, error::ContractError, msg::{ExecuteMsg as WormholeExecuteMsg, QueryMsg as WormholeQueryMsg}, state::{vaa_archive_add, vaa_archive_check, GovernancePacket, ParsedVAA}, }; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ coin, to_binary, BankMsg, Binary, CanonicalAddr, Coin, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper, QueryRequest, Reply, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, WasmQuery, }; use crate::{ msg::{ ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, TransferInfoResponse, WrappedRegistryResponse, }, state::{ bridge_contracts, bridge_contracts_read, bridge_deposit, config, config_read, receive_native, send_native, wrapped_asset, wrapped_asset_address, wrapped_asset_address_read, wrapped_asset_read, wrapped_asset_seq, wrapped_asset_seq_read, wrapped_transfer_tmp, Action, AssetMeta, ConfigInfo, RegisterChain, TokenBridgeMessage, TransferInfo, TransferState, TransferWithPayloadInfo, UpgradeContract, }, }; type HumanAddr = String; // Chain ID of Terra const CHAIN_ID: u16 = 3; const WRAPPED_ASSET_UPDATING: &str = "updating"; pub enum TransferType { WithoutPayload, WithPayload { payload: A }, } /// Migration code that runs the next time the contract is upgraded. /// This function will contain ephemeral code that we want to run once, and thus /// can (and should be) safely deleted after the upgrade happened successfully. /// /// For example, when the code id of the wrapped assets is updated, this /// function will take care of upgrading all the deployed wrapped asset /// contracts. See [`migrate_wrapped_assets`]. #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> StdResult { let new_code_id = 767; // On a previous deployment, the wrapped assets have been migrated to code // id 767: // https://finder.terra.money/classic/tx/67e8fcff48eefe11bf6a975e621b6866ba930f9d2a85bc9ac5a70f009ee354c7 // However, that upgrade didn't change the [`wrapped_asset_code_id`] field // of the config state of this contract (the token bridge), so every wrapped // asset that's been deployed since by the token bridge still uses the old // code id. // Thus, we update that variable here. let mut c = config(deps.storage).load()?; c.wrapped_asset_code_id = new_code_id; config(deps.storage).save(&c)?; // Ideally, we would want to run [`migrate_wrapped_assets`] to upgrade all the // wrapped assets to 767. However, an upgrade has been attempted to do just that: // https://finder.terra.money/classic/tx/FE39E9549770F59E2AAA1C6B0B86DDF36A4C56CED0CFB0CA4C9D4CC9FBE1E5BA // and it failed with a cryptic 'out of gas error' (notice that the // 'gasWanted' is larger than 'gasUsed', which should never happen. The // process also fails with the same error in simulation, which should // absolutely never happen). When attempting the update locally, it proceeds // without problems, so the current theory is that on mainnet the process // sends too many messages and somehow overflows the gas counter (although // that should result in a different message). // The following code doesn't succeed on mainnet (but does locally): /* let messages = migrate_wrapped_assets(deps, env, new_code_id)?; let count = messages.len(); Ok(Response::new() .add_messages(messages) .add_attribute("migrate", "upgrade cw20 wrappers") .add_attribute("count", count.to_string())) */ // tldr: The migration strategy currently is to just upgrade the wrapped // asset id variable, so at least new contracts are instantiated from the // correct code id. // Just to be safe, we add the above VAA to the replay protection cache so // it can't be reused later if the gas parameters of terra classic change. let data: Binary = Binary::from_base64("AQAAAAINAJG2fJDyk1xTNYrByMq/IyMPXCSLw7uPBviki4lxrp7VMid+Zg5maFTA7L/hr0wjqg2HvStl1ThaVgs+gtP7r88BA8C6uVKxqz+T35SPMuO5EfRXVppuKk4kgC8AaeMXiT/mKN+G4Ywhq7BhtDfFWBT8r/CONWcr4ukcR4WI9SLo6egABJWNvBNzS87mGaYxNIbh9IJRz8IWt/bUbEM01IHxYDLlcKyF+F5XGcbgtolO9BH0SqJiCSuZf0p2Q3i6JbQPqZQABljhQuTzWBrfh+e/tKwo4woE3I7m7ypwgixDiYu5M7NmE1yRlK/1pvqZDKEQQuwRNeF2bB9Feig95jNhyOl8m4AACOQWt3zZAgFON5iebfkfbGR2j0ct0aYCfZgDLS3Tbs+Zb65tN5otY/JB+276JBsV+neyerpYjEv40aw+myj/9hwACkUWUVQ+fiU3PbQI35LLv1bVEUxMnjNsNnVAx2Zcx3trE/c05ODKWLmcgZC7kYR1TXEUgJDEyR7aRx6lh0fYTNMBC5XgNXIgV0QMepMo+rqEO8GSLje0KgiBuESUwerfb0+gbt3HMiCO+4qnGQLHR9H2e0ENo6i+CVd5ArNbUJOZK7IBDMiWbmpGwiTqPKxRDoRjpcDSCVxBU38JkQhA28mJ/swlFisffm8P9mUjFXm75bWqjxWquhLotlD4wHD8n6WKt8YBDThD1TsiQtGY7PpCV1fXc8JGtnxatZAqBiflM9Byo2hEHpVGHoaB2xC+b5ZPnU7SQb/5yXRwxuNGnB07+8P5rUcBDnu+FszGdOsFKLot3vZj8m6dznXAbxi8VHBjf+DZg0b4VUeAEJO5y4iAHEJf+vzXyaHDuw6uaZy/nE6Cmh2G2WoBDxHFcHxwEmPPImzO3qd/ccUiLE8WLVD7awpYQzuSRhbPbBfluVx7ayBD4tzojqQmO0ST+2FWVjZLYIc2o8a6cwsBEfMoHr6CDGnRx/JYvffw1TCQN4bc736cjPvrto++VciZNXE7/EEWyM8oFiDUDX58COwoZacwkE9ujwlPX9MutOEBEqVTBk6s5r136OH6OXNi7SmCJiMwiN+cNghtIKvXt+dvONA56YZrzsTu2N0skC9xgykjGyxGQBmKTI18I/3r9cMAAAAAAORtswQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEXKpMi7kC0wYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAVG9rZW5CcmlkZ2UCAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXHw==")?; let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), &data)?; vaa_archive_add(deps.storage, vaa.hash.as_slice())?; Ok(Response::new()) } /// Migrate all wrapped assets to a new code id. /// This function should be called in [`migrate`]. /// TODO: according to the comments in [`migrate`] above, this approach probably /// doesn't scale above a certain number of wrapped assets, so need to rethink it. #[allow(dead_code)] fn migrate_wrapped_assets(deps: DepsMut, _env: Env, new_code_id: u64) -> StdResult> { let bucket = wrapped_asset_address(deps.storage); // Produce a migrate message for each wrapped asset. let mut messages = vec![]; for item in bucket.range(None, None, Order::Ascending) { let contract_address = item?.0; messages.push(CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: deps .api .addr_humanize(&contract_address.into())? .to_string(), new_code_id, msg: to_binary(&MigrateMsg {})?, })); } // Update config so future wrapped assets will be deployed with new code id let mut c = config(deps.storage).load()?; c.wrapped_asset_code_id = new_code_id; config(deps.storage).save(&c)?; Ok(messages) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> StdResult { // Save general wormhole info let state = ConfigInfo { gov_chain: msg.gov_chain, gov_address: msg.gov_address.into(), wormhole_contract: msg.wormhole_contract, wrapped_asset_code_id: msg.wrapped_asset_code_id, }; config(deps.storage).save(&state)?; Ok(Response::default()) } // When CW20 transfers complete, we need to verify the actual amount that is being transferred out // of the bridge. This is to handle fee tokens where the amount expected to be transferred may be // less due to burns, fees, etc. #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult { let cfg = config_read(deps.storage).load()?; let state = wrapped_transfer_tmp(deps.storage).load()?; // NOTE: Reentrancy protection. See note in `handle_initiate_transfer_token` // for why this is necessary. wrapped_transfer_tmp(deps.storage).remove(); let token_bridge_message = TokenBridgeMessage::deserialize(&state.message)?; let (mut transfer_info, transfer_type) = match token_bridge_message.action { Action::TRANSFER => { let info = TransferInfo::deserialize(&token_bridge_message.payload)?; Ok((info, TransferType::WithoutPayload)) } Action::TRANSFER_WITH_PAYLOAD => { let info = TransferWithPayloadInfo::deserialize(&token_bridge_message.payload)?; Ok(( info.as_transfer_info(), TransferType::WithPayload { // put both the payload and sender_address into the payload // field here (which we can do, since [`TransferType`] is // parametric) payload: (info.payload, info.sender_address), }, )) } _ => Err(StdError::generic_err("Unreachable")), }?; // Fetch CW20 Balance post-transfer. let new_balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: state.token_address.clone(), msg: to_binary(&TokenQuery::Balance { address: env.contract.address.to_string(), })?, }))?; // Actual amount should be the difference in balance of the CW20 account in question to account // for fee tokens. let multiplier = Uint128::from_str(&state.multiplier)?; let real_amount = new_balance.balance - Uint128::from_str(&state.previous_balance)?; let real_amount = real_amount / multiplier; // If the fee is too large the user would receive nothing. if transfer_info.fee.1 > real_amount.u128() { return Err(StdError::generic_err("fee greater than sent amount")); } // Update Wormhole message to correct amount. transfer_info.amount.1 = real_amount.u128(); let token_bridge_message = match transfer_type { TransferType::WithoutPayload => TokenBridgeMessage { action: Action::TRANSFER, payload: transfer_info.serialize(), }, TransferType::WithPayload { payload } => TokenBridgeMessage { action: Action::TRANSFER_WITH_PAYLOAD, payload: TransferWithPayloadInfo { amount: transfer_info.amount, token_address: transfer_info.token_address, token_chain: transfer_info.token_chain, recipient: transfer_info.recipient, recipient_chain: transfer_info.recipient_chain, sender_address: payload.1, payload: payload.0, } .serialize(), }, }; // Post Wormhole Message let message = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: cfg.wormhole_contract, funds: vec![], msg: to_binary(&WormholeExecuteMsg::PostMessage { message: Binary::from(token_bridge_message.serialize()), nonce: state.nonce, })?, }); send_native(deps.storage, &state.token_canonical, real_amount)?; Ok(Response::default() .add_message(message) .add_attribute("action", "reply_handler")) } pub fn coins_after_tax(deps: DepsMut, coins: Vec) -> StdResult> { let mut res = vec![]; for coin in coins { let asset = Asset { amount: coin.amount, info: AssetInfo::NativeToken { denom: coin.denom.clone(), }, }; res.push(deduct_tax(&asset, &deps.querier)?); } Ok(res) } fn parse_vaa(deps: Deps, block_time: u64, data: &Binary) -> StdResult { let cfg = config_read(deps.storage).load()?; let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: cfg.wormhole_contract, msg: to_binary(&WormholeQueryMsg::VerifyVAA { vaa: data.clone(), block_time, })?, }))?; Ok(vaa) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { match msg { ExecuteMsg::RegisterAssetHook { asset_id } => { handle_register_asset(deps, env, info, asset_id.as_slice()) } ExecuteMsg::InitiateTransfer { asset, recipient_chain, recipient, fee, nonce, } => handle_initiate_transfer( deps, env, info, asset, recipient_chain, recipient.to_array()?, fee, TransferType::WithoutPayload, nonce, ), ExecuteMsg::InitiateTransferWithPayload { asset, recipient_chain, recipient, fee, payload, nonce, } => handle_initiate_transfer( deps, env, info, asset, recipient_chain, recipient.to_array()?, fee, TransferType::WithPayload { payload: payload.into(), }, nonce, ), ExecuteMsg::DepositTokens {} => deposit_tokens(deps, env, info), ExecuteMsg::WithdrawTokens { asset } => withdraw_tokens(deps, env, info, asset), ExecuteMsg::SubmitVaa { data } => submit_vaa(deps, env, info, &data), ExecuteMsg::CreateAssetMeta { asset_info, nonce } => { handle_create_asset_meta(deps, env, info, asset_info, nonce) } ExecuteMsg::CompleteTransferWithPayload { data, relayer } => { handle_complete_transfer_with_payload(deps, env, info, &data, &relayer) } } } fn deposit_tokens(deps: DepsMut, _env: Env, info: MessageInfo) -> StdResult { for coin in info.funds { let deposit_key = format!("{}:{}", info.sender, coin.denom); bridge_deposit(deps.storage).update( deposit_key.as_bytes(), |amount: Option| -> StdResult { Ok(amount.unwrap_or(Uint128::new(0)) + coin.amount) }, )?; } Ok(Response::new().add_attribute("action", "deposit_tokens")) } fn withdraw_tokens( deps: DepsMut, _env: Env, info: MessageInfo, data: AssetInfo, ) -> StdResult { let mut messages: Vec = vec![]; if let AssetInfo::NativeToken { denom } = data { let deposit_key = format!("{}:{}", info.sender, denom); let mut deposited_amount: u128 = 0; bridge_deposit(deps.storage).update( deposit_key.as_bytes(), |current: Option| match current { Some(v) => { deposited_amount = v.u128(); Ok(Uint128::new(0)) } None => Err(StdError::generic_err("no deposit found to withdraw")), }, )?; messages.push(CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), amount: coins_after_tax(deps, vec![coin(deposited_amount, &denom)])?, })); } Ok(Response::new() .add_messages(messages) .add_attribute("action", "withdraw_tokens")) } /// Handle wrapped asset registration messages fn handle_register_asset( deps: DepsMut, _env: Env, info: MessageInfo, asset_id: &[u8], ) -> StdResult { let mut bucket = wrapped_asset(deps.storage); let result = bucket.load(asset_id); let result = result.map_err(|_| ContractError::RegistrationForbidden.std())?; if result != WRAPPED_ASSET_UPDATING { return ContractError::AssetAlreadyRegistered.std_err(); } bucket.save(asset_id, &info.sender.to_string())?; let contract_address: CanonicalAddr = deps.api.addr_canonicalize(info.sender.as_str())?; wrapped_asset_address(deps.storage).save(contract_address.as_slice(), &asset_id.to_vec())?; Ok(Response::new() .add_attribute("action", "register_asset") .add_attribute("asset_id", format!("{:?}", asset_id)) .add_attribute("contract_addr", info.sender)) } fn handle_attest_meta( deps: DepsMut, env: Env, emitter_chain: u16, emitter_address: Vec, sequence: u64, data: &Vec, ) -> StdResult { let meta = AssetMeta::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::generic_err("invalid emitter")); } if CHAIN_ID == meta.token_chain { return Err(StdError::generic_err( "this asset is native to this chain and should not be attested", )); } let cfg = config_read(deps.storage).load()?; let asset_id = build_asset_id(meta.token_chain, meta.token_address.as_slice()); // If a CW20 wrapped already exists and this message has a newer sequence ID // we allow updating the metadata. If not, we create a brand new token. let message = if let Ok(contract) = wrapped_asset_read(deps.storage).load(&asset_id) { // Prevent anyone from re-attesting with old VAAs. if sequence <= wrapped_asset_seq_read(deps.storage).load(&asset_id)? { return Err(StdError::generic_err( "this asset has already been attested", )); } CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: contract, msg: to_binary(&WrappedMsg::UpdateMetadata { name: get_string_from_32(&meta.name), symbol: get_string_from_32(&meta.symbol), })?, funds: vec![], }) } else { wrapped_asset(deps.storage).save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?; CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.clone().into_string()), code_id: cfg.wrapped_asset_code_id, msg: to_binary(&WrappedInit { name: get_string_from_32(&meta.name), symbol: get_string_from_32(&meta.symbol), asset_chain: meta.token_chain, asset_address: meta.token_address.to_vec().into(), decimals: min(meta.decimals, 8u8), mint: None, init_hook: Some(InitHook { contract_addr: env.contract.address.to_string(), msg: to_binary(&ExecuteMsg::RegisterAssetHook { asset_id: asset_id.to_vec().into(), })?, }), })?, funds: vec![], label: String::new(), }) }; wrapped_asset_seq(deps.storage).save(&asset_id, &sequence)?; Ok(Response::new().add_message(message)) } fn handle_create_asset_meta( deps: DepsMut, env: Env, info: MessageInfo, asset_info: AssetInfo, nonce: u32, ) -> StdResult { match asset_info { AssetInfo::Token { contract_addr } => { handle_create_asset_meta_token(deps, env, info, contract_addr, nonce) } AssetInfo::NativeToken { ref denom } => { handle_create_asset_meta_native_token(deps, env, info, denom.clone(), nonce) } } } fn handle_create_asset_meta_token( deps: DepsMut, env: Env, info: MessageInfo, asset_address: HumanAddr, nonce: u32, ) -> StdResult { let cfg = config_read(deps.storage).load()?; let request = QueryRequest::Wasm(WasmQuery::Smart { contract_addr: asset_address.clone(), msg: to_binary(&TokenQuery::TokenInfo {})?, }); let asset_canonical = deps.api.addr_canonicalize(&asset_address)?; let token_info: TokenInfoResponse = deps.querier.query(&request)?; let meta: AssetMeta = AssetMeta { token_chain: CHAIN_ID, token_address: extend_address_to_32(&asset_canonical), decimals: token_info.decimals, symbol: extend_string_to_32(&token_info.symbol), name: extend_string_to_32(&token_info.name), }; let token_bridge_message = TokenBridgeMessage { action: Action::ATTEST_META, payload: meta.serialize().to_vec(), }; Ok(Response::new() .add_message(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: cfg.wormhole_contract, msg: to_binary(&WormholeExecuteMsg::PostMessage { message: Binary::from(token_bridge_message.serialize()), nonce, })?, // forward coins sent to this message funds: coins_after_tax(deps, info.funds)?, })) .add_attribute("meta.token_chain", CHAIN_ID.to_string()) .add_attribute("meta.token", asset_address) .add_attribute("meta.nonce", nonce.to_string()) .add_attribute("meta.block_time", env.block.time.seconds().to_string())) } fn handle_create_asset_meta_native_token( deps: DepsMut, env: Env, info: MessageInfo, 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(Response::new() .add_message(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: cfg.wormhole_contract, msg: to_binary(&WormholeExecuteMsg::PostMessage { message: Binary::from(token_bridge_message.serialize()), nonce, })?, // forward coins sent to this message funds: coins_after_tax(deps, info.funds)?, })) .add_attribute("meta.token_chain", CHAIN_ID.to_string()) .add_attribute("meta.symbol", symbol) .add_attribute("meta.asset_id", hex::encode(asset_id)) .add_attribute("meta.nonce", nonce.to_string()) .add_attribute("meta.block_time", env.block.time.seconds().to_string())) } fn handle_complete_transfer_with_payload( deps: DepsMut, env: Env, info: MessageInfo, data: &Binary, relayer_address: &HumanAddr, ) -> StdResult { let state = config_read(deps.storage).load()?; let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), data)?; let data = vaa.payload; if vaa_archive_check(deps.storage, vaa.hash.as_slice()) { return ContractError::VaaAlreadyExecuted.std_err(); } vaa_archive_add(deps.storage, vaa.hash.as_slice())?; // check if vaa is from governance if is_governance_emitter(&state, vaa.emitter_chain, &vaa.emitter_address) { return ContractError::InvalidVAAAction.std_err(); } let message = TokenBridgeMessage::deserialize(&data)?; match message.action { Action::TRANSFER_WITH_PAYLOAD => handle_complete_transfer( deps, env, info, vaa.emitter_chain, vaa.emitter_address, TransferType::WithPayload { payload: () }, &message.payload, relayer_address, ), _ => ContractError::InvalidVAAAction.std_err(), } } fn submit_vaa(deps: DepsMut, env: Env, info: MessageInfo, data: &Binary) -> StdResult { let state = config_read(deps.storage).load()?; let vaa = parse_vaa(deps.as_ref(), env.block.time.seconds(), data)?; let data = vaa.payload; if vaa_archive_check(deps.storage, vaa.hash.as_slice()) { return ContractError::VaaAlreadyExecuted.std_err(); } vaa_archive_add(deps.storage, vaa.hash.as_slice())?; // check if vaa is from governance if is_governance_emitter(&state, vaa.emitter_chain, &vaa.emitter_address) { return handle_governance_payload(deps, env, &data); } let message = TokenBridgeMessage::deserialize(&data)?; match message.action { Action::TRANSFER => { let sender = info.sender.to_string(); handle_complete_transfer( deps, env, info, vaa.emitter_chain, vaa.emitter_address, TransferType::WithoutPayload, &message.payload, &sender, ) } Action::ATTEST_META => handle_attest_meta( deps, env, vaa.emitter_chain, vaa.emitter_address, vaa.sequence, &message.payload, ), _ => ContractError::InvalidVAAAction.std_err(), } } fn handle_governance_payload(deps: DepsMut, env: Env, data: &[u8]) -> StdResult { let gov_packet = GovernancePacket::deserialize(data)?; let module = get_string_from_32(&gov_packet.module); if module != "TokenBridge" { return Err(StdError::generic_err("this is not a valid module")); } if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID { return Err(StdError::generic_err( "the governance VAA is for another chain", )); } match gov_packet.action { 1u8 => handle_register_chain(deps, env, &gov_packet.payload), 2u8 => handle_upgrade_contract(deps, env, &gov_packet.payload), _ => ContractError::InvalidVAAAction.std_err(), } } fn handle_upgrade_contract(_deps: DepsMut, env: Env, data: &Vec) -> StdResult { let UpgradeContract { new_contract } = UpgradeContract::deserialize(data)?; Ok(Response::new() .add_message(CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: env.contract.address.to_string(), new_code_id: new_contract, msg: to_binary(&MigrateMsg {})?, })) .add_attribute("action", "contract_upgrade")) } fn handle_register_chain(deps: DepsMut, _env: Env, data: &Vec) -> StdResult { let RegisterChain { chain_id, chain_address, } = RegisterChain::deserialize(data)?; let existing = bridge_contracts_read(deps.storage).load(&chain_id.to_be_bytes()); if existing.is_ok() { return Err(StdError::generic_err( "bridge contract already exists for this chain", )); } let mut bucket = bridge_contracts(deps.storage); bucket.save(&chain_id.to_be_bytes(), &chain_address)?; Ok(Response::new() .add_attribute("chain_id", chain_id.to_string()) .add_attribute("chain_address", hex::encode(chain_address))) } #[allow(clippy::too_many_arguments)] fn handle_complete_transfer( deps: DepsMut, env: Env, info: MessageInfo, emitter_chain: u16, emitter_address: Vec, transfer_type: TransferType<()>, data: &Vec, relayer_address: &HumanAddr, ) -> StdResult { let transfer_info = TransferInfo::deserialize(data)?; let marker_byte = transfer_info.token_address.as_slice()[0]; if transfer_info.token_chain == CHAIN_ID { match marker_byte { 1 => handle_complete_transfer_token_native( deps, env, info, emitter_chain, emitter_address, transfer_type, data, relayer_address, ), 0 => handle_complete_transfer_token( deps, env, info, emitter_chain, emitter_address, transfer_type, data, relayer_address, ), b => Err(StdError::generic_err(format!("Unknown marker byte: {}", b))), } } else { handle_complete_transfer_token( deps, env, info, emitter_chain, emitter_address, transfer_type, data, relayer_address, ) } } #[allow(clippy::too_many_arguments)] fn handle_complete_transfer_token( deps: DepsMut, _env: Env, info: MessageInfo, emitter_chain: u16, emitter_address: Vec, transfer_type: TransferType<()>, data: &Vec, relayer_address: &HumanAddr, ) -> StdResult { let transfer_info = match transfer_type { TransferType::WithoutPayload => TransferInfo::deserialize(data)?, TransferType::WithPayload { payload: _ } => { TransferWithPayloadInfo::deserialize(data)?.as_transfer_info() } }; 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::generic_err("invalid emitter")); } 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 recipient = deps.api.addr_humanize(&target_address)?; if let TransferType::WithPayload { payload: _ } = transfer_type { if recipient != info.sender { return Err(StdError::generic_err( "transfers with payload can only be redeemed by the recipient", )); } }; let (not_supported_amount, mut amount) = transfer_info.amount; let (not_supported_fee, mut 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(); } if token_chain != CHAIN_ID { let asset_address = transfer_info.token_address; let asset_id = build_asset_id(token_chain, &asset_address); // Check if this asset is already deployed let contract_addr = wrapped_asset_read(deps.storage).load(&asset_id).ok(); if let Some(contract_addr) = contract_addr { // Asset already deployed, just mint let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: contract_addr.clone(), msg: to_binary(&WrappedMsg::Mint { recipient: recipient.to_string(), amount: Uint128::from(amount), })?, funds: vec![], })]; if fee != 0 { messages.push(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: contract_addr.clone(), msg: to_binary(&WrappedMsg::Mint { recipient: relayer_address.to_string(), amount: Uint128::from(fee), })?, funds: vec![], })) } Ok(Response::new() .add_messages(messages) .add_attribute("action", "complete_transfer_wrapped") .add_attribute("contract", contract_addr) .add_attribute("recipient", recipient) .add_attribute("amount", amount.to_string()) .add_attribute("relayer", relayer_address) .add_attribute("fee", fee.to_string())) } else { Err(StdError::generic_err("Wrapped asset not deployed. To deploy, invoke CreateWrapped with the associated AssetMeta")) } } else { let token_address = transfer_info.token_address.as_slice().get_address(0); let contract_addr = deps.api.addr_humanize(&token_address)?; // note -- here the amount is the amount the recipient will receive; // amount + fee is the total sent receive_native(deps.storage, &token_address, Uint128::new(amount + fee))?; // undo normalization to 8 decimals let token_info: TokenInfoResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: contract_addr.to_string(), msg: to_binary(&TokenQuery::TokenInfo {})?, }))?; let decimals = token_info.decimals; let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32); amount = amount.checked_mul(multiplier).unwrap(); fee = fee.checked_mul(multiplier).unwrap(); let mut messages = vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: contract_addr.to_string(), msg: to_binary(&TokenMsg::Transfer { recipient: recipient.to_string(), amount: Uint128::from(amount), })?, funds: vec![], })]; if fee != 0 { messages.push(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: contract_addr.to_string(), msg: to_binary(&TokenMsg::Transfer { recipient: relayer_address.to_string(), amount: Uint128::from(fee), })?, funds: vec![], })) } Ok(Response::new() .add_messages(messages) .add_attribute("action", "complete_transfer_native") .add_attribute("recipient", recipient) .add_attribute("contract", contract_addr) .add_attribute("amount", amount.to_string()) .add_attribute("relayer", relayer_address) .add_attribute("fee", fee.to_string())) } } #[allow(clippy::too_many_arguments)] fn handle_complete_transfer_token_native( mut deps: DepsMut, _env: Env, info: MessageInfo, emitter_chain: u16, emitter_address: Vec, transfer_type: TransferType<()>, data: &Vec, relayer_address: &HumanAddr, ) -> StdResult { let transfer_info = match transfer_type { TransferType::WithoutPayload => TransferInfo::deserialize(data)?, TransferType::WithPayload { payload: () } => { TransferWithPayloadInfo::deserialize(data)?.as_transfer_info() } }; 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::generic_err("invalid emitter")); } 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 recipient = deps.api.addr_humanize(&target_address)?; if let TransferType::WithPayload { payload: _ } = transfer_type { if recipient != info.sender { return Err(StdError::generic_err( "transfers with payload can only be redeemed by the recipient", )); } }; 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; 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 token_address = (&*token_address).get_address(0); receive_native(deps.storage, &token_address, Uint128::new(amount + fee))?; let mut messages = vec![CosmosMsg::Bank(BankMsg::Send { to_address: recipient.to_string(), amount: coins_after_tax(deps.branch(), vec![coin(amount, &denom)])?, })]; if fee != 0 { messages.push(CosmosMsg::Bank(BankMsg::Send { to_address: relayer_address.to_string(), amount: coins_after_tax(deps, vec![coin(fee, &denom)])?, })); } Ok(Response::new() .add_messages(messages) .add_attribute("action", "complete_transfer_terra_native") .add_attribute("recipient", recipient) .add_attribute("denom", denom) .add_attribute("amount", amount.to_string()) .add_attribute("relayer", relayer_address) .add_attribute("fee", fee.to_string())) } #[allow(clippy::too_many_arguments)] fn handle_initiate_transfer( deps: DepsMut, env: Env, info: MessageInfo, asset: Asset, recipient_chain: u16, recipient: [u8; 32], fee: Uint128, transfer_type: TransferType>, nonce: u32, ) -> StdResult { match asset.info { AssetInfo::Token { contract_addr } => handle_initiate_transfer_token( deps, env, info, contract_addr, asset.amount, recipient_chain, recipient, fee, transfer_type, nonce, ), AssetInfo::NativeToken { ref denom } => handle_initiate_transfer_native_token( deps, env, info, denom.clone(), asset.amount, recipient_chain, recipient, fee, transfer_type, nonce, ), } } #[allow(clippy::too_many_arguments)] fn handle_initiate_transfer_token( mut deps: DepsMut, env: Env, info: MessageInfo, asset: HumanAddr, mut amount: Uint128, recipient_chain: u16, recipient: [u8; 32], mut fee: Uint128, transfer_type: TransferType>, nonce: u32, ) -> StdResult { if recipient_chain == CHAIN_ID { return ContractError::SameSourceAndTarget.std_err(); } if amount.is_zero() { return ContractError::AmountTooLow.std_err(); } let asset_chain: u16; let asset_address: [u8; 32]; let cfg: ConfigInfo = config_read(deps.storage).load()?; let asset_canonical: CanonicalAddr = deps.api.addr_canonicalize(&asset)?; let mut messages: Vec = vec![]; let mut submessages: Vec = vec![]; // we'll only need this for payload 3 transfers let sender_address = deps.api.addr_canonicalize(info.sender.as_ref())?; let sender_address = extend_address_to_32_array(&sender_address); match wrapped_asset_address_read(deps.storage).load(asset_canonical.as_slice()) { Ok(_) => { // If the fee is too large the user will receive nothing. if fee > amount { return Err(StdError::generic_err("fee greater than sent amount")); } // This is a deployed wrapped asset, burn it messages.push(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: asset.clone(), msg: to_binary(&WrappedMsg::Burn { account: info.sender.to_string(), amount, })?, funds: vec![], })); let request = QueryRequest::::Wasm(WasmQuery::Smart { contract_addr: asset, msg: to_binary(&WrappedQuery::WrappedAssetInfo {})?, }); let wrapped_token_info: WrappedAssetInfoResponse = deps.querier.custom_query(&request)?; asset_chain = wrapped_token_info.asset_chain; asset_address = wrapped_token_info.asset_address.to_array()?; let token_bridge_message: TokenBridgeMessage = match transfer_type { TransferType::WithoutPayload => { let transfer_info = TransferInfo { token_chain: asset_chain, token_address: asset_address, amount: (0, amount.u128()), recipient_chain, recipient, fee: (0, fee.u128()), }; TokenBridgeMessage { action: Action::TRANSFER, payload: transfer_info.serialize(), } } TransferType::WithPayload { payload } => { let transfer_info = TransferWithPayloadInfo { token_chain: asset_chain, token_address: asset_address, amount: (0, amount.u128()), recipient_chain, recipient, sender_address, payload, }; TokenBridgeMessage { action: Action::TRANSFER_WITH_PAYLOAD, payload: transfer_info.serialize(), } } }; messages.push(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: cfg.wormhole_contract, msg: to_binary(&WormholeExecuteMsg::PostMessage { message: Binary::from(token_bridge_message.serialize()), nonce, })?, // forward coins sent to this message funds: coins_after_tax(deps.branch(), info.funds)?, })); } Err(_) => { // normalize amount to 8 decimals when it sent over the wormhole let token_info: TokenInfoResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: asset.clone(), msg: to_binary(&TokenQuery::TokenInfo {})?, }))?; let decimals = token_info.decimals; let multiplier = 10u128.pow((max(decimals, 8u8) - 8u8) as u32); // chop off dust amount = Uint128::new( amount .u128() .checked_sub(amount.u128().checked_rem(multiplier).unwrap()) .unwrap(), ); fee = Uint128::new( fee.u128() .checked_sub(fee.u128().checked_rem(multiplier).unwrap()) .unwrap(), ); // This is a regular asset, transfer its balance submessages.push(SubMsg::reply_on_success( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: asset.clone(), msg: to_binary(&TokenMsg::TransferFrom { owner: info.sender.to_string(), recipient: env.contract.address.to_string(), amount, })?, funds: vec![], }), 1, )); asset_address = extend_address_to_32_array(&asset_canonical); asset_chain = CHAIN_ID; // convert to normalized amounts before recording & posting vaa amount = Uint128::new(amount.u128().checked_div(multiplier).unwrap()); fee = Uint128::new(fee.u128().checked_div(multiplier).unwrap()); // Fetch current CW20 Balance pre-transfer. let balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: asset.to_string(), msg: to_binary(&TokenQuery::Balance { address: env.contract.address.to_string(), })?, }))?; // NOTE: Reentrancy protection. It is crucial that there's no // ongoing transfer in progress here, otherwise we would override // its state. This could happen if the asset's TransferFrom handler // sends us an InitiateTransfer message, which would be executed // before the reply handler due the the depth-first semantics of // message execution. A simple protection mechanism is to require // that there's no execution in progress. The reply handler takes // care of clearing out this temporary storage when done. assert!(wrapped_transfer_tmp(deps.storage).load().is_err()); let token_bridge_message: TokenBridgeMessage = match transfer_type { TransferType::WithoutPayload => { let transfer_info = TransferInfo { amount: (0, amount.u128()), token_address: asset_address, token_chain: asset_chain, recipient, recipient_chain, fee: (0, fee.u128()), }; TokenBridgeMessage { action: Action::TRANSFER, payload: transfer_info.serialize(), } } TransferType::WithPayload { payload } => { let transfer_info = TransferWithPayloadInfo { amount: (0, amount.u128()), token_address: asset_address, token_chain: asset_chain, recipient, recipient_chain, sender_address, payload, }; TokenBridgeMessage { action: Action::TRANSFER_WITH_PAYLOAD, payload: transfer_info.serialize(), } } }; // Wrap up state to be captured by the submessage reply. wrapped_transfer_tmp(deps.storage).save(&TransferState { previous_balance: balance.balance.to_string(), account: info.sender.to_string(), token_address: asset, token_canonical: asset_canonical.clone(), message: token_bridge_message.serialize(), multiplier: Uint128::new(multiplier).to_string(), nonce, })?; } }; Ok(Response::new() .add_messages(messages) .add_submessages(submessages) .add_attribute("transfer.token_chain", asset_chain.to_string()) .add_attribute("transfer.token", hex::encode(asset_address)) .add_attribute( "transfer.sender", hex::encode(extend_address_to_32( &deps.api.addr_canonicalize(info.sender.as_str())?, )), ) .add_attribute("transfer.recipient_chain", recipient_chain.to_string()) .add_attribute("transfer.recipient", hex::encode(recipient)) .add_attribute("transfer.amount", amount.to_string()) .add_attribute("transfer.nonce", nonce.to_string()) .add_attribute("transfer.block_time", env.block.time.seconds().to_string())) } /// 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" } #[allow(clippy::too_many_arguments)] fn handle_initiate_transfer_native_token( deps: DepsMut, env: Env, info: MessageInfo, denom: String, amount: Uint128, recipient_chain: u16, recipient: [u8; 32], fee: Uint128, transfer_type: TransferType>, 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!("{}:{}", info.sender, denom); bridge_deposit(deps.storage).update(deposit_key.as_bytes(), |current: Option| { match current { Some(v) => Ok(v.checked_sub(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 asset_address: CanonicalAddr = build_native_id(&denom).into(); send_native(deps.storage, &asset_address, amount)?; // Mark the first byte of the address to distinguish it as native. let mut asset_address = extend_address_to_32_array(&asset_address); asset_address[0] = 1; let token_bridge_message: TokenBridgeMessage = match transfer_type { TransferType::WithoutPayload => { let transfer_info = TransferInfo { amount: (0, amount.u128()), token_address: asset_address, token_chain: asset_chain, recipient, recipient_chain, fee: (0, fee.u128()), }; TokenBridgeMessage { action: Action::TRANSFER, payload: transfer_info.serialize(), } } TransferType::WithPayload { payload } => { let sender_address = deps.api.addr_canonicalize(info.sender.as_ref())?; let sender_address = extend_address_to_32_array(&sender_address); let transfer_info = TransferWithPayloadInfo { amount: (0, amount.u128()), token_address: asset_address, token_chain: asset_chain, recipient, recipient_chain, sender_address, payload, }; TokenBridgeMessage { action: Action::TRANSFER_WITH_PAYLOAD, payload: transfer_info.serialize(), } } }; let sender = deps.api.addr_canonicalize(info.sender.as_str())?; messages.push(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: cfg.wormhole_contract, msg: to_binary(&WormholeExecuteMsg::PostMessage { message: Binary::from(token_bridge_message.serialize()), nonce, })?, funds: coins_after_tax(deps, info.funds)?, })); Ok(Response::new() .add_messages(messages) .add_attribute("transfer.token_chain", asset_chain.to_string()) .add_attribute("transfer.token", hex::encode(asset_address)) .add_attribute( "transfer.sender", hex::encode(extend_address_to_32(&sender)), ) .add_attribute("transfer.recipient_chain", recipient_chain.to_string()) .add_attribute("transfer.recipient", hex::encode(recipient)) .add_attribute("transfer.amount", amount.to_string()) .add_attribute("transfer.nonce", nonce.to_string()) .add_attribute("transfer.block_time", env.block.time.seconds().to_string())) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::WrappedRegistry { chain, address } => { to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?) } QueryMsg::TransferInfo { vaa } => to_binary(&query_transfer_info(deps, env, &vaa)?), } } pub fn query_wrapped_registry( deps: Deps, chain: u16, address: &[u8], ) -> StdResult { let asset_id = build_asset_id(chain, address); // Check if this asset is already deployed match wrapped_asset_read(deps.storage).load(&asset_id) { Ok(address) => Ok(WrappedRegistryResponse { address }), Err(_) => ContractError::AssetNotFound.std_err(), } } fn query_transfer_info(deps: Deps, env: Env, vaa: &Binary) -> StdResult { let cfg = config_read(deps.storage).load()?; let parsed = parse_vaa(deps, env.block.time.seconds(), vaa)?; let data = parsed.payload; // check if vaa is from governance if is_governance_emitter(&cfg, parsed.emitter_chain, &parsed.emitter_address) { return ContractError::InvalidVAAAction.std_err(); } let message = TokenBridgeMessage::deserialize(&data)?; match message.action { Action::ATTEST_META => ContractError::InvalidVAAAction.std_err(), Action::TRANSFER => { let core = TransferInfo::deserialize(&message.payload)?; Ok(TransferInfoResponse { amount: core.amount.1.into(), token_address: core.token_address, token_chain: core.token_chain, recipient: core.recipient, recipient_chain: core.recipient_chain, fee: core.fee.1.into(), payload: vec![], }) } Action::TRANSFER_WITH_PAYLOAD => { let info = TransferWithPayloadInfo::deserialize(&message.payload)?; let core = info.as_transfer_info(); Ok(TransferInfoResponse { amount: core.amount.1.into(), token_address: core.token_address, token_chain: core.token_chain, recipient: core.recipient, recipient_chain: core.recipient_chain, fee: core.fee.1.into(), payload: info.payload, }) } other => Err(StdError::generic_err(format!("Invalid action: {}", other))), } } pub fn build_asset_id(chain: u16, address: &[u8]) -> Vec { let chain = &chain.to_be_bytes(); let mut asset_id = Vec::with_capacity(chain.len() + address.len()); asset_id.extend_from_slice(chain); asset_id.extend_from_slice(address); let mut hasher = Keccak256::new(); hasher.update(asset_id); hasher.finalize().to_vec() } // Produce a 20 byte asset "address" from a native terra denom. pub fn build_native_id(denom: &str) -> Vec { let n = denom.len(); assert!(n < 20); let mut asset_address = Vec::with_capacity(20); asset_address.resize(20 - n, 0u8); asset_address.extend_from_slice(denom.as_bytes()); asset_address } fn is_governance_emitter(cfg: &ConfigInfo, emitter_chain: u16, emitter_address: &[u8]) -> bool { cfg.gov_chain == emitter_chain && cfg.gov_address == emitter_address } //////////////////////////////////////////////////////////////////////////////// // Tax calculation // the code below has been lifted from // https://github.com/terraswap/terraswap/blob/7cf47f5e811fe0c4643a7cd09500702c1e7f3a6b/packages/terraswap/src/asset.rs#L25-L64 // but with luna tax enabled instead of defaulting it to 0 static DECIMAL_FRACTION: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); pub fn compute_tax(asset: &Asset, querier: &QuerierWrapper) -> StdResult { let amount = asset.amount; if let AssetInfo::NativeToken { denom } = &asset.info { let terra_querier = TerraQuerier::new(querier); let tax_rate: Decimal = (terra_querier.query_tax_rate()?).rate; let tax_cap: Uint128 = (terra_querier.query_tax_cap(denom.to_string())?).cap; Ok(std::cmp::min( amount.checked_sub(amount.multiply_ratio( DECIMAL_FRACTION, DECIMAL_FRACTION * tax_rate + DECIMAL_FRACTION, ))?, tax_cap, )) } else { Ok(Uint128::zero()) } } pub fn deduct_tax(asset: &Asset, querier: &QuerierWrapper) -> StdResult { let amount = asset.amount; if let AssetInfo::NativeToken { denom } = &asset.info { Ok(Coin { denom: denom.to_string(), amount: amount.checked_sub(compute_tax(asset, querier)?)?, }) } else { Err(StdError::generic_err("cannot deduct tax from token asset")) } }