referrerpolicy=no-referrer-when-downgrade

pallet_revive/
impl_fungibles.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Implementation of the `fungibles::*` family of traits for `pallet-revive`.
19//!
20//! This is meant to allow ERC20 tokens stored on this pallet to be used with
21//! the fungibles traits.
22//! This is only meant for tests since gas limits are not taken into account,
23//! the feature flags make sure of that.
24
25#![cfg(any(feature = "std", feature = "runtime-benchmarks", test))]
26
27use crate::{metering::TransactionLimits, OriginFor};
28use alloy_core::{
29	primitives::{Address, U256 as EU256},
30	sol_types::*,
31};
32use frame_support::{
33	traits::{
34		tokens::{
35			fungible, fungibles, DepositConsequence, Fortitude, Precision, Preservation,
36			Provenance, WithdrawConsequence,
37		},
38		OriginTrait,
39	},
40	PalletId,
41};
42use sp_core::{H160, U256};
43use sp_runtime::{traits::AccountIdConversion, DispatchError};
44
45use super::{address::AddressMapper, pallet, Config, ContractResult, ExecConfig, Pallet, Weight};
46use ethereum_standards::IERC20;
47
48const WEIGHT_LIMIT: Weight = Weight::from_parts(10_000_000_000, 100_000);
49
50impl<T: Config> Pallet<T> {
51	// Test checking account for the `fungibles::*` implementation.
52	//
53	// Still needs to be mapped in tests for it to be usable.
54	pub fn checking_account() -> <T as frame_system::Config>::AccountId {
55		PalletId(*b"py/revch").into_account_truncating()
56	}
57}
58
59impl<T: Config> fungibles::Inspect<<T as frame_system::Config>::AccountId> for Pallet<T> {
60	// The asset id of an ERC20 is its origin contract's address.
61	type AssetId = H160;
62	// The balance is always u128.
63	type Balance = u128;
64
65	// Need to call a view function here.
66	fn total_issuance(asset_id: Self::AssetId) -> Self::Balance {
67		let data = IERC20::totalSupplyCall {}.abi_encode();
68		let ContractResult { result, .. } = Self::bare_call(
69			OriginFor::<T>::signed(Self::checking_account()),
70			asset_id,
71			U256::zero(),
72			TransactionLimits::WeightAndDeposit {
73				weight_limit: WEIGHT_LIMIT,
74				deposit_limit:
75					<<T as pallet::Config>::Currency as fungible::Inspect<_>>::total_issuance(),
76			},
77			data,
78			ExecConfig::new_substrate_tx(),
79		);
80		if let Ok(return_value) = result {
81			if let Ok(eu256) = EU256::abi_decode_validate(&return_value.data) {
82				eu256.to::<u128>()
83			} else {
84				0
85			}
86		} else {
87			0
88		}
89	}
90
91	fn minimum_balance(_: Self::AssetId) -> Self::Balance {
92		// ERC20s don't have this concept.
93		1
94	}
95
96	fn total_balance(asset_id: Self::AssetId, account_id: &T::AccountId) -> Self::Balance {
97		// Since ERC20s don't have the concept of freezes and locks,
98		// total balance is the same as balance.
99		Self::balance(asset_id, account_id)
100	}
101
102	fn balance(asset_id: Self::AssetId, account_id: &T::AccountId) -> Self::Balance {
103		let eth_address = T::AddressMapper::to_address(account_id);
104		let address = Address::from(Into::<[u8; 20]>::into(eth_address));
105		let data = IERC20::balanceOfCall { account: address }.abi_encode();
106		let ContractResult { result, .. } = Self::bare_call(
107			OriginFor::<T>::signed(account_id.clone()),
108			asset_id,
109			U256::zero(),
110			TransactionLimits::WeightAndDeposit {
111				weight_limit: WEIGHT_LIMIT,
112				deposit_limit:
113					<<T as pallet::Config>::Currency as fungible::Inspect<_>>::total_issuance(),
114			},
115			data,
116			ExecConfig::new_substrate_tx(),
117		);
118		if let Ok(return_value) = result {
119			if let Ok(eu256) = EU256::abi_decode_validate(&return_value.data) {
120				eu256.to::<u128>()
121			} else {
122				0
123			}
124		} else {
125			0
126		}
127	}
128
129	fn reducible_balance(
130		asset_id: Self::AssetId,
131		account_id: &T::AccountId,
132		_: Preservation,
133		_: Fortitude,
134	) -> Self::Balance {
135		// Since ERC20s don't have minimum amounts, this is the same
136		// as balance.
137		Self::balance(asset_id, account_id)
138	}
139
140	fn can_deposit(
141		_: Self::AssetId,
142		_: &T::AccountId,
143		_: Self::Balance,
144		_: Provenance,
145	) -> DepositConsequence {
146		DepositConsequence::Success
147	}
148
149	fn can_withdraw(
150		_: Self::AssetId,
151		_: &T::AccountId,
152		_: Self::Balance,
153	) -> WithdrawConsequence<Self::Balance> {
154		WithdrawConsequence::Success
155	}
156
157	fn asset_exists(_: Self::AssetId) -> bool {
158		false
159	}
160}
161
162// We implement `fungibles::Mutate` to override `burn_from` and `mint_to`.
163//
164// These functions are used in [`xcm_builder::FungiblesAdapter`].
165impl<T: Config> fungibles::Mutate<<T as frame_system::Config>::AccountId> for Pallet<T> {
166	fn burn_from(
167		asset_id: Self::AssetId,
168		who: &T::AccountId,
169		amount: Self::Balance,
170		_: Preservation,
171		_: Precision,
172		_: Fortitude,
173	) -> Result<Self::Balance, DispatchError> {
174		let checking_account_eth = T::AddressMapper::to_address(&Self::checking_account());
175		let checking_address = Address::from(Into::<[u8; 20]>::into(checking_account_eth));
176		let data =
177			IERC20::transferCall { to: checking_address, value: EU256::from(amount) }.abi_encode();
178		let ContractResult { result, weight_consumed, .. } = Self::bare_call(
179			OriginFor::<T>::signed(who.clone()),
180			asset_id,
181			U256::zero(),
182			TransactionLimits::WeightAndDeposit {
183				weight_limit: WEIGHT_LIMIT,
184				deposit_limit:
185					<<T as pallet::Config>::Currency as fungible::Inspect<_>>::total_issuance(),
186			},
187			data,
188			ExecConfig::new_substrate_tx(),
189		);
190		log::trace!(target: "whatiwant", "{weight_consumed}");
191		if let Ok(return_value) = result {
192			if return_value.did_revert() {
193				Err("Contract reverted".into())
194			} else {
195				let is_success =
196					bool::abi_decode_validate(&return_value.data).expect("Failed to ABI decode");
197				if is_success {
198					let balance = <Self as fungibles::Inspect<_>>::balance(asset_id, who);
199					Ok(balance)
200				} else {
201					Err("Contract transfer failed".into())
202				}
203			}
204		} else {
205			Err("Contract out of gas".into())
206		}
207	}
208
209	fn mint_into(
210		asset_id: Self::AssetId,
211		who: &T::AccountId,
212		amount: Self::Balance,
213	) -> Result<Self::Balance, DispatchError> {
214		let eth_address = T::AddressMapper::to_address(who);
215		let address = Address::from(Into::<[u8; 20]>::into(eth_address));
216		let data = IERC20::transferCall { to: address, value: EU256::from(amount) }.abi_encode();
217		let ContractResult { result, .. } = Self::bare_call(
218			OriginFor::<T>::signed(Self::checking_account()),
219			asset_id,
220			U256::zero(),
221			TransactionLimits::WeightAndDeposit {
222				weight_limit: WEIGHT_LIMIT,
223				deposit_limit:
224					<<T as pallet::Config>::Currency as fungible::Inspect<_>>::total_issuance(),
225			},
226			data,
227			ExecConfig::new_substrate_tx(),
228		);
229		if let Ok(return_value) = result {
230			if return_value.did_revert() {
231				Err("Contract reverted".into())
232			} else {
233				let is_success =
234					bool::abi_decode_validate(&return_value.data).expect("Failed to ABI decode");
235				if is_success {
236					let balance = <Self as fungibles::Inspect<_>>::balance(asset_id, who);
237					Ok(balance)
238				} else {
239					Err("Contract transfer failed".into())
240				}
241			}
242		} else {
243			Err("Contract out of gas".into())
244		}
245	}
246}
247
248// This impl is needed for implementing `fungibles::Mutate`.
249// However, we don't have this type of access to smart contracts.
250// Withdraw and deposit happen via the custom `fungibles::Mutate` impl above.
251// Because of this, all functions here return an error, when possible.
252impl<T: Config> fungibles::Unbalanced<<T as frame_system::Config>::AccountId> for Pallet<T> {
253	fn handle_raw_dust(_: Self::AssetId, _: Self::Balance) {}
254	fn handle_dust(_: fungibles::Dust<T::AccountId, Self>) {}
255	fn write_balance(
256		_: Self::AssetId,
257		_: &T::AccountId,
258		_: Self::Balance,
259	) -> Result<Option<Self::Balance>, DispatchError> {
260		Err(DispatchError::Unavailable)
261	}
262	fn set_total_issuance(_id: Self::AssetId, _amount: Self::Balance) {
263		// Empty.
264	}
265
266	fn decrease_balance(
267		_: Self::AssetId,
268		_: &T::AccountId,
269		_: Self::Balance,
270		_: Precision,
271		_: Preservation,
272		_: Fortitude,
273	) -> Result<Self::Balance, DispatchError> {
274		Err(DispatchError::Unavailable)
275	}
276
277	fn increase_balance(
278		_: Self::AssetId,
279		_: &T::AccountId,
280		_: Self::Balance,
281		_: Precision,
282	) -> Result<Self::Balance, DispatchError> {
283		Err(DispatchError::Unavailable)
284	}
285}
286
287#[cfg(test)]
288mod tests {
289	use super::*;
290	use crate::{
291		test_utils::{builder::*, ALICE},
292		tests::{Contracts, ExtBuilder, RuntimeOrigin, Test},
293		AccountInfoOf, Code,
294	};
295	use frame_support::assert_ok;
296	const ERC20_PVM_CODE: &[u8] = include_bytes!("../fixtures/erc20/erc20.polkavm");
297
298	#[test]
299	fn call_erc20_contract() {
300		ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
301			let _ =
302				<<Test as Config>::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000);
303			let code = ERC20_PVM_CODE.to_vec();
304			let amount = EU256::from(1000);
305			let constructor_data = sol_data::Uint::<256>::abi_encode(&amount);
306			let Contract { addr, .. } = BareInstantiateBuilder::<Test>::bare_instantiate(
307				RuntimeOrigin::signed(ALICE),
308				Code::Upload(code),
309			)
310			.data(constructor_data)
311			.build_and_unwrap_contract();
312			let result = BareCallBuilder::<Test>::bare_call(RuntimeOrigin::signed(ALICE), addr)
313				.data(IERC20::totalSupplyCall {}.abi_encode())
314				.build_and_unwrap_result();
315			let balance =
316				EU256::abi_decode_validate(&result.data).expect("Failed to decode ABI response");
317			assert_eq!(balance, EU256::from(amount));
318			// Contract is uploaded.
319			assert_eq!(AccountInfoOf::<Test>::contains_key(&addr), true);
320		});
321	}
322
323	#[test]
324	fn total_issuance_works() {
325		ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
326			let _ =
327				<<Test as Config>::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000);
328			let code = ERC20_PVM_CODE.to_vec();
329			let amount = 1000;
330			let constructor_data = sol_data::Uint::<256>::abi_encode(&EU256::from(amount));
331			let Contract { addr, .. } = BareInstantiateBuilder::<Test>::bare_instantiate(
332				RuntimeOrigin::signed(ALICE),
333				Code::Upload(code),
334			)
335			.data(constructor_data)
336			.build_and_unwrap_contract();
337
338			let total_issuance = <Contracts as fungibles::Inspect<_>>::total_issuance(addr);
339			assert_eq!(total_issuance, amount);
340		});
341	}
342
343	#[test]
344	fn get_balance_of_erc20() {
345		ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
346			let _ =
347				<<Test as Config>::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000);
348			let code = ERC20_PVM_CODE.to_vec();
349			let amount = 1000;
350			let constructor_data = sol_data::Uint::<256>::abi_encode(&EU256::from(amount));
351			let Contract { addr, .. } = BareInstantiateBuilder::<Test>::bare_instantiate(
352				RuntimeOrigin::signed(ALICE),
353				Code::Upload(code),
354			)
355			.data(constructor_data)
356			.build_and_unwrap_contract();
357			assert_eq!(<Contracts as fungibles::Inspect<_>>::balance(addr, &ALICE), amount);
358		});
359	}
360
361	#[test]
362	fn burn_from_impl_works() {
363		ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
364			let _ =
365				<<Test as Config>::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000);
366			let code = ERC20_PVM_CODE.to_vec();
367			let amount = 1000;
368			let constructor_data = sol_data::Uint::<256>::abi_encode(&(EU256::from(amount * 2)));
369			let Contract { addr, .. } = BareInstantiateBuilder::<Test>::bare_instantiate(
370				RuntimeOrigin::signed(ALICE),
371				Code::Upload(code),
372			)
373			.data(constructor_data)
374			.build_and_unwrap_contract();
375			let _ = BareCallBuilder::<Test>::bare_call(RuntimeOrigin::signed(ALICE), addr)
376				.build_and_unwrap_result();
377			assert_eq!(<Contracts as fungibles::Inspect<_>>::balance(addr, &ALICE), amount * 2);
378
379			// Use `fungibles::Mutate<_>::burn_from`.
380			assert_ok!(<Contracts as fungibles::Mutate<_>>::burn_from(
381				addr,
382				&ALICE,
383				amount,
384				Preservation::Expendable,
385				Precision::Exact,
386				Fortitude::Polite
387			));
388			assert_eq!(<Contracts as fungibles::Inspect<_>>::balance(addr, &ALICE), amount);
389		});
390	}
391
392	#[test]
393	fn mint_into_impl_works() {
394		ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
395			let checking_account = Pallet::<Test>::checking_account();
396			let _ =
397				<<Test as Config>::Currency as fungible::Mutate<_>>::set_balance(&ALICE, 1_000_000);
398			let _ = <<Test as Config>::Currency as fungible::Mutate<_>>::set_balance(
399				&checking_account,
400				1_000_000,
401			);
402			let code = ERC20_PVM_CODE.to_vec();
403			let amount = 1000;
404			let constructor_data = sol_data::Uint::<256>::abi_encode(&EU256::from(amount));
405			// We're instantiating the contract with the `CheckingAccount` so it has `amount` in it.
406			let Contract { addr, .. } = BareInstantiateBuilder::<Test>::bare_instantiate(
407				RuntimeOrigin::signed(checking_account.clone()),
408				Code::Upload(code),
409			)
410			.transaction_limits(TransactionLimits::WeightAndDeposit {
411				weight_limit: WEIGHT_LIMIT,
412				deposit_limit: 1_000_000_000_000,
413			})
414			.data(constructor_data)
415			.build_and_unwrap_contract();
416			assert_eq!(
417				<Contracts as fungibles::Inspect<_>>::balance(addr, &checking_account),
418				amount
419			);
420			assert_eq!(<Contracts as fungibles::Inspect<_>>::balance(addr, &ALICE), 0);
421
422			// We use `mint_into` to transfer assets from the checking account to `ALICE`.
423			assert_ok!(<Contracts as fungibles::Mutate<_>>::mint_into(addr, &ALICE, amount));
424			// Balances changed accordingly.
425			assert_eq!(<Contracts as fungibles::Inspect<_>>::balance(addr, &checking_account), 0);
426			assert_eq!(<Contracts as fungibles::Inspect<_>>::balance(addr, &ALICE), amount);
427		});
428	}
429}