pallet_evm_precompile_assets_erc20/
lib.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19// Copyright 2019-2022 PureStake Inc.
20// Copyright 2022      Stake Technologies
21// This file is part of AssetsERC20 package, originally developed by Purestake Inc.
22// AssetsERC20 package used in Astar Network in terms of GPLv3.
23//
24// AssetsERC20 is free software: you can redistribute it and/or modify
25// it under the terms of the GNU General Public License as published by
26// the Free Software Foundation, either version 3 of the License, or
27// (at your option) any later version.
28
29// AssetsERC20 is distributed in the hope that it will be useful,
30// but WITHOUT ANY WARRANTY; without even the implied warranty of
31// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
32// GNU General Public License for more details.
33
34// You should have received a copy of the GNU General Public License
35// along with AssetsERC20.  If not, see <http://www.gnu.org/licenses/>.
36
37#![cfg_attr(not(feature = "std"), no_std)]
38
39use fp_evm::{ExitError, PrecompileHandle};
40use frame_support::{
41    dispatch::{GetDispatchInfo, PostDispatchInfo},
42    traits::{
43        fungibles::{
44            approvals::Inspect as ApprovalInspect, metadata::Inspect as MetadataInspect, Inspect,
45        },
46        OriginTrait,
47    },
48    DefaultNoBound,
49};
50use pallet_evm::AddressMapping;
51use precompile_utils::prelude::*;
52use sp_runtime::traits::{Bounded, Dispatchable, StaticLookup};
53
54use sp_core::{Get, MaxEncodedLen, H160, U256};
55use sp_std::{
56    convert::{TryFrom, TryInto},
57    marker::PhantomData,
58};
59
60#[cfg(test)]
61mod mock;
62#[cfg(test)]
63mod tests;
64
65/// Solidity selector of the Transfer log, which is the Keccak of the Log signature.
66pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)");
67
68/// Solidity selector of the Approval log, which is the Keccak of the Log signature.
69pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)");
70
71/// Alias for the Balance type for the provided Runtime and Instance.
72pub type BalanceOf<Runtime, Instance = ()> = <Runtime as pallet_assets::Config<Instance>>::Balance;
73
74/// Alias for the Asset Id type for the provided Runtime and Instance.
75pub type AssetIdOf<Runtime, Instance = ()> = <Runtime as pallet_assets::Config<Instance>>::AssetId;
76
77/// This trait ensure we can convert EVM address to AssetIds
78/// We will require Runtime to have this trait implemented
79pub trait AddressToAssetId<AssetId> {
80    // Get assetId from address
81    fn address_to_asset_id(address: H160) -> Option<AssetId>;
82
83    // Get address from AssetId
84    fn asset_id_to_address(asset_id: AssetId) -> H160;
85}
86
87/// The following distribution has been decided for the precompiles
88/// 0-1023: Ethereum Mainnet Precompiles
89/// 1024-2047 Precompiles that are not in Ethereum Mainnet but are neither Astar specific
90/// 2048-4095 Astar specific precompiles
91/// Asset precompiles can only fall between
92///     0xFFFFFFFF00000000000000000000000000000000 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
93/// The precompile for AssetId X, where X is a u128 (i.e.16 bytes), if 0XFFFFFFFF + Bytes(AssetId)
94/// In order to route the address to Erc20AssetsPrecompile<R>, we first check whether the AssetId
95/// exists in pallet-assets
96/// We cannot do this right now, so instead we check whether the total supply is zero. If so, we
97/// do not route to the precompiles
98
99/// This means that every address that starts with 0xFFFFFFFF will go through an additional db read,
100/// but the probability for this to happen is 2^-32 for random addresses
101#[derive(Clone, DefaultNoBound)]
102pub struct Erc20AssetsPrecompileSet<Runtime, Instance: 'static = ()>(
103    PhantomData<(Runtime, Instance)>,
104);
105
106impl<Runtime, Instance> Erc20AssetsPrecompileSet<Runtime, Instance> {
107    pub fn new() -> Self {
108        Self(PhantomData)
109    }
110}
111
112#[precompile_utils::precompile]
113#[precompile::precompile_set]
114#[precompile::test_concrete_types(mock::Runtime, ())]
115impl<Runtime, Instance> Erc20AssetsPrecompileSet<Runtime, Instance>
116where
117    Instance: 'static,
118    Runtime: pallet_assets::Config<Instance> + pallet_evm::Config + frame_system::Config,
119    Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
120    Runtime::RuntimeCall: From<pallet_assets::Call<Runtime, Instance>>,
121    <Runtime::RuntimeCall as Dispatchable>::RuntimeOrigin: From<Option<Runtime::AccountId>>,
122    BalanceOf<Runtime, Instance>: TryFrom<U256> + Into<U256> + solidity::Codec,
123    Runtime: AddressToAssetId<AssetIdOf<Runtime, Instance>>,
124    <<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin: OriginTrait,
125    AssetIdOf<Runtime, Instance>: Copy,
126    <Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
127{
128    /// PrecompileSet discriminant. Allows to knows if the address maps to an asset id,
129    /// and if this is the case which one.
130    #[precompile::discriminant]
131    fn discriminant(address: H160, gas: u64) -> DiscriminantResult<AssetIdOf<Runtime, Instance>> {
132        let extra_cost = RuntimeHelper::<Runtime>::db_read_gas_cost();
133        if gas < extra_cost {
134            return DiscriminantResult::OutOfGas;
135        }
136
137        let asset_id = match Runtime::address_to_asset_id(address) {
138            Some(asset_id) => asset_id,
139            None => return DiscriminantResult::None(extra_cost),
140        };
141
142        if pallet_assets::Pallet::<Runtime, Instance>::maybe_total_supply(asset_id).is_some() {
143            DiscriminantResult::Some(asset_id, extra_cost)
144        } else {
145            DiscriminantResult::None(extra_cost)
146        }
147    }
148
149    #[precompile::public("totalSupply()")]
150    #[precompile::view]
151    fn total_supply(
152        asset_id: AssetIdOf<Runtime, Instance>,
153        handle: &mut impl PrecompileHandle,
154    ) -> EvmResult<U256> {
155        // TODO: benchmark this function so we can measure ref time & PoV correctly
156        // Storage item: Asset:
157        // Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(32)) + (3 * Balance(16)) + 15)
158        handle.record_db_read::<Runtime>(223)?;
159
160        Ok(pallet_assets::Pallet::<Runtime, Instance>::total_issuance(asset_id).into())
161    }
162
163    #[precompile::public("balanceOf(address)")]
164    #[precompile::view]
165    fn balance_of(
166        asset_id: AssetIdOf<Runtime, Instance>,
167        handle: &mut impl PrecompileHandle,
168        who: Address,
169    ) -> EvmResult<U256> {
170        // TODO: benchmark this function so we can measure ref time & PoV correctly
171        // Storage item: Account:
172        // Blake2_128(16) + AssetId(16) + Blake2_128(16) + AccountId(32) + AssetAccount(19 + Extra)
173        handle.record_db_read::<Runtime>(
174            99 + <Runtime as pallet_assets::Config<Instance>>::Extra::max_encoded_len(),
175        )?;
176
177        let who: H160 = who.into();
178
179        // Fetch info.
180        let amount: U256 = {
181            let who: Runtime::AccountId = Runtime::AddressMapping::into_account_id(who);
182            pallet_assets::Pallet::<Runtime, Instance>::balance(asset_id, &who).into()
183        };
184
185        // Build output.
186        Ok(amount)
187    }
188
189    #[precompile::public("allowance(address,address)")]
190    #[precompile::view]
191    fn allowance(
192        asset_id: AssetIdOf<Runtime, Instance>,
193        handle: &mut impl PrecompileHandle,
194        owner: Address,
195        spender: Address,
196    ) -> EvmResult<U256> {
197        // TODO: benchmark this function so we can measure ref time & PoV correctly
198        // Storage item: Approvals:
199        // Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(32)) + Approval(32)
200        handle.record_db_read::<Runtime>(148)?;
201
202        let owner: H160 = owner.into();
203        let spender: H160 = spender.into();
204
205        // Fetch info.
206        let amount: U256 = {
207            let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner);
208            let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender);
209
210            // Fetch info.
211            pallet_assets::Pallet::<Runtime, Instance>::allowance(asset_id, &owner, &spender).into()
212        };
213
214        // Build output.
215        Ok(amount)
216    }
217
218    #[precompile::public("approve(address,uint256)")]
219    fn approve(
220        asset_id: AssetIdOf<Runtime, Instance>,
221        handle: &mut impl PrecompileHandle,
222        spender: Address,
223        value: U256,
224    ) -> EvmResult<bool> {
225        handle.record_log_costs_manual(3, 32)?;
226
227        let spender: H160 = spender.into();
228
229        Self::approve_inner(asset_id, handle, handle.context().caller, spender, value)?;
230
231        log3(
232            handle.context().address,
233            SELECTOR_LOG_APPROVAL,
234            handle.context().caller,
235            spender,
236            solidity::encode_event_data(value),
237        )
238        .record(handle)?;
239
240        // Build output.
241        Ok(true)
242    }
243
244    fn approve_inner(
245        asset_id: AssetIdOf<Runtime, Instance>,
246        handle: &mut impl PrecompileHandle,
247        owner: H160,
248        spender: H160,
249        value: U256,
250    ) -> EvmResult {
251        let owner = Runtime::AddressMapping::into_account_id(owner);
252        let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender);
253        // Amount saturate if too high.
254        let amount: BalanceOf<Runtime, Instance> =
255            value.try_into().unwrap_or_else(|_| Bounded::max_value());
256
257        // Storage item: Approvals:
258        // Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(32)) + Approval(32)
259        handle.record_db_read::<Runtime>(148)?;
260
261        // If previous approval exists, we need to clean it
262        if pallet_assets::Pallet::<Runtime, Instance>::allowance(asset_id, &owner, &spender)
263            != 0u32.into()
264        {
265            RuntimeHelper::<Runtime>::try_dispatch(
266                handle,
267                Some(owner.clone()).into(),
268                pallet_assets::Call::<Runtime, Instance>::cancel_approval {
269                    id: asset_id.into(),
270                    delegate: Runtime::Lookup::unlookup(spender.clone()),
271                },
272                0,
273            )?;
274        }
275        // Dispatch call (if enough gas).
276        RuntimeHelper::<Runtime>::try_dispatch(
277            handle,
278            Some(owner).into(),
279            pallet_assets::Call::<Runtime, Instance>::approve_transfer {
280                id: asset_id.into(),
281                delegate: Runtime::Lookup::unlookup(spender),
282                amount,
283            },
284            0,
285        )?;
286
287        Ok(())
288    }
289
290    #[precompile::public("transfer(address,uint256)")]
291    fn transfer(
292        asset_id: AssetIdOf<Runtime, Instance>,
293        handle: &mut impl PrecompileHandle,
294        to: Address,
295        value: U256,
296    ) -> EvmResult<bool> {
297        handle.record_log_costs_manual(3, 32)?;
298
299        let to: H160 = to.into();
300        let value = Self::u256_to_amount(value).in_field("value")?;
301
302        // Build call with origin.
303        {
304            let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
305            let to = Runtime::AddressMapping::into_account_id(to);
306
307            // Dispatch call (if enough gas).
308            RuntimeHelper::<Runtime>::try_dispatch(
309                handle,
310                Some(origin).into(),
311                pallet_assets::Call::<Runtime, Instance>::transfer {
312                    id: asset_id.into(),
313                    target: Runtime::Lookup::unlookup(to),
314                    amount: value,
315                },
316                0,
317            )?;
318        }
319
320        log3(
321            handle.context().address,
322            SELECTOR_LOG_TRANSFER,
323            handle.context().caller,
324            to,
325            solidity::encode_event_data(value),
326        )
327        .record(handle)?;
328
329        Ok(true)
330    }
331
332    #[precompile::public("transferFrom(address,address,uint256)")]
333    fn transfer_from(
334        asset_id: AssetIdOf<Runtime, Instance>,
335        handle: &mut impl PrecompileHandle,
336        from: Address,
337        to: Address,
338        value: U256,
339    ) -> EvmResult<bool> {
340        handle.record_log_costs_manual(3, 32)?;
341
342        let from: H160 = from.into();
343        let to: H160 = to.into();
344        let value = Self::u256_to_amount(value).in_field("value")?;
345
346        {
347            let caller: Runtime::AccountId =
348                Runtime::AddressMapping::into_account_id(handle.context().caller);
349            let from: Runtime::AccountId = Runtime::AddressMapping::into_account_id(from);
350            let to: Runtime::AccountId = Runtime::AddressMapping::into_account_id(to);
351
352            // If caller is "from", it can spend as much as it wants from its own balance.
353            if caller != from {
354                // Dispatch call (if enough gas).
355                RuntimeHelper::<Runtime>::try_dispatch(
356                    handle,
357                    Some(caller).into(),
358                    pallet_assets::Call::<Runtime, Instance>::transfer_approved {
359                        id: asset_id.into(),
360                        owner: Runtime::Lookup::unlookup(from),
361                        destination: Runtime::Lookup::unlookup(to),
362                        amount: value,
363                    },
364                    0,
365                )?;
366            } else {
367                // Dispatch call (if enough gas).
368                RuntimeHelper::<Runtime>::try_dispatch(
369                    handle,
370                    Some(from).into(),
371                    pallet_assets::Call::<Runtime, Instance>::transfer {
372                        id: asset_id.into(),
373                        target: Runtime::Lookup::unlookup(to),
374                        amount: value,
375                    },
376                    0,
377                )?;
378            }
379        }
380
381        log3(
382            handle.context().address,
383            SELECTOR_LOG_TRANSFER,
384            from,
385            to,
386            solidity::encode_event_data(value),
387        )
388        .record(handle)?;
389
390        // Build output.
391        Ok(true)
392    }
393
394    #[precompile::public("name()")]
395    #[precompile::view]
396    fn name(
397        asset_id: AssetIdOf<Runtime, Instance>,
398        handle: &mut impl PrecompileHandle,
399    ) -> EvmResult<UnboundedBytes> {
400        // Storage item: Metadata:
401        // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit)
402        // + symbol(StringLimit) + decimals(1) + is_frozen(1)]
403        handle.record_db_read::<Runtime>(
404            50 + (2 * <Runtime as pallet_assets::Config<Instance>>::StringLimit::get()) as usize,
405        )?;
406
407        let name = pallet_assets::Pallet::<Runtime, Instance>::name(asset_id)
408            .as_slice()
409            .into();
410
411        Ok(name)
412    }
413
414    #[precompile::public("symbol()")]
415    #[precompile::view]
416    fn symbol(
417        asset_id: AssetIdOf<Runtime, Instance>,
418        handle: &mut impl PrecompileHandle,
419    ) -> EvmResult<UnboundedBytes> {
420        // Storage item: Metadata:
421        // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit)
422        // + symbol(StringLimit) + decimals(1) + is_frozen(1)]
423        handle.record_db_read::<Runtime>(
424            50 + (2 * <Runtime as pallet_assets::Config<Instance>>::StringLimit::get()) as usize,
425        )?;
426
427        let symbol = pallet_assets::Pallet::<Runtime, Instance>::symbol(asset_id)
428            .as_slice()
429            .into();
430
431        Ok(symbol)
432    }
433
434    #[precompile::public("decimals()")]
435    #[precompile::view]
436    fn decimals(
437        asset_id: AssetIdOf<Runtime, Instance>,
438        handle: &mut impl PrecompileHandle,
439    ) -> EvmResult<u8> {
440        // Storage item: Metadata:
441        // Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit)
442        // + symbol(StringLimit) + decimals(1) + is_frozen(1)]
443        handle.record_db_read::<Runtime>(
444            50 + (2 * <Runtime as pallet_assets::Config<Instance>>::StringLimit::get()) as usize,
445        )?;
446
447        Ok(pallet_assets::Pallet::<Runtime, Instance>::decimals(
448            asset_id,
449        ))
450    }
451
452    #[precompile::public("minimumBalance()")]
453    #[precompile::view]
454    fn minimum_balance(
455        asset_id: AssetIdOf<Runtime, Instance>,
456        handle: &mut impl PrecompileHandle,
457    ) -> EvmResult<U256> {
458        // TODO: benchmark this function so we can measure ref time & PoV correctly
459        // Storage item: AssetDetails:
460        // Blake2_128(16) + AssetDetails((4 * AccountId(32)) + (3 * Balance(16)) + 15)
461        handle.record_db_read::<Runtime>(207)?;
462
463        Ok(pallet_assets::Pallet::<Runtime, Instance>::minimum_balance(asset_id).into())
464    }
465
466    #[precompile::public("mint(address,uint256)")]
467    fn mint(
468        asset_id: AssetIdOf<Runtime, Instance>,
469        handle: &mut impl PrecompileHandle,
470        to: Address,
471        value: U256,
472    ) -> EvmResult<bool> {
473        handle.record_log_costs_manual(3, 32)?;
474
475        let to: H160 = to.into();
476        let value = Self::u256_to_amount(value).in_field("value")?;
477
478        // Build call with origin.
479        {
480            let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
481            let to = Runtime::AddressMapping::into_account_id(to);
482
483            // Dispatch call (if enough gas).
484            RuntimeHelper::<Runtime>::try_dispatch(
485                handle,
486                Some(origin).into(),
487                pallet_assets::Call::<Runtime, Instance>::mint {
488                    id: asset_id.into(),
489                    beneficiary: Runtime::Lookup::unlookup(to),
490                    amount: value,
491                },
492                0,
493            )?;
494        }
495
496        log3(
497            handle.context().address,
498            SELECTOR_LOG_TRANSFER,
499            H160::default(),
500            to,
501            solidity::encode_event_data(value),
502        )
503        .record(handle)?;
504
505        Ok(true)
506    }
507
508    #[precompile::public("burn(address,uint256)")]
509    fn burn(
510        asset_id: AssetIdOf<Runtime, Instance>,
511        handle: &mut impl PrecompileHandle,
512        from: Address,
513        value: U256,
514    ) -> EvmResult<bool> {
515        handle.record_log_costs_manual(3, 32)?;
516
517        let from: H160 = from.into();
518        let value = Self::u256_to_amount(value).in_field("value")?;
519
520        // Build call with origin.
521        {
522            let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
523            let from = Runtime::AddressMapping::into_account_id(from);
524
525            // Dispatch call (if enough gas).
526            RuntimeHelper::<Runtime>::try_dispatch(
527                handle,
528                Some(origin).into(),
529                pallet_assets::Call::<Runtime, Instance>::burn {
530                    id: asset_id.into(),
531                    who: Runtime::Lookup::unlookup(from),
532                    amount: value,
533                },
534                0,
535            )?;
536        }
537
538        log3(
539            handle.context().address,
540            SELECTOR_LOG_TRANSFER,
541            from,
542            H160::default(),
543            solidity::encode_event_data(value),
544        )
545        .record(handle)?;
546
547        Ok(true)
548    }
549
550    fn u256_to_amount(value: U256) -> MayRevert<BalanceOf<Runtime, Instance>> {
551        value
552            .try_into()
553            .map_err(|_| RevertReason::value_is_too_large("balance type").into())
554    }
555}