Beanstalk Notion
Beanstalk Notion
/
🪲
Bug Reports
/
BIC Notes
/
📄
Report #43424
📄

Report #43424

Report Date
April 5, 2025
Status
Closed
Payout

``setApprovalForAll`` Function Fails to Grant Approval

‣
Report Info

Report ID

#43424

Report Type

Smart Contract

Has PoC

Yes

Target

https://arbiscan.io/address/0xD1A0060ba708BC4BCD3DA6C37EFa8deDF015FB70

Impacts

Contract fails to deliver promised returns, but doesn't lose value

Details

In the ApprovalFacet contract of the Beanstalk protocol (using a Diamond proxy pattern), the setApprovalForAll function is intended to allow a user to authorize a spender to manage all of their ERC-1155 deposits. While the function emits the expected event and updates the internal mapping, the approval does not take effect in practice — the spender remains unable to interact with the user's deposits.

Vulnerability Details

Contract: ApprovalFacet (Diamond Proxy)

Functions Affected: setApprovalForAll(address spender, bool approved) and transferDeposits function

Issue:

The vulnerability lies in the setApprovalForAll function within the ApprovalFacet contract of Beanstalk. This function is meant to allow a user to grant blanket approval to another address (a spender) to manage all of their ERC-1155 deposits. Internally, the function sets s.accts[LibTractor._user()].isApprovedForAll[spender] = approved and emits the corresponding ApprovalForAll event.

However, this approval is not respected by other parts of the protocol — most notably, the Depot contract's transferDeposit function. Even after successfully calling setApprovalForAll, the spender is unable to move the user's deposits; the call to transferDeposit reverts.

This is demonstrated in the provided proof of concept (PoC), where a call to transferDeposit fails after setApprovalForAll, but succeeds when using approveDeposit instead. This suggests a disconnect between the approval state set in setApprovalForAll and the logic that checks permissions for deposit transfers.

As a result, the setApprovalForAll function appears to update internal state correctly but is functionally inert, leading to unexpected behavior and broken delegation flows across the protocol.

Impact Details

Contract fails to deliver promised returns, since users can't use the setApprovalForAll function to approve deposit transfers, despite the function appearing to work correctly.

Mitigation

Change the _spendDepositAllowance in the LibSilo contract to:

By changing this function, we'll start to check the isApprovedForAll variable when spending the allowance, different from before (now) where we are only checking the currentAllowance value.

Proof of Concept

Run with:

forge test --fork-url https://arb1.lava.build --match-test test_IssueSetApprovalsForAll -vv

BIC Response

This is a known issue, therefore we are closing the report and no reward will be issued.

function _spendDepositAllowance(
        address owner,
        address spender,
        address token,
        uint256 amount
    ) external {
        uint256 currentAllowance = depositAllowance(owner, spender, token);
        AppStorage storage s = LibAppStorage.diamondStorage(); // added
        bool isApproved = s.accts[owner].isApprovedForAll[spender]; //added
        if (isApproved) return; //added
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "Silo: insufficient allowance");
            _approveDeposit(owner, spender, token, currentAllowance - amount);
        }
    }
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract WalterPOC is Test {
    function test_IssueSetApprovalsForAll() external{
        address alice = address(20);
        address randomContractThatNeedsApproval = address(100);
        address bean = 0xBEA0005B8599265D41256905A9B3073D397812E4;
        address beanDeposit = 0xD1A0060ba708BC4BCD3DA6C37EFa8deDF015FB70;
        Bean silo = Bean(beanDeposit);
        vm.startPrank(alice);
        deal(bean,alice,1_000e6);
        ERC20(bean).approve(beanDeposit,type(uint256).max);
        (,,int96 stem) = silo.deposit(bean, 1_000e6, 0);
        vm.stopPrank();

        // Transfering after setApprovalForAll will revert
        vm.prank(alice);
        silo.setApprovalForAll(randomContractThatNeedsApproval, true);
        vm.prank(randomContractThatNeedsApproval);
        vm.expectRevert();
        silo.transferDeposit(alice, randomContractThatNeedsApproval, bean, stem, 1_000e6);

        // Transfer of ERC1155(deposits) works if approving manually
        vm.prank(alice);
        silo.approveDeposit(randomContractThatNeedsApproval, bean, type(uint256).max);
        vm.prank(randomContractThatNeedsApproval);
        silo.transferDeposit(alice, randomContractThatNeedsApproval, bean, stem, 1_000e6);
    }
}

interface Bean{
    function approveDeposit(address,address,uint256) external;
    function transferDeposit(
        address sender,
        address recipient,
        address token,
        int96 stem,
        uint256 amount
    ) external;
    function deposit(address,uint256,uint8) external returns(uint256 amount, uint256 _bdv, int96 stem);
    function setApprovalForAll(address,bool) external;
}