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

User can call `L2ContractMigrationFacet#verifyDepositsAndInternalBalances` repeatedly and redeem each time

Summary

User can get infinite funds on L2

Vulnerability Details

In order for ownable contracts that have deposits in Beanstalk to be able to migrate, a merkle tree is set and signatures from the owner is required. Then, using the merkle tree and the signature, the owner would be able to claim the funds on the L2.

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 {
// 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);
}
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 reciever,
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, reciever, deadline)
);
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
require(signer == owner, "Migration: permit invalid signature");
}

The problem is that even upon a successful call, the merkle leaf is never set to used. This allows the owner to repeatedly call the function and receive funds each time.

Impact

Infinite minting of assets

Tools Used

Manual review

Recommendations

Upon a successful call, mark the leaf as used.

Updates

Lead Judging Commences

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

Replay attack vulnerability in `redeemDepositsAndInternalBalances` function - it could be replayed

Support

FAQs

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