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 {
Fertilizer fertilizer = new Fertilizer();
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];
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;
for (uint j; j < f.accountData.length; j++) {
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.