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}