NFTBridge
60,000 USDC
View results
Submission Details
Severity: high
Invalid

Double Consumption of Messages will Lead to Double-Spending (`StarklaneMessaging::_consumeMessageStarknet`)

Summary

Vulnerability Detail

The Starklane contract, which inherits from StarklaneMessaging, implements a bridge for token transfers between Layer 1 and Layer 2. It uses two methods for message consumption: an auto-withdrawal method and a Starknet core contract method. The StarklaneMessaging contract uses a mapping _autoWithdrawn to track the status of messages that can be consumed via the auto method.

The issue lies in the withdrawTokens() function of the Starklane contract, which calls _consumeMessageStarknet() from StarklaneMessaging. The _consumeMessageStarknet() function does not adequately check for messages that have already been consumed via the auto method. While it verifies if a message is configured for auto-withdrawal, it fails to check if the message has already been processed through the auto method.

This inconsistency in message status checking creates a critical vulnerability where a single message could theoretically be consumed twice, once through each method. The root cause lies in the incomplete validation within the _consumeMessageStarknet() function, which only checks for WITHDRAW_AUTO_NONE status but not for WITHDRAW_AUTO_CONSUMED.

Impact

The issue allows for double consumption of messages, directly enabling double-spending of tokens. Malicious actors can exploit this to withdraw tokens multiple times for a single valid message, resulting in unauthorized token acquisition.

Proof of Concept

  • An attacker identifies a valid message for token withdrawal.

  • The attacker calls a function that uses _consumeMessageAutoWithdraw() to process the message:

function _consumeMessageAutoWithdraw(
snaddress fromL2Address,
uint256[] memory request
)
internal
{
bytes32 msgHash = keccak256(
abi.encodePacked(
snaddress.unwrap(fromL2Address),
uint256(uint160(address(this))),
request.length,
request)
);
uint256 status = _autoWithdrawn[msgHash];
if (status == WITHDRAW_AUTO_CONSUMED) {
revert WithdrawAlreadyError();
}
_autoWithdrawn[msgHash] = WITHDRAW_AUTO_CONSUMED;
}
  • The message is now marked as WITHDRAW_AUTO_CONSUMED in the _autoWithdrawn mapping.

  • The attacker then calls withdrawTokens(), which internally calls _consumeMessageStarknet():

function _consumeMessageStarknet(
IStarknetMessaging starknetCore,
snaddress fromL2Address,
uint256[] memory request
)
internal
{
bytes32 msgHash = starknetCore.consumeMessageFromL2(
snaddress.unwrap(fromL2Address),
request
);
if (_autoWithdrawn[msgHash] != WITHDRAW_AUTO_NONE) {
revert WithdrawMethodError();
}
}
  • _consumeMessageStarknet() successfully consumes the message again through the Starknet core contract, as it doesn't check for the WITHDRAW_AUTO_CONSUMED status.

  • The attacker potentially withdraws tokens twice for the same message.

Tools Used

Manual review

Recommended Mitigation Steps

To address this issue, implement an additional check in the _consumeMessageStarknet() function to ensure that the message has not already been consumed via the auto method:

function _consumeMessageStarknet(
IStarknetMessaging starknetCore,
snaddress fromL2Address,
uint256[] memory request
)
internal
{
bytes32 msgHash = starknetCore.consumeMessageFromL2(
snaddress.unwrap(fromL2Address),
request
);
if (_autoWithdrawn[msgHash] != WITHDRAW_AUTO_NONE) {
revert WithdrawMethodError();
}
+ // Add this check to prevent double consumption
+ if (_autoWithdrawn[msgHash] == WITHDRAW_AUTO_CONSUMED) {
+ revert WithdrawAlreadyError();
+ }
}
Updates

Lead Judging Commences

n0kto Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.