Exploring Precompiled Contracts on Ethereum: A Deep Dive

Lucas Martin Calderon
3 min readJul 26, 2023

--

Ethereum, a leading decentralized platform, offers native, highly optimized functions known as precompiled contracts. Precompiled contracts are a set of core routines integral to Ethereum’s cryptographic functionalities. In this comprehensive guide, we will explore these contracts, their creation process, and their application in Solidity using in-line assembly.

Unveiling Precompiled Contracts

Precompiled contracts on Ethereum are like the platform’s built-in functions, they live at specific addresses starting from 0x1 to 0x8 and offer specific, often cryptographic, functionality. Here are a few of them:

  • 0x1: ECDSA recovery function (ecrecover)
  • 0x2: SHA-256 hashing function (sha256hash)
  • 0x3: RIPEMD-160 hashing function (ripemd160hash)
  • 0x4: Identity function (dataCopy)

A precompiled contract in Go Ethereum (geth), Ethereum's client implementation, would look something like this:

var PrecompiledContractsHomestead = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
common.BytesToAddress([]byte{4}): &dataCopy{},
}

Here, we define a map of precompiled contracts. The keys are the contract addresses, converted from bytes to a common Ethereum address format. The values are the corresponding contract implementations.

Interfacing with Precompiled Contracts in Solidity

Solidity, Ethereum’s native language, has most precompiled contracts’ functionalities readily available as built-in functions. Let’s use the SHA-256 hashing function as an example:

pragma solidity ^0.8.0;
contract Sha256Example {
function calculateHash(string memory input) public pure returns (bytes32) {
bytes32 hash = sha256(abi.encodePacked(input));
return hash;
}
}

In this simple contract, calculateHash receives a string input, hashes it using the sha256 function, and returns the corresponding bytes32 hash. It uses abi.encodePacked to pack the string before hashing.

But what if we want to directly utilize these precompiled contracts for advanced use cases?

This is where Solidity’s inline assembly comes into play.

Harnessing Precompiled Contracts using In-line Assembly

In-line assembly provides a way to directly interact with the Ethereum Virtual Machine (EVM). Here’s how you can use in-line assembly to call the sha256hash precompiled contract:

pragma solidity ^0.8.0; 

contract Sha256Example {
function calculateHash(bytes memory data) public returns (bytes32 result) {
assembly {
// Free memory pointer
let memPtr := mload(0x40)

// Data length
let dataLength := mload(data)

// 32 bytes offset to access the data bytes
let dataStart := add(data, 0x20)

// Store data into memory
mstore(memPtr, dataLength)
mstore(add(memPtr, 0x20), dataStart)
// Call precompiled contract for SHA256 at address 0x2
// Input is memPtr (start) and add(memPtr, 0x40) (length)
// Output is memPtr (start) and 0x20 (length)
if iszero(call(not(0), 0x2, 0, memPtr, add(memPtr, 0x40), memPtr, 0x20)) {
revert(0, 0)
}
result := mload(memPtr)

// Update free memory pointer
mstore(0x40, add(memPtr, 0x60))
}
}
}

This function does the same thing as the previous example, but directly calls the sha256hash precompiled contract using in-line assembly. The process of preparing data and making a direct call is more complex, but it allows for granular control and is sometimes necessary for optimizing contract code.

Security Considerations

Precompiled contracts are considered secure; however, if used incorrectly, they can expose vulnerabilities.

  1. Incorrect use of ecrecover: ecrecover verifies Ethereum signatures. However, incorrect implementation can lead to signature malleability issues. An attacker can manipulate the v parameter in the signature to generate a different public key.
  2. Underestimating Gas Costs: Each operation on Ethereum, including calling precompiled contracts, costs gas. Misestimating these costs can lead to out-of-gas exceptions and transaction failure.
  3. Assuming Deterministic Output: Contracts like bn256Add, bn256ScalarMul, and bn256Pairing might not always produce deterministic output due to underlying mathematical exceptions. This can be exploited if a contract assumes deterministic output from these precompiled contracts.
  4. Failing to Handle Call Failures: The call opcode can fail for multiple reasons. If a contract does not handle these failures correctly when calling a precompiled contract, it can lead to unintended behavior.
if iszero(call(not(0), 0x2, 0, memPtr, add(memPtr, 0x40), memPtr, 0x20)) {
revert(0, 0)
}

Here, call invokes the sha256hash precompiled contract. If call returns zero (indicating failure), the contract reverts, preventing potential exploits.

Precompiled contracts offer efficient computational routines, but their usage requires comprehensive understanding and careful handling. Regular audits, formal verification, staying up-to-date with Ethereum Improvement Proposals (EIPs), and following best practices are vital to ensure the security of your smart contracts.

--

--

Lucas Martin Calderon

Founder & CEO @ Pentestify | Driving Blockchain Security with AI | SuperNova Under 30