Lines
100 %
Functions
Branches
// Copyright 2019-2025 PureStake Inc.
// This file is part of Moonbeam.
// Moonbeam is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Moonbeam is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Moonbeam. If not, see <http://www.gnu.org/licenses/>.
use std::str::from_utf8;
use crate::{eip2612::Eip2612, mock::*, *};
use libsecp256k1::{sign, Message, SecretKey};
use precompile_utils::testing::*;
use sha3::{Digest, Keccak256};
use sp_core::{H256, U256};
// No test of invalid selectors since we have a fallback behavior (deposit).
fn precompiles() -> Precompiles<Runtime> {
PrecompilesValue::get()
}
#[test]
fn selectors() {
assert!(PCall::balance_of_selectors().contains(&0x70a08231));
assert!(PCall::total_supply_selectors().contains(&0x18160ddd));
assert!(PCall::approve_selectors().contains(&0x095ea7b3));
assert!(PCall::allowance_selectors().contains(&0xdd62ed3e));
assert!(PCall::transfer_selectors().contains(&0xa9059cbb));
assert!(PCall::transfer_from_selectors().contains(&0x23b872dd));
assert!(PCall::name_selectors().contains(&0x06fdde03));
assert!(PCall::symbol_selectors().contains(&0x95d89b41));
assert!(PCall::deposit_selectors().contains(&0xd0e30db0));
assert!(PCall::withdraw_selectors().contains(&0x2e1a7d4d));
assert!(PCall::eip2612_nonces_selectors().contains(&0x7ecebe00));
assert!(PCall::eip2612_permit_selectors().contains(&0xd505accf));
assert!(PCall::eip2612_domain_separator_selectors().contains(&0x3644e515));
assert_eq!(
crate::SELECTOR_LOG_TRANSFER,
&Keccak256::digest(b"Transfer(address,address,uint256)")[..]
);
crate::SELECTOR_LOG_APPROVAL,
&Keccak256::digest(b"Approval(address,address,uint256)")[..]
crate::SELECTOR_LOG_DEPOSIT,
&Keccak256::digest(b"Deposit(address,uint256)")[..]
crate::SELECTOR_LOG_WITHDRAWAL,
&Keccak256::digest(b"Withdrawal(address,uint256)")[..]
fn modifiers() {
ExtBuilder::default()
.with_balances(vec![(CryptoAlith.into(), 1000)])
.build()
.execute_with(|| {
let mut tester =
PrecompilesModifierTester::new(precompiles(), CryptoAlith, Precompile1);
tester.test_view_modifier(PCall::balance_of_selectors());
tester.test_view_modifier(PCall::total_supply_selectors());
tester.test_default_modifier(PCall::approve_selectors());
tester.test_view_modifier(PCall::allowance_selectors());
tester.test_default_modifier(PCall::transfer_selectors());
tester.test_default_modifier(PCall::transfer_from_selectors());
tester.test_view_modifier(PCall::name_selectors());
tester.test_view_modifier(PCall::symbol_selectors());
tester.test_view_modifier(PCall::decimals_selectors());
tester.test_payable_modifier(PCall::deposit_selectors());
tester.test_default_modifier(PCall::withdraw_selectors());
tester.test_view_modifier(PCall::eip2612_nonces_selectors());
tester.test_default_modifier(PCall::eip2612_permit_selectors());
tester.test_view_modifier(PCall::eip2612_domain_separator_selectors());
});
fn get_total_supply() {
.with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)])
precompiles()
.prepare_test(CryptoAlith, Precompile1, PCall::total_supply {})
.expect_cost(0) // TODO: Test db read/write costs
.expect_no_logs()
.execute_returns(U256::from(3500u64));
fn get_balances_known_user() {
.prepare_test(
CryptoAlith,
Precompile1,
PCall::balance_of {
owner: Address(CryptoAlith.into()),
},
)
.execute_returns(U256::from(1000u64));
fn get_balances_unknown_user() {
owner: Address(Bob.into()),
.execute_returns(U256::from(0u64));
fn approve() {
PCall::approve {
spender: Address(Bob.into()),
value: 500.into(),
.expect_cost(1756)
.expect_log(log3(
SELECTOR_LOG_APPROVAL,
Bob,
solidity::encode_event_data(U256::from(500)),
))
.execute_returns(true);
fn approve_saturating() {
value: U256::MAX,
.expect_cost(1756u64)
solidity::encode_event_data(U256::MAX),
PCall::allowance {
.expect_cost(0)
.execute_returns(U256::from(u128::MAX));
fn check_allowance_existing() {
.execute_some();
.execute_returns(U256::from(500u64));
fn check_allowance_not_existing() {
fn transfer() {
PCall::transfer {
to: Address(Bob.into()),
value: 400.into(),
.expect_cost(176106756) // 1 weight => 1 gas in mock
SELECTOR_LOG_TRANSFER,
solidity::encode_event_data(U256::from(400)),
.execute_returns(U256::from(600));
.execute_returns(U256::from(400));
fn transfer_not_enough_funds() {
.with_balances(vec![
(CryptoAlith.into(), 1000),
(CryptoBaltathar.into(), 1000),
])
value: 1400.into(),
.execute_reverts(|output| {
from_utf8(&output)
.unwrap()
.contains("Dispatched call failed with error: ")
&& from_utf8(&output).unwrap().contains("FundsUnavailable")
fn transfer_from() {
PCall::transfer_from {
from: Address(CryptoAlith.into()),
.execute_returns(U256::from(100u64));
fn transfer_from_above_allowance() {
value: 300.into(),
Bob, // Bob is the one sending transferFrom!
.execute_reverts(|output| output == b"trying to spend more than allowed");
fn transfer_from_self() {
CryptoAlith, // CryptoAlith sending transferFrom herself, no need for allowance.
fn get_metadata_name() {
.prepare_test(CryptoAlith, Precompile1, PCall::name {})
.execute_returns(UnboundedBytes::from("Mock token"));
fn get_metadata_symbol() {
.prepare_test(CryptoAlith, Precompile1, PCall::symbol {})
.execute_returns(UnboundedBytes::from("MOCK"));
fn get_metadata_decimals() {
.prepare_test(CryptoAlith, Precompile1, PCall::decimals {})
.execute_returns(18u8);
fn deposit(data: Vec<u8>) {
// Check precompile balance is 0.
owner: Address(Precompile1.into()),
.execute_returns(U256::from(0));
// Deposit
// We need to call using EVM pallet so we can check the EVM correctly sends the amount
// to the precompile.
Evm::call(
RuntimeOrigin::root(),
CryptoAlith.into(),
Precompile1.into(),
data,
From::from(500), // amount sent
u64::MAX, // gas limit
0u32.into(), // gas price
None, // max priority
None, // nonce
vec![], // access list
.expect("it works");
events(),
vec![
RuntimeEvent::System(frame_system::Event::NewAccount {
account: Precompile1.into()
}),
RuntimeEvent::Balances(pallet_balances::Event::Endowed {
account: Precompile1.into(),
free_balance: 500
// EVM make a transfer because some value is provided.
RuntimeEvent::Balances(pallet_balances::Event::Transfer {
from: CryptoAlith.into(),
to: Precompile1.into(),
amount: 500
// Precompile1 send it back since deposit should be a no-op.
from: Precompile1.into(),
to: CryptoAlith.into(),
// Log is correctly emited.
RuntimeEvent::Evm(pallet_evm::Event::Log {
log: log2(
SELECTOR_LOG_DEPOSIT,
RuntimeEvent::Evm(pallet_evm::Event::Executed {
address: Precompile1.into()
]
// Check precompile balance is still 0.
// Check CryptoAlith balance is still 1000.
.execute_returns(U256::from(1000));
fn deposit_function() {
deposit(PCall::deposit {}.into())
fn deposit_fallback() {
deposit(solidity::encode_with_selector(0x01234567u32, ()))
fn deposit_receive() {
deposit(vec![])
fn deposit_zero() {
PCall::deposit {}.into(),
From::from(0), // amount sent
vec![RuntimeEvent::Evm(pallet_evm::Event::ExecutedFailed {
}),]
fn withdraw() {
// Withdraw
PCall::withdraw { value: 500.into() },
.expect_cost(1381)
.expect_log(log2(
SELECTOR_LOG_WITHDRAWAL,
.execute_returns(());
fn withdraw_more_than_owned() {
PCall::withdraw { value: 1001.into() },
.execute_reverts(|output| output == b"Trying to withdraw more than owned");
fn permit_valid() {
let owner: H160 = CryptoAlith.into();
let spender: H160 = Bob.into();
let value: U256 = 500u16.into();
let deadline: U256 = 0u8.into(); // todo: proper timestamp
let permit = Eip2612::<Runtime, NativeErc20Metadata>::generate_permit(
owner,
spender,
value,
0u8.into(), // nonce
deadline,
let secret_key = SecretKey::parse(&alith_secret_key()).unwrap();
let message = Message::parse(&permit);
let (rs, v) = sign(&message, &secret_key);
PCall::eip2612_nonces {
.execute_returns(U256::from(0u8));
Charlie, // can be anyone
PCall::eip2612_permit {
owner: Address(owner),
spender: Address(spender),
v: v.serialize(),
r: rs.r.b32().into(),
s: rs.s.b32().into(),
solidity::encode_event_data(U256::from(value)),
.execute_returns(U256::from(500u16));
.execute_returns(U256::from(1u8));
fn permit_invalid_nonce() {
let deadline: U256 = 0u8.into();
1u8.into(), // nonce
.execute_reverts(|output| output == b"Invalid permit");
.execute_returns(U256::from(0u16));
fn permit_invalid_signature() {
v: 0,
r: H256::repeat_byte(0x11),
s: H256::repeat_byte(0x11),
fn permit_invalid_deadline() {
pallet_timestamp::Pallet::<Runtime>::set_timestamp(10_000);
let deadline: U256 = 5u8.into(); // deadline < timestamp => expired
.execute_reverts(|output| output == b"Permit expired");
// This test checks the validity of a metamask signed message against the permit precompile
// The code used to generate the signature is the following.
// You will need to import ALICE_PRIV_KEY in metamask.
// If you put this code in the developer tools console, it will log the signature
/*
await window.ethereum.enable();
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
const value = 1000;
const fromAddress = "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac";
const deadline = 1;
const nonce = 0;
const spender = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const from = accounts[0];
const createPermitMessageData = function () {
const message = {
owner: from,
spender: spender,
value: value,
nonce: nonce,
deadline: deadline,
};
const typedData = JSON.stringify({
types: {
EIP712Domain: [
{
name: "name",
type: "string",
name: "version",
name: "chainId",
type: "uint256",
name: "verifyingContract",
type: "address",
],
Permit: [
name: "owner",
name: "spender",
name: "value",
name: "nonce",
name: "deadline",
primaryType: "Permit",
domain: {
name: "Mock token",
version: "1",
chainId: 0,
verifyingContract: "0x0000000000000000000000000000000000000001",
message: message,
return {
typedData,
message,
const method = "eth_signTypedData_v4"
const messageData = createPermitMessageData();
const params = [from, messageData.typedData];
web3.currentProvider.sendAsync(
method,
params,
from,
function (err, result) {
if (err) return console.dir(err);
if (result.error) {
alert(result.error.message);
if (result.error) return console.error('ERROR', result);
console.log('TYPED SIGNED:' + JSON.stringify(result.result));
const recovered = sigUtil.recoverTypedSignature_v4({
data: JSON.parse(msgParams),
sig: result.result,
if (
ethUtil.toChecksumAddress(recovered) === ethUtil.toChecksumAddress(from)
) {
alert('Successfully recovered signer as ' + from);
} else {
alert(
'Failed to verify signer when comparing ' + result + ' to ' + from
*/
fn permit_valid_with_metamask_signed_data() {
let value: U256 = 1000u16.into();
let deadline: U256 = 1u16.into(); // todo: proper timestamp
let rsv = hex_literal::hex!(
"612960858951e133d05483804be5456a030be4ce6c000a855d865c0be75a8fc11d89ca96d5a153e8c
7155ab1147f0f6d3326388b8d866c2406ce34567b7501a01b"
.as_slice();
let (r, sv) = rsv.split_at(32);
let (s, v) = sv.split_at(32);
let v_real = v[0];
let r_real: [u8; 32] = r.try_into().unwrap();
let s_real: [u8; 32] = s.try_into().unwrap();
Charlie, // can be anyone,
v: v_real,
r: r_real.into(),
s: s_real.into(),
solidity::encode_event_data(U256::from(1000)),
fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() {
check_precompile_implements_solidity_interfaces(
&["ERC20.sol", "Permit.sol"],
PCall::supports_selector,