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,
address reciever,
AccountDepositData[] calldata deposits,
AccountInternalBalance[] calldata internalBalances,
uint256 ownerRoots,
bytes32[] calldata proof,
uint256 deadline,
bytes calldata signature
) external payable fundsSafu noSupplyChange nonReentrant {
verifyDepositsAndInternalBalances(owner, deposits, internalBalances, ownerRoots, proof);
verifySignature(owner, reciever, deadline, signature);
uint256 accountStalk;
for (uint256 i; i < deposits.length; i++) {
accountStalk += addMigratedDepositsToAccount(reciever, deposits[i]);
}
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 {
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");
}