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