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

Reentracy vulnerability when minitng fertilizers due to the use of `_safeMint`

Relevant GitHub Links

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/tokens/Fertilizer/Fertilizer.sol#L59

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/beanstalk/barn/FertilizerFacet.sol#L85

https://github.com/Cyfrin/2024-05-beanstalk-the-finale/blob/8c8710df547f7d7c5dd82c5381eb6b34532e4484/protocol/contracts/beanstalk/init/reseed/L2/ReseedBarn.sol#L88

Summary

The _safeMint function contains a __doSafeTransferAcceptanceCheck hook which can be leveraged by malicious user to reenter the functions in which its used, to mint multiple Fertilizer tokens.

Vulnerability Details

We're interested in the beanstalkMint function, which is used to mint fertilizer tokens using the _safeMint method which contains the __doSafeTransferAcceptanceCheck hook.

function _safeMint(address to, uint256 id, uint256 amount, bytes memory data) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");
address operator = _msgSender();
_transfer(address(0), to, id, amount);
emit TransferSingle(operator, address(0), to, id, amount);
__doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}
function __doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) private {
if (LibFertilizer.isContract(to)) {
try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (
bytes4 response
) {
if (response != IERC1155Receiver.onERC1155Received.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non ERC1155Receiver implementer");
}
}
}

As can be see, the presence of the onERC1155Received hook in the __doSafeTransferAcceptanceCheck can then be weaponized by an attacker who has his malicious contract address containing callback to the init/mintFertilizer functions in its onERC1155Received function). Due to the absence of CEI pattern and nonReentrant modifiers on both function, will allow the attacker to be able to easily reenter the functions to mint more fertilizers.

For instance, in the init function in ReseedBarn.sol, the fertilizer implementations are deployed and then fertilizers minted. Notice the absence of CEI pattern and absence of the non reentrant modifier.

function init(
Fertilizers[] calldata fertilizerIds,
uint256 activeFertilizer,
uint256 fertilizedIndex,
uint256 unfertilizedIndex,
uint128 bpf
) external {
// deploy fertilizer implmentation.
Fertilizer fertilizer = new Fertilizer();
// deploy fertilizer proxy. Set owner to beanstalk.
TransparentUpgradeableProxy fertilizerProxy = new TransparentUpgradeableProxy(
address(fertilizer),
address(this),
abi.encode(IFertilizer.init.selector)
);
mintFertilizers(Fertilizer(address(fertilizerProxy)), fertilizerIds);
s.sys.season.fertilizing = true;
s.sys.fert.activeFertilizer = activeFertilizer;
s.sys.fert.fertilizedIndex = fertilizedIndex;
s.sys.fert.unfertilizedIndex = unfertilizedIndex;
s.sys.fert.bpf = bpf;
}

The mintFertilizers function is then called, a number of state chnages are made, and beanstalkMint is called, minitng fertilizers to the account.

function mintFertilizers(
Fertilizer fertilizerProxy,
Fertilizers[] calldata fertilizerIds
) internal {
for (uint i; i < fertilizerIds.length; i++) {
Fertilizers memory f = fertilizerIds[i];
// set s.firstFid, s.nextFid, s.lastFid
uint128 fid = f.fertilizerId;
if (i == 0) s.sys.fert.fertFirst = fid;
if (i != 0) s.sys.fert.nextFid[fertilizerIds[i - 1].fertilizerId] = fid;
if (i == fertilizerIds.length - 1) s.sys.fert.fertLast = fid;
// reissue fertilizer to each holder.
for (uint j; j < f.accountData.length; j++) {
// `id` only needs to be set once per account, but is set on each fertilizer
// as `Fertilizer` does not have a function to set `id` once on a batch.
fertilizerProxy.beanstalkMint(
f.accountData[j].account,
fid,
f.accountData[j].amount,
f.accountData[j].lastBpf
);
}
}
}

beanstalkMint makes cetain state updates and mints the the fertilizers to the accounts through the safeMint. The maliciious attacker will simply set the account to a contract address that he controls which has an onERC1155Received hook that also calls the init function, and through that, reenter the init function. The same can be leveraged in the mintFertilizer function in FertilizerFacet.sol

function beanstalkMint(
address account,
uint256 id,
uint128 amount,
uint128 bpf
) external onlyOwner {
if (_balances[id][account].amount > 0) {
uint256[] memory ids = new uint256[](1);
ids[0] = id;
_update(account, ids, bpf);
}
_balances[id][account].lastBpf = bpf;
_safeMint(account, id, amount, bytes("0"));
}

Impact

Multiple minting of fertilizers by leveraging reentracy.

Tools Used

Manual Review

Recommendations

Add the non-reentrant modifier to these functions.

Updates

Lead Judging Commences

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

Informational/Gas

Invalid as per docs https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity

safeMint reentrancy

Support

FAQs

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