Skip to content

L1 Broadcaster

Once the governance vote on Ethereum Mainnet is concluded, a corresponding sequence of messages needs to be communicated to the Layer 2. This is done via the Broadcaster's broadcast function.

GitHub

Because L2's provide different infrastructures to broadcast messages, there are three slighlty different versions for the Broadcaster.vy contract:

A comprehensive list of all deployed contracts is available here .


Arbitrum

More on how L1 to L2 messaging on Arbitrum works can be found on the official Arbitrum documentation.

broadcast

Broadcaster.broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint256, _max_fee_per_gas: uint256):

Guarded Method

This function is only callable by one of the agents (ownership, parameter or emergency).

Function to broadcast a sequence of messages to the Relayer contract on a L2.

Input Type Description
_messages DynArray[Message, MAX_MESSAGES] Sequence of messages to broadcast
_gas_limit uint256 Gas limit for execution on L2
_max_fee_per_gas uint256 maximum gas price bid for the execution on L2
Source code
agent: HashMap[address, Agent]

arb_inbox: public(address)
arb_refund: public(address)

@external
def broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint256, _max_fee_per_gas: uint256):
    """
    @notice Broadcast a sequence of messeages.
    @param _messages The sequence of messages to broadcast.
    @param _gas_limit The gas limit for the execution on L2.
    @param _max_fee_per_gas The maximum gas price bid for the execution on L2.
    """
    agent: Agent = self.agent[msg.sender]
    assert agent != empty(Agent)

    # define all variables here before expanding memory enormously
    arb_inbox: address = self.arb_inbox
    arb_refund: address = self.arb_refund
    submission_cost: uint256 = 0

    data: Bytes[MAXSIZE] = _abi_encode(
        agent,
        _messages,
        method_id=method_id("relay(uint256,(address,bytes)[])"),
    )
    submission_cost = IArbInbox(arb_inbox).calculateRetryableSubmissionFee(len(data), block.basefee)

    # NOTE: using `unsafeCreateRetryableTicket` so that refund address is not aliased
    raw_call(
        arb_inbox,
        _abi_encode(
            self,  # to
            empty(uint256),  # l2CallValue
            submission_cost,  # maxSubmissionCost
            arb_refund,  # excessFeeRefundAddress
            arb_refund,  # callValueRefundAddress
            _gas_limit,
            _max_fee_per_gas,
            data,
            method_id=method_id("unsafeCreateRetryableTicket(address,uint256,uint256,address,address,uint256,uint256,bytes)"),
        ),
        value=submission_cost + _gas_limit * _max_fee_per_gas,
    )
>>> soon

arb_inbox

Broadcaster.arb_inbox() -> address: view

Getter for the Arbitrum Delayed Inbox contract.

Source code
arb_inbox: public(address)
>>> Broadcaster.arb_inbox()
'0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f'

arb_refund

Broadcaster.arb_refund() -> address: view

Getter for the refund address, which is the L2 Vault.

Source code
arb_refund: public(address)
>>> Broadcaster.arb_refund()
'0x25877b9413Cc7832A6d142891b50bd53935feF82'

set_arb_inbox

Broadcaster.set_arb_inbox(_arb_inbox: address):

Guarded Method

This function is only callable by the ownership admin.

Function to set a new Arbitrum Inbox contract.

Emits: SetArbInbox

Input Type Description
_arb_inbox address New Arbitrum inbox address
Source code
event SetArbInbox:
    arb_inbox: address

@external
def set_arb_inbox(_arb_inbox: address):
    assert msg.sender == self.admins.ownership

    self.arb_inbox = _arb_inbox
    log SetArbInbox(_arb_inbox)
>>> soon

set_arb_refund

Broadcaster.set_arb_refund(_arb_refund: address):

Guarded Method

This function is only callable by the ownership admin.

Function to set a new refund address.

Emits: SetArbRefund

Input Type Description
set_arb_refund address New refund address
Source code
event SetArbRefund:
    arb_refund: address

@external
def set_arb_refund(_arb_refund: address):
    assert msg.sender == self.admins.ownership

    self.arb_refund = _arb_refund
    log SetArbRefund(_arb_refund)
>>> soon

Optimism and Optimistic Rollups

Base and Mantle conduct messaging the same way as Optimism, as they are Optimistic Rollups.

More on how L1 to L2 messaging on Arbitrum works can be found in the official Optimism documentation.

broadcast

OptimismBroadcaster.broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint32 = 0):

Guarded Method

This function is only callable by one of the agents (ownership, parameter or emergency).

Function to broadcast a sequence of messages to the Relayer contract on a L2.

Input Type Description
_messages DynArray[Message, MAX_MESSAGES] Sequence of messages to broadcast
_gas_limit uint256 Gas limit for execution on L2
Source code
@external
def broadcast(_messages: DynArray[Message, MAX_MESSAGES], _gas_limit: uint32 = 0):
    """
    @notice Broadcast a sequence of messeages.
    @param _messages The sequence of messages to broadcast.
    @param _gas_limit The L2 gas limit required to execute the sequence of messages.
    """
    agent: Agent = self.agent[msg.sender]
    assert agent != empty(Agent)

    # https://community.optimism.io/docs/developers/bridge/messaging/#for-l1-%E2%87%92-l2-transactions
    gas_limit: uint32 = _gas_limit
    if gas_limit == 0:
        gas_limit = OVMChain(self.ovm_chain).enqueueL2GasPrepaid()

    raw_call(
        self.ovm_messenger,
        _abi_encode(  # sendMessage(address,bytes,uint32)
            self,
            _abi_encode(  # relay(uint256,(address,bytes)[])
                agent,
                _messages,
                method_id=method_id("relay(uint256,(address,bytes)[])"),
            ),
            gas_limit,
            method_id=method_id("sendMessage(address,bytes,uint32)"),
        ),
    )
>>> soon

ovm_chain

OptimismBroadcaster.ovm_chain(): view

Getter for the Optimism CanonicalTransactionChain.

Source code
ovm_chain: public(address)  # CanonicalTransactionChain
>>> OptimismBroadcaster.ovm_chain()
'0x5E4e65926BA27467555EB562121fac00D24E9dD2'

ovm_messenger

OptimismBroadcaster.ovm_messenger(): view

Getter for the Optimism Proxy OVM L1 Cross Domain Messenger.

Source code
ovm_messenger: public(address)  # CrossDomainMessenger
>>> OptimismBroadcaster.ovm_messenger()
'0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1'

set_ovm_chain

OptimismBroadcaster.set_ovm_chain(_ovm_chain: address):

Guarded Method

This function can only be called by the ownership admin.

Function to set a new OVM Canonical Transaction Chain contract.

Emits: SetOVMChain

Input Type Description
_ovm_chain address New ovm chain address
Source code
event SetOVMChain:
    ovm_chain: address

@external
def set_ovm_chain(_ovm_chain: address):
    """
    @notice Set the OVM Canonical Transaction Chain storage variable.
    """
    assert msg.sender == self.admins.ownership

    self.ovm_chain = _ovm_chain
    log SetOVMChain(_ovm_chain)
>>> soon

set_ovm_messenger

OptimismBroadcaster.set_ovm_messenger):

Guarded Method

This function can only be called by the ownership admin.

Function to set a new OVM Cross Domain messenger contract.

Emits: SetOVMMessenger

Input Type Description
_ovm_messenger address New ovm messenger address
Source code
event SetOVMMessenger:
    ovm_messenger: address

@external
def set_ovm_messenger(_ovm_messenger: address):
    """
    @notice Set the OVM Cross Domain Messenger storage variable.
    """
    assert msg.sender == self.admins.ownership

    self.ovm_messenger = _ovm_messenger
    log SetOVMMessenger(_ovm_messenger)
>>> soon

Other Chains

Outside of Arbitrum, Optimism, and Optimistic Rollups, Curves cross-chain infrastructure uses a single XYZBroadcaster.vy contract deployed at 0x5786696bB5bE7fCDb9997E7f89355d9e97FF8d89.

This contract is responsible for broadcasting messages across several blockchains including Avalanche, Fantom, BinanceSmartChain, Kava, and Polygon.

broadcast

XYZBroadcaster.broadcast(_chain_id: uint256, _messages: DynArray[Message, MAX_MESSAGES])

Guarded Method

This function is only callable by one of the agents (ownership, parameter or emergency).

Function to broadcast a sequence of messages to the Relayer contract on a L2.

Input Type Description
_chain_id uint256 Chain ID to broadcast to
_messages DynArray[Message, MAX_MESSAGES] Sequence of messages to broadcast
Source code
event Broadcast:
    agent: Agent
    chain_id: uint256
    nonce: uint256
    digest: bytes32

enum Agent:
    OWNERSHIP
    PARAMETER
    EMERGENCY

admins: public(AdminSet)
future_admins: public(AdminSet)

agent: HashMap[address, Agent]

nonce: public(HashMap[Agent, HashMap[uint256, uint256]])  # agent -> chainId -> nonce
digest: public(HashMap[Agent, HashMap[uint256, HashMap[uint256, bytes32]]])  # agent -> chainId -> nonce -> messageDigest

@external
def broadcast(_chain_id: uint256, _messages: DynArray[Message, MAX_MESSAGES]):
    """
    @notice Broadcast a sequence of messeages.
    @param _chain_id The chain id to have messages executed on.
    @param _messages The sequence of messages to broadcast.
    """
    agent: Agent = self.agent[msg.sender]
    assert agent != empty(Agent)

    digest: bytes32 = keccak256(_abi_encode(_messages))
    nonce: uint256 = self.nonce[agent][_chain_id]

    self.digest[agent][_chain_id][nonce] = digest
    self.nonce[agent][_chain_id] = nonce + 1

    log Broadcast(agent, _chain_id, nonce, digest)
>>> XYZBroadcaster.broadcast
'todo'

Contract Ownership

The Broadcaster contracts are managed by three admins, defined as follows:

Upgradable Ownership

The admins are upgradable. Changes follow a two-step process: First, new admins must be committed using the commit_admins function. Then, these changes are applied by calling apply_admins. Only the current Ownership Admin, represented by the Curve DAO, can initiate these changes.

Jupyter Notebook

For a practical demonstration on how to change the ownership of such contracts, refer to this notebook: https://try.vyperlang.org/hub/user-redirect/lab/tree/shared/mo-anon/basic/ownership.ipynb.