Skip to main content

interchain_token_transfer_gmp/
lib.rs

1use std::borrow::Cow;
2
3pub use alloy_primitives;
4use alloy_primitives::U256;
5use alloy_sol_types::{sol, SolValue};
6
7/// The messages going through the Axelar Network between
8/// InterchainTokenServices need to have a consistent format to be
9/// understood properly. We chose to use abi encoding because it is easy to
10/// use in EVM chains, which are at the front and center of programmable
11/// blockchains, and because it is easy to implement in other ecosystems
12/// which tend to be more gas efficient.
13#[derive(Clone, Debug, PartialEq)]
14pub enum GMPPayload {
15    InterchainTransfer(InterchainTransfer),
16    DeployInterchainToken(DeployInterchainToken),
17    SendToHub(SendToHub),
18    ReceiveFromHub(ReceiveFromHub),
19    LinkToken(LinkToken),
20    RegisterTokenMetadata(RegisterTokenMetadata),
21}
22
23sol! {
24    /// This message has the following data encoded and should only be sent
25    /// after the proper tokens have been procured by the service. It should
26    /// result in the proper funds being transferred to the user at the
27    /// destination chain.
28    #[derive(Debug, PartialEq)]
29    #[repr(C)]
30    struct InterchainTransfer {
31        /// Will always have a value of 0
32        uint256 selector;
33        /// The interchainTokenId of the token being transferred
34        bytes32 token_id;
35        /// The address of the sender, encoded as bytes to account for different chain
36        /// architectures
37        bytes source_address;
38        /// The address of the recipient, encoded as bytes as well
39        bytes destination_address;
40        /// The amount of token being send, not accounting for decimals (1 ETH would be 1018)
41        uint256 amount;
42        /// Either empty, for just a transfer, or any data to be passed to the destination address
43        /// as a contract call
44        bytes data;
45    }
46
47    /// This message has the following data encoded and should only be sent
48    /// after the interchainTokenId has been properly generated (a user should
49    /// not be able to claim just any interchainTokenId)
50    #[derive(Debug, PartialEq)]
51    #[repr(C)]
52    struct DeployInterchainToken {
53        uint256 selector;
54        /// The interchainTokenId of the token being deployed
55        bytes32 token_id;
56        /// The name for the token
57        string name;
58        /// The symbol for the token
59        string symbol;
60        /// The decimals for the token
61        uint8 decimals;
62        /// An address on the destination chain that can mint/burn the deployed
63        /// token on the destination chain, empty for no minter
64        bytes minter;
65    }
66
67    /// This message is used to route an ITS message via the ITS Hub. The ITS Hub applies certain
68    /// security checks, and then routes it to the true destination chain. This mode is enabled if the
69    /// trusted address corresponding to the destination chain is set to the ITS Hub identifier.
70    #[derive(Debug, PartialEq)]
71    #[repr(C)]
72    struct SendToHub {
73        /// Should always have a value of 3
74        uint256 selector;
75
76        /// The true destination chain for the ITS call
77        string destination_chain;
78
79        /// The actual ITS message that's being routed through ITS Hub
80        bytes payload;
81    }
82
83    /// This message is used to receive an ITS message from the ITS Hub. The ITS Hub applies
84    /// certain security checks, and then routes it to the ITS contract. The message is accepted if the
85    /// trusted address corresponding to the original source chain is set to the ITS Hub identifier.
86    #[derive(Debug, PartialEq)]
87    #[repr(C)]
88    struct ReceiveFromHub {
89        /// Will always have a value of 4
90        uint256 selector;
91
92        /// The original source chain for the ITS call
93        string source_chain;
94
95        /// The actual ITS message that's being routed through ITS Hub
96        bytes payload;
97    }
98
99    #[derive(Debug, PartialEq)]
100    #[repr(C)]
101    struct LinkToken {
102        /// Will always have a value of 5
103        uint256 selector;
104        /// The token_id associated with the token being linked
105        bytes32 token_id;
106        /// The type of the token manager to use to send and receive tokens
107        uint256 token_manager_type;
108        /// The token address in the source chain
109        bytes source_token_address;
110        /// The token address in the destination chain
111        bytes destination_token_address;
112        /// Additional parameters to use to link the token. Currently it's just the address of the
113        /// operator
114        bytes link_params;
115    }
116
117    #[derive(Debug, PartialEq)]
118    #[repr(C)]
119    struct RegisterTokenMetadata {
120        /// Will always have a value of 6
121        uint256 selector;
122        /// The token address
123        bytes token_address;
124        /// The number of decimals for the token
125        uint8 decimals;
126    }
127
128
129}
130
131/// Converts a `u8` value to a `U256` by placing the value in the least significant limb
132/// (little-endian order) and zero-extending the rest. This is suitable for representing
133/// small message type IDs as `U256` values, as required by the message format.
134const fn u8_to_u256(x: u8) -> U256 {
135    U256::from_limbs([x as u64, 0, 0, 0])
136}
137
138impl InterchainTransfer {
139    pub const MESSAGE_TYPE_ID: u8 = 0;
140    pub const MESSAGE_TYPE_ID_UINT: U256 = u8_to_u256(Self::MESSAGE_TYPE_ID);
141}
142
143impl DeployInterchainToken {
144    pub const MESSAGE_TYPE_ID: u8 = 1;
145    pub const MESSAGE_TYPE_ID_UINT: U256 = u8_to_u256(Self::MESSAGE_TYPE_ID);
146}
147
148// Value 2 is the MESSAGE_TYPE_DEPLOY_TOKEN_MANAGER, which is now unsupported and therefore
149// skipped.
150
151impl SendToHub {
152    pub const MESSAGE_TYPE_ID: u8 = 3;
153    pub const MESSAGE_TYPE_ID_UINT: U256 = u8_to_u256(Self::MESSAGE_TYPE_ID);
154}
155
156impl ReceiveFromHub {
157    pub const MESSAGE_TYPE_ID: u8 = 4;
158    pub const MESSAGE_TYPE_ID_UINT: U256 = u8_to_u256(Self::MESSAGE_TYPE_ID);
159}
160
161impl LinkToken {
162    pub const MESSAGE_TYPE_ID: u8 = 5;
163    pub const MESSAGE_TYPE_ID_UINT: U256 = u8_to_u256(Self::MESSAGE_TYPE_ID);
164}
165
166impl RegisterTokenMetadata {
167    pub const MESSAGE_TYPE_ID: u8 = 6;
168    pub const MESSAGE_TYPE_ID_UINT: U256 = u8_to_u256(Self::MESSAGE_TYPE_ID);
169}
170
171impl GMPPayload {
172    pub fn decode(bytes: &[u8]) -> Result<Self, alloy_sol_types::Error> {
173        if bytes.len() < 32 {
174            return Err(alloy_sol_types::Error::custom(
175                "Insufficient payload length",
176            ));
177        }
178
179        let variant = alloy_primitives::U256::abi_decode(&bytes[0..32], true)?;
180
181        // Check that only the lowest 8 bits are used (upper bytes must be zero)
182        let lower = variant & alloy_primitives::U256::from(0xffu8);
183        if variant != lower {
184            return Err(alloy_sol_types::Error::custom(
185                "Invalid payload (nonzero upper bits)",
186            ));
187        }
188
189        match variant.byte(0) {
190            InterchainTransfer::MESSAGE_TYPE_ID => Ok(GMPPayload::InterchainTransfer(
191                InterchainTransfer::abi_decode_params(bytes, true)?,
192            )),
193            DeployInterchainToken::MESSAGE_TYPE_ID => Ok(GMPPayload::DeployInterchainToken(
194                DeployInterchainToken::abi_decode_params(bytes, true)?,
195            )),
196            SendToHub::MESSAGE_TYPE_ID => Ok(GMPPayload::SendToHub(SendToHub::abi_decode_params(
197                bytes, true,
198            )?)),
199            ReceiveFromHub::MESSAGE_TYPE_ID => Ok(GMPPayload::ReceiveFromHub(
200                ReceiveFromHub::abi_decode_params(bytes, true)?,
201            )),
202            RegisterTokenMetadata::MESSAGE_TYPE_ID => Ok(GMPPayload::RegisterTokenMetadata(
203                RegisterTokenMetadata::abi_decode_params(bytes, true)?,
204            )),
205            LinkToken::MESSAGE_TYPE_ID => Ok(GMPPayload::LinkToken(LinkToken::abi_decode_params(
206                bytes, true,
207            )?)),
208            _ => Err(alloy_sol_types::Error::custom(
209                "Invalid selector for InterchainTokenService message",
210            )),
211        }
212    }
213
214    pub fn encode(&self) -> Vec<u8> {
215        match self {
216            GMPPayload::InterchainTransfer(data) => data.abi_encode_params(),
217            GMPPayload::DeployInterchainToken(data) => data.abi_encode_params(),
218            GMPPayload::SendToHub(data) => data.abi_encode_params(),
219            GMPPayload::ReceiveFromHub(data) => data.abi_encode_params(),
220            GMPPayload::LinkToken(data) => data.abi_encode_params(),
221            GMPPayload::RegisterTokenMetadata(data) => data.abi_encode_params(),
222        }
223    }
224
225    pub fn token_id(&self) -> Result<[u8; 32], alloy_sol_types::Error> {
226        match self {
227            GMPPayload::InterchainTransfer(data) => Ok(*data.token_id),
228            GMPPayload::DeployInterchainToken(data) => Ok(*data.token_id),
229            GMPPayload::SendToHub(inner) => GMPPayload::decode(&inner.payload)?.token_id(),
230            GMPPayload::ReceiveFromHub(inner) => GMPPayload::decode(&inner.payload)?.token_id(),
231            GMPPayload::LinkToken(data) => Ok(*data.token_id),
232            GMPPayload::RegisterTokenMetadata(_) => Err(alloy_sol_types::Error::Other(
233                Cow::Borrowed("RegisterTokenMetadata does not have a token_id"),
234            )),
235        }
236    }
237}
238
239impl From<InterchainTransfer> for GMPPayload {
240    fn from(data: InterchainTransfer) -> Self {
241        GMPPayload::InterchainTransfer(data)
242    }
243}
244
245impl From<DeployInterchainToken> for GMPPayload {
246    fn from(data: DeployInterchainToken) -> Self {
247        GMPPayload::DeployInterchainToken(data)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253
254    use alloy_primitives::U256;
255
256    use super::*;
257
258    #[test]
259    fn u256_into_u8() {
260        let u256 = U256::from(42);
261        let byte = u256.byte(0);
262        assert_eq!(byte, 42);
263    }
264
265    /// fixture from https://github.com/axelarnetwork/interchain-token-service/blob/0977738a1d7df5551cb3bd2e18f13c0e09944ff2/test/InterchainTokenService.js
266    /// [ 0,
267    ///   '0xcccdb55f29bb017269049e59732c01ac41239e7b61e8a83be5c0ae1143ed8064',
268    ///   '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
269    ///   '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
270    ///   1234,
271    ///   '0x'
272    /// ]
273    const INTERCHAIN_TRANSFER: &str = "0000000000000000000000000000000000000000000000000000000000000000cccdb55f29bb017269049e59732c01ac41239e7b61e8a83be5c0ae1143ed806400000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000014f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000000000000000000000000000014f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
274
275    #[test]
276    fn interchain_transfer_decode() {
277        // Setup
278        let gmp = GMPPayload::decode(&hex::decode(INTERCHAIN_TRANSFER).unwrap()).unwrap();
279
280        // Action
281        let GMPPayload::InterchainTransfer(data) = gmp else {
282            panic!("wrong variant");
283        };
284
285        // Assert
286        assert_eq!(
287            hex::encode(data.token_id.abi_encode()),
288            "cccdb55f29bb017269049e59732c01ac41239e7b61e8a83be5c0ae1143ed8064"
289        );
290        assert_eq!(
291            data.source_address,
292            hex::decode("f39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
293        );
294        assert_eq!(
295            data.destination_address,
296            hex::decode("f39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
297        );
298        assert_eq!(data.amount, U256::from(1234));
299        assert_eq!(data.data, Vec::<u8>::new());
300    }
301
302    #[test]
303    fn interchain_transfer_encode() {
304        assert_eq!(
305            hex::encode(
306                GMPPayload::decode(&hex::decode(INTERCHAIN_TRANSFER).unwrap())
307                    .unwrap()
308                    .encode()
309            ),
310            INTERCHAIN_TRANSFER,
311            "encode-decode should be idempotent"
312        );
313    }
314
315    /// fixture from https://github.com/axelarnetwork/interchain-token-service/blob/0977738a1d7df5551cb3bd2e18f13c0e09944ff2/test/InterchainTokenService.js
316    /// [
317    ///   1,
318    ///   '0xd8a4ae903349d12f4f96391cb47ea769a5535e57963562a5ae0ef932b18137e2',
319    ///   'Token Name',
320    ///   'TN',
321    ///   13,
322    ///   '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
323    /// ]
324    const DEPLOY_INTERCHAIN_TOKEN: &str = "0000000000000000000000000000000000000000000000000000000000000001d8a4ae903349d12f4f96391cb47ea769a5535e57963562a5ae0ef932b18137e200000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d0000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000a546f6b656e204e616d65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002544e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000";
325
326    #[test]
327    fn deploy_token_interchain_token_decode() {
328        // Setup
329        let gmp = GMPPayload::decode(&hex::decode(DEPLOY_INTERCHAIN_TOKEN).unwrap()).unwrap();
330
331        // Action
332        let GMPPayload::DeployInterchainToken(data) = gmp else {
333            panic!("wrong variant");
334        };
335
336        // Assert
337        assert_eq!(
338            hex::encode(data.token_id.abi_encode()),
339            "d8a4ae903349d12f4f96391cb47ea769a5535e57963562a5ae0ef932b18137e2"
340        );
341        assert_eq!(data.name, "Token Name".to_string());
342        assert_eq!(data.symbol, "TN".to_string(),);
343        assert_eq!(data.decimals, 13,);
344        assert_eq!(
345            data.minter,
346            hex::decode("f39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
347        );
348    }
349
350    #[test]
351    fn deploy_token_interchain_token_encode() {
352        assert_eq!(
353            hex::encode(
354                GMPPayload::decode(&hex::decode(DEPLOY_INTERCHAIN_TOKEN).unwrap())
355                    .unwrap()
356                    .encode()
357            ),
358            DEPLOY_INTERCHAIN_TOKEN,
359            "encode-decode should be idempotent"
360        );
361    }
362}