Calling the following BaseV2Gauge.detachUser function only calls the ERC20Boost.detach function below without calling the ERC20Boost.updateUserBoost function below. Although the corresponding gauge is removed from the user's _userGauges and all of the user's boost for such gauge are deleted from the corresponding getUserGaugeBoost, not calling the ERC20Boost.updateUserBoost function causes the user's getUserBoost to not be updated.
function updateUserBoost(address user) external {
uint256 userBoost = 0;
address[] memory gaugeList = _userGauges[user].values();
uint256 length = gaugeList.length;
for (uint256 i = 0; i < length;) {
address gauge = gaugeList[i];
if (!_deprecatedGauges.contains(gauge)) {
uint256 gaugeBoost = getUserGaugeBoost[user][gauge].userGaugeBoost;
if (userBoost < gaugeBoost) userBoost = gaugeBoost;
}
unchecked {
i++;
}
}
getUserBoost[user] = userBoost;
emit UpdateUserBoost(user, userBoost);
}
This is unlike the following BoostAggregator.withdrawGaugeBoost function, which calls the ERC20Boost.decrementAllGaugesBoost function below and the ERC20Boost.updateUserBoost function. Besides calling the ERC20Boost.decrementAllGaugesBoost function, which further calls the ERC20Boost.decrementGaugesBoostIndexed function that can remove the corresponding gauge from the user's _userGauges and delete all of the user's boost for such gauge from the corresponding getUserGaugeBoost too, the ERC20Boost.updateUserBoost function is also called to update the user's getUserBoost accordingly. Because the ERC20Boost.updateUserBoost function is called in the BoostAggregator.withdrawGaugeBoost function, calling the BoostAggregator.withdrawGaugeBoost function does update the user's getUserBoost correctly.
function withdrawGaugeBoost(address to, uint256 amount) external onlyOwner {
/// @dev May run out of gas.
hermesGaugeBoost.decrementAllGaugesBoost(amount);
hermesGaugeBoost.updateUserBoost(address(this));
address(hermesGaugeBoost).safeTransfer(to, amount);
}
function decrementGaugesBoostIndexed(uint256 boost, uint256 offset, uint256 num) public {
address[] memory gaugeList = _userGauges[msg.sender].values();
uint256 length = gaugeList.length;
for (uint256 i = 0; i < num && i < length;) {
address gauge = gaugeList[offset + i];
GaugeState storage gaugeState = getUserGaugeBoost[msg.sender][gauge];
if (_deprecatedGauges.contains(gauge) || boost >= gaugeState.userGaugeBoost) {
require(_userGauges[msg.sender].remove(gauge)); // Remove from set. Should never fail.
delete getUserGaugeBoost[msg.sender][gauge];
emit Detach(msg.sender, gauge);
} else {
gaugeState.userGaugeBoost -= boost.toUint128();
emit DecrementUserGaugeBoost(msg.sender, gauge, gaugeState.userGaugeBoost);
}
unchecked {
i++;
}
}
}
In comparison, since the ERC20Boost.updateUserBoost function is not called after executing hermesGaugeBoost.detach(user) in the BaseV2Gauge.detachUser function, calling the BaseV2Gauge.detachUser function does not update the user's getUserBoost at all. If the user's boost for the gauge to be detached was the highest among all gauges that were attached for the user, the user's getUserBoost should become lower after detaching such gauge but the user's getUserBoost remains the same when the BaseV2Gauge.detachUser function is called, which is an accounting error for the user's getUserBoost.
Proof of Concept
The following steps can occur for the described scenario.
Alice's boost for Gauge 1 is the highest among all gauges that were attached for her.
The BaseV2Gauge.detachUser function is called to detach Gauge 1 for Alice.
Gauge 1 is removed from Alice's _userGauges, and all of Alice's boost for Gauge 1 are deleted from the corresponding getUserGaugeBoost.
Alice's getUserBoost is not updated and still remains the same, which equals Alice's old boost for Gauge 1, incorrectly.
Tools Used
VSCode
Recommended Mitigation Steps
The BaseV2Gauge.detachUser function can be updated to call the ERC20Boost.updateUserBoost function with user being the input after executing hermesGaugeBoost.detach(user).
Lines of code
https://github.com/code-423n4/2023-05-maia/blob/53c7fe9d5e55754960eafe936b6cb592773d614c/src/gauges/BaseV2Gauge.sol#L106-L108 https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L138-L143 https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L150-L172 https://github.com/code-423n4/2023-05-maia/blob/7492906aff17e3ee8d7773ba5bf51a171051e401/src/talos/boost-aggregator/BoostAggregator.sol#L172-L177 https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L198-L200 https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L203-L227
Vulnerability details
Impact
Calling the following
BaseV2Gauge.detachUser
function only calls theERC20Boost.detach
function below without calling theERC20Boost.updateUserBoost
function below. Although the corresponding gauge is removed from the user's_userGauges
and all of the user's boost for such gauge are deleted from the correspondinggetUserGaugeBoost
, not calling theERC20Boost.updateUserBoost
function causes the user'sgetUserBoost
to not be updated.https://github.com/code-423n4/2023-05-maia/blob/53c7fe9d5e55754960eafe936b6cb592773d614c/src/gauges/BaseV2Gauge.sol#L106-L108
https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L138-L143
https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L150-L172
This is unlike the following
BoostAggregator.withdrawGaugeBoost
function, which calls theERC20Boost.decrementAllGaugesBoost
function below and theERC20Boost.updateUserBoost
function. Besides calling theERC20Boost.decrementAllGaugesBoost
function, which further calls theERC20Boost.decrementGaugesBoostIndexed
function that can remove the corresponding gauge from the user's_userGauges
and delete all of the user's boost for such gauge from the correspondinggetUserGaugeBoost
too, theERC20Boost.updateUserBoost
function is also called to update the user'sgetUserBoost
accordingly. Because theERC20Boost.updateUserBoost
function is called in theBoostAggregator.withdrawGaugeBoost
function, calling theBoostAggregator.withdrawGaugeBoost
function does update the user'sgetUserBoost
correctly.https://github.com/code-423n4/2023-05-maia/blob/7492906aff17e3ee8d7773ba5bf51a171051e401/src/talos/boost-aggregator/BoostAggregator.sol#L172-L177
https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L198-L200
https://github.com/code-423n4/2023-05-maia/blob/62f4f01a522dcbb4b9abfe2f6783bbb67c0da022/src/erc-20/ERC20Boost.sol#L203-L227
In comparison, since the
ERC20Boost.updateUserBoost
function is not called after executinghermesGaugeBoost.detach(user)
in theBaseV2Gauge.detachUser
function, calling theBaseV2Gauge.detachUser
function does not update the user'sgetUserBoost
at all. If the user's boost for the gauge to be detached was the highest among all gauges that were attached for the user, the user'sgetUserBoost
should become lower after detaching such gauge but the user'sgetUserBoost
remains the same when theBaseV2Gauge.detachUser
function is called, which is an accounting error for the user'sgetUserBoost
.Proof of Concept
The following steps can occur for the described scenario.
BaseV2Gauge.detachUser
function is called to detach Gauge 1 for Alice._userGauges
, and all of Alice's boost for Gauge 1 are deleted from the correspondinggetUserGaugeBoost
.getUserBoost
is not updated and still remains the same, which equals Alice's old boost for Gauge 1, incorrectly.Tools Used
VSCode
Recommended Mitigation Steps
The
BaseV2Gauge.detachUser
function can be updated to call theERC20Boost.updateUserBoost
function withuser
being the input after executinghermesGaugeBoost.detach(user)
.Assessed type
Other