ABI (Application Binary Interface)¶
Overview¶
ABI (Application Binary Interface) is the standardized interface specification for Ethereum smart contracts, defining how to encode function calls and data so that external applications (such as frontends, scripts, and other contracts) can interact with smart contracts. The ABI serves as the bridge connecting on-chain contracts with off-chain applications, similar to API interface definitions in traditional software development.
Core Purpose:
ABI in the Smart Contract Lifecycle:
Development Phase:
Solidity source code → Compiler → Bytecode + ABI JSON
Deployment Phase:
Bytecode → Deploy to blockchain → Contract address
Invocation Phase:
Frontend application + ABI JSON + Contract address
↓
Encode function call (ABI encoding)
↓
Send transaction to blockchain
↓
EVM decodes and executes
↓
Return result (ABI decoding)
↓
Frontend application receives data
ABI vs API:
Traditional API (Application Programming Interface):
- Defined in high-level languages (e.g., JSON, XML)
- Human-readable
- Transmitted via protocols like HTTP
Blockchain ABI:
- Binary encoding (byte sequences)
- Machine-readable, requires decoding
- Transmitted via transaction calldata
- Strict encoding rules (deterministic)
Historical Development:
- 2014: Ethereum early versions define the basic ABI specification
- 2015: Formalization of ABI encoding rules (Solidity ABI specification)
- 2017: Introduction of ABI encoding v2 (supporting complex types such as nested structs)
- 2020+: Tooling ecosystem matures (ethers.js, viem, etc. provide comprehensive support)
ABI Description Format¶
JSON Format Specification¶
ABI uses a JSON array to describe contract interfaces, where each element describes a function, event, error, or constructor.
Complete Example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Example {
uint256 public value;
address public owner;
event ValueChanged(uint256 indexed oldValue, uint256 newValue, address indexed changer);
error Unauthorized(address caller);
constructor(uint256 _initialValue) {
value = _initialValue;
owner = msg.sender;
}
function setValue(uint256 _newValue) public {
if (msg.sender != owner) revert Unauthorized(msg.sender);
uint256 oldValue = value;
value = _newValue;
emit ValueChanged(oldValue, _newValue, msg.sender);
}
function getValue() public view returns (uint256) {
return value;
}
function transfer(address _newOwner) external {
require(msg.sender == owner, "Not owner");
owner = _newOwner;
}
}
Corresponding ABI JSON:
[
{
"type": "constructor",
"inputs": [
{
"name": "_initialValue",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "setValue",
"inputs": [
{
"name": "_newValue",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "getValue",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "transfer",
"inputs": [
{
"name": "_newOwner",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "value",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "owner",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"type": "event",
"name": "ValueChanged",
"inputs": [
{
"name": "oldValue",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "newValue",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "changer",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"type": "error",
"name": "Unauthorized",
"inputs": [
{
"name": "caller",
"type": "address",
"internalType": "address"
}
]
}
]
Field Descriptions¶
Common Fields:
type: Type
- "function": Function
- "constructor": Constructor
- "receive": receive function (for receiving ETH)
- "fallback": fallback function
- "event": Event
- "error": Custom error
name: Name
- Name of the function/event/error
- constructor, receive, and fallback have no name field
inputs: Input parameter array
- name: Parameter name
- type: Parameter type (canonical type)
- internalType: Internal type (Solidity type)
- indexed: (events only) Whether indexed
- components: (complex types) Sub-fields
outputs: Output parameter array
- Only functions have this field
- Same structure as inputs
stateMutability: State mutability
- "pure": Does not read or modify state
- "view": Reads state but does not modify
- "nonpayable": Modifies state, does not accept ETH
- "payable": Modifies state, accepts ETH
anonymous: (events only) Whether anonymous
- false: Normal event (includes event signature)
- true: Anonymous event (no event signature, saves Gas)
ABI Encoding Rules¶
Function Selector¶
Calculation Method:
Steps:
1. Get the function signature (without parameter names)
2. Compute the keccak256 hash
3. Take the first 4 bytes
Example:
function transfer(address _to, uint256 _amount)
1. Function signature:
"transfer(address,uint256)"
Note:
- No spaces
- No parameter names
- Use canonical type names
2. keccak256 hash:
keccak256("transfer(address,uint256)")
= 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
3. First 4 bytes:
0xa9059cbb
Code Implementation:
// Solidity
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
// selector = 0xa9059cbb
// JavaScript (ethers.js)
const selector = ethers.utils.id("transfer(address,uint256)").slice(0, 10);
// selector = "0xa9059cbb"
// JavaScript (viem)
import { keccak256, toBytes } from 'viem';
const selector = keccak256(toBytes("transfer(address,uint256)")).slice(0, 10);
Common Function Selectors:
ERC20:
- transfer(address,uint256): 0xa9059cbb
- approve(address,uint256): 0x095ea7b3
- transferFrom(address,address,uint256): 0x23b872dd
- balanceOf(address): 0x70a08231
ERC721:
- safeTransferFrom(address,address,uint256): 0x42842e0e
- ownerOf(uint256): 0x6352211e
Common functions:
- initialize(): 0x8129fc1c
- upgradeTo(address): 0x3659cfe6
Basic Type Encoding¶
Encoding Rules:
All types are encoded as 32 bytes (256 bits):
uint<N> (N = 8 to 256, steps of 8):
- Right-aligned, left-padded with zeros
- Example: uint256(42)
→ 0x000000000000000000000000000000000000000000000000000000000000002a
int<N>:
- Signed integer, two's complement representation
- Example: int256(-1)
→ 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
address:
- 20-byte address, right-aligned
- Example: 0x1234567890123456789012345678901234567890
→ 0x0000000000000000000000001234567890123456789012345678901234567890
bool:
- true = 1, false = 0
- Example: true
→ 0x0000000000000000000000000000000000000000000000000000000000000001
bytes<N> (N = 1 to 32):
- Fixed-length bytes, left-aligned, right-padded with zeros
- Example: bytes4(0x12345678)
→ 0x1234567800000000000000000000000000000000000000000000000000000000
Code Examples:
// Solidity encoding
function encodeBasicTypes() public pure returns (bytes memory) {
return abi.encode(
uint256(42), // 0x000...02a
int256(-1), // 0xfff...fff
address(0x1234...7890), // 0x000...1234567890
true, // 0x000...001
bytes4(0x12345678) // 0x123...000
);
}
// JavaScript (ethers.js)
const encoded = ethers.utils.defaultAbiCoder.encode(
['uint256', 'int256', 'address', 'bool', 'bytes4'],
[42, -1, '0x1234567890123456789012345678901234567890', true, '0x12345678']
);
Dynamic Type Encoding¶
Dynamic types include: - bytes (variable-length bytes) - string (strings) - Dynamic arrays (T[])
Encoding Rules:
Dynamic type encoding consists of two parts:
1. Head: Data offset (32 bytes)
2. Tail: Actual data
Structure:
[Head] [Tail]
↓ ↓
offset length + data
Example: bytes
function encodeBytes() public pure returns (bytes memory) {
bytes memory data = hex"1234";
return abi.encode(data);
}
Encoding result:
0x0000000000000000000000000000000000000000000000000000000000000020 // offset = 32
0000000000000000000000000000000000000000000000000000000000000002 // length = 2
1234000000000000000000000000000000000000000000000000000000000000 // data (left-aligned)
Explanation:
- First 32 bytes: offset = 0x20 (32), indicating data starts at byte 32
- Second 32 bytes: length = 0x02, indicating 2 bytes
- Third 32 bytes: data = 0x1234 (left-aligned, right-padded with zeros)
Example: string
function encodeString() public pure returns (bytes memory) {
return abi.encode("Hello");
}
Encoding result:
0x0000000000000000000000000000000000000000000000000000000000000020 // offset
0000000000000000000000000000000000000000000000000000000000000005 // length = 5
48656c6c6f000000000000000000000000000000000000000000000000000000 // "Hello" (UTF-8)
"Hello" in UTF-8 encoding:
H = 0x48
e = 0x65
l = 0x6c
l = 0x6c
o = 0x6f
Example: Dynamic Array
function encodeDynamicArray() public pure returns (bytes memory) {
uint256[] memory arr = new uint256[](3);
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
return abi.encode(arr);
}
Encoding result:
0x0000000000000000000000000000000000000000000000000000000000000020 // offset
0000000000000000000000000000000000000000000000000000000000000003 // length = 3
0000000000000000000000000000000000000000000000000000000000000001 // arr[0] = 1
0000000000000000000000000000000000000000000000000000000000000002 // arr[1] = 2
0000000000000000000000000000000000000000000000000000000000000003 // arr[2] = 3
Complex Type Encoding¶
Multiple Mixed Parameters:
function complexEncode(
uint256 a,
bytes memory b,
uint256 c
) public pure returns (bytes memory) {
return abi.encode(a, b, c);
}
Call: complexEncode(42, hex"1234", 100)
Encoding result:
// Head
0x000000000000000000000000000000000000000000000000000000000000002a // a = 42 (static)
0000000000000000000000000000000000000000000000000000000000000060 // b offset = 96
0000000000000000000000000000000000000000000000000000000000000064 // c = 100 (static)
// Tail - b data
0000000000000000000000000000000000000000000000000000000000000002 // b.length = 2
1234000000000000000000000000000000000000000000000000000000000000 // b.data
Rules:
- Static types (uint256) are encoded directly in the head
- Dynamic types (bytes) store an offset in the head, with data in the tail
- Offsets are calculated from the start of the head
Struct Encoding:
struct Person {
string name;
uint256 age;
address wallet;
}
function encodeStruct() public pure returns (bytes memory) {
Person memory p = Person({
name: "Alice",
age: 30,
wallet: 0x1234567890123456789012345678901234567890
});
return abi.encode(p);
}
Encoding result:
// Head
0x0000000000000000000000000000000000000000000000000000000000000020 // struct offset
0000000000000000000000000000000000000000000000000000000000000060 // name offset (from struct start)
000000000000000000000000000000000000000000000000000000000000001e // age = 30
0000000000000000000000001234567890123456789012345678901234567890 // wallet
// Tail - name data
0000000000000000000000000000000000000000000000000000000000000005 // name.length = 5
416c696365000000000000000000000000000000000000000000000000000000 // "Alice"
Nested Arrays:
function encodeNestedArray() public pure returns (bytes memory) {
uint256[][] memory nested = new uint256[][](2);
nested[0] = new uint256[](2);
nested[0][0] = 1;
nested[0][1] = 2;
nested[1] = new uint256[](1);
nested[1][0] = 3;
return abi.encode(nested);
}
Encoding result:
// Main array head
0x0000000000000000000000000000000000000000000000000000000000000020 // offset
0000000000000000000000000000000000000000000000000000000000000002 // outer array length = 2
0000000000000000000000000000000000000000000000000000000000000040 // nested[0] offset
00000000000000000000000000000000000000000000000000000000000000a0 // nested[1] offset
// nested[0] data
0000000000000000000000000000000000000000000000000000000000000002 // length = 2
0000000000000000000000000000000000000000000000000000000000000001 // nested[0][0] = 1
0000000000000000000000000000000000000000000000000000000000000002 // nested[0][1] = 2
// nested[1] data
0000000000000000000000000000000000000000000000000000000000000001 // length = 1
0000000000000000000000000000000000000000000000000000000000000003 // nested[1][0] = 3
Function Call Encoding¶
Calldata Structure¶
Complete Format:
Calldata = Function Selector (4 bytes) + Encoded Arguments
Example:
transfer(address _to, uint256 _amount)
Call:
transfer(0x1234567890123456789012345678901234567890, 100)
Calldata:
0xa9059cbb // selector (4 bytes)
0000000000000000000000001234567890123456789012345678901234567890 // _to (32 bytes)
0000000000000000000000000000000000000000000000000000000000000064 // _amount = 100 (32 bytes)
Total length: 4 + 32 + 32 = 68 bytes
Generating Calldata:
// ethers.js
const iface = new ethers.utils.Interface([
"function transfer(address _to, uint256 _amount)"
]);
const calldata = iface.encodeFunctionData("transfer", [
"0x1234567890123456789012345678901234567890",
100
]);
// calldata = "0xa9059cbb0000000000000000000000001234567890123456789012345678901234567890000000000000000000000000000000000000000000000000000000000000064"
// viem
import { encodeFunctionData } from 'viem';
const calldata = encodeFunctionData({
abi: [{
name: 'transfer',
type: 'function',
inputs: [
{ name: '_to', type: 'address' },
{ name: '_amount', type: 'uint256' }
]
}],
functionName: 'transfer',
args: ['0x1234567890123456789012345678901234567890', 100n]
});
// Solidity
bytes memory calldata = abi.encodeWithSelector(
bytes4(keccak256("transfer(address,uint256)")),
0x1234567890123456789012345678901234567890,
100
);
// Or
calldata = abi.encodeCall(
IERC20.transfer,
(0x1234567890123456789012345678901234567890, 100)
);
Decoding Calldata¶
Manual Decoding:
contract CalldataDecoder {
function decodeTransfer(bytes calldata data)
public
pure
returns (address to, uint256 amount)
{
require(data.length >= 68, "Invalid calldata length");
// Check function selector
bytes4 selector = bytes4(data[0:4]);
require(
selector == bytes4(keccak256("transfer(address,uint256)")),
"Wrong function"
);
// Decode parameters
assembly {
// Skip selector (4 bytes), read first parameter
to := calldataload(add(data.offset, 4))
// Read second parameter
amount := calldataload(add(data.offset, 36))
}
}
}
// JavaScript
function decodeCalldata(calldata) {
// Extract selector
const selector = calldata.slice(0, 10); // "0x" + 8 hex chars
// Extract parameters
const to = "0x" + calldata.slice(34, 74); // Skip 0x + selector(8) + padding(24)
const amount = parseInt(calldata.slice(74, 138), 16);
return { selector, to, amount };
}
Using Libraries to Decode:
// ethers.js
const iface = new ethers.utils.Interface([
"function transfer(address _to, uint256 _amount)"
]);
const decoded = iface.decodeFunctionData("transfer", calldata);
// decoded = {
// _to: "0x1234567890123456789012345678901234567890",
// _amount: BigNumber(100)
// }
// viem
import { decodeFunctionData } from 'viem';
const decoded = decodeFunctionData({
abi: [...],
data: calldata
});
Event Encoding¶
Log Structure¶
Event Log Format:
event Transfer(address indexed from, address indexed to, uint256 value);
emit Transfer(0xaaa..., 0xbbb..., 100);
Log structure:
{
address: "0xContractAddress",
topics: [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", // event signature
"0x000000000000000000000000aaa...", // indexed: from
"0x000000000000000000000000bbb..." // indexed: to
],
data: "0x0000000000000000000000000000000000000000000000000000000000000064" // value = 100
}
Event Signature Calculation:
Steps similar to function selector, but using the full 32 bytes:
1. Event signature:
"Transfer(address,address,uint256)"
2. keccak256 hash:
keccak256("Transfer(address,address,uint256)")
= 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
3. Full 32 bytes as topics[0]
Note:
- Anonymous events do not have topics[0]
- Maximum 3 indexed parameters (topics[1-3])
- Non-indexed parameters are encoded in data
Common Event Signatures:
ERC20:
Transfer(address,address,uint256):
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Approval(address,address,uint256):
0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
ERC721:
Transfer(address,address,uint256):
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Approval(address,address,uint256):
0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
ApprovalForAll(address,address,bool):
0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31
Parsing Event Logs¶
Code Examples:
// ethers.js
const iface = new ethers.utils.Interface([
"event Transfer(address indexed from, address indexed to, uint256 value)"
]);
// Parse log
const log = {
topics: [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000aaa...",
"0x000000000000000000000000bbb..."
],
data: "0x0000000000000000000000000000000000000000000000000000000000000064"
};
const parsed = iface.parseLog(log);
// parsed = {
// name: "Transfer",
// args: {
// from: "0xaaa...",
// to: "0xbbb...",
// value: BigNumber(100)
// }
// }
// viem
import { decodeEventLog } from 'viem';
const decoded = decodeEventLog({
abi: [{
name: 'Transfer',
type: 'event',
inputs: [
{ name: 'from', type: 'address', indexed: true },
{ name: 'to', type: 'address', indexed: true },
{ name: 'value', type: 'uint256', indexed: false }
]
}],
data: log.data,
topics: log.topics
});
Filtering Event Logs:
// Get Transfer events for a specific address
const filter = {
address: contractAddress,
topics: [
ethers.utils.id("Transfer(address,address,uint256)"),
null, // from (any)
ethers.utils.hexZeroPad(myAddress, 32) // to (my address)
],
fromBlock: 'latest',
toBlock: 'latest'
};
const logs = await provider.getLogs(filter);
ABI Encoding Variants¶
abi.encode vs abi.encodePacked¶
Standard Encoding (abi.encode):
// Each parameter occupies 32 bytes
bytes memory encoded = abi.encode(uint8(1), uint16(2));
Result:
0x0000000000000000000000000000000000000000000000000000000000000001 // uint8(1) - 32 bytes
0000000000000000000000000000000000000000000000000000000000000002 // uint16(2) - 32 bytes
Length: 64 bytes
Packed Encoding (abi.encodePacked):
// Minimal byte representation, no padding
bytes memory packed = abi.encodePacked(uint8(1), uint16(2));
Result:
0x010002 // uint8(1) - 1 byte, uint16(2) - 2 bytes
Length: 3 bytes
Warning:
- Cannot be reversibly decoded (type information is lost)
- May cause hash collisions
Security Issue Example:
// ❌ Dangerous: hash collision
bytes32 hash1 = keccak256(abi.encodePacked("a", "bc"));
bytes32 hash2 = keccak256(abi.encodePacked("ab", "c"));
// hash1 == hash2 ✅ Collision!
// ✅ Safe: use standard encoding
bytes32 hash1 = keccak256(abi.encode("a", "bc"));
bytes32 hash2 = keccak256(abi.encode("ab", "c"));
// hash1 != hash2 ✅ Different
abi.encodeWithSelector vs abi.encodeWithSignature¶
Using Selector:
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
bytes memory data = abi.encodeWithSelector(
selector,
0x1234567890123456789012345678901234567890,
100
);
Using Signature String:
bytes memory data = abi.encodeWithSignature(
"transfer(address,uint256)",
0x1234567890123456789012345678901234567890,
100
);
// Equivalent to encodeWithSelector, but computes the selector at runtime
// Higher Gas cost
Type-Safe Version (Solidity** 0.8.13+):**
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
// ✅ Type-safe, compile-time checking
bytes memory data = abi.encodeCall(
IERC20.transfer,
(0x1234567890123456789012345678901234567890, 100)
);
// Advantages:
// - Type checking
// - IDE autocompletion
// - Prevents typos
Tools and Libraries¶
Web3 Library Support¶
*ethers.js:*
const { ethers } = require("ethers");
// 1. Create interface
const iface = new ethers.utils.Interface([
"function transfer(address to, uint256 amount)",
"event Transfer(address indexed from, address indexed to, uint256 value)"
]);
// 2. Encode function call
const calldata = iface.encodeFunctionData("transfer", [
"0x1234567890123456789012345678901234567890",
ethers.utils.parseEther("1.0")
]);
// 3. Decode function call
const decoded = iface.decodeFunctionData("transfer", calldata);
// 4. Encode function result
const result = iface.encodeFunctionResult("transfer", [true]);
// 5. Decode function result
const [success] = iface.decodeFunctionResult("transfer", result);
// 6. Parse event
const log = {
topics: [...],
data: "0x..."
};
const event = iface.parseLog(log);
// 7. Encode event filter
const filter = iface.encodeFilterTopics("Transfer", [
"0xaaa...", // from
null // to (any)
]);
viem:
import {
encodeFunctionData,
decodeFunctionData,
encodeFunctionResult,
decodeFunctionResult,
encodeEventTopics,
decodeEventLog
} from 'viem';
// Encode function
const data = encodeFunctionData({
abi: [...],
functionName: 'transfer',
args: ['0x...', 100n]
});
// Decode function
const result = decodeFunctionData({
abi: [...],
data: '0x...'
});
// Encode event topics
const topics = encodeEventTopics({
abi: [...],
eventName: 'Transfer',
args: {
from: '0x...',
to: null // wildcard
}
});
// Decode event
const event = decodeEventLog({
abi: [...],
data: log.data,
topics: log.topics
});
web3.py:
from web3 import Web3
# Encode function call
encoded = contract.encode_abi(
fn_name='transfer',
args=['0x1234567890123456789012345678901234567890', 100]
)
# Decode function call
decoded = contract.decode_function_input(encoded)
# Parse event
event = contract.events.Transfer().processLog(log)
Online Tools¶
ABI Encoding/Decoding Tools:
1. ChainTool
- URL: https://chaintool.tech/calldata
- Features: Calldata encoding/decoding, visualization
2. HashEx ABI Decoder
- URL: https://abi.hashex.org/
- Features: Decode transaction input data
3. OpenChain
- URL: https://openchain.xyz/tools/abi
- Features: ABI encoding/decoding, call stack analysis
4. Ethereum Signature Database
- URL: https://www.4byte.directory/
- Features: Function signature lookup
5. Samczsun's Calldata Decoder
- URL: https://calldata.swiss-knife.xyz/
- Features: Advanced calldata decoding
Function Selector Lookup:
ChainTool querySelector:
https://chaintool.tech/querySelector
Input: Function signature
Output: 4-byte selector
Example:
Input: transfer(address,uint256)
Output: 0xa9059cbb
Best Practices¶
Type Safety¶
// ❌ Unsafe: string concatenation, error-prone
bytes memory data = abi.encodeWithSignature(
"tranfer(address,uint256)", // Typo!
to,
amount
);
// ✅ Safe: use interface, compile-time checking
bytes memory data = abi.encodeCall(
IERC20.transfer,
(to, amount)
);
Gas Optimization¶
// ❌ High Gas: runtime selector computation
function badCall(address token, address to, uint256 amount) external {
bytes memory data = abi.encodeWithSignature(
"transfer(address,uint256)",
to,
amount
);
token.call(data);
}
// ✅ Low Gas: precomputed selector
bytes4 constant TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)"));
function goodCall(address token, address to, uint256 amount) external {
bytes memory data = abi.encodeWithSelector(
TRANSFER_SELECTOR,
to,
amount
);
token.call(data);
}
// ✅✅ Optimal: direct call
function bestCall(address token, address to, uint256 amount) external {
IERC20(token).transfer(to, amount);
}
Security Considerations¶
1. Validate Decoded Data:
function processCalldata(bytes calldata data) external {
// ✅ Validate length
require(data.length >= 4, "Invalid calldata");
// ✅ Validate selector
bytes4 selector = bytes4(data[0:4]);
require(
selector == this.expectedFunction.selector,
"Wrong function"
);
// ✅ Decode and validate parameters
(address to, uint256 amount) = abi.decode(data[4:], (address, uint256));
require(to != address(0), "Invalid address");
require(amount > 0, "Invalid amount");
}
2. Avoid Hash Collisions with encodePacked:
// ❌ Dangerous
function dangerousHash(string memory a, string memory b)
public
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(a, b));
}
// ✅ Safe
function safeHash(string memory a, string memory b)
public
pure
returns (bytes32)
{
return keccak256(abi.encode(a, b));
}
// ✅ Or use a separator
function safeHashWithSeparator(string memory a, string memory b)
public
pure
returns (bytes32)
{
return keccak256(abi.encodePacked(a, "|", b));
}
Recommended Reading¶
- Solidity ABI Specification - Official ABI specification
- Ethereum Contract ABI - ABI specification reference
- ethers.js Documentation - ethers.js ABI encoding documentation
- viem Documentation - viem ABI tools documentation
- EIP-712: Typed structured data hashing - Structured data signing
- 4byte Directory - Function signature database
- OpenChain ABI Tools - ABI toolset
Related Concepts¶
- Calldata: Transaction input data
- Function Selector: Function selector (first 4 bytes)
- Event Signature: Event signature (32 bytes)
- Topics: Indexed fields in event logs
- Bytecode: Contract bytecode
- Interface: Contract interface definition
- EIP-712: Structured data signing standard
- Type Hashing: Type hashing