Skip to main content

snowbridge_outbound_queue_primitives/v2/
message.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! # Outbound V2 primitives
4
5use codec::{Decode, DecodeWithMemTracking, Encode};
6use frame_support::{pallet_prelude::ConstU32, BoundedVec};
7use scale_info::TypeInfo;
8use sp_core::{H160, H256};
9use sp_std::vec::Vec;
10
11use crate::{OperatingMode, SendError};
12use abi::{
13	CallContractParams, MintForeignTokenParams, RegisterForeignTokenParams, SetOperatingModeParams,
14	UnlockNativeTokenParams, UpgradeParams,
15};
16use alloy_core::{
17	primitives::{Address, Bytes, FixedBytes, U256},
18	sol_types::SolValue,
19};
20
21pub mod abi {
22	use alloy_core::sol;
23
24	sol! {
25		struct OutboundMessageWrapper {
26			// origin
27			bytes32 origin;
28			// Message nonce
29			uint64 nonce;
30			// Topic
31			bytes32 topic;
32			// Commands
33			CommandWrapper[] commands;
34		}
35
36		struct CommandWrapper {
37			uint8 kind;
38			uint64 gas;
39			bytes payload;
40		}
41
42		// Payload for Upgrade
43		struct UpgradeParams {
44			// The address of the implementation contract
45			address implAddress;
46			// Codehash of the new implementation contract.
47			bytes32 implCodeHash;
48			// Parameters used to upgrade storage of the gateway
49			bytes initParams;
50		}
51
52		// Payload for SetOperatingMode instruction
53		struct SetOperatingModeParams {
54			/// The new operating mode
55			uint8 mode;
56		}
57
58		// Payload for NativeTokenUnlock instruction
59		struct UnlockNativeTokenParams {
60			// Token address
61			address token;
62			// Recipient address
63			address recipient;
64			// Amount to unlock
65			uint128 amount;
66		}
67
68		// Payload for RegisterForeignToken
69		struct RegisterForeignTokenParams {
70			/// @dev The token ID (hash of stable location id of token)
71			bytes32 foreignTokenID;
72			/// @dev The name of the token
73			bytes name;
74			/// @dev The symbol of the token
75			bytes symbol;
76			/// @dev The decimal of the token
77			uint8 decimals;
78		}
79
80		// Payload for MintForeignTokenParams instruction
81		struct MintForeignTokenParams {
82			// Foreign token ID
83			bytes32 foreignTokenID;
84			// Recipient address
85			address recipient;
86			// Amount to mint
87			uint128 amount;
88		}
89
90		// Payload for CallContract
91		struct CallContractParams {
92			// target contract
93			address target;
94			// Call data
95			bytes data;
96			// Ether value
97			uint256 value;
98		}
99	}
100}
101
102#[derive(Encode, Decode, TypeInfo, PartialEq, Clone, Debug)]
103pub struct OutboundCommandWrapper {
104	pub kind: u8,
105	pub gas: u64,
106	pub payload: Vec<u8>,
107}
108
109#[derive(Encode, Decode, TypeInfo, PartialEq, Clone, Debug)]
110pub struct OutboundMessage {
111	/// Origin
112	pub origin: H256,
113	/// Nonce
114	pub nonce: u64,
115	/// Topic
116	pub topic: H256,
117	/// Commands
118	pub commands: BoundedVec<OutboundCommandWrapper, ConstU32<MAX_COMMANDS>>,
119}
120
121pub const MAX_COMMANDS: u32 = 8;
122
123/// A message which can be accepted by implementations of `/[`SendMessage`\]`
124#[derive(Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Clone, Debug)]
125pub struct Message {
126	/// Origin
127	pub origin: H256,
128	/// ID
129	pub id: H256,
130	/// Fee
131	pub fee: u128,
132	/// Commands
133	pub commands: BoundedVec<Command, ConstU32<MAX_COMMANDS>>,
134}
135
136/// A command which is executable by the Gateway contract on Ethereum
137#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Debug, TypeInfo)]
138pub enum Command {
139	/// Upgrade the Gateway contract
140	Upgrade {
141		/// Address of the new implementation contract
142		impl_address: H160,
143		/// Codehash of the implementation contract
144		impl_code_hash: H256,
145		/// Invoke an initializer in the implementation contract
146		initializer: Initializer,
147	},
148	/// Set the global operating mode of the Gateway contract
149	SetOperatingMode {
150		/// The new operating mode
151		mode: OperatingMode,
152	},
153	/// Unlock ERC20 tokens
154	UnlockNativeToken {
155		/// Address of the ERC20 token
156		token: H160,
157		/// The recipient of the tokens
158		recipient: H160,
159		/// The amount of tokens to transfer
160		amount: u128,
161	},
162	/// Register foreign token from Polkadot
163	RegisterForeignToken {
164		/// ID for the token
165		token_id: H256,
166		/// Name of the token
167		name: Vec<u8>,
168		/// Short symbol for the token
169		symbol: Vec<u8>,
170		/// Number of decimal places
171		decimals: u8,
172	},
173	/// Mint foreign token from Polkadot
174	MintForeignToken {
175		/// ID for the token
176		token_id: H256,
177		/// The recipient of the newly minted tokens
178		recipient: H160,
179		/// The amount of tokens to mint
180		amount: u128,
181	},
182	/// Call Contract on Ethereum
183	CallContract {
184		/// Target contract address
185		target: H160,
186		/// ABI-encoded calldata
187		calldata: Vec<u8>,
188		/// Maximum gas to forward to target contract
189		gas: u64,
190		/// Include ether held by agent contract
191		value: u128,
192	},
193}
194
195impl Command {
196	/// Compute the enum variant index
197	pub fn index(&self) -> u8 {
198		match self {
199			Command::Upgrade { .. } => 0,
200			Command::SetOperatingMode { .. } => 1,
201			Command::UnlockNativeToken { .. } => 2,
202			Command::RegisterForeignToken { .. } => 3,
203			Command::MintForeignToken { .. } => 4,
204			Command::CallContract { .. } => 5,
205		}
206	}
207
208	/// ABI-encode the Command.
209	pub fn abi_encode(&self) -> Vec<u8> {
210		match self {
211			Command::Upgrade { impl_address, impl_code_hash, initializer, .. } => UpgradeParams {
212				implAddress: Address::from(impl_address.as_fixed_bytes()),
213				implCodeHash: FixedBytes::from(impl_code_hash.as_fixed_bytes()),
214				initParams: Bytes::from(initializer.params.clone()),
215			}
216			.abi_encode(),
217			Command::SetOperatingMode { mode } => {
218				SetOperatingModeParams { mode: (*mode) as u8 }.abi_encode()
219			},
220			Command::UnlockNativeToken { token, recipient, amount, .. } => {
221				UnlockNativeTokenParams {
222					token: Address::from(token.as_fixed_bytes()),
223					recipient: Address::from(recipient.as_fixed_bytes()),
224					amount: *amount,
225				}
226				.abi_encode()
227			},
228			Command::RegisterForeignToken { token_id, name, symbol, decimals } => {
229				RegisterForeignTokenParams {
230					foreignTokenID: FixedBytes::from(token_id.as_fixed_bytes()),
231					name: Bytes::from(name.to_vec()),
232					symbol: Bytes::from(symbol.to_vec()),
233					decimals: *decimals,
234				}
235				.abi_encode()
236			},
237			Command::MintForeignToken { token_id, recipient, amount } => MintForeignTokenParams {
238				foreignTokenID: FixedBytes::from(token_id.as_fixed_bytes()),
239				recipient: Address::from(recipient.as_fixed_bytes()),
240				amount: *amount,
241			}
242			.abi_encode(),
243			Command::CallContract { target, calldata: data, value, .. } => CallContractParams {
244				target: Address::from(target.as_fixed_bytes()),
245				data: Bytes::from(data.to_vec()),
246				value: U256::try_from(*value).unwrap(),
247			}
248			.abi_encode(),
249		}
250	}
251}
252
253/// Representation of a call to the initializer of an implementation contract.
254/// The initializer has the following ABI signature: `initialize(bytes)`.
255#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Debug, TypeInfo)]
256pub struct Initializer {
257	/// ABI-encoded params of type `bytes` to pass to the initializer
258	pub params: Vec<u8>,
259	/// The initializer is allowed to consume this much gas at most.
260	pub maximum_required_gas: u64,
261}
262
263pub trait SendMessage {
264	type Ticket: Clone + Encode + Decode;
265
266	/// Validate an outbound message and return a tuple:
267	/// 1. Ticket for submitting the message
268	/// 2. Delivery fee
269	fn validate(message: &Message) -> Result<Self::Ticket, SendError>;
270
271	/// Submit the message ticket for eventual delivery to Ethereum
272	fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
273}
274
275pub trait GasMeter {
276	/// Measures the maximum amount of gas a command payload will require to *dispatch*, NOT
277	/// including validation & verification.
278	fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
279}
280
281/// A meter that assigns a constant amount of gas for the execution of a command
282///
283/// The gas figures are extracted from this report:
284/// > forge test --match-path test/Gateway.t.sol --gas-report
285///
286/// A healthy buffer is added on top of these figures to account for:
287/// * The EIP-150 63/64 rule
288/// * Future EVM upgrades that may increase gas cost
289pub struct ConstantGasMeter;
290
291impl GasMeter for ConstantGasMeter {
292	fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
293		match command {
294			Command::SetOperatingMode { .. } => 40_000,
295			Command::Upgrade { initializer, .. } => {
296				// total maximum gas must also include the gas used for updating the proxy before
297				// the the initializer is called.
298				50_000 + initializer.maximum_required_gas
299			},
300			Command::UnlockNativeToken { .. } => 200_000,
301			Command::RegisterForeignToken { .. } => 1_200_000,
302			Command::MintForeignToken { .. } => 100_000,
303			Command::CallContract { gas: gas_limit, .. } => *gas_limit,
304		}
305	}
306}
307
308impl GasMeter for () {
309	fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
310		1
311	}
312}