Summary
The modifySupportedPool
function in BoostController.sol
is vulnerable to front-running attacks, allowing malicious users to gain unfair advantages by monitoring and acting on pending pool support changes before they are executed.
Vulnerability Detail
The modifySupportedPool
function allows managers to add or remove pool support without any time delay or protection mechanism:
function modifySupportedPool(
address pool,
bool isSupported
) external onlyRole(MANAGER_ROLE) {
if (pool == address(0)) revert InvalidPool();
if (supportedPools[pool] == isSupported) revert PoolNotSupported();
supportedPools[pool] = isSupported;
if (isSupported) {
emit PoolAdded(pool);
} else {
emit PoolRemoved(pool);
}
}
This implementation allows users to:
Monitor the mempool for modifySupportedPool
transactions
Front-run these transactions to perform actions before the pool status changes
Potentially gain unfair advantages or exploit the system
Impact
The vulnerability can be exploited in two scenarios:
Scenario 1: Front-running Pool Removal
Manager submits transaction to remove pool support
Attacker sees this pending transaction in mempool
Attacker front-runs with their own transaction to:
Manager's transaction executes, removing pool support
Attacker has gained benefits that should have been prevented
Scenario 2: Front-running Pool Addition
Manager submits transaction to add new pool support
Attacker sees this pending transaction in mempool
Attacker prepares and front-runs with optimized transactions
Attacker gains first-mover advantage in the newly supported pool
Proof of Concept
A test demonstrating this vulnerability has been created in test/BoostController.t.sol
:
function testFrontRunningVulnerability() public {
vm.startPrank(manager);
address testPool = address(0x123);
boostController.modifySupportedPool(testPool, true);
vm.stopPrank();
bytes memory managerCalldata = abi.encodeWithSelector(
BoostController.modifySupportedPool.selector,
testPool,
false
);
vm.roll(block.number + 1);
vm.startPrank(user1);
assertTrue(boostController.supportedPools(testPool));
vm.stopPrank();
vm.startPrank(manager);
boostController.modifySupportedPool(testPool, false);
vm.stopPrank();
}
Tools Used
Manual review
Foundry for POC testing
Recommended Mitigation Steps
Implement one of the following protection mechanisms:
1. Timelock Mechanism (Recommended)
uint256 public constant TIMELOCK_DELAY = 2 days;
mapping(address => uint256) public poolStatusChangeTime;
function modifySupportedPool(address pool, bool isSupported) external onlyRole(MANAGER_ROLE) {
if (pool == address(0)) revert InvalidPool();
if (supportedPools[pool] == isSupported) revert PoolNotSupported();
poolStatusChangeTime[pool] = block.timestamp + TIMELOCK_DELAY;
emit PoolStatusChangeScheduled(pool, isSupported, poolStatusChangeTime[pool]);
}
function executePoolStatusChange(address pool, bool isSupported) external onlyRole(MANAGER_ROLE) {
if (poolStatusChangeTime[pool] == 0) revert NoChangeScheduled();
if (block.timestamp < poolStatusChangeTime[pool]) revert TimelockNotExpired();
supportedPools[pool] = isSupported;
delete poolStatusChangeTime[pool];
if (isSupported) {
emit PoolAdded(pool);
} else {
emit PoolRemoved(pool);
}
}
2. OpenZeppelin TimelockController
Integrate with OpenZeppelin's TimelockController for a battle-tested solution:
contract BoostController is IBoostController, TimelockController {
function modifySupportedPool(address pool, bool isSupported)
external
onlyRole(TIMELOCK_PROPOSER_ROLE)
{
bytes memory data = abi.encodeWithSelector(
this.executePoolChange.selector,
pool,
isSupported
);
schedule(
address(this),
0,
data,
bytes32(0),
bytes32(0),
TIMELOCK_DELAY
);
}
}
3. Commit-Reveal Scheme
struct PoolChange {
bytes32 commitment;
uint256 revealTime;
bool revealed;
}
mapping(address => PoolChange) public poolChanges;
function commitPoolChange(bytes32 commitment) external onlyRole(MANAGER_ROLE) {
poolChanges[msg.sender] = PoolChange({
commitment: commitment,
revealTime: block.timestamp + TIMELOCK_DELAY,
revealed: false
});
}
function revealPoolChange(address pool, bool isSupported, bytes32 salt)
external
onlyRole(MANAGER_ROLE)
{
PoolChange storage change = poolChanges[msg.sender];
require(block.timestamp >= change.revealTime, "Too early");
require(!change.revealed, "Already revealed");
require(
keccak256(abi.encodePacked(pool, isSupported, salt)) == change.commitment,
"Invalid reveal"
);
supportedPools[pool] = isSupported;
change.revealed = true;
if (isSupported) {
emit PoolAdded(pool);
} else {
emit PoolRemoved(pool);
}
}
Risk Rating
References