DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: high
Valid

Tokens can get stuck during migration if the L2 side fails leading to loss of funds

Summary

During the mirgration process between BeanL1ReceiverFacet and BeanL2MigrationFacet, when transactions fail on the L2 side, tokens are forever burnt with no existing method to reclaw them.

Vulnerability Details

The migration process involves two key contracts: BeanL1ReceiverFacet and BeanL2MigrationFacet. The process starts on L1 where tokens are burned, and a message is sent to L2 to mint the equivalent amount of tokens.

Steps Involved:

  1. Burning on L1: The BeanL2MigrationFacet contract burns the tokens from the user's L1 balance.

  2. Message to L2: The contract then sends a message to L2 using the IL2Bridge interface, instructing the L2 contract to mint the equivalent amount of tokens.

Vulnerable Scenario:

  • If the message sent from L1 to L2 fails to execute successfully on L2 (e.g., due to contract limitations or gas issues), the tokens will have already been burned on L1, but the user will not receive the corresponding tokens on L2.

  • Specifically, the recieveL1Beans function on L2 could revert due to various reasons such as exceeding the maximum migrated beans or other contract-specific checks.

function recieveL1Beans(address reciever, uint256 amount) external nonReentrant {
require(
msg.sender == address(BRIDGE) &&
IL2Messenger(BRIDGE).xDomainMessageSender() == L1BEANSTALK
);
s.sys.migration.migratedL1Beans += amount;
require(
EXTERNAL_L1_BEANS >= s.sys.migration.migratedL1Beans,
"L2Migration: exceeds maximum migrated"
);
C.bean().mint(reciever, amount);
}
function migrateL2Beans(
address reciever,
address L2Beanstalk,
uint256 amount,
uint32 gasLimit
) external nonReentrant {
C.bean().burnFrom(msg.sender, amount);
IL2Bridge(BRIDGE).sendMessage(
L2Beanstalk,
abi.encodeCall(IBeanL1RecieverFacet(L2Beanstalk).recieveL1Beans, (reciever, amount)),
gasLimit
);
}

Impact

If a migration request causes the total migrated beans to exceed this limit, the recieveL1Beans function will revert with the error "L2Migration: exceeds maximum migrated". Additionally, if the specified gas limit (gasLimit) is too low, the transaction might run out of gas during execution on L2.

Users will permanently lose their tokens as they are burned on L1 but not minted on L2.

Tools Used

Manual Review

Recommendation

It is recommended to comprise a refund/reclaw mechanism for failed transactions on L2, so that tokens can be retrieved.

By implementing a retry mechanism and tracking migration requests, the potential issue of tokens getting stuck during the L1 to L2 migration can be mitigated. This approach ensures that users do not lose their tokens even if there are issues during the migration process.

function recieveL1Beans(address reciever, uint256 amount) external nonReentrant {
require(
msg.sender == address(BRIDGE) &&
IL2Messenger(BRIDGE).xDomainMessageSender() == L1BEANSTALK
);
s.sys.migration.migratedL1Beans += amount;
require(
EXTERNAL_L1_BEANS >= s.sys.migration.migratedL1Beans,
"L2Migration: exceeds maximum migrated"
);
C.bean().mint(reciever, amount);
// Mark the migration as completed on L1
bytes32 requestId = keccak256(abi.encodePacked(reciever, amount, block.timestamp));
IL2Bridge(BRIDGE).sendMessage(
L1Beanstalk,
abi.encodeCall(BeanL2MigrationFacet(L1Beanstalk).markMigrationCompleted, (requestId)),
gasLimit
);
}
mapping(bytes32 => MigrationRequest) public migrationRequests;
struct MigrationRequest {
address reciever;
uint256 amount;
uint256 timestamp;
bool completed;
}
function migrateL2Beans(
address reciever,
address L2Beanstalk,
uint256 amount,
uint32 gasLimit
) external nonReentrant {
C.bean().burnFrom(msg.sender, amount);
bytes32 requestId = keccak256(abi.encodePacked(reciever, amount, block.timestamp));
migrationRequests[requestId] = MigrationRequest(reciever, amount, block.timestamp, false);
IL2Bridge(BRIDGE).sendMessage(
L2Beanstalk,
abi.encodeCall(IBeanL1RecieverFacet(L2Beanstalk).recieveL1Beans, (reciever, amount)),
gasLimit
);
}
function refundFailedMigration(bytes32 requestId) external {
MigrationRequest storage request = migrationRequests[requestId];
require(!request.completed, "Migration already completed");
require(block.timestamp > request.timestamp + 1 days, "Migration still in process");
C.bean().mint(request.reciever, request.amount);
request.completed = true;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

If EXTERNAL_L1_BEANS check performed on L2 fails then the burned beans are lost for ever

Appeal created

holydevoti0n Judge
12 months ago
golanger85 Submitter
12 months ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

If EXTERNAL_L1_BEANS check performed on L2 fails then the burned beans are lost for ever

Support

FAQs

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