Skip to content

ABIContract

Description

ABIContract is a contract class for interacting with contracts via their ABI (Application Binary Interface). This is useful when you have the ABI of a contract but not its source code, such as when interacting with external contracts or contracts deployed by others.


Overview

ABIContract provides a way to interact with any Ethereum contract using just its ABI. It handles: - Encoding function calls according to the ABI - Decoding return values - Event log parsing - Type checking of inputs


Creating an ABIContract

From ABI and Address

import boa

# Load contract from ABI
abi = [
    {
        "name": "balanceOf",
        "type": "function",
        "inputs": [{"name": "account", "type": "address"}],
        "outputs": [{"name": "", "type": "uint256"}],
        "stateMutability": "view"
    },
    {
        "name": "transfer",
        "type": "function",
        "inputs": [
            {"name": "to", "type": "address"},
            {"name": "amount", "type": "uint256"}
        ],
        "outputs": [{"name": "", "type": "bool"}],
        "stateMutability": "nonpayable"
    }
]

# Connect to deployed contract
contract = boa.loads_abi(abi).at("0x...")

# Use the contract
balance = contract.balanceOf(user_address)
contract.transfer(recipient, 100)

From Etherscan

# Automatically fetches ABI from Etherscan
usdc = boa.from_etherscan("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name="USDC")

# The returned object is an ABIContract
print(type(usdc))  # <class 'boa.contracts.abi.abi_contract.ABIContract'>

Working with ABIContract

Function Calls

ABIContract automatically creates Python methods for each function in the ABI:

# View functions (read-only)
total_supply = contract.totalSupply()
balance = contract.balanceOf(account)
allowance = contract.allowance(owner, spender)

# State-changing functions
contract.transfer(recipient, amount)
contract.approve(spender, amount)
contract.transferFrom(sender, recipient, amount)

Working with Already Deployed Contracts

ABIContract is designed for interacting with contracts that are already deployed on the blockchain. It cannot deploy new contracts.

# Load an existing contract at a known address
contract = boa.loads_abi(abi, at="0x...")

# For deploying new contracts, use boa.load() with Vyper source code instead

Events

Access and decode events:

# Get all logs
logs = contract.get_logs()

# Filter specific events
# Decoded logs are returned as namedtuples where the type name is the event name
transfers = [log for log in logs if type(log).__name__ == "Transfer"]

# Access event data (namedtuple attributes)
for transfer in transfers:
    # Note: 'from' is a Python keyword, so it's renamed to 'from_' in the namedtuple
    print(f"From: {transfer.from_}")
    print(f"To: {transfer.to}")
    print(f"Value: {transfer.value}")

Type Handling

ABIContract automatically handles type conversions between Python and Solidity types:

Basic Types

# Integers
contract.setUint(42)  # uint256
contract.setInt(-100)  # int256

# Addresses
contract.setOwner("0x742d35Cc6634C0532925a3b844Bc9e7595f6E86f")

# Booleans
contract.setPaused(True)

# Bytes
contract.setData(b"hello")  # bytes
contract.setBytes32(b"0" * 32)  # bytes32

Arrays

# Fixed arrays
contract.setFixedArray([1, 2, 3, 4, 5])  # uint256[5]

# Dynamic arrays
contract.setDynamicArray([10, 20, 30])  # uint256[]

# Array of addresses
contract.setAddresses(["0x...", "0x...", "0x..."])

Tuples/Structs

# Function expecting tuple/struct
contract.setPosition((100, 50000, True))  # (amount, price, isLong)

# Returning structs
position = contract.getPosition(user)
amount, price, is_long = position

Advanced Usage

Custom Gas Settings

# Set gas limit
contract.expensive_operation(gas=1000000)

# Send ETH with call
contract.deposit(value=10**18)  # 1 ETH

Overloaded Functions

For contracts with overloaded functions (same name, different parameters), you need to disambiguate by providing the full signature:

# If contract has multiple transfer functions
# Use the disambiguate_signature parameter
contract.transfer(recipient, amount, disambiguate_signature="transfer(address,uint256)")
contract.transfer(recipient, amount, data, disambiguate_signature="transfer(address,uint256,bytes)")

Raw Calls

For low-level interactions:

# Get the function selector and prepare calldata
fn = contract.transfer
data = fn.prepare_calldata(recipient, amount)

# Execute raw call
result = boa.env.raw_call(
    contract.address,
    data=data,
    value=0,
    gas=100000
)

# Decode result using abi_decode
from eth.codecs import abi
success = abi.decode("(bool)", result)[0]

Network Mode

ABIContract works seamlessly with network mode:

# Fork mainnet
boa.fork("https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY")

# Load any contract by ABI
abi = fetch_abi_from_somewhere()
contract = boa.loads_abi(abi).at("0x...")

# Interact with forked state
contract.someMethod()

Common Patterns

ERC20 Token Interaction

# Standard ERC20 ABI
erc20_abi = [
    {"name": "balanceOf", "type": "function", "inputs": [{"name": "account", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}]},
    {"name": "transfer", "type": "function", "inputs": [{"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [{"name": "", "type": "bool"}]},
    {"name": "approve", "type": "function", "inputs": [{"name": "spender", "type": "address"}, {"name": "amount", "type": "uint256"}], "outputs": [{"name": "", "type": "bool"}]},
    {"name": "allowance", "type": "function", "inputs": [{"name": "owner", "type": "address"}, {"name": "spender", "type": "address"}], "outputs": [{"name": "", "type": "uint256"}]},
]

# Create reusable token interface
def get_token(address):
    return boa.loads_abi(erc20_abi).at(address)

# Use with any ERC20 token
usdc = get_token("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
dai = get_token("0x6B175474E89094C44Da98b954EedeAC495271d0F")

Proxy Contract Interaction

# Load implementation ABI
implementation_abi = [...]

# Connect to proxy address with implementation ABI
proxy_address = "0x..."
contract = boa.loads_abi(implementation_abi).at(proxy_address)

# Calls go through proxy to implementation
contract.implementation_function()

Limitations

  1. No Source Code: ABIContract doesn't have access to contract source, so features like storage introspection (_storage) are not available

  2. No Internal Functions: Only external/public functions in the ABI can be called

  3. Type Safety: Type checking is based on ABI definitions, which may be less strict than Vyper's type system

  4. Gas Estimation: May be less accurate than VyperContract since it lacks source code context


See Also