Skip to content

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));
}
  • 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