referrerpolicy=no-referrer-when-downgrade

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::{RuntimeDebug, 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, RuntimeDebug)]
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, RuntimeDebug)]
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, RuntimeDebug)]
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, RuntimeDebug, 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			Command::UnlockNativeToken { token, recipient, amount, .. } =>
220				UnlockNativeTokenParams {
221					token: Address::from(token.as_fixed_bytes()),
222					recipient: Address::from(recipient.as_fixed_bytes()),
223					amount: *amount,
224				}
225				.abi_encode(),
226			Command::RegisterForeignToken { token_id, name, symbol, decimals } =>
227				RegisterForeignTokenParams {
228					foreignTokenID: FixedBytes::from(token_id.as_fixed_bytes()),
229					name: Bytes::from(name.to_vec()),
230					symbol: Bytes::from(symbol.to_vec()),
231					decimals: *decimals,
232				}
233				.abi_encode(),
234			Command::MintForeignToken { token_id, recipient, amount } => MintForeignTokenParams {
235				foreignTokenID: FixedBytes::from(token_id.as_fixed_bytes()),
236				recipient: Address::from(recipient.as_fixed_bytes()),
237				amount: *amount,
238			}
239			.abi_encode(),
240			Command::CallContract { target, calldata: data, value, .. } => CallContractParams {
241				target: Address::from(target.as_fixed_bytes()),
242				data: Bytes::from(data.to_vec()),
243				value: U256::try_from(*value).unwrap(),
244			}
245			.abi_encode(),
246		}
247	}
248}
249
250/// Representation of a call to the initializer of an implementation contract.
251/// The initializer has the following ABI signature: `initialize(bytes)`.
252#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, RuntimeDebug, TypeInfo)]
253pub struct Initializer {
254	/// ABI-encoded params of type `bytes` to pass to the initializer
255	pub params: Vec<u8>,
256	/// The initializer is allowed to consume this much gas at most.
257	pub maximum_required_gas: u64,
258}
259
260pub trait SendMessage {
261	type Ticket: Clone + Encode + Decode;
262
263	/// Validate an outbound message and return a tuple:
264	/// 1. Ticket for submitting the message
265	/// 2. Delivery fee
266	fn validate(message: &Message) -> Result<Self::Ticket, SendError>;
267
268	/// Submit the message ticket for eventual delivery to Ethereum
269	fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
270}
271
272pub trait GasMeter {
273	/// Measures the maximum amount of gas a command payload will require to *dispatch*, NOT
274	/// including validation & verification.
275	fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
276}
277
278/// A meter that assigns a constant amount of gas for the execution of a command
279///
280/// The gas figures are extracted from this report:
281/// > forge test --match-path test/Gateway.t.sol --gas-report
282///
283/// A healthy buffer is added on top of these figures to account for:
284/// * The EIP-150 63/64 rule
285/// * Future EVM upgrades that may increase gas cost
286pub struct ConstantGasMeter;
287
288impl GasMeter for ConstantGasMeter {
289	fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
290		match command {
291			Command::SetOperatingMode { .. } => 40_000,
292			Command::Upgrade { initializer, .. } => {
293				// total maximum gas must also include the gas used for updating the proxy before
294				// the the initializer is called.
295				50_000 + initializer.maximum_required_gas
296			},
297			Command::UnlockNativeToken { .. } => 200_000,
298			Command::RegisterForeignToken { .. } => 1_200_000,
299			Command::MintForeignToken { .. } => 100_000,
300			Command::CallContract { gas: gas_limit, .. } => *gas_limit,
301		}
302	}
303}
304
305impl GasMeter for () {
306	fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
307		1
308	}
309}