Intelligent Contracts
Interacting with Intelligent Contracts

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:

  1. The appeal changes the contract state such that the message should not have been emitted — but it was already sent and cannot be recalled.
  2. 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_addr

3. 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))