diff --git a/terra/contracts/token-bridge/src/contract.rs b/terra/contracts/token-bridge/src/contract.rs index 020fa6f2b..2e4f42de8 100644 --- a/terra/contracts/token-bridge/src/contract.rs +++ b/terra/contracts/token-bridge/src/contract.rs @@ -33,6 +33,7 @@ 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, @@ -256,9 +257,12 @@ pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult { Action::TRANSFER_WITH_PAYLOAD => { let info = TransferWithPayloadInfo::deserialize(&token_bridge_message.payload)?; Ok(( - info.transfer_info, + info.as_transfer_info(), TransferType::WithPayload { - payload: info.payload, + // 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), }, )) } @@ -296,8 +300,13 @@ pub fn reply(deps: DepsMut, env: Env, _msg: Reply) -> StdResult { TransferType::WithPayload { payload } => TokenBridgeMessage { action: Action::TRANSFER_WITH_PAYLOAD, payload: TransferWithPayloadInfo { - transfer_info, - payload, + 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(), }, @@ -843,7 +852,7 @@ fn handle_complete_transfer_token( let transfer_info = match transfer_type { TransferType::WithoutPayload => TransferInfo::deserialize(&data)?, TransferType::WithPayload { payload: _ } => { - TransferWithPayloadInfo::deserialize(&data)?.transfer_info + TransferWithPayloadInfo::deserialize(&data)?.as_transfer_info() } }; @@ -988,7 +997,7 @@ fn handle_complete_transfer_token_native( let transfer_info = match transfer_type { TransferType::WithoutPayload => TransferInfo::deserialize(&data)?, TransferType::WithPayload { payload: () } => { - TransferWithPayloadInfo::deserialize(&data)?.transfer_info + TransferWithPayloadInfo::deserialize(&data)?.as_transfer_info() } }; @@ -1122,7 +1131,7 @@ fn handle_initiate_transfer_token( } let asset_chain: u16; - let asset_address: Vec; + let asset_address: [u8; 32]; let cfg: ConfigInfo = config_read(deps.storage).load()?; let asset_canonical: CanonicalAddr = deps.api.addr_canonicalize(&asset)?; @@ -1130,6 +1139,10 @@ fn handle_initiate_transfer_token( 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.to_string())?; + 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. @@ -1153,30 +1166,38 @@ fn handle_initiate_transfer_token( let wrapped_token_info: WrappedAssetInfoResponse = deps.querier.custom_query(&request)?; asset_chain = wrapped_token_info.asset_chain; - asset_address = wrapped_token_info.asset_address.into(); - - let transfer_info = TransferInfo { - token_chain: asset_chain, - token_address: asset_address.clone(), - amount: (0, amount.u128()), - recipient_chain, - recipient: recipient.to_vec(), - fee: (0, fee.u128()), - }; + asset_address = wrapped_token_info.asset_address.to_array()?; let token_bridge_message: TokenBridgeMessage = match transfer_type { - TransferType::WithoutPayload => TokenBridgeMessage { - action: Action::TRANSFER, - payload: transfer_info.serialize(), - }, - TransferType::WithPayload { payload } => TokenBridgeMessage { - action: Action::TRANSFER_WITH_PAYLOAD, - payload: TransferWithPayloadInfo { - transfer_info, - payload, + 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(), } - .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 { @@ -1228,22 +1249,13 @@ fn handle_initiate_transfer_token( 1, )); - asset_address = extend_address_to_32(&asset_canonical); + 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()); - let transfer_info = TransferInfo { - token_chain: asset_chain, - token_address: asset_address.clone(), - amount: (0, amount.u128()), - recipient_chain, - recipient: recipient.to_vec(), - fee: (0, fee.u128()), - }; - // Fetch current CW20 Balance pre-transfer. let balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { @@ -1264,18 +1276,35 @@ fn handle_initiate_transfer_token( assert!(wrapped_transfer_tmp(deps.storage).load().is_err()); let token_bridge_message: TokenBridgeMessage = match transfer_type { - TransferType::WithoutPayload => TokenBridgeMessage { - action: Action::TRANSFER, - payload: transfer_info.serialize(), - }, - TransferType::WithPayload { payload } => TokenBridgeMessage { - action: Action::TRANSFER_WITH_PAYLOAD, - payload: TransferWithPayloadInfo { - transfer_info, - payload, + TransferType::WithoutPayload => { + let transfer_info = TransferInfo { + amount: (0, amount.u128()), + token_address: asset_address.clone(), + token_chain: asset_chain, + recipient, + recipient_chain, + fee: (0, fee.u128()), + }; + TokenBridgeMessage { + action: Action::TRANSFER, + payload: transfer_info.serialize(), } - .serialize(), - }, + } + TransferType::WithPayload { payload } => { + let transfer_info = TransferWithPayloadInfo { + amount: (0, amount.u128()), + token_address: asset_address.clone(), + 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. @@ -1353,36 +1382,46 @@ fn handle_initiate_transfer_native_token( let mut messages: Vec = vec![]; let asset_chain: u16 = CHAIN_ID; - let mut asset_address: Vec = build_native_id(&denom); + let asset_address: CanonicalAddr = build_native_id(&denom).into(); - send_native(deps.storage, &asset_address[..].into(), amount)?; + send_native(deps.storage, &asset_address, amount)?; // Mark the first byte of the address to distinguish it as native. - asset_address = extend_address_to_32(&asset_address.into()); + let mut asset_address = extend_address_to_32_array(&asset_address); 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.to_vec(), - fee: (0, fee.u128()), - }; - let token_bridge_message: TokenBridgeMessage = match transfer_type { - TransferType::WithoutPayload => TokenBridgeMessage { - action: Action::TRANSFER, - payload: transfer_info.serialize(), - }, - TransferType::WithPayload { payload } => TokenBridgeMessage { - action: Action::TRANSFER_WITH_PAYLOAD, - payload: TransferWithPayloadInfo { - transfer_info, - payload, + 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(), } - .serialize(), - }, + } + TransferType::WithPayload { payload } => { + let sender_address = deps.api.addr_canonicalize(&info.sender.to_string())?; + 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())?; @@ -1447,9 +1486,22 @@ fn query_transfer_info(deps: Deps, env: Env, vaa: &Binary) -> StdResult 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.transfer_info; + let core = info.as_transfer_info(); Ok(TransferInfoResponse { amount: core.amount.1.into(), @@ -1461,6 +1513,7 @@ fn query_transfer_info(deps: Deps, env: Env, vaa: &Binary) -> StdResult Err(StdError::generic_err(format!("Invalid action: {}", other))), } } diff --git a/terra/contracts/token-bridge/src/msg.rs b/terra/contracts/token-bridge/src/msg.rs index c08471aa5..8b70eae02 100644 --- a/terra/contracts/token-bridge/src/msg.rs +++ b/terra/contracts/token-bridge/src/msg.rs @@ -89,9 +89,9 @@ pub struct WrappedRegistryResponse { #[serde(rename_all = "snake_case")] pub struct TransferInfoResponse { pub amount: Uint128, - pub token_address: Vec, + pub token_address: [u8; 32], pub token_chain: u16, - pub recipient: Vec, + pub recipient: [u8; 32], pub recipient_chain: u16, pub fee: Uint128, pub payload: Vec, diff --git a/terra/contracts/token-bridge/src/state.rs b/terra/contracts/token-bridge/src/state.rs index 63e8b374a..bab2ab55b 100644 --- a/terra/contracts/token-bridge/src/state.rs +++ b/terra/contracts/token-bridge/src/state.rs @@ -185,9 +185,9 @@ impl TokenBridgeMessage { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct TransferInfo { pub amount: (u128, u128), - pub token_address: Vec, + pub token_address: [u8; 32], pub token_chain: u16, - pub recipient: Vec, + pub recipient: [u8; 32], pub recipient_chain: u16, pub fee: (u128, u128), } @@ -196,9 +196,9 @@ impl TransferInfo { pub fn deserialize(data: &Vec) -> StdResult { let data = data.as_slice(); let amount = data.get_u256(0); - let token_address = data.get_bytes32(32).to_vec(); + let token_address = data.get_const_bytes(32); let token_chain = data.get_u16(64); - let recipient = data.get_bytes32(66).to_vec(); + let recipient = data.get_const_bytes(66); let recipient_chain = data.get_u16(98); let fee = data.get_u256(100); @@ -215,7 +215,7 @@ impl TransferInfo { [ self.amount.0.to_be_bytes().to_vec(), self.amount.1.to_be_bytes().to_vec(), - self.token_address.clone(), + self.token_address.to_vec(), self.token_chain.to_be_bytes().to_vec(), self.recipient.to_vec(), self.recipient_chain.to_be_bytes().to_vec(), @@ -231,30 +231,72 @@ impl TransferInfo { // 64 u16 token_chain // 66 [u8; 32] recipient // 98 u16 recipient_chain -// 100 u256 fee +// 100 [u8; 32] sender_address // 132 [u8] payload #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct TransferWithPayloadInfo { - pub transfer_info: TransferInfo, + pub amount: (u128, u128), + pub token_address: [u8; 32], + pub token_chain: u16, + pub recipient: [u8; 32], + pub recipient_chain: u16, + pub sender_address: [u8; 32], pub payload: Vec, } impl TransferWithPayloadInfo { pub fn deserialize(data: &Vec) -> StdResult { - let transfer_info = TransferInfo::deserialize(data)?; + let data = data.as_slice(); + let amount = data.get_u256(0); + let token_address = data.get_const_bytes::<32>(32); + let token_chain = data.get_u16(64); + let recipient = data.get_const_bytes::<32>(66); + let recipient_chain = data.get_u16(98); + let sender_address = data.get_const_bytes::<32>(100); let payload = TransferWithPayloadInfo::get_payload(data); Ok(TransferWithPayloadInfo { - transfer_info, + amount, + token_address, + token_chain, + recipient, + recipient_chain, + sender_address, payload, }) + } + pub fn serialize(&self) -> Vec { - [self.transfer_info.serialize(), self.payload.clone()].concat() + [ + self.amount.0.to_be_bytes().to_vec(), + self.amount.1.to_be_bytes().to_vec(), + self.token_address.to_vec(), + self.token_chain.to_be_bytes().to_vec(), + self.recipient.to_vec(), + self.recipient_chain.to_be_bytes().to_vec(), + self.sender_address.to_vec(), + self.payload.clone(), + ] + .concat() } - pub fn get_payload(data: &Vec) -> Vec { - return data[132..].to_vec(); + + pub fn get_payload(data: &[u8]) -> Vec { + data[132..].to_vec() + } + + /// Convert [`TransferWithPayloadInfo`] into [`TransferInfo`] for the + /// purpose of handling them uniformly. Transfers with payload have 0 fees. + pub fn as_transfer_info(&self) -> TransferInfo { + TransferInfo { + amount: self.amount, + token_address: self.token_address.clone(), + token_chain: self.token_chain, + recipient: self.recipient, + recipient_chain: self.recipient_chain, + fee: (0, 0), + } } } diff --git a/terra/contracts/token-bridge/src/testing/tests.rs b/terra/contracts/token-bridge/src/testing/tests.rs index 84d4372ca..ed67c5785 100644 --- a/terra/contracts/token-bridge/src/testing/tests.rs +++ b/terra/contracts/token-bridge/src/testing/tests.rs @@ -105,7 +105,8 @@ fn deserialize_transfer_vaa() -> StdResult<()> { let token_address = "0100000000000000000000000000000000000000000000000000000075757364"; let token_address = hex::decode(token_address).unwrap(); assert_eq!( - info.token_address, token_address, + info.token_address.to_vec(), + token_address, "info.token_address != expected" ); @@ -117,7 +118,11 @@ fn deserialize_transfer_vaa() -> StdResult<()> { let recipient = "000000000000000000000000f7f7dde848e7450a029cd0a9bd9bdae4b5147db3"; let recipient = hex::decode(recipient).unwrap(); - assert_eq!(info.recipient, recipient, "info.recipient != expected"); + assert_eq!( + info.recipient.to_vec(), + recipient, + "info.recipient != expected" + ); let recipient_chain = 3u16; assert_eq!( @@ -133,15 +138,37 @@ fn deserialize_transfer_vaa() -> StdResult<()> { #[test] fn deserialize_transfer_with_payload_vaa() -> StdResult<()> { + +// ┌──────────────────────────────────────────────────────────────────────────────┐ +// │ Wormhole VAA v1 │ nonce: 2080370133 │ time: 0 │ +// │ guardian set #0 │ #4568529024235897313 │ consistency: 32 │ +// ├──────────────────────────────────────────────────────────────────────────────┤ +// │ Signature: │ +// │ #0: 2565e7ae10421624fd81118855acda893e752aeeef31c13fbfc417591ada... │ +// ├──────────────────────────────────────────────────────────────────────────────┤ +// │ Emitter: 11111111111111111111111111111115 (Solana) │ +// ╞══════════════════════════════════════════════════════════════════════════════╡ +// │ Token transfer with payload (aka payload 3) │ +// │ Amount: 1.0 │ +// │ Token: terra1qqqqqqqqqqqqqqqqqqqqqqqqqp6h2umyswfh6y (Terra) │ +// │ Recipient: terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh (Terra) │ +// │ From: 1399a4e782b935d2bb36b97586d3df8747b07dc66902d807eed0ae99e00ed256 │ +// ╞══════════════════════════════════════════════════════════════════════════════╡ +// │ Custom payload: │ +// │ Length: 30 (0x1e) bytes │ +// │ 0000: 41 6c 6c 20 79 6f 75 72 20 62 61 73 65 20 61 72 All your base ar│ +// │ 0010: 65 20 62 65 6c 6f 6e 67 20 74 6f 20 75 73 e belong to us │ +// └──────────────────────────────────────────────────────────────────────────────┘ + let signed_vaa = "\ - 010000000001002b0e392ebe370e718b91dcafbba21094efd8e7f1f12e28bd90\ - a178b4dfbbc708675152a3cd2edd20e8e018600026b73b6c6cbf02622903409e\ - 8b48ab7fa30ef001000000010000000100010000000000000000000000000000\ - 00000000000000000000000000000000ffff0000000000000002000300000000\ - 00000000000000000000000000000000000000000000000005f5e10001000000\ + 010000000001002565e7ae10421624fd81118855acda893e752aeeef31c13fbf\ + c417591ada039822195a1321a72cc4bac1c6031e0595f1c1361ca2a30d941a41\ + 95fad8020d43d500000000007bffedd500010000000000000000000000000000\ + 0000000000000000000000000000000000043f66acf143a481e1200300000000\ + 00000000000000000000000000000000000000000000000005f5e10000000000\ 0000000000000000000000000000000000000000000000007575736400030000\ 000000000000000000008cec800d24df11e556e708461c98122df4a2c3b10003\ - 00000000000000000000000000000000000000000000000000000000000f4240\ + 1399a4e782b935d2bb36b97586d3df8747b07dc66902d807eed0ae99e00ed256\ 416c6c20796f75722062617365206172652062656c6f6e6720746f207573"; let signed_vaa = hex::decode(signed_vaa).unwrap(); @@ -153,16 +180,16 @@ fn deserialize_transfer_with_payload_vaa() -> StdResult<()> { "message.action != expected" ); - let info_with_payload = TransferWithPayloadInfo::deserialize(&message.payload)?; - let info = info_with_payload.transfer_info; + let info = TransferWithPayloadInfo::deserialize(&message.payload)?; let amount = (0u128, 100_000_000u128); assert_eq!(info.amount, amount, "info.amount != expected"); - let token_address = "0100000000000000000000000000000000000000000000000000000075757364"; + let token_address = "0000000000000000000000000000000000000000000000000000000075757364"; let token_address = hex::decode(token_address).unwrap(); assert_eq!( - info.token_address, token_address, + info.token_address.to_vec(), + token_address, "info.token_address != expected" ); @@ -174,7 +201,19 @@ fn deserialize_transfer_with_payload_vaa() -> StdResult<()> { let recipient = "0000000000000000000000008cec800d24df11e556e708461c98122df4a2c3b1"; let recipient = hex::decode(recipient).unwrap(); - assert_eq!(info.recipient, recipient, "info.recipient != expected"); + assert_eq!( + info.recipient.to_vec(), + recipient, + "info.recipient != expected" + ); + + let sender = "1399a4e782b935d2bb36b97586d3df8747b07dc66902d807eed0ae99e00ed256"; + let sender = hex::decode(sender).unwrap(); + assert_eq!( + info.sender_address.to_vec(), + sender, + "info.sender != expected" + ); let recipient_chain = 3u16; assert_eq!( @@ -182,13 +221,11 @@ fn deserialize_transfer_with_payload_vaa() -> StdResult<()> { "info.recipient_chain != expected" ); - let fee = (0u128, 1_000_000u128); - assert_eq!(info.fee, fee, "info.fee != expected"); let transfer_payload = "All your base are belong to us"; let transfer_payload = transfer_payload.as_bytes(); assert_eq!( - info_with_payload.payload.as_slice(), + info.payload.as_slice(), transfer_payload, "info.payload != expected" ); diff --git a/terra/test/src/__tests__/bridge.ts b/terra/test/src/__tests__/bridge.ts index a0f663908..5180a3454 100644 --- a/terra/test/src/__tests__/bridge.ts +++ b/terra/test/src/__tests__/bridge.ts @@ -60,7 +60,7 @@ const contracts = new Map(); > should handle ETH deposits with payload correctly (uusd) > should handle ETH withdrawals with payload correctly (uusd) > should revert on transfer out of a total of > max(uint64) tokens - + */ describe("Bridge Tests", () => { @@ -455,7 +455,6 @@ describe("Bridge Tests", () => { const denom = "uusd"; const amount = "100000000"; // one benjamin - const relayerFee = "1000000"; // one dolla const walletAddress = wallet.key.accAddress; @@ -471,7 +470,7 @@ describe("Bridge Tests", () => { ustAddress, encodedTo, 3, - relayerFee, + "0", additionalPayload ); console.info("vaaPayload", vaaPayload); @@ -524,33 +523,13 @@ describe("Bridge Tests", () => { const receipt = await transactWithoutMemo(client, wallet, [submitVaa]); console.info("receipt txHash", receipt.txhash); - // check wallet (relayer) balance change - const walletBalanceAfter = await getNativeBalance( - client, - walletAddress, - denom - ); - const gasPaid = computeGasPaid(receipt); - const walletExpectedChange = new Int(relayerFee).sub(gasPaid); - - // due to rounding, we should expect the balances to reconcile - // within 1 unit (equivalent to 1e-6 uusd). Best-case scenario - // we end up with slightly more balance than expected - const reconciled = walletBalanceAfter - .minus(walletExpectedChange) - .minus(walletBalanceBefore); - expect( - reconciled.greaterThanOrEqualTo("0") && - reconciled.lessThanOrEqualTo("1") - ).toBeTruthy(); - // check contract balance change const contractBalanceAfter = await getNativeBalance( client, mockBridgeIntegration, denom ); - const contractExpectedChange = new Int(amount).sub(relayerFee); + const contractExpectedChange = new Int(amount); expect( contractBalanceBefore .add(contractExpectedChange)