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