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

Replay Attack on claimFertilized Function Leading to Double Spending

Summary

The claimFertilized function in the FertilizerFacet contract is designed to allow users to claim Beans from specified Fertilizer IDs. However, the function is susceptible to replay attacks due to the lack of mechanisms ensuring each Fertilizer ID is claimed only once.
The root cause of the vulnerability is the absence of checks to mark Fertilizer IDs as claimed within the claimFertilized function or the underlying beanstalkUpdate function. This omission allows an attacker to reuse the same calldata to claim Beans multiple times.

Vulnerable claimFertilized Function

function claimFertilized(
uint256[] calldata ids,
LibTransfer.To mode
) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) {
uint256 amount = C.fertilizer().beanstalkUpdate(LibTractor._user(), ids, s.sys.fert.bpf);
s.sys.fert.fertilizedPaidIndex += amount;
LibTransfer.sendToken(C.bean(), amount, LibTractor._user(), mode);
}

Interaction with C.sol

function fertilizer() internal pure returns (IFertilizer) {
return IFertilizer(FERTILIZER);
}

Interaction with LibTractor

The LibTractor library is used to determine the current user making the claim:

function _user() internal view returns (address payable user) {
user = _getActivePublisher();
if (uint160(bytes20(address(user))) <= 1) {
user = payable(msg.sender);
}
}
  • User Determination: The LibTractor._user() function ensures that the correct user is identified, either the active publisher or msg.sender. This helps in attributing the claims to the right user but does not directly prevent replay attacks.

  • State Checks and Updates: The claimFertilized function relies on C.fertilizer().beanstalkUpdate() to handle state updates. However, this function does not mark IDs as claimed, the function is vulnerable to replay attacks.

Proof of Concept

1: Initial Claim::

  • User A calls the claimFertilized function with Fertilizer ID 123.

  • The function processes the claim and transfers Beans to User A.

2: Replay Attack:

  • User A captures the transaction data (calldata) used in the initial claim.

  • User A reuses the same calldata to call the claimFertilized function again.

  • Without proper checks, the function processes the same Fertilizer ID 123 again, transferring additional Beans to User A.

Impact

  • Malicious users can claim more Beans than they are entitled to, leading to inflation of the token supply.

  • The protocol and its honest users suffer significant economic losses due to unauthorized claims.

Tools Used

Manual review

Recommendations

To ensure replay attacks are mitigated, the claimFertilized function or the underlying beanstalkUpdate function should be modified to include checks that mark Fertilizer IDs as claimed and prevent multiple claims.

function claimFertilized(
uint256[] calldata ids,
LibTransfer.To mode
) external payable fundsSafu noSupplyChange oneOutFlow(C.BEAN) {
uint256 amount = 0;
address user = LibTractor._user();
for (uint256 i = 0; i < ids.length; i++) {
uint256 id = ids[i];
// Ensure the Fertilizer ID exists and has not been claimed
require(s.sys.fert.fertilizer[id] > 0, "Invalid Fertilizer ID");
require(!s.sys.fert.claimed[id], "Fertilizer already claimed");
// Update the amount to be claimed
amount += C.fertilizer().beanstalkUpdate(user, id, s.sys.fert.bpf);
// Mark the Fertilizer ID as claimed
s.sys.fert.claimed[id] = true;
}
s.sys.fert.fertilizedPaidIndex += amount;
LibTransfer.sendToken(C.bean(), amount, user, mode);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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