Intelligent Contracts
Messages

Messages

GenLayer has three types of interaction, each operating at a different layer of the architecture.

Transactions (→ Intelligent Contract)

A transaction is the entry point for all IC execution. The caller signs an EVM transaction that calls addTransaction() on ConsensusMain or on the IC's ghost contract (which relays to ConsensusMain). This is submitted via eth_sendRawTransaction.

The caller can be an EOA or an EVM contract on GenLayer Chain.

  • gl.message.sender_address = caller address
  • gl.message.origin_address = caller address

The transaction enters the consensus pipeline: activation → proposal → commit → reveal → finalization.

⚠️

Studio: The SDK sends the same signed addTransaction EVM transaction via eth_sendRawTransaction, but Studio intercepts and simulates the consensus pipeline rather than executing on a real EVM layer.

Internal Messages (IC → IC)

An internal message is an asynchronous call from one Intelligent Contract to another. It stays within the GenVM layer. During execution, emit() records the message as part of the execution result — the message is not sent immediately. The actual child transaction is only created when the parent transaction reaches the specified state (on='accepted' or on='finalized'). The child transaction then goes through consensus independently.

other = gl.get_contract_at(addr)
 
# Call a write method on another IC
other.emit(on='finalized').update_status("active")
 
# Call with value (recipient must be payable)
other.emit(value=u256(100), on='finalized').deposit()
 
# Pure value transfer (triggers __receive__ on recipient)
other.emit_transfer(value=u256(100), on='finalized')
 
# Deploy a child contract
addr = gl.deploy_contract(
    code=contract_code,
    args=[],
    salt_nonce=u256(1),
    on='finalized',
)

In the child transaction's execution context:

  • gl.message.sender_address = calling contract's address
  • gl.message.origin_address = original caller (preserved through the chain)

See Interacting with Intelligent Contracts for full syntax reference.

External Messages (IC → Chain Layer)

An external message crosses from the GenVM layer back to the GenLayer Chain (EVM layer). This is how an IC sends value or calls an EOA or EVM contract.

External messages are executed by the IC's ghost contract via handleOp(), so the msg.sender seen by the recipient is the ghost contract address (which is the same as the IC's address).

@gl.evm.contract_interface
class ERC20:
    class View:
        def balance_of(self, owner: Address) -> u256: ...
    class Write:
        def transfer(self, to: Address, amount: u256) -> None: ...
 
# Read from an EVM contract (gl.message.contract_address is your own address)
balance = ERC20(token_address).view().balance_of(gl.message.contract_address)
 
# Write to an EVM contract (value is optional, defaults to 0)
ERC20(token_address).emit().transfer(recipient, amount)
 
# Send value to an EOA
@gl.evm.contract_interface
class _Recipient:
    class View:
        pass
    class Write:
        pass
 
_Recipient(Address(eoa_address)).emit_transfer(value=u256(amount))

External messages can only be emitted on='finalized'. Using on='accepted' for external messages is not supported.

⚠️

Studio: EVM contract interaction beyond value transfers to EOAs is not implemented. The @gl.evm.contract_interface calls are not functional in Studio.

Ghost Contracts

Every Intelligent Contract has a corresponding ghost contract on GenLayer Chain (the EVM layer). The ghost and IC share the same address.

What Ghost Contracts Do

  • Hold the IC's GEN balance — the native balance on the ghost contract on-chain is the source of truth for self.balance
  • Relay transactionsaddTransaction() on the ghost forwards to ConsensusMain
  • Execute external messages — on finalization, handleOp() forwards calls to recipients so msg.sender is the IC's address
  • Bridge the two layers — ghost on EVM ↔ IC on GenVM, same address

Lifecycle

  1. A deploy transaction is submitted
  2. GhostFactory deploys a ghost contract at address 0xABC on GenLayer Chain
  3. Consensus processes the deployment
  4. If successful: IC deploys at the same address 0xABC on GenVM
  5. If deployment is appealed and reverted: the ghost remains but has no IC behind it — it has no purpose or effect, but may be reused by consensus if deployment succeeds later

Ghost contract existence does not guarantee the IC is deployed. A reverted deployment leaves an empty ghost.

⚠️

Studio: Ghost contracts are not implemented. The IC address is used directly, and balance is stored in a database table.

Message Context (gl.message)

Every contract execution has access to gl.message:

FieldTypeDescription
sender_addressAddressImmediate caller — EOA, EVM contract, or IC depending on how the call was initiated
origin_addressAddressOriginal transaction submitter — preserved through internal message chains
contract_addressAddressThe current contract's own address
valueu256GEN sent with the call (only in @gl.public.write.payable methods)
chain_idu256Current chain ID

Timing: Accepted vs Finalized

Internal messages and contract deployments can execute at two different points:

on='finalized' (default)

The message executes after the parent transaction is fully finalized (appeal window has closed). This is the safe default.

on='accepted'

The message executes as soon as the parent transaction is accepted by initial consensus, before the appeal window closes.

⚠️

Appeal risks with on='accepted':

  • The re-execution may emit the message again — potentially multiple times across appeal rounds.
  • If the appeal changes the outcome, the message may be "invalid" (would not have been emitted with the final outcome), but it cannot be taken back — it was already sent and executed.
  • The receiving contract must be idempotent and must handle duplicate or unexpected messages gracefully.
  • Contract design must account for the possibility that accepted messages may not reflect the final state.

External messages always use on='finalized' and cannot be emitted on acceptance.

Interfaces

IC Interfaces (@gl.contract_interface)

Define typed stubs for calling other Intelligent Contracts:

@gl.contract_interface
class Token:
    class View:
        def balance_of(self, owner: Address) -> u256: ...
    class Write:
        def transfer(self, to: Address, amount: u256) -> None: ...
 
token = Token(token_address)
balance = token.view().balance_of(user)          # typed view call
token.emit(on='finalized').transfer(to, amount)  # typed write call

This is purely for type safety and IDE autocompletion — at runtime it behaves identically to gl.get_contract_at(). No overhead.

EVM Interfaces (@gl.evm.contract_interface)

Define typed stubs matching Solidity function signatures:

@gl.evm.contract_interface
class IERC20:
    class View:
        def balance_of(self, owner: Address) -> u256: ...
        def total_supply(self) -> u256: ...
    class Write:
        def transfer(self, to: Address, amount: u256) -> bool: ...
        def approve(self, spender: Address, amount: u256) -> bool: ...

Parameters are automatically ABI-encoded. Type mapping:

GenLayerSolidity
u256, u128, u64, ...uint256, uint128, uint64, ...
i256, i128, i64, ...int256, int128, int64, ...
Addressaddress
boolbool
strstring
bytesbytes