1use crate::{weights::WeightInfo, Call, Config, PhantomData, TransferFlags};
19use alloc::vec::Vec;
20use ethereum_standards::{
21 IERC20,
22 IERC20::{IERC20Calls, IERC20Events},
23};
24use pallet_revive::precompiles::{
25 alloy::{
26 self,
27 primitives::IntoLogData,
28 sol_types::{Revert, SolCall},
29 },
30 AddressMapper, AddressMatcher, Error, Ext, Precompile, RuntimeCosts, H160, H256,
31};
32
33pub trait AssetIdExtractor {
35 type AssetId;
36 fn asset_id_from_address(address: &[u8; 20]) -> Result<Self::AssetId, Error>;
38}
39
40pub trait AssetPrecompileConfig {
42 const MATCHER: AddressMatcher;
44
45 type AssetIdExtractor: AssetIdExtractor;
47}
48
49pub struct InlineAssetIdExtractor;
51
52impl AssetIdExtractor for InlineAssetIdExtractor {
53 type AssetId = u32;
54 fn asset_id_from_address(addr: &[u8; 20]) -> Result<Self::AssetId, Error> {
55 let bytes: [u8; 4] = addr[0..4].try_into().expect("slice is 4 bytes; qed");
56 let index = u32::from_be_bytes(bytes);
57 return Ok(index.into());
58 }
59}
60
61pub struct InlineIdConfig<const PREFIX: u16>;
63
64impl<const P: u16> AssetPrecompileConfig for InlineIdConfig<P> {
65 const MATCHER: AddressMatcher = AddressMatcher::Prefix(core::num::NonZero::new(P).unwrap());
66 type AssetIdExtractor = InlineAssetIdExtractor;
67}
68
69pub struct ERC20<Runtime, PrecompileConfig, Instance = ()> {
71 _phantom: PhantomData<(Runtime, PrecompileConfig, Instance)>,
72}
73
74impl<Runtime, PrecompileConfig, Instance: 'static> Precompile
75 for ERC20<Runtime, PrecompileConfig, Instance>
76where
77 PrecompileConfig: AssetPrecompileConfig,
78 Runtime: crate::Config<Instance> + pallet_revive::Config,
79 <<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
80 Into<<Runtime as Config<Instance>>::AssetId>,
81 Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
82 alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
83
84 alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
86{
87 type T = Runtime;
88 type Interface = IERC20::IERC20Calls;
89 const MATCHER: AddressMatcher = PrecompileConfig::MATCHER;
90 const HAS_CONTRACT_INFO: bool = false;
91
92 fn call(
93 address: &[u8; 20],
94 input: &Self::Interface,
95 env: &mut impl Ext<T = Self::T>,
96 ) -> Result<Vec<u8>, Error> {
97 let asset_id = PrecompileConfig::AssetIdExtractor::asset_id_from_address(address)?.into();
98
99 match input {
100 IERC20Calls::transfer(call) => Self::transfer(asset_id, call, env),
101 IERC20Calls::totalSupply(_) => Self::total_supply(asset_id, env),
102 IERC20Calls::balanceOf(call) => Self::balance_of(asset_id, call, env),
103 IERC20Calls::allowance(call) => Self::allowance(asset_id, call, env),
104 IERC20Calls::approve(call) => Self::approve(asset_id, call, env),
105 IERC20Calls::transferFrom(call) => Self::transfer_from(asset_id, call, env),
106 }
107 }
108}
109
110const ERR_INVALID_CALLER: &str = "Invalid caller";
111const ERR_BALANCE_CONVERSION_FAILED: &str = "Balance conversion failed";
112
113impl<Runtime, PrecompileConfig, Instance: 'static> ERC20<Runtime, PrecompileConfig, Instance>
114where
115 PrecompileConfig: AssetPrecompileConfig,
116 Runtime: crate::Config<Instance> + pallet_revive::Config,
117 <<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
118 Into<<Runtime as Config<Instance>>::AssetId>,
119 Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
120 alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
121
122 alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
124{
125 fn caller(env: &mut impl Ext<T = Runtime>) -> Result<H160, Error> {
127 env.caller()
128 .account_id()
129 .map(<Runtime as pallet_revive::Config>::AddressMapper::to_address)
130 .map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_CALLER.into() }))
131 }
132
133 fn to_balance(
135 value: alloy::primitives::U256,
136 ) -> Result<<Runtime as Config<Instance>>::Balance, Error> {
137 value
138 .try_into()
139 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
140 }
141
142 fn to_u256(
145 value: <Runtime as Config<Instance>>::Balance,
146 ) -> Result<alloy::primitives::U256, Error> {
147 alloy::primitives::U256::try_from(value)
148 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
149 }
150
151 fn deposit_event(env: &mut impl Ext<T = Runtime>, event: IERC20Events) -> Result<(), Error> {
153 let (topics, data) = event.into_log_data().split();
154 let topics = topics.into_iter().map(|v| H256(v.0)).collect::<Vec<_>>();
155 env.gas_meter_mut().charge(RuntimeCosts::DepositEvent {
156 num_topic: topics.len() as u32,
157 len: topics.len() as u32,
158 })?;
159 env.deposit_event(topics, data.to_vec());
160 Ok(())
161 }
162
163 fn transfer(
165 asset_id: <Runtime as Config<Instance>>::AssetId,
166 call: &IERC20::transferCall,
167 env: &mut impl Ext<T = Runtime>,
168 ) -> Result<Vec<u8>, Error> {
169 env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer())?;
170
171 let from = Self::caller(env)?;
172 let dest = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(
173 &call.to.into_array().into(),
174 );
175
176 let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false };
177 crate::Pallet::<Runtime, Instance>::do_transfer(
178 asset_id,
179 &<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from),
180 &dest,
181 Self::to_balance(call.value)?,
182 None,
183 f,
184 )?;
185
186 Self::deposit_event(
187 env,
188 IERC20Events::Transfer(IERC20::Transfer {
189 from: from.0.into(),
190 to: call.to,
191 value: call.value,
192 }),
193 )?;
194
195 return Ok(IERC20::transferCall::abi_encode_returns(&true));
196 }
197
198 fn total_supply(
200 asset_id: <Runtime as Config<Instance>>::AssetId,
201 env: &mut impl Ext<T = Runtime>,
202 ) -> Result<Vec<u8>, Error> {
203 use frame_support::traits::fungibles::Inspect;
204 env.charge(<Runtime as Config<Instance>>::WeightInfo::total_issuance())?;
205
206 let value = Self::to_u256(crate::Pallet::<Runtime, Instance>::total_issuance(asset_id))?;
207 return Ok(IERC20::totalSupplyCall::abi_encode_returns(&value));
208 }
209
210 fn balance_of(
212 asset_id: <Runtime as Config<Instance>>::AssetId,
213 call: &IERC20::balanceOfCall,
214 env: &mut impl Ext<T = Runtime>,
215 ) -> Result<Vec<u8>, Error> {
216 env.charge(<Runtime as Config<Instance>>::WeightInfo::balance())?;
217 let account = call.account.into_array().into();
218 let account = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&account);
219 let value = Self::to_u256(crate::Pallet::<Runtime, Instance>::balance(asset_id, account))?;
220 return Ok(IERC20::balanceOfCall::abi_encode_returns(&value));
221 }
222
223 fn allowance(
225 asset_id: <Runtime as Config<Instance>>::AssetId,
226 call: &IERC20::allowanceCall,
227 env: &mut impl Ext<T = Runtime>,
228 ) -> Result<Vec<u8>, Error> {
229 env.charge(<Runtime as Config<Instance>>::WeightInfo::allowance())?;
230 use frame_support::traits::fungibles::approvals::Inspect;
231 let owner = call.owner.into_array().into();
232 let owner = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner);
233
234 let spender = call.spender.into_array().into();
235 let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
236 let value = Self::to_u256(crate::Pallet::<Runtime, Instance>::allowance(
237 asset_id, &owner, &spender,
238 ))?;
239
240 return Ok(IERC20::balanceOfCall::abi_encode_returns(&value));
241 }
242
243 fn approve(
245 asset_id: <Runtime as Config<Instance>>::AssetId,
246 call: &IERC20::approveCall,
247 env: &mut impl Ext<T = Runtime>,
248 ) -> Result<Vec<u8>, Error> {
249 env.charge(<Runtime as Config<Instance>>::WeightInfo::approve_transfer())?;
250 let owner = Self::caller(env)?;
251 let spender = call.spender.into_array().into();
252 let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
253
254 crate::Pallet::<Runtime, Instance>::do_approve_transfer(
255 asset_id,
256 &<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner),
257 &spender,
258 Self::to_balance(call.value)?,
259 )?;
260
261 Self::deposit_event(
262 env,
263 IERC20Events::Approval(IERC20::Approval {
264 owner: owner.0.into(),
265 spender: call.spender,
266 value: call.value,
267 }),
268 )?;
269
270 return Ok(IERC20::approveCall::abi_encode_returns(&true));
271 }
272
273 fn transfer_from(
275 asset_id: <Runtime as Config<Instance>>::AssetId,
276 call: &IERC20::transferFromCall,
277 env: &mut impl Ext<T = Runtime>,
278 ) -> Result<Vec<u8>, Error> {
279 env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer_approved())?;
280 let spender = Self::caller(env)?;
281 let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
282
283 let from = call.from.into_array().into();
284 let from = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from);
285
286 let to = call.to.into_array().into();
287 let to = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&to);
288
289 crate::Pallet::<Runtime, Instance>::do_transfer_approved(
290 asset_id,
291 &from,
292 &spender,
293 &to,
294 Self::to_balance(call.value)?,
295 )?;
296
297 Self::deposit_event(
298 env,
299 IERC20Events::Transfer(IERC20::Transfer {
300 from: call.from,
301 to: call.to,
302 value: call.value,
303 }),
304 )?;
305
306 return Ok(IERC20::transferFromCall::abi_encode_returns(&true));
307 }
308}
309
310#[cfg(test)]
311mod test {
312 use super::*;
313 use crate::{
314 mock::{new_test_ext, Assets, Balances, RuntimeEvent, RuntimeOrigin, System, Test},
315 precompiles::alloy::hex,
316 };
317 use alloy::primitives::U256;
318 use frame_support::{assert_ok, traits::Currency};
319 use pallet_revive::DepositLimit;
320 use sp_core::H160;
321 use sp_runtime::Weight;
322
323 fn assert_contract_event(contract: H160, event: IERC20Events) {
324 let (topics, data) = event.into_log_data().split();
325 let topics = topics.into_iter().map(|v| H256(v.0)).collect::<Vec<_>>();
326 System::assert_has_event(RuntimeEvent::Revive(pallet_revive::Event::ContractEmitted {
327 contract,
328 data: data.to_vec(),
329 topics,
330 }));
331 }
332
333 #[test]
334 fn asset_id_extractor_works() {
335 let address: [u8; 20] =
336 hex::const_decode_to_array(b"0000053900000000000000000000000001200000").unwrap();
337 assert!(InlineIdConfig::<0x0120>::MATCHER.matches(&address));
338 assert_eq!(
339 <InlineIdConfig<0x0120> as AssetPrecompileConfig>::AssetIdExtractor::asset_id_from_address(
340 &address
341 )
342 .unwrap(),
343 1337u32
344 );
345 }
346
347 #[test]
348 fn precompile_transfer_works() {
349 new_test_ext().execute_with(|| {
350 let asset_id = 0u32;
351 let asset_addr = H160::from(
352 hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap(),
353 );
354
355 let from = 123456789;
356 let to = 987654321;
357
358 Balances::make_free_balance_be(&from, 100);
359 Balances::make_free_balance_be(&to, 100);
360
361 let from_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&from);
362 let to_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&to);
363 assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, from, true, 1));
364 assert_ok!(Assets::mint(RuntimeOrigin::signed(from), asset_id, from, 100));
365
366 let data =
367 IERC20::transferCall { to: to_addr.0.into(), value: U256::from(10) }.abi_encode();
368
369 pallet_revive::Pallet::<Test>::bare_call(
370 RuntimeOrigin::signed(from),
371 H160::from(asset_addr),
372 0u32.into(),
373 Weight::MAX,
374 DepositLimit::UnsafeOnlyForDryRun,
375 data,
376 );
377
378 assert_contract_event(
379 asset_addr,
380 IERC20Events::Transfer(IERC20::Transfer {
381 from: from_addr.0.into(),
382 to: to_addr.0.into(),
383 value: U256::from(10),
384 }),
385 );
386
387 assert_eq!(Assets::balance(asset_id, from), 90);
388 assert_eq!(Assets::balance(asset_id, to), 10);
389 });
390 }
391
392 #[test]
393 fn total_supply_works() {
394 new_test_ext().execute_with(|| {
395 let asset_id = 0u32;
396 let asset_addr =
397 hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap();
398
399 let owner = 123456789;
400
401 Balances::make_free_balance_be(&owner, 100);
402 assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
403 assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 1000));
404
405 let data = IERC20::totalSupplyCall {}.abi_encode();
406
407 let data = pallet_revive::Pallet::<Test>::bare_call(
408 RuntimeOrigin::signed(owner),
409 H160::from(asset_addr),
410 0u32.into(),
411 Weight::MAX,
412 DepositLimit::UnsafeOnlyForDryRun,
413 data,
414 )
415 .result
416 .unwrap()
417 .data;
418
419 let ret = IERC20::totalSupplyCall::abi_decode_returns(&data).unwrap();
420 assert_eq!(ret, U256::from(1000));
421 });
422 }
423
424 #[test]
425 fn balance_of_works() {
426 new_test_ext().execute_with(|| {
427 let asset_id = 0u32;
428 let asset_addr =
429 hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap();
430
431 let owner = 123456789;
432
433 assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
434 assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 1000));
435
436 let account =
437 <Test as pallet_revive::Config>::AddressMapper::to_address(&owner).0.into();
438 let data = IERC20::balanceOfCall { account }.abi_encode();
439
440 let data = pallet_revive::Pallet::<Test>::bare_call(
441 RuntimeOrigin::signed(owner),
442 H160::from(asset_addr),
443 0u32.into(),
444 Weight::MAX,
445 DepositLimit::UnsafeOnlyForDryRun,
446 data,
447 )
448 .result
449 .unwrap()
450 .data;
451
452 let ret = IERC20::balanceOfCall::abi_decode_returns(&data).unwrap();
453 assert_eq!(ret, U256::from(1000));
454 });
455 }
456
457 #[test]
458 fn approval_works() {
459 use frame_support::traits::fungibles::approvals::Inspect;
460
461 new_test_ext().execute_with(|| {
462 let asset_id = 0u32;
463 let asset_addr = H160::from(
464 hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap(),
465 );
466
467 let owner = 123456789;
468 let spender = 987654321;
469 let other = 1122334455;
470
471 Balances::make_free_balance_be(&owner, 100);
472 Balances::make_free_balance_be(&spender, 100);
473 Balances::make_free_balance_be(&other, 100);
474
475 let owner_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&owner);
476 let spender_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&spender);
477 let other_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&other);
478
479 assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
480 assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 100));
481
482 let data =
483 IERC20::approveCall { spender: spender_addr.0.into(), value: U256::from(25) }
484 .abi_encode();
485
486 pallet_revive::Pallet::<Test>::bare_call(
487 RuntimeOrigin::signed(owner),
488 H160::from(asset_addr),
489 0u32.into(),
490 Weight::MAX,
491 DepositLimit::UnsafeOnlyForDryRun,
492 data,
493 );
494
495 assert_contract_event(
496 asset_addr,
497 IERC20Events::Approval(IERC20::Approval {
498 owner: owner_addr.0.into(),
499 spender: spender_addr.0.into(),
500 value: U256::from(25),
501 }),
502 );
503
504 let data = IERC20::allowanceCall {
505 owner: owner_addr.0.into(),
506 spender: spender_addr.0.into(),
507 }
508 .abi_encode();
509
510 let data = pallet_revive::Pallet::<Test>::bare_call(
511 RuntimeOrigin::signed(owner),
512 H160::from(asset_addr),
513 0u32.into(),
514 Weight::MAX,
515 DepositLimit::UnsafeOnlyForDryRun,
516 data,
517 )
518 .result
519 .unwrap()
520 .data;
521
522 let ret = IERC20::allowanceCall::abi_decode_returns(&data).unwrap();
523 assert_eq!(ret, U256::from(25));
524
525 let data = IERC20::transferFromCall {
526 from: owner_addr.0.into(),
527 to: other_addr.0.into(),
528 value: U256::from(10),
529 }
530 .abi_encode();
531
532 pallet_revive::Pallet::<Test>::bare_call(
533 RuntimeOrigin::signed(spender),
534 H160::from(asset_addr),
535 0u32.into(),
536 Weight::MAX,
537 DepositLimit::UnsafeOnlyForDryRun,
538 data,
539 );
540 assert_eq!(Assets::balance(asset_id, owner), 90);
541 assert_eq!(Assets::allowance(asset_id, &owner, &spender), 15);
542 assert_eq!(Assets::balance(asset_id, other), 10);
543
544 assert_contract_event(
545 asset_addr,
546 IERC20Events::Transfer(IERC20::Transfer {
547 from: owner_addr.0.into(),
548 to: other_addr.0.into(),
549 value: U256::from(10),
550 }),
551 );
552 });
553 }
554}