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: 31/75
Findings: 2
Award: $178.88
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xf15ers
Also found by: 0x52, Ruhum, WatchPug, berndartmueller, cccz, horsefacts, hyh, minhquanym, pauliax
The Bribe.notifyRewardAmount
function does not have any access restriction. Anyone (an attacker) can frontrun and call this function to add arbitrary (even malicious) reward tokens up to MAX_REWARD_TOKENS = 16
.
An attacker is able to frontrun and add 16
fake ERC20 token contracts. Due to the limit of MAX_REWARD_TOKENS
, no more reward tokens can be added. Any attempt to add a new token will revert.
This will prevent adding the proper reward tokens due to DoS when calling the Bribe.notifyRewardAmount
and results in DoS when calling the Gauge.claimFees
function.
function notifyRewardAmount(address token, uint amount) external lock { // @audit-info no access restriction require(amount > 0); if (!isReward[token]) { require(rewards.length < MAX_REWARD_TOKENS, "too many rewards tokens"); } // bribes kick in at the start of next bribe period uint adjustedTstamp = getEpochStart(block.timestamp); uint epochRewards = tokenRewardsPerEpoch[token][adjustedTstamp]; _safeTransferFrom(token, msg.sender, address(this), amount); tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount; if (!isReward[token]) { isReward[token] = true; rewards.push(token); IGauge(gauge).addBribeRewardToken(token); } emit NotifyReward(msg.sender, token, adjustedTstamp, amount); }
Manual review
Consider adding access restriction to the Bribe.notifyRewardAmount
function to prevent malicious actors from calling the function or add a whitelist of possible reward tokens.
#0 - pooltypes
2022-06-13T15:53:05Z
Duplicate of #182
#1 - GalloDaSballo
2022-06-29T00:03:55Z
Dup of #182
🌟 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
A message should be specified in the require
statement so that if the condition fails, the caller can know the reason for failing.
Require messages are missing in many cases across contracts. You can find all occurrences with the following regex:
require\([^,]*\);$
Add require messages
The storage variable last_gauge
suggests storing the last gauge. However, it gets the address of the most recently created Bribe
assigned.
address public last_gauge; // @audit-info misleading variable name - should be `last_bribe`
Consider renaming the variable to something like last_bribe
.
pure
Functions that do not read and modify any storage variables and only use the arguments provided, can be restricted to pure
.
Set function state mutability to pure
.
When changing state variables events are not emitted. Emitting events allows monitoring activities with off-chain monitoring tools.
Emit events for state variable changes.
Zero-address checks are a best practice for input validation of critical address parameters. While the codebase applies this to most cases, there are many places where this is missing in constructors and setters.
Impact: Accidental use of zero-addresses may result in exceptions, burn fees/tokens or force redeployment of contracts.
L32: gauge = _gauge;
97: stake = _stake;
98: bribe = _bribe;
99: _ve = __ve;
100: voter = _voter;
L43: _voter = IVoter(__voter);
L44: _ve = IVotingEscrow(__ve);
L45: _rewards_distributor = IRewardsDistributor(__rewards_distributor);
L41: team = newTeam;
L14: pairFactory = _pairFactory;
L19: team = _team;
L25: USDC = IERC20(_usdc);
L26: VELO = IVelo(_velo);
L28: endpoint = _endpoint;
L53: fantomSender = _fantomSender;
L22: weve = _weve;
L24: endpoint = _endpoint;
L25: optimismReceiver = _optimismReceiver;
Add zero-address checks, e.g.:
require(_asset != address(0), "Zero-address");
@TODOs
left in the codeThere are several open TODOs left in the code.
// TODO delegates
// TODO add delegates
// TODO add delegates
// TODO: decide on whether to abstract from VELO or not. currently, it's only somewhat abstracted (e.g. L38) // @audit open todo @LOW
IRouter internal immutable router; // TODO make modifiable?
Check, fix and remove the todos before it is deployed in production
The team
plays a critical role in the Velodrome
protocol.
Given that changing team
uses a single-step process for ownership transfer, it is very risky to perform those changes in a single step because it is irrecoverable from any mistakes.
factories/GaugeFactory.sol#L17-L20
function setTeam(address _team) external { require(msg.sender == team); team = _team; }
function setTeam(address newTeam) external { require(msg.sender == team, "not team"); team = newTeam; }
Consider implementing a two-step process where the current team
nominates an account and the nominated account needs to call an acceptTeam()
function (similar as it's already done in the Minter
contract) for the transfer of team
ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.
#0 - GalloDaSballo
2022-07-02T00:56:45Z
NC
NC
Valid NC
Valid NC
##Â [L-02] - Zero-address checks are missing Valid Low
Valid NC
Valid NC
Nice short and sweet report, 1L 6 NC