referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_parachains/hrmp/
deposits.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Channel deposits.
18//!
19//! The channel logic never touches the funds directly. Each call that takes or gives back a
20//! deposit sends a [`DepositChangeRequest`] to [`Config::DepositProvider`]. If the provider
21//! applies the request within the call, the call completes right away. If the provider holds the
22//! funds on another chain, it answers pending and the call completes in
23//! [`OnDepositSet::on_deposit_set`] once the provider reports back, or stops there if the
24//! provider refused the request.
25//!
26//! The pallet implements [`ChannelDepositProvider`] itself to hold the deposits in
27//! [`Config::Currency`], which keeps the behaviour from before the provider existed.
28
29use super::*;
30
31/// Holds HRMP channel deposits.
32///
33/// A provider either applies a request within the call or accepts it and reports the outcome
34/// later through [`Self::OnDepositSet`]. While a request for one side of a channel is pending, it
35/// refuses another request for that side.
36pub trait ChannelDepositProvider {
37	/// Receives the report of a pending request once it is done.
38	type OnDepositSet: OnDepositSet;
39
40	/// Applies `request`, or accepts it to apply it later. Refuses it with an error, e.g. when
41	/// the funds are short or a request for the same side is still pending.
42	fn try_set_deposit(
43		request: DepositChangeRequest,
44	) -> Result<DepositChangeResponse, DispatchError>;
45
46	/// Upper bound of what [`Self::try_set_deposit`] spends on top of what the calls' benchmarks
47	/// measure with the in-pallet provider, e.g. sending a message.
48	fn weight_of_try_set_deposit() -> Weight;
49}
50
51/// Receives the outcome of a pending [`DepositChangeRequest`].
52pub trait OnDepositSet {
53	/// Called once `request` is applied, or refused with `Err`.
54	fn on_deposit_set(request: DepositChangeRequest, result: Result<(), DepositError>);
55}
56
57/// Why a provider refused a pending [`DepositChangeRequest`].
58#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
59pub enum DepositError {
60	/// A side cannot cover its deposit.
61	InsufficientFunds,
62	/// A reason of the provider's own.
63	Other,
64}
65
66/// A request to change the deposits of a channel, with the action to resume once they are
67/// changed.
68///
69/// It carries everything the action needs, so the requester keeps no pending state of its own:
70/// the request comes back unchanged in [`OnDepositSet::on_deposit_set`] and the action resumes
71/// from it.
72#[derive(Encode, Decode, Clone, Eq, PartialEq, Debug, TypeInfo)]
73pub struct DepositChangeRequest {
74	/// The channel whose deposits change.
75	pub channel_id: HrmpChannelId,
76	/// How the sender's deposit changes, or `None` to leave it as is.
77	pub sender: Option<DepositChange>,
78	/// How the recipient's deposit changes, or `None` to leave it as is.
79	pub recipient: Option<DepositChange>,
80	/// The action to resume once the deposits are changed.
81	pub action: DepositAction,
82}
83
84/// What one side of a channel holds before and after a [`DepositChangeRequest`].
85#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
86pub struct DepositChange {
87	/// What the side holds now, by the requester's records. A provider with records of its own
88	/// goes by those and acts on `target` alone.
89	pub current: Balance,
90	/// What the side holds once the request is done.
91	pub target: Balance,
92}
93
94/// The provider's answer to a [`DepositChangeRequest`].
95#[derive(Clone, Copy, Eq, PartialEq, Debug)]
96pub enum DepositChangeResponse {
97	/// The request is applied. The action resumes right away.
98	Applied,
99	/// The request is accepted and its outcome is reported through [`OnDepositSet`].
100	Pending,
101}
102
103/// The action a [`DepositChangeRequest`] resumes once the deposits are changed.
104#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
105pub enum DepositAction {
106	/// Record the open channel request of [`Pallet::init_open_channel`]. The request is only
107	/// recorded once the sender's deposit is taken, so the limits it was asked with travel here.
108	InitOpenChannel { max_capacity: u32, max_message_size: u32, max_total_size: u32 },
109	/// Confirm the open channel request of [`Pallet::accept_open_channel`].
110	AcceptOpenChannel,
111	/// Record the deposits of [`Pallet::poke_channel_deposits`].
112	PokeChannelDeposits,
113	/// Deposits given back after their request or channel is already removed. Nothing to resume.
114	Release,
115}
116
117/// The in-pallet provider: holds the deposits in [`Config::Currency`] and applies every request
118/// within the call.
119impl<T: Config> ChannelDepositProvider for Pallet<T> {
120	type OnDepositSet = Self;
121
122	fn try_set_deposit(
123		request: DepositChangeRequest,
124	) -> Result<DepositChangeResponse, DispatchError> {
125		let DepositChangeRequest { channel_id, sender, recipient, .. } = request;
126		for (para, change) in [(channel_id.sender, sender), (channel_id.recipient, recipient)] {
127			let Some(DepositChange { current, target }) = change else { continue };
128			let account: T::AccountId = para.into_account_truncating();
129			if target > current {
130				T::Currency::reserve(&account, (target - current).unique_saturated_into())?;
131			} else {
132				T::Currency::unreserve(&account, (current - target).unique_saturated_into());
133			}
134		}
135		Ok(DepositChangeResponse::Applied)
136	}
137
138	fn weight_of_try_set_deposit() -> Weight {
139		// Measured by the calls' benchmarks.
140		Weight::zero()
141	}
142}
143
144impl<T: Config> OnDepositSet for Pallet<T> {
145	fn on_deposit_set(request: DepositChangeRequest, result: Result<(), DepositError>) {
146		match result {
147			Ok(()) => Self::resume(&request),
148			// TODO(ahm-v2): emit an event (later on hrmp_relay) so the para learns its deposit was
149			// refused.
150			Err(e) => {
151				log::warn!(target: LOG_TARGET, "deposit refused: {e:?}, request: {request:?}")
152			},
153		}
154		// TODO(ahm-v2): the report arrives through xcm message so the weight of resuming is
155		// registered here: per action, the weight of the call it belongs to. Zero for now as no
156		// async provider.
157		frame_system::Pallet::<T>::register_extra_weight_unchecked(
158			Weight::zero(),
159			DispatchClass::Normal,
160		);
161	}
162}
163
164impl<T: Config> Pallet<T> {
165	/// What the sender and the recipient must each hold for `channel_id` at the prices in
166	/// `config`. Channels with or amongst the system are free.
167	pub(super) fn required_deposits(
168		channel_id: &HrmpChannelId,
169		config: &HostConfiguration<BlockNumberFor<T>>,
170	) -> (Balance, Balance) {
171		if channel_id.sender.is_system() || channel_id.recipient.is_system() {
172			return (0, 0);
173		}
174		(config.hrmp_sender_deposit, config.hrmp_recipient_deposit)
175	}
176
177	/// Sends `request` to [`Config::DepositProvider`] and resumes its action if the provider
178	/// applied it right away. A pending request resumes in [`OnDepositSet::on_deposit_set`]
179	/// instead. Fails only if the provider refuses the request right away.
180	pub(super) fn set_deposit(request: DepositChangeRequest) -> DispatchResult {
181		// Nothing to change, e.g. on a channel with the system, needs no provider.
182		let unchanged =
183			|change: Option<DepositChange>| change.is_none_or(|c| c.current == c.target);
184		if unchanged(request.sender) && unchanged(request.recipient) {
185			Self::resume(&request);
186			return Ok(());
187		}
188		match T::DepositProvider::try_set_deposit(request.clone())? {
189			DepositChangeResponse::Applied => Self::resume(&request),
190			DepositChangeResponse::Pending => {},
191		}
192		Ok(())
193	}
194
195	/// Gives back the deposits of `channel_id`: `sender` and `recipient` are what each side holds
196	/// by this pallet's records, or `None` to leave that side alone. A refused release is logged.
197	pub(super) fn release_deposits(
198		channel_id: &HrmpChannelId,
199		sender: Option<Balance>,
200		recipient: Option<Balance>,
201	) {
202		let release = |current| DepositChange { current, target: 0 };
203		let request = DepositChangeRequest {
204			channel_id: channel_id.clone(),
205			sender: sender.map(release),
206			recipient: recipient.map(release),
207			action: DepositAction::Release,
208		};
209		if let Err(e) = Self::set_deposit(request.clone()) {
210			// TODO(ahm-v2): emit an event (later on hrmp_relay) if release fails.
211			log::error!(target: LOG_TARGET, "giving back deposits failed: {e:?}, request: {request:?}");
212		}
213	}
214
215	/// Resumes the action of `request` now that its deposits are updated.
216	fn resume(request: &DepositChangeRequest) {
217		let channel_id = &request.channel_id;
218		match request.action {
219			DepositAction::InitOpenChannel { max_capacity, max_message_size, max_total_size } => {
220				// Always set by `init_open_channel`.
221				let sender_deposit = request.sender.map(|c| c.target).unwrap_or_default();
222				Self::finish_init_open_channel(
223					channel_id,
224					sender_deposit,
225					max_capacity,
226					max_message_size,
227					max_total_size,
228				);
229			},
230			DepositAction::AcceptOpenChannel => Self::finish_accept_open_channel(channel_id),
231			DepositAction::PokeChannelDeposits => {
232				Self::finish_poke_channel_deposits(channel_id, request.sender, request.recipient)
233			},
234			DepositAction::Release => {},
235		}
236	}
237}