Security Insights on Starknet Bridges (Messaging)

Cross-chain bridging is a crucial component for transferring assets from one blockchain to another. Starknet offers a built-in messaging system that facilitates communication between Ethereum (L1) and Starknet (L2). By using this messaging functionality, developers can build bridges that seamlessly transmit data and trigger actions across layers.

Cross-chain bridging is a crucial component for transferring assets from one blockchain to another. This process generally involves off-chain mechanisms: assets are monitored on the source chain, and corresponding actions (such as mints or transfers) are executed on the destination chain.

Starknet offers a built-in messaging system that facilitates communication between Ethereum (L1) and Starknet (L2). By using this messaging functionality, developers can build bridges that seamlessly transmit data and trigger actions across layers.

Understanding L1 → L2 Messaging

In the Starknet ecosystem, messaging from L1 to L2 relies on Starknet’s L1 contracts, which include interfaces like IStarknetMessaging.sol. This interface provides functions essential for sending messages between Ethereum and Starknet.

Sending a Message from Ethereum to Starknet

To initiate messaging from Ethereum to Starknet, developers call the sendMessageToL2 function:

function sendMessageToL2(
	uint256 toAddress,
	uint256 selector,
	uint256[] calldata payload
) external payable returns (bytes32, uint256);

The function’s parameters are key to secure and effective communication:

  • toAddress – Identifies the target contract on Starknet (L2) where the message should be delivered.

  • selector – Specifies the l1_handler function of the L2 contract that will handle the message.

  • payload – Contains the data needed as inputs for the L2 function.

When developing this functionality, it’s crucial to decide which parameters should be controlled by the function itself and which can be supplied by the caller:

  • Typically, toAddress should be set by the function to prevent arbitrary messaging to different L2 contracts.

  • Similarly, selector should be controlled internally for security.

  • payload is more flexible: it may be fully controlled, or some specific inputs may be restricted, depending on the intended use case.

Example: ArkBridge NFT Bridge

To illustrate this, let’s examine the ArkBridge, an NFT bridge that transfers NFTs between Ethereum and Starknet.

function depositTokens(
        uint256 salt,
        address collectionL1,
        snaddress ownerL2,
        uint256[] calldata ids,
        bool useAutoBurn
    ) external payable {
	// ...
	IStarknetMessaging(_starknetCoreAddress).sendMessageToL2{
			value: msg.value
		}(
			snaddress.unwrap(_starklaneL2Address),
			felt252.unwrap(_starklaneL2Selector),
			payload
		);
	// ...
	}

In ArkBridge’s depositTokens(...) function, a NFT is first deposited on Ethereum. The bridge then sends a message to Starknet, where a corresponding NFT is minted. Here, the toAddress and selector are drawn from storage variables (_starklaneL2Address and _starklaneL2Selector), ensuring consistency. The payload is constructed based on user-supplied parameters, which undergo proper validation.

Let’s break down some important points to consider when sending messages to Starknet.

1. Handling Starknet Addresses in Solidity

In the depositTokens(...) function, you may notice the ownerL2 parameter, which has the snaddress type. This type represents a ContractAddress on Starknet and must fit within the felt252 range. Why not simply use a regular EVM address type?

The key difference is how addresses are represented between Ethereum and Starknet. On Ethereum, addresses are 160 bits, while Starknet’s ContractAddress spans a larger 251-bit space. In Solidity, the ArkBridge wraps these as snAddress (a uint256 type), enabling compatibility with Ethereum. Understanding this difference is critical when working across Ethereum and Starknet, as it affects how addresses are handled.

2. Bridging Fees

Messaging requires a fee paid in ETH, which is why sendMessageToL2 is marked as payable. The required fee is calculated similarly to an L2 transaction fee, which compensates the sequencer for executing the l1_handler transaction. However, Starknet’s L1 contracts don’t enforce fee verification, so contracts implementing messaging should validate that msg.value is within a reasonable range. This precaution helps prevent issues, such as assets or NFTs becoming stuck due to insufficient fees.

Additionally, users may overpay, but unlike other cross-chain solutions like LayerZero or Wormhole, Starknet’s contracts currently don’t offer refunds for overpaid fees. This is an important detail for developers to consider when designing user interactions with their bridges.

Message Delivery to L2

When a message is sent from an L1 contract, the Starknet sequencer executes the specified function as indicated by the selector in sendMessageToL2(...). This function in the Cairo contract (Starknet contract) must be marked with the [l1_handler] macro. This macro ensures that only the sequencer can call the function during message delivery.

Let's revisit the ArkBridge example, this time in the context of the bridge.cairo contract.

#[l1_handler]
fn withdraw_auto_from_l1(ref self: ContractState, from_address: felt252, req: Request) {
   ensure_is_enabled(@self);
   assert(self.bridge_l1_address.read().into() == from_address,     'Invalid L1 msg sender');
   // ...
   }

The withdraw_auto_from_l1 function is triggered by an L1 contract to mint or transfer an NFT on Starknet upon message delivery.

Let’s highlight a couple of important considerations here:

1. Sender Verification and Access Control

When implementing an l1_handler function, it’s essential to enforce proper access control to prevent unauthorized messages from untrusted L1 contracts. While access control typically involves validating the caller’s address, in this case, such checks are ineffective because the l1_handler function is invoked by the sequencer, and get_caller_address() (similar to msg.sender) would return zero. Instead, the from_address parameter represents the original sender's L1 address (in felt252 format) and must be verified to ensure it matches the expected sender.

2. Potential for Execution Failure

The l1_handler function may include various logic. In the ArkBridge example, ensure_is_enabled(...) checks if the bridge is enabled (like a pausing mechanism). If the bridge is disabled, the l1_handler function will revert, but the sequencer won’t attempt to re-execute it.

This can be problematic for assets already deposited or burned on the source chain that aren’t minted or transferred on the destination chain. To handle such cases, a contract should implement a mechanism allowing users to retrieve their assets. For example, Starknet brings a mechanism of canceling messages through Starknet’s L1 contracts.

Cancelling Messages

Message cancellation on Starknet is managed by Starknet’s L1 contracts for messages that haven’t been successfully delivered to the L2 contract. Starknet L1 contracts maintain a mapping that tracks the status of each message to determine whether it has been delivered.

The message sender can initiate a cancellation, which is a two-step process:

  1. Initiate Cancellation: The sender calls startL1ToL2MessageCancellation(...).

    function startL1ToL2MessageCancellation(
            uint256 toAddress,
            uint256 selector,
            uint256[] calldata payload,
            uint256 nonce
        ) external;
  2. Waiting Period: After initiating cancellation, a five-day waiting period is required before finalizing it.

  3. Complete Cancellation: The sender can then call cancelL1ToL2Message(...) to complete the cancellation.

    function cancelL1ToL2Message(
            uint256 toAddress,
            uint256 selector,
            uint256[] calldata payload,
            uint256 nonce
        ) external;

When implementing such cancellation, developers should consider the following:

Cancellation Access Control

Access control is essential to prevent unauthorized entities from cancelling messages, which could disrupt asset transfers or intended messaging logic.

In the ArkBridge example, only the contract owner can call the function that invokes startL1ToL2MessageCancellation(...) (reference). Finalization, however, is open to anyone, with secure steps in place to ensure the NFT is returned to its rightful owner.

From a design perspective, it might be preferable to track which user sent each message and allow them to cancel their own messages. This approach provides more user-level control, although the onlyOwner method is simpler to implement and may suffice for many cases.

In any case, access control must be implemented carefully to prevent attackers from disrupting users' bridging processes. Therefore, cancellation should never be initiated by unauthorized parties.

The L2 -> L1 messaging will be described in the next part...

References

Are you building a bridge on Starknet? CODESPECT is here to help! Our specialized team of Cairo auditors is dedicated to ensuring the seamless operation and security of your bridge. With our extensive experience in smart contract audits, we provide thorough assessments and recommendations tailored to your needs. Let us help you build a robust and secure protocol on Starknet!!!

Contact our founder: talfao