Platform: Code4rena
Start Date: 23/05/2022
Pot Size: $75,000 USDC
Total HM: 23
Participants: 75
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 13
Id: 130
League: ETH
Rank: 26/75
Findings: 3
Award: $228.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xf15ers
Also found by: 0x52, Ruhum, WatchPug, berndartmueller, cccz, horsefacts, hyh, minhquanym, pauliax
75.235 USDC - $75.24
Judge has assessed an item in Issue #164 as Medium risk. The relevant finding follows:
#0 - GalloDaSballo
2022-07-05T21:12:18Z
Griefing/denial of service in notifyRewardAmount Both Bribe#notifyRewardAmount and Gauge#notifyRewardAmount are unrestricted external functions that add the provided token as an approved reward if it does not already exist. A malicious caller can grief rewards functionality by calling these functions to add the maximum number of reward tokens on a new gauge/bribe contract, or set malicious tokens that revert or waste gas as rewards.
The team does have the ability to retroactively swap out misbehaving or malicious tokens, so the impact of this attack is limited, but they would have to detect and prevent potential griefing attacks for every new gauge/bribe.
function notifyRewardAmount(address token, uint amount) external lock { ...
if (!isReward[token]) { isReward[token] = true; rewards.push(token); IGauge(gauge).addBribeRewardToken(token); } ...
function notifyRewardAmount(address token, uint amount) external lock { ... if (!isReward[token]) { isReward[token] = true; rewards.push(token); IBribe(bribe).addRewardToken(token); } ... }
Test cases (added to Bribes.t.sol):
function testNotifyRewardAmountGaugeDoS() public { // Three rewards tokens already exist from test setup for (uint256 i; i < 13; ++i) { // Create a fake token MockERC20 token = new MockERC20("DoS", "DOS", 18); // Mint a sufficient amount to call notifyRewardAmount // on the Gauge contract token.mint(address(this), 10_000 ether); token.approve(address(gauge), 10_000 ether); // Call notifyRewardAmount and deposit gauge.notifyRewardAmount(address(token), 10_000 ether); } // Attempt to add a new token MockERC20 newToken = new MockERC20("Legit", "LEGIT", 18); newToken.mint(address(this), 1); newToken.approve(address(gauge), 1); vm.prank(address(bribe)); vm.expectRevert("too many rewards tokens"); gauge.addBribeRewardToken(address(newToken)); } function testNotifyRewardAmountBribeDoS() public { // Three rewards tokens already exist from test setup for (uint256 i; i < 13; ++i) { // Create a fake token MockERC20 token = new MockERC20("DoS", "DOS", 18); token.mint(address(this), 1); token.approve(address(bribe), 1); // Call notifyRewardAmount and deposit 1 wei bribe.notifyRewardAmount(address(token), 1); } // Attempt to add a new token MockERC20 newToken = new MockERC20("Legit", "LEGIT", 18); newToken.mint(address(this), 1); newToken.approve(address(bribe), 1); vm.prank(address(gauge)); vm.expectRevert("too many rewards tokens"); bribe.addRewardToken(address(newToken)); }
Recommendation: allow only approved tokens as rewards.
#1 - GalloDaSballo
2022-07-05T21:12:28Z
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x52, 0xNazgul, 0xNineDec, AlleyCat, BouSalman, CertoraInc, Chom, Dravee, Funen, GimelSec, Hawkeye, MaratCerby, Nethermind, Picodes, RoiEvenHaim, SooYa, TerrierLover, WatchPug, _Adam, asutorufos, berndartmueller, c3phas, catchup, cccz, cryptphi, csanuragjain, delfin454000, djxploit, fatherOfBlocks, gzeon, hake, hansfriese, horsefacts, hyh, jayjonah8, minhquanym, oyc_109, p_crypt0, pauliax, robee, rotcivegaf, sach1r0, sashik_eth, simon135, sorrynotsorry, teddav, unforgiven, xiaoming90
103.6351 USDC - $103.64
notifyRewardAmount
Both Bribe#notifyRewardAmount
and Gauge#notifyRewardAmount
are unrestricted external functions that add the provided token as an approved reward if it does not already exist. A malicious caller can grief rewards functionality by calling these functions to add the maximum number of reward tokens on a new gauge/bribe contract, or set malicious tokens that revert or waste gas as rewards.
The team does have the ability to retroactively swap out misbehaving or malicious tokens, so the impact of this attack is limited, but they would have to detect and prevent potential griefing attacks for every new gauge/bribe.
function notifyRewardAmount(address token, uint amount) external lock { ... if (!isReward[token]) { isReward[token] = true; rewards.push(token); IGauge(gauge).addBribeRewardToken(token); } ... }
function notifyRewardAmount(address token, uint amount) external lock { ... if (!isReward[token]) { isReward[token] = true; rewards.push(token); IBribe(bribe).addRewardToken(token); } ... }
Test cases (added to Bribes.t.sol
):
function testNotifyRewardAmountGaugeDoS() public { // Three rewards tokens already exist from test setup for (uint256 i; i < 13; ++i) { // Create a fake token MockERC20 token = new MockERC20("DoS", "DOS", 18); // Mint a sufficient amount to call notifyRewardAmount // on the Gauge contract token.mint(address(this), 10_000 ether); token.approve(address(gauge), 10_000 ether); // Call notifyRewardAmount and deposit gauge.notifyRewardAmount(address(token), 10_000 ether); } // Attempt to add a new token MockERC20 newToken = new MockERC20("Legit", "LEGIT", 18); newToken.mint(address(this), 1); newToken.approve(address(gauge), 1); vm.prank(address(bribe)); vm.expectRevert("too many rewards tokens"); gauge.addBribeRewardToken(address(newToken)); } function testNotifyRewardAmountBribeDoS() public { // Three rewards tokens already exist from test setup for (uint256 i; i < 13; ++i) { // Create a fake token MockERC20 token = new MockERC20("DoS", "DOS", 18); token.mint(address(this), 1); token.approve(address(bribe), 1); // Call notifyRewardAmount and deposit 1 wei bribe.notifyRewardAmount(address(token), 1); } // Attempt to add a new token MockERC20 newToken = new MockERC20("Legit", "LEGIT", 18); newToken.mint(address(this), 1); newToken.approve(address(bribe), 1); vm.prank(address(gauge)); vm.expectRevert("too many rewards tokens"); bribe.addRewardToken(address(newToken)); }
Recommendation: allow only approved tokens as rewards.
Mutability for Bribe#getEpochStart
can be restricted to pure
:
function getEpochStart(uint timestamp) public view returns (uint) { uint bribeStart = timestamp - (timestamp % (7 days)) + BRIBE_LAG; uint bribeEnd = bribeStart + DURATION - COOLDOWN; return timestamp < bribeEnd ? bribeStart : bribeStart + 7 days; }
address.code.size
VotingEscrow#_isContract
can use account.code.size
to access codesize for an address rather than inline assembly in Solidiy 0.8.13
function _isContract(address account) internal view returns (bool) { // This method relies on extcodesize, which returns 0 for contracts in // construction, since the code is only stored at the end of the // constructor execution. uint size; assembly { size := extcodesize(account) } return size > 0; }
require
to assert
VotingEscrow#withdraw
uses an assert
statement to check the result of an external ERC20 transfer. It's a best practice to use assert
only for internal errors and invariants. Consider using require
instead.
assert(IERC20(token).transfer(msg.sender, value));
The _safeTransfer
and _safeTransferFrom
functions are duplicated throughout the codebase. Consider extracting a shared library to reuse this code.
Duplicated code appears in Bribe.sol
, Gauge.sol
, Pair.sol
, PairFees.sol
, Router.sol
, and Voter.sol
.
There are a number of open TODOs in comments throughout the codebase. Review and remove these if they are resolved.
#0 - GalloDaSballo
2022-07-04T00:26:17Z
Med Finding -> Dup of #68
Valid NC
I believe the code is equivalent
Agree
Valid NC
Valid NC
1 L, 3 NC, also 1 Med
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xNazgul, 0xf15ers, 0xkatana, Chom, ControlCplusControlV, DavidGialdi, Deivitto, Dravee, ElKu, Fitraldys, Funen, GimelSec, MadWookie, MaratCerby, MiloTruck, Picodes, Randyyy, TerrierLover, TomJ, Tomio, UnusualTurtle, WatchPug, Waze, _Adam, asutorufos, c3phas, catchup, csanuragjain, delfin454000, djxploit, fatherOfBlocks, gzeon, hake, hansfriese, horsefacts, orion, oyc_109, pauliax, reassor, rfa, rotcivegaf, sach1r0, saian, sashik_eth, simon135, supernova, teddav, z3s
49.9472 USDC - $49.95
Gauge#getReward
The rewards claiming loop in Gauge#getReward
can be optimized with a few loop optimization tricks: caching loop length, using unchecked increments, and caching the intermediate token
value. Since this will be a frequently called function, it's probably worth the extra optimization:
for (uint i = 0; i < tokens.length; i++) { (rewardPerTokenStored[tokens[i]], lastUpdateTime[tokens[i]]) = _updateRewardPerToken(tokens[i]); uint _reward = earned(tokens[i], account); lastEarn[tokens[i]][account] = block.timestamp; userRewardPerTokenStored[tokens[i]][account] = rewardPerTokenStored[tokens[i]]; if (_reward > 0) _safeTransfer(tokens[i], account, _reward); emit ClaimRewards(msg.sender, tokens[i], _reward); }
Recommendation:
uint256 length = tokens.length; for (uint i = 0; i < length;) { address token = tokens[i]; (rewardPerTokenStored[token], lastUpdateTime[token]) = _updateRewardPerToken(token); uint _reward = earned(token, account); lastEarn[token][account] = block.timestamp; userRewardPerTokenStored[token][account] = rewardPerTokenStored[token]; if (_reward > 0) _safeTransfer(token, account, _reward); emit ClaimRewards(msg.sender, token, _reward); unchecked { ++i; } }
#0 - GalloDaSballo
2022-06-30T01:48:30Z
25 gas for the loop + 6 for the length