Skip to main content

snowbridge_outbound_queue_primitives/v1/
message.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
3//! # Outbound V1 primitives
4
5use crate::{OperatingMode, SendError, SendMessageFeeProvider};
6use codec::{Decode, DecodeWithMemTracking, Encode};
7use ethabi::Token;
8use scale_info::TypeInfo;
9use snowbridge_core::{pricing::UD60x18, ChannelId};
10use sp_arithmetic::traits::{BaseArithmetic, Unsigned};
11use sp_core::{H160, H256, U256};
12use sp_std::{borrow::ToOwned, vec, vec::Vec};
13
14/// Enqueued outbound messages need to be versioned to prevent data corruption
15/// or loss after forkless runtime upgrades
16#[derive(Encode, Decode, TypeInfo, Clone, Debug)]
17#[cfg_attr(feature = "std", derive(PartialEq))]
18pub enum VersionedQueuedMessage {
19	V1(QueuedMessage),
20}
21
22impl TryFrom<VersionedQueuedMessage> for QueuedMessage {
23	type Error = ();
24	fn try_from(x: VersionedQueuedMessage) -> Result<Self, Self::Error> {
25		use VersionedQueuedMessage::*;
26		match x {
27			V1(x) => Ok(x),
28		}
29	}
30}
31
32impl<T: Into<QueuedMessage>> From<T> for VersionedQueuedMessage {
33	fn from(x: T) -> Self {
34		VersionedQueuedMessage::V1(x.into())
35	}
36}
37
38/// A message which can be accepted by implementations of `/[`SendMessage`\]`
39#[derive(Encode, Decode, TypeInfo, Clone, Debug)]
40#[cfg_attr(feature = "std", derive(PartialEq))]
41pub struct Message {
42	/// ID for this message. One will be automatically generated if not provided.
43	///
44	/// When this message is created from an XCM message, the ID should be extracted
45	/// from the `SetTopic` instruction.
46	///
47	/// The ID plays no role in bridge consensus, and is purely meant for message tracing.
48	pub id: Option<H256>,
49	/// The message channel ID
50	pub channel_id: ChannelId,
51	/// The stable ID for a receiving gateway contract
52	pub command: Command,
53}
54
55/// A command which is executable by the Gateway contract on Ethereum
56#[derive(Clone, Encode, Decode, Debug, TypeInfo)]
57#[cfg_attr(feature = "std", derive(PartialEq))]
58pub enum Command {
59	/// Execute a sub-command within an agent for a consensus system in Polkadot
60	/// DEPRECATED in favour of `UnlockNativeToken`. We still have to keep it around in
61	/// case buffered and uncommitted messages are using this variant.
62	AgentExecute {
63		/// The ID of the agent
64		agent_id: H256,
65		/// The sub-command to be executed
66		command: AgentExecuteCommand,
67	},
68	/// Upgrade the Gateway contract
69	Upgrade {
70		/// Address of the new implementation contract
71		impl_address: H160,
72		/// Codehash of the implementation contract
73		impl_code_hash: H256,
74		/// Optionally invoke an initializer in the implementation contract
75		initializer: Option<Initializer>,
76	},
77	/// Set the global operating mode of the Gateway contract
78	SetOperatingMode {
79		/// The new operating mode
80		mode: OperatingMode,
81	},
82	/// Set token fees of the Gateway contract
83	SetTokenTransferFees {
84		/// The fee(DOT) for the cost of creating asset on AssetHub
85		create_asset_xcm: u128,
86		/// The fee(DOT) for the cost of sending asset on AssetHub
87		transfer_asset_xcm: u128,
88		/// The fee(Ether) for register token to discourage spamming
89		register_token: U256,
90	},
91	/// Set pricing parameters
92	SetPricingParameters {
93		// ETH/DOT exchange rate
94		exchange_rate: UD60x18,
95		// Cost of delivering a message from Ethereum to BridgeHub, in ROC/KSM/DOT
96		delivery_cost: u128,
97		// Fee multiplier
98		multiplier: UD60x18,
99	},
100	/// Transfer ERC20 tokens
101	UnlockNativeToken {
102		/// ID of the agent
103		agent_id: H256,
104		/// Address of the ERC20 token
105		token: H160,
106		/// The recipient of the tokens
107		recipient: H160,
108		/// The amount of tokens to transfer
109		amount: u128,
110	},
111	/// Register foreign token from Polkadot
112	RegisterForeignToken {
113		/// ID for the token
114		token_id: H256,
115		/// Name of the token
116		name: Vec<u8>,
117		/// Short symbol for the token
118		symbol: Vec<u8>,
119		/// Number of decimal places
120		decimals: u8,
121	},
122	/// Mint foreign token from Polkadot
123	MintForeignToken {
124		/// ID for the token
125		token_id: H256,
126		/// The recipient of the newly minted tokens
127		recipient: H160,
128		/// The amount of tokens to mint
129		amount: u128,
130	},
131}
132
133impl Command {
134	/// Compute the enum variant index
135	pub fn index(&self) -> u8 {
136		match self {
137			Command::AgentExecute { .. } => 0,
138			Command::Upgrade { .. } => 1,
139			Command::SetOperatingMode { .. } => 5,
140			Command::SetTokenTransferFees { .. } => 7,
141			Command::SetPricingParameters { .. } => 8,
142			Command::UnlockNativeToken { .. } => 9,
143			Command::RegisterForeignToken { .. } => 10,
144			Command::MintForeignToken { .. } => 11,
145		}
146	}
147
148	/// ABI-encode the Command.
149	pub fn abi_encode(&self) -> Vec<u8> {
150		match self {
151			Command::AgentExecute { agent_id, command } => ethabi::encode(&[Token::Tuple(vec![
152				Token::FixedBytes(agent_id.as_bytes().to_owned()),
153				Token::Bytes(command.abi_encode()),
154			])]),
155			Command::Upgrade { impl_address, impl_code_hash, initializer, .. } => {
156				ethabi::encode(&[Token::Tuple(vec![
157					Token::Address(*impl_address),
158					Token::FixedBytes(impl_code_hash.as_bytes().to_owned()),
159					initializer.clone().map_or(Token::Bytes(vec![]), |i| Token::Bytes(i.params)),
160				])])
161			},
162			Command::SetOperatingMode { mode } => {
163				ethabi::encode(&[Token::Tuple(vec![Token::Uint(U256::from((*mode) as u64))])])
164			},
165			Command::SetTokenTransferFees {
166				create_asset_xcm,
167				transfer_asset_xcm,
168				register_token,
169			} => ethabi::encode(&[Token::Tuple(vec![
170				Token::Uint(U256::from(*create_asset_xcm)),
171				Token::Uint(U256::from(*transfer_asset_xcm)),
172				Token::Uint(*register_token),
173			])]),
174			Command::SetPricingParameters { exchange_rate, delivery_cost, multiplier } => {
175				ethabi::encode(&[Token::Tuple(vec![
176					Token::Uint(exchange_rate.clone().into_inner()),
177					Token::Uint(U256::from(*delivery_cost)),
178					Token::Uint(multiplier.clone().into_inner()),
179				])])
180			},
181			Command::UnlockNativeToken { agent_id, token, recipient, amount } => {
182				ethabi::encode(&[Token::Tuple(vec![
183					Token::FixedBytes(agent_id.as_bytes().to_owned()),
184					Token::Address(*token),
185					Token::Address(*recipient),
186					Token::Uint(U256::from(*amount)),
187				])])
188			},
189			Command::RegisterForeignToken { token_id, name, symbol, decimals } => {
190				ethabi::encode(&[Token::Tuple(vec![
191					Token::FixedBytes(token_id.as_bytes().to_owned()),
192					Token::String(name.to_owned()),
193					Token::String(symbol.to_owned()),
194					Token::Uint(U256::from(*decimals)),
195				])])
196			},
197			Command::MintForeignToken { token_id, recipient, amount } => {
198				ethabi::encode(&[Token::Tuple(vec![
199					Token::FixedBytes(token_id.as_bytes().to_owned()),
200					Token::Address(*recipient),
201					Token::Uint(U256::from(*amount)),
202				])])
203			},
204		}
205	}
206}
207
208/// Representation of a call to the initializer of an implementation contract.
209/// The initializer has the following ABI signature: `initialize(bytes)`.
210#[derive(Clone, Encode, Decode, DecodeWithMemTracking, PartialEq, Debug, TypeInfo)]
211pub struct Initializer {
212	/// ABI-encoded params of type `bytes` to pass to the initializer
213	pub params: Vec<u8>,
214	/// The initializer is allowed to consume this much gas at most.
215	pub maximum_required_gas: u64,
216}
217
218/// A Sub-command executable within an agent
219#[derive(Clone, Encode, Decode, Debug, TypeInfo)]
220#[cfg_attr(feature = "std", derive(PartialEq))]
221pub enum AgentExecuteCommand {
222	/// Transfer ERC20 tokens
223	TransferToken {
224		/// Address of the ERC20 token
225		token: H160,
226		/// The recipient of the tokens
227		recipient: H160,
228		/// The amount of tokens to transfer
229		amount: u128,
230	},
231}
232
233impl AgentExecuteCommand {
234	fn index(&self) -> u8 {
235		match self {
236			AgentExecuteCommand::TransferToken { .. } => 0,
237		}
238	}
239
240	/// ABI-encode the sub-command
241	pub fn abi_encode(&self) -> Vec<u8> {
242		match self {
243			AgentExecuteCommand::TransferToken { token, recipient, amount } => ethabi::encode(&[
244				Token::Uint(self.index().into()),
245				Token::Bytes(ethabi::encode(&[
246					Token::Address(*token),
247					Token::Address(*recipient),
248					Token::Uint(U256::from(*amount)),
249				])),
250			]),
251		}
252	}
253}
254
255/// Message which is awaiting processing in the MessageQueue pallet
256#[derive(Clone, Encode, Decode, Debug, TypeInfo)]
257#[cfg_attr(feature = "std", derive(PartialEq))]
258pub struct QueuedMessage {
259	/// Message ID
260	pub id: H256,
261	/// Channel ID
262	pub channel_id: ChannelId,
263	/// Command to execute in the Gateway contract
264	pub command: Command,
265}
266
267#[derive(Clone, Encode, Decode, Debug, TypeInfo)]
268#[cfg_attr(feature = "std", derive(PartialEq))]
269/// Fee for delivering message
270pub struct Fee<Balance>
271where
272	Balance: BaseArithmetic + Unsigned + Copy,
273{
274	/// Fee to cover cost of processing the message locally
275	pub local: Balance,
276	/// Fee to cover cost processing the message remotely
277	pub remote: Balance,
278}
279
280impl<Balance> Fee<Balance>
281where
282	Balance: BaseArithmetic + Unsigned + Copy,
283{
284	pub fn total(&self) -> Balance {
285		self.local.saturating_add(self.remote)
286	}
287}
288
289impl<Balance> From<(Balance, Balance)> for Fee<Balance>
290where
291	Balance: BaseArithmetic + Unsigned + Copy,
292{
293	fn from((local, remote): (Balance, Balance)) -> Self {
294		Self { local, remote }
295	}
296}
297
298/// A trait for sending messages to Ethereum
299pub trait SendMessage: SendMessageFeeProvider {
300	type Ticket: Clone + Encode + Decode;
301
302	/// Validate an outbound message and return a tuple:
303	/// 1. Ticket for submitting the message
304	/// 2. Delivery fee
305	fn validate(
306		message: &Message,
307	) -> Result<(Self::Ticket, Fee<<Self as SendMessageFeeProvider>::Balance>), SendError>;
308
309	/// Submit the message ticket for eventual delivery to Ethereum
310	fn deliver(ticket: Self::Ticket) -> Result<H256, SendError>;
311}
312
313pub trait Ticket: Encode + Decode + Clone {
314	fn message_id(&self) -> H256;
315}
316
317pub trait GasMeter {
318	/// All the gas used for submitting a message to Ethereum, minus the cost of dispatching
319	/// the command within the message
320	const MAXIMUM_BASE_GAS: u64;
321
322	/// Total gas consumed at most, including verification & dispatch
323	fn maximum_gas_used_at_most(command: &Command) -> u64 {
324		Self::MAXIMUM_BASE_GAS + Self::maximum_dispatch_gas_used_at_most(command)
325	}
326
327	/// Measures the maximum amount of gas a command payload will require to *dispatch*, NOT
328	/// including validation & verification.
329	fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64;
330}
331
332/// A meter that assigns a constant amount of gas for the execution of a command
333///
334/// The gas figures are extracted from this report:
335/// > forge test --match-path test/Gateway.t.sol --gas-report
336///
337/// A healthy buffer is added on top of these figures to account for:
338/// * The EIP-150 63/64 rule
339/// * Future EVM upgrades that may increase gas cost
340pub struct ConstantGasMeter;
341
342impl GasMeter for ConstantGasMeter {
343	// The base transaction cost, which includes:
344	// 21_000 transaction cost, roughly worst case 64_000 for calldata, and 100_000
345	// for message verification
346	const MAXIMUM_BASE_GAS: u64 = 185_000;
347
348	fn maximum_dispatch_gas_used_at_most(command: &Command) -> u64 {
349		match command {
350			Command::SetOperatingMode { .. } => 40_000,
351			Command::AgentExecute { command, .. } => match command {
352				// Execute IERC20.transferFrom
353				//
354				// Worst-case assumptions are important:
355				// * No gas refund for clearing storage slot of source account in ERC20 contract
356				// * Assume dest account in ERC20 contract does not yet have a storage slot
357				// * ERC20.transferFrom possibly does other business logic besides updating balances
358				AgentExecuteCommand::TransferToken { .. } => 200_000,
359			},
360			Command::Upgrade { initializer, .. } => {
361				let initializer_max_gas = match *initializer {
362					Some(Initializer { maximum_required_gas, .. }) => maximum_required_gas,
363					None => 0,
364				};
365				// total maximum gas must also include the gas used for updating the proxy before
366				// the the initializer is called.
367				50_000 + initializer_max_gas
368			},
369			Command::SetTokenTransferFees { .. } => 60_000,
370			Command::SetPricingParameters { .. } => 60_000,
371			Command::UnlockNativeToken { .. } => 200_000,
372			Command::RegisterForeignToken { .. } => 1_200_000,
373			Command::MintForeignToken { .. } => 100_000,
374		}
375	}
376}
377
378impl GasMeter for () {
379	const MAXIMUM_BASE_GAS: u64 = 1;
380
381	fn maximum_dispatch_gas_used_at_most(_: &Command) -> u64 {
382		1
383	}
384}
385
386pub const ETHER_DECIMALS: u8 = 18;