- Talfao's insights
- Posts
- Security Insights on Starknet Bridges (Messaging)
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 thel1_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:
Initiate Cancellation: The sender calls
startL1ToL2MessageCancellation(...)
.function startL1ToL2MessageCancellation( uint256 toAddress, uint256 selector, uint256[] calldata payload, uint256 nonce ) external;
Waiting Period: After initiating cancellation, a five-day waiting period is required before finalizing it.
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