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

Cross-Chain Replay Attack Vulnerability in Beanstalk's L2ContractMigrationFacet

Summary

Beanstalk is migrating from an L1 network to an L2 network, such as Arbitrum. During this process, the redeemDepositsAndInternalBalances function in the L2ContractMigrationFacet contract is vulnerable to cross-chain replay attacks due to the way signatures are verified without incorporating the chainId.

This can happen when there is also a fork in the chain.

Vulnerability Details

The redeemDepositsAndInternalBalances function verifies the authenticity of transactions using EIP-712 signatures. However, the current implementation of the verifySignature function does not include the chainId in the hash computation, making it possible for an attacker to replay the same transaction on different chains.

function redeemDepositsAndInternalBalances(
address owner,// L1 address
address reciever,// L2 address
AccountDepositData[] calldata deposits,
AccountInternalBalance[] calldata internalBalances,
uint256 ownerRoots,
bytes32[] calldata proof,
uint256 deadline,
bytes calldata signature
) external payable fundsSafu noSupplyChange nonReentrant {
// verify deposits are valid.
// note: if the number of contracts that own deposits is small,
// deposits can be stored in bytecode rather than relying on a merkle tree.
verifyDepositsAndInternalBalances(owner, deposits, internalBalances, ownerRoots, proof);
// signature verification.
verifySignature(owner, reciever, deadline, signature);
// set deposits for `reciever`.
uint256 accountStalk;
for (uint256 i; i < deposits.length; i++) {
accountStalk += addMigratedDepositsToAccount(reciever, deposits[i]);
}
// set stalk for account.
setStalk(reciever, accountStalk, ownerRoots);
}
/**
* @notice verfies that the input parameters for deposits
* are correct.
*/
function verifyDepositsAndInternalBalances(
address account,
AccountDepositData[] calldata deposits,
AccountInternalBalance[] calldata internalBalances,
uint256 ownerRoots,
bytes32[] calldata proof
) internal pure {//@audit not including chainId
bytes32 leaf = keccak256(abi.encode(account, deposits, internalBalances, ownerRoots));
require(MerkleProof.verify(proof, MERKLE_ROOT, leaf), "Migration: invalid proof");
}
function verifySignature(
address owner,
address receiver,
uint256 deadline,
bytes calldata signature
) internal view {
require(block.timestamp <= deadline, "Migration: permit expired deadline");
bytes32 structHash = keccak256(
abi.encode(REDEEM_DEPOSIT_TYPE_HASH, owner, receiver, deadline)
);
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
require(signer == owner, "Migration: permit invalid signature");
}

Impact

An attacker can exploit this vulnerability by monitoring transactions on one chain and replaying them on another chain(fork) where the same contract is deployed. This can lead to unauthorized claims of tokens and transfers, causing significant financial losses and misdirection of assets.

Tools Used

Manual Review

Recommendations

To mitigate this vulnerability, it is crucial to include the chainId in the EIP-712 signature verification process. This ensures that each signature is unique to the specific chain it was intended for, preventing replay attacks across different chains.

The updated verifySignature function should include the chainId in the structHash computation, as shown below.

function verifySignature(
address owner,
address receiver,
uint256 deadline,
bytes calldata signature
) internal view {
require(block.timestamp <= deadline, "Migration: permit expired deadline");
bytes32 structHash = keccak256(
abi.encode(REDEEM_DEPOSIT_TYPE_HASH, owner, receiver, deadline, block.chainid)
);
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
require(signer == owner, "Migration: permit invalid signature");
}
Updates

Lead Judging Commences

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

Appeal created

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

Signature in L2ContractMigrationFacet should contain the chain ID to avoid replay attacks in case of a fork,

Support

FAQs

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