Summary
It won't be possible to cancel an old L1 to L2 deposit request if _starklaneL2Address
or _starklaneL2Selector
is changed. This will lead to NFTs being stuck in the contract.
Vulnerability Details
Currently, the codebase supports the cancellation of L1 to L2 NFT deposit requests with the following sequence of actions :
step 1 : Bridge owner
calls startRequestCancellation()
in Bridge.sol
for the deposit request.
step 2 : A user calls cancelRequest()
in Bridge.sol
to finalize the cancellation request and withdraw the NFTs from the escrow.
This seems pretty straightforward, however, if either of _starklaneL2Address
or _starklaneL2Selector
is altered before or after step 1, step 2 will revert. This is because in both the startRequestCancellation()
and cancelRequest()
functions, the storage variables _starklaneL2Address
and _starklaneL2Selector
are used to calculate the msgHash
in StarknetMessaging.sol
.
Bridge.sol
:
function startRequestCancellation(
uint256[] memory payload,
uint256 nonce
) external onlyOwner {
IStarknetMessaging(_starknetCoreAddress).startL1ToL2MessageCancellation(
snaddress.unwrap(_starklaneL2Address),
felt252.unwrap(_starklaneL2Selector),
payload,
nonce
);
Request memory req = Protocol.requestDeserialize(payload, 0);
emit CancelRequestStarted(req.hash, block.timestamp);
}
@notice Cancel a given request.
@param payload Request to cancel
@param nonce Nonce used for request sending.
*/
function cancelRequest(
uint256[] memory payload,
uint256 nonce
) external {
IStarknetMessaging(_starknetCoreAddress).cancelL1ToL2Message(
snaddress.unwrap(_starklaneL2Address),
felt252.unwrap(_starklaneL2Selector),
payload,
nonce
);
Request memory req = Protocol.requestDeserialize(payload, 0);
_cancelRequest(req);
emit CancelRequestCompleted(req.hash, block.timestamp);
}
StarknetMessaging.sol
:
function startL1ToL2MessageCancellation(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload,
uint256 nonce
) external override returns (bytes32) {
emit MessageToL2CancellationStarted(msg.sender, toAddress, selector, payload, nonce);
bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce);
uint256 msgFeePlusOne = l1ToL2Messages()[msgHash];
require(msgFeePlusOne > 0, "NO_MESSAGE_TO_CANCEL");
l1ToL2MessageCancellations()[msgHash] = block.timestamp;
return msgHash;
}
function cancelL1ToL2Message(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload,
uint256 nonce
) external override returns (bytes32) {
emit MessageToL2Canceled(msg.sender, toAddress, selector, payload, nonce);
bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce);
uint256 msgFeePlusOne = l1ToL2Messages()[msgHash];
require(msgFeePlusOne != 0, "NO_MESSAGE_TO_CANCEL");
uint256 requestTime = l1ToL2MessageCancellations()[msgHash];
require(requestTime != 0, "MESSAGE_CANCELLATION_NOT_REQUESTED");
uint256 cancelAllowedTime = requestTime + messageCancellationDelay();
require(cancelAllowedTime >= requestTime, "CANCEL_ALLOWED_TIME_OVERFLOW");
require(block.timestamp >= cancelAllowedTime, "MESSAGE_CANCELLATION_NOT_ALLOWED_YET");
l1ToL2Messages()[msgHash] = 0;
return (msgHash);
}
There are two possible scenarios :
The owner
alters _starklaneL2Address
or _starklaneL2Selector
before step 1
The owner won't be able to initiate the cancellation of a deposit request that uses the old values of the variables.
The owner
alters _starklaneL2Address
or _starklaneL2Selector
after step 1
The user won't be able to finalize the cancellation of a deposit request that uses the old values of the variables.
In theory, the owner can always switch the variables back to their old values and allow the cancellation to occur, however, during this window, new deposit requests created would be ambiguous and would need further mitigation, resulting in a neverending cycle.
Impact
Stuck NFTs/ DOS
Tools Used
Recommendations
Add _starklaneL2Address
, _starklaneL2Selector
as input variables in both the startRequestCancellation()
and cancelRequest()
functions.
function startRequestCancellation(
uint256[] memory payload,
- uint256 nonce
+ uint256 nonce,
+ snaddress starklaneL2Address,
+ felt252 starklaneL2Selector
+
) external onlyOwner {
IStarknetMessaging(_starknetCoreAddress).startL1ToL2MessageCancellation(
- snaddress.unwrap(_starklaneL2Address),
- felt252.unwrap(_starklaneL2Selector),
+ snaddress.unwrap(starklaneL2Address),
+ felt252.unwrap(starklaneL2Selector),
payload,
nonce
);
function cancelRequest(
uint256[] memory payload,
- uint256 nonce
+ uint256 nonce,
+ snaddress starklaneL2Address,
+ felt252 starklaneL2Selector
) external {
IStarknetMessaging(_starknetCoreAddress).cancelL1ToL2Message(
- snaddress.unwrap(_starklaneL2Address),
- felt252.unwrap(_starklaneL2Selector),
+ snaddress.unwrap(starklaneL2Address),
+ felt252.unwrap(starklaneL2Selector),
payload,
nonce