Interacting with Intelligent Contracts
This page covers syntax for internal messages (IC → IC). See Messages for the conceptual model and Value Transfers for sending GEN.
Getting Contract References
Access other contracts by their address:
contract_address = Address("0x03FB09251eC05ee9Ca36c98644070B89111D4b3F")
dynamically_typed_contract = gl.get_contract_at(contract_address)
@gl.contract_interface
class GenLayerContractIface:
class View:
def method_name(self, a: int, b: str): ...
class Write:
pass
statically_typed_contract = GenLayerContractIface(contract_address)Both approaches result in the same runtime value, however the statically typed approach provides type checking and autocompletion in IDEs.
Calling View Methods
Call read-only methods on other contracts:
addr: Address = ...
other = gl.get_contract_at(addr)
result = other.view().get_token_balance()Emitting Messages
Send asynchronous messages to other contracts:
other = gl.get_contract_at(addr)
other.emit(on='accepted').update_status("active")
other.emit(on='finalized').update_status("active")
# With value (recipient method must be @gl.public.write.payable)
other.emit(value=u256(100), on='finalized').deposit()Deploying New Contracts
gl.deploy_contract(code=contract_code)
salt: u256 = u256(1) # not zero
child_address = gl.deploy_contract(code=contract_code, salt=salt)View vs Emit
view()is synchronous — it reads state from another contract and returns the result immediately. The target contract's state is read as of the current block.emit()is asynchronous — it queues a write call that executes after the current transaction completes. The call is not blocking.
Accepted vs Finalized
# Fast — executes after the transaction is accepted by initial consensus
other.emit(on='accepted').do_something()
# Safe — waits until the transaction is fully finalized (after appeal window)
other.emit(on='finalized').do_something()on='accepted' has important implications. If the emitting transaction is appealed, two things can happen:
- The appeal changes the contract state such that the message should not have been emitted — but it was already sent and cannot be recalled.
- The transaction is re-executed during the appeal, and the message (or a similar one) is emitted again. This can repeat across multiple appeal rounds — up to ~6 times depending on the validator set size.
The receiving contract must be idempotent — it must handle duplicate or unexpected messages gracefully. If the receiving logic cannot tolerate duplicates or messages that "shouldn't have been sent" based on the final state, use on='finalized' instead.
Factory Pattern
Deploy child contracts from a parent:
def __init__(self, num_workers: int):
with open("/contract/Worker.py", "rt") as f:
worker_code = f.read()
for i in range(num_workers):
addr = gl.deploy_contract(
code=worker_code.encode("utf-8"),
args=[i, gl.message.contract_address],
salt_nonce=i + 1,
on="accepted",
)
self.worker_addresses.append(addr)Child contracts are immutable after deployment. To update worker logic, redeploy through the factory.
Complete Working Example
Here's a complete example showing a token contract interacting with another contract:
from genlayer import *
@gl.contract_interface
class TokenInterface:
class View:
def balance_of(self, account: Address) -> u256: ...
def total_supply(self) -> u256: ...
class Write:
def transfer(self, to: Address, amount: u256): ...
def approve(self, spender: Address, amount: u256): ...
class TokenGatedVault(gl.Contract):
"""A vault that requires minimum token balance to access"""
token_address: Address
min_balance: u256
stored_value: u256
def __init__(self, token_addr: Address, min_bal: u256):
self.token_address = token_addr
self.min_balance = min_bal
self.stored_value = u256(0)
@gl.public.write.payable
def deposit(self):
"""Deposit GEN into the vault (requires token balance)"""
# Get token contract reference
token = TokenInterface(self.token_address)
# Check caller's token balance
balance = token.view().balance_of(gl.message.sender_address)
if balance < self.min_balance:
raise gl.UserError(f"Insufficient token balance. Need {self.min_balance}, have {balance}")
# Accept the deposit
self.stored_value += gl.message.value
@gl.public.write
def withdraw(self, amount: u256):
"""Withdraw GEN from the vault (requires token balance)"""
token = TokenInterface(self.token_address)
balance = token.view().balance_of(gl.message.sender_address)
if balance < self.min_balance:
raise gl.UserError("Insufficient token balance")
if amount > self.stored_value:
raise gl.UserError("Insufficient vault balance")
self.stored_value -= amount
gl.transfer(gl.message.sender_address, amount)Error Handling
Always handle errors when calling other contracts:
@gl.public.write
def safe_transfer(self, token_addr: Address, to: Address, amount: u256):
"""Transfer tokens with proper error handling"""
try:
token = gl.get_contract_at(token_addr)
token.emit(on='finalized').transfer(to, amount)
except gl.UserError as e:
# Handle contract-specific errors
raise gl.UserError(f"Transfer failed: {e}")
except Exception as e:
# Handle unexpected errors
raise gl.UserError(f"Unexpected error: {e}")Common Mistakes
❌ Don't: Call view() on emit()
# WRONG - view() is for reading, emit() is for writing
other.view().update_status("active") # This will fail!# CORRECT
other.emit(on='finalized').update_status("active")❌ Don't: Forget to check return values
# WRONG - Not checking if the call succeeded
other.view().transfer(recipient, amount)# CORRECT - Check the return value
success = other.view().transfer(recipient, amount)
if not success:
raise gl.UserError("Transfer failed")❌ Don't: Use accepted when order matters
# WRONG - These might execute out of order or multiple times
other.emit(on='accepted').step_one()
other.emit(on='accepted').step_two()# CORRECT - Use finalized for sequential operations
other.emit(on='finalized').step_one()
other.emit(on='finalized').step_two()Best Practices
1. Use Static Typing for Better IDE Support
# Preferred: Define interface upfront
@gl.contract_interface
class MyContractInterface:
class View:
def get_balance(self, addr: Address) -> u256: ...
class Write:
def update_balance(self, addr: Address, amount: u256): ...
other = MyContractInterface(contract_address)
# Now you get autocomplete and type checking!2. Validate Contract Addresses
def __init__(self, oracle_addr: Address):
# Verify the address is valid
if oracle_addr == Address("0x0000000000000000000000000000000000000000"):
raise gl.UserError("Invalid oracle address")
# Verify the contract exists (optional but recommended)
oracle = gl.get_contract_at(oracle_addr)
try:
# Try calling a view method to verify it's the right contract
oracle.view().get_version()
except:
raise gl.UserError("Oracle contract not found or incompatible")
self.oracle_address = oracle_addr3. Handle Reentrancy
class Vault(gl.Contract):
balances: TreeMap[Address, u256] = TreeMap()
@gl.public.write
def withdraw(self, amount: u256):
# Check balance FIRST
balance = self.balances[gl.message.sender_address]
if amount > balance:
raise gl.UserError("Insufficient balance")
# Update state BEFORE external call
self.balances[gl.message.sender_address] = balance - amount
# External call last (prevents reentrancy)
gl.transfer(gl.message.sender_address, amount)4. Use Finalized for Critical Operations
@gl.public.write
def execute_payment(self, recipient: Address, amount: u256):
"""Critical payment - must be finalized"""
token = gl.get_contract_at(self.token_address)
# Use 'finalized' for important operations
# This ensures the transaction won't be appealed
token.emit(on='finalized').transfer(recipient, amount)Testing Contract Interactions
Example test for contract-to-contract calls:
# In your test file
def test_token_gated_vault():
# Deploy token contract first
token = deploy_contract("Token", args=[u256(1000000)])
# Deploy vault with token requirement
vault = deploy_contract("TokenGatedVault", args=[
token.address,
u256(100) # minimum balance
])
# Give user some tokens
token.transfer(user_address, u256(500))
# Test deposit with sufficient balance
vault.deposit(value=u256(1000))
assert vault.view().stored_value() == u256(1000)
# Test deposit with insufficient balance
vault2 = deploy_contract("TokenGatedVault", args=[
token.address,
u256(10000) # very high requirement
])
with pytest.raises(gl.UserError, match="Insufficient token balance"):
vault2.deposit(value=u256(1000))