Protocol Decentralization
- ❌Upgrades with potential of “loss of funds or unclaimed yield” not protected with onchain governance AND Exit Window >= 30 days
- ✅There are no external dependencies
- ✅Alternative third-party frontends exist
Risk Areas
Summary
Velodrome Finance is a next-generation AMM that combines the best of Curve, Convex and Uniswap, designed to serve as the liquidity hub for the Superchain. Velodrome NFTs vote on token emissions and receive incentives and fees generated by the protocol.
Ratings
## Chain
Velodrome is deployed on the Optimism chain, an Ethereum L2 in Stage 1 according to L2BEAT.
Chain score: Medium
Upgradeability
To a large part the protocol is immutable. In particular, no permissions with a potential impact on user funds have been identified.
The remaining permissions are related to the protocol's rewards system and periphery contracts. These permissions are controlled by a number of multisigs including an undeclared multisig.
Among these, the undeclared multisig has control over a whitelist of special voters with the ability to change the rewards distributed to LPs. Specifically, "regular" veNFT holders compete in weekly voting rounds to distribute a fixed amount of rewards, paid in the protocol's native token, VELO. Regular voters are able to signal their support for a specific gauge which, after the conclusion of a voting round, receives and distributes rewards among the LPs of the linked pool. This public voting period is followed by a "private" voting window of 1 hour during which only the whitelisted veNFT holders are able to participate. As a result, these whitelisted veNFT holders, and by extension the undeclared multisig, are able to control the final vote outcome. The impact of this is classified as a potential material change of the expected performance including the loss of accrued yield (if public votes are overwritten by the whitelisted veNFTs).
The undeclared multisig further is the owner of various contracts, and the associated permissions, including the FactoryRegistry. This contract allows the undeclared multisig to (un-) approve factory contracts within the protocol. User's swaps are routed through the pools created from the approved factories. Users are protected from malicious factories (and pools) through a user defined slippage tolerance which is enforced on all swaps on the router contract. On the other hand, only pools created from the default factory are enabled for LP provisioning functions effectively protecting LPs in the protocol.
Another noteworthy permission in the protocol is the ability to kill and revive gauges and thereby stop, or enable, a gauge to receive and distribute rewards to LPs and voters. However, killing a gauge does not affect the rewards already distributed to LPs or voters and also does not affect the rewards, or any other aspect, of other gauges. This permission hence can not materially affect the protocol's expected performance.
Upgradeability score: Medium
Autonomy
Velodrome does not have external dependencies 🎉
Autonomy score: Low
Exit Window
Existing permissions have a Medium risk score and are not protected by an Exit Window. Users are not able to withdraw funds in case of an unwanted update.
Exit Window score: Medium
Accessibility
Velodrome provides several access points to the protocol, both decentralized and centralized. Decentralized links include velo.drome.eth, velo.drome.eth.limo, and velo.drome.eth.link, while centralized options include velodrome.finance and alt.velodrome.finance. This diversity of user interfaces offers redundancy and reduces risks associated with access issues.
Accessibility score: Low
Conclusion
The Velodrome protocol achieves Medium centralization risk scores for its Upgradeability and Exit Window dimensions. It thus ranks Stage 1.
The protocol could reach Stage 2 by transferring the permissions to an onchain governance with 30 days Exit Window.
Overall score: Stage 1
Reviewer notes
There were no particular discoveries made during the analysis of this protocol.
Protocol Analysis
Killing of a gauge
After each period, VELO tokens from the Minter
contract flow into the Voter
contract via calling updatePeriod()
. Once the tokens are in the Voter
contract, they flow according to the voting weight to each Gauge
through calling distribute()
on the Voter
contract. Once the VELO tokens ended up in the different Gauge
contracts, the users can claim their reward no matter of the living status of the Gauge
. It's the function call distribute()
that sends the tokens to the Gauge
which reverts if the Gauge
is killed. For rewards to be claimable for users the function calls updatePeriod()
and distribute()
need to occur in succession and the killing of the Gauge
before either one of them results in yield not being available to the user. But, since both functions are public everyone, can call them. And thus during the first block of each period where updatePeriod()
is callable, the permission owner that kills the Gauge
needs to frontrun the transactions which are calling updatePeriod()
and distribute()
. This centralisation vector is thus not definitive and only probabilistic.
On the other hand, Voters earn a claim on yield proportional to their allocated votes when calling the vote()
for a gauge on the Voter
contract. Function vote()
will revert if the respective gauge is killed, thus not crediting a claim on yield for the killed gauge. However, previous votes and the credited yield claims, before the gauge is killed, are not affected by this. Voters will thus still be able to claim their accumulated yield.
Dependencies
No external dependency has been found.
Governance
Security Council
Name | Account | Type | ≥ 7 signers | ≥ 51% threshold | ≥ 50% non-insider | Signers public |
---|---|---|---|---|---|---|
Emergency Council | 0x838352F4E3992187a33a04826273dB3992Ee2b3f | Multisig 4/7 | ✅ | ❌ | ❌ | ❌ |
Undeclared Multisig | 0xBA4BB89f4d1E66AA86B60696534892aE0cCf91F5 | Multisig 3/7 | ✅ | ✅ | ❌ | ❌ |
Info sourced from here: https://velodrome.finance/security#contracts.
Exit Window
No timelocks have been found protecting the various permissioned functions in the protocol. All updates take effect immediately.
Contracts & Permissions
Contracts
All Permission Owners
Name | Account | Type |
---|---|---|
Undeclared Multisig | 0xBA4BB89f4d1E66AA86B60696534892aE0cCf91F5 | Multisig 3/7 |
Emergency Council | 0x838352F4E3992187a33a04826273dB3992Ee2b3f | Multisig 4/7 |
Permissions
Contract | Function | Impact | Owner |
---|---|---|---|
FactoryRegistry | renounceOwnership | The function allows the permission owner to renounce ownership over all permissioned functions in this contract. | Undeclared Multisig |
FactoryRegistry | transferOwnership | The function allows the permission owner to transfer ownership over all permissioned functions in this contract. | Undeclared Multisig |
FactoryRegistry | approve | This function allows the permission owner to approve a set of factories used in Velodrome Protocol. The Router contract (0xa062aE8A9c5e11aaA026fc2670B0D65cCc8B2858 ) checks whether the submitted factory is approved with the function isPoolFactoryApproved exposed by the FactoryRegistry. If a factory is approved, the router allows swaps on the pools from this factory. LiquidityManagement (addLiquidity() or removeLiquidity() ) is not affected, as it only allows pools from the original PoolFactory 0xF1046053aa5682b4F9a81b5481394DA16BE5FF5a | Undeclared Multisig |
FactoryRegistry | unapprove | This function allows the permission owner to unapprove a set of factories used in Velodrome Protocol. Calling removes approval for routing swaps. | Undeclared Multisig |
FactoryRegistry | setManagedRewardsFactory | The permission owner can set the managed rewards factory as default for managed positions. This factory is used by the VotingEscrow contract to generate rewards for users that have a managed ve position (veNFT contract looks up the registered ManagedRewardsFactory inside createManagedLockFor). The reward contracts deployed from the ManagedRewardsFactory expose getReward function which could be a malicious implementation (not paying out rewards for managed positions after calling setManagedRewardsFactory with a malicious factory). | Undeclared Multisig |
Minter | nudge | Allows epoch governor to modify the tail emission rate within specific bounds. Tail emissions continue to reward LPs after the initial reward emissions decayed below a certain threshold. The tail emissions rate can be nudged by at most 1 basis point per epoch to a maximum of 100 basis points or to a minimum of 1 basis point. | Undeclared Multisig |
PoolFactory | setVoter | Sets a new voter for the pool factory and will set it for all future pools. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
PoolFactory | setSinkConverter | Sets a new sinkConverter for the pool factory for all future deployed pools. | Contract: 0x585Af0b397AC42dbeF7f18395426BF878634f18D |
PoolFactory | setPauser | Sets a pauser for all pools deployed with this factory. The pauser can halt the swaps for these pools. | Undeclared Multisig |
PoolFactory | setPauseState | Sets or removes the paused state of the factory. This affects swaps. | Undeclared Multisig |
PoolFactory | setFeeManager | Designates a new address to be the FeeManager. The FeeManager can call setFee and setCustomFee. | Undeclared Multisig |
PoolFactory | setFee | Sets a new fee for all future deployed fees, within a maximum of 100 basis points. | Undeclared Multisig |
PoolFactory | setCustomFee | Sets a custom fee for a specific pool that was deployed through the factory. | Undeclared Multisig |
Pool | setName | Set a name for the pool. | Emergency Council referenced from Voter contract |
Pool | setSymbol | Set a symbol for the pool. | Emergency Council referenced from Voter contract |
SinkDrain | renounceOwnership | The function allows the permission owner to renounce ownership over all permissioned functions in this contract. | 0x0 (renounced) |
SinkDrain | transferOwnership | The function allows the permission owner to transfer ownership over all permissioned functions in this contract. | 0x0 (renounced) |
SinkDrain | mint | The function was used to mint to sinkManager so that it can provide all the liquidity. | 0x0 (renounced) |
SinkManager | renounceOwnership | The function allows the permission owner to renounce ownership over all permissioned functions in this contract. | Undeclared Multisig |
SinkManager | transferOwnership | The function allows the permission owner to transfer ownership over all permissioned functions in this contract. | Undeclared Multisig |
SinkManager | setOwnedTokenId | Deposit all SinkDrain tokens to gauge to earn 100% rewards. | 0x0 (renounced) |
SinkManager | setupSinkDrain | Initial setup of the ownedTokenId, the v1 veNFT which votes for the SinkDrain. | 0x0 (renounced) |
VELO | setMinter | This function allows to set the minter for the V2 VELO token. | Minter contract: 0x6dc9E1C04eE59ed3531d73a72256C0da46D10982 |
VELO | setSinkManager | This function allows to set the SinkManager for the VELO V2 token. | 0x0 (renounced) |
VELO | mint | The mint function allows the permission owner to mint new VELO V2 tokens into existence. | Minter contract: 0x6dc9E1C04eE59ed3531d73a72256C0da46D10982 |
Voter | setGovernor | Sets a new governor. Governor alone owns the permission to call setEpochGovernor, setMaxVotingNum, whitelistToken and whitelistNFT on the Voter contract. | Undeclared Multisig |
Voter | setEpochGovernor | Sets a new epoch-based governor. Epoch governor can call nudge on Minter contract to modify the tail emission rate by at most 1 basis point per epoch to a maximum of 100 basis points or to a minimum of 1 basis point. | Undeclared Multisig |
Voter | setEmergencyCouncil | Sets a new emergency council which can call killGauge and reviveGauge. See Upgradability section for more details. | Emergency Council |
Voter | setMaxVotingNum | Sets a maximum number of pools a user can vote for. Code enforces that this number is not below 10. | Undeclared Multisig |
Voter | whitelistToken | Whitelists (or unwhitelists) a token for use in bribes. If not whitelisted, no gauge can be created for a pool and thus the pool cannot participate in the reward distribution as it cannot receive votes. | Undeclared Multisig |
Voter | whitelistNFT | Whitelists or unwhitelists a Ve Token for voting in the last hour prior to epoch flip, all regular veNFTs cannot vote during this period. The voting power for whitelisted veNFT is the regular voting power: IVotingEscrow(ve).balanceOfNFT(_tokenId) . | Undeclared Multisig |
Voter | killGauge | Kills a gauge, preventing new emissions and deposits, but allowing withdrawals (see "Notes regarding the killing of a gauge"). | Emergency Council |
Voter | reviveGauge | Revives a killed gauge, allowing it to receive emissions and deposits again (see "Notes regarding the killing of a gauge"). | Emergency Council |
VotingEscrow | createManagedLockFor | Manager can create a managed ve position for a user (a permanent lock). | Undeclared Multisig |
VotingEscrow | depositManaged | Delegates balance to managed NFT, re-locking NFTs to maximum lock time upon withdrawal. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
VotingEscrow | withdrawManaged | Withdraws balance from managed NFT, re-locking NFT to the maximum lock time. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
VotingEscrow | setAllowedManager | Allows one address to call createManagedLockFor() that is not the governor. | Undeclared Multisig |
VotingEscrow | setManagedState | Sets Managed NFT state. Inactive NFTs cannot be deposited into. | Emergency Council |
VotingEscrow | setManagedState | Sets Managed NFT state. Inactive NFTs cannot be deposited into. | Emergency Council or governor (Undeclared Multisig) |
VotingEscrow | setVoterAndDistributor | Sets Voter and distributor reference in this contract. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
VotingEscrow | voting | Sets voted status for _tokenId to true or false. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
RestrictedTeam | setArtProxy | The function was used to mint to sinkManager so that it can provide all the liquidity. | Factory registry owner (Undeclared Multisig) |
Splitter | toggleSplit | Toggles split for a specific address or address(0) to enable/disable for all. | Factory registry owner (Undeclared Multisig) |
Registry (RelayFactoryRegistry) | approve | Approves the given address. | Undeclared Multisig |
Registry (RelayFactoryRegistry) | unapprove | Revokes approval from the given address. | Undeclared Multisig |
Registry (KeeperRegistry) | approve | Approves the given address. | Undeclared Multisig |
Registry (KeeperRegistry) | unapprove | Revokes approval from the given address. | Undeclared Multisig |
Registry (OptimizerRegistry) | approve | Approves the given address. | Undeclared Multisig |
Registry (OptimizerRegistry) | unapprove | Revokes approval from the given address. | Undeclared Multisig |
CLGaugeFactory | setNotifyAdmin | Sets the notifyAdmin value on gauge factory. | Undeclared Multisig |
CLGaugeFactory | setNonfungiblePositionManager | Sets Nonfungible Position Manager, callable only once. | 0x0 (renounced) |
CLGaugeFactory | createGauge | Allows the creation of a gauge, callable only by the voter contract. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
CL Gauge | getReward | Retrieves rewards for all tokens owned by an account. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
CL Gauge | notifyRewardWithoutClaim | Notifies gauge of gauge rewards without distributing fees. | Notify admin: Undeclared Multisig |
CL Gauge | notifyRewardAmount | Notifies gauge of gauge rewards. | Voter contract: 0x41C914ee0c7E1A5edCD0295623e6dC557B5aBf3C |
NonfungiblePositionManager | setTokenDescriptor | Sets a new token descriptor. | Undeclared Multisig |
NonfungiblePositionManager | setOwner | Sets a new owner for the contract. The only permission the owner has is to change the token descriptor. | Undeclared Multisig |
CLFactory | setOwner | Sets a new owner for the pool factory. The only permission the owner role has on this contract is to call enableTickSpacing which enables a certain tick spacing with a default associated fee. | Undeclared Multisig |
CLFactory | setSwapFeeManager | SwapFeeManager can set a new SwapFeeManager. The SwapFeeManager can call setSwapFeeModule. | Undeclared Multisig |
CLFactory | setUnstakedFeeManager | The UnstakedFeeManager can set a new UnstakedFeeManager. The UnstakedFeeManager can call setUnstakedFeeModule. | Undeclared Multisig |
CLFactory | setSwapFeeModule | Sets a new swap fee module for all the pools deployed by the factory. This fee module indicates the fee for a certain swap based on the pool. The fee is capped to 0.1% max irrespective of the fee module. | Undeclared Multisig |
CLFactory | setUnstakedFeeModule | Updates the unstaked fee module for all the pools deployed by the factory. This fee module indicates the fee for unstaking based on the pool. The fee is capped to 1% max irrespective of the fee module. | Undeclared Multisig |
CLFactory | setDefaultUnstakedFee | Updates the defaultUnstakedFee of the factory. | Undeclared Multisig |
CLFactory | enableTickSpacing | Enables a specific tickSpacing for pools deployed from the factory. | Undeclared Multisig |
CustomSwapFeeModule | setCustomFee | Sets a custom fee between 0 and a max fee for a specific pool. | SwapFeeManager (Undeclared Multisig) |
CustomUnstakedFeeModule | setCustomFee | Sets a custom fee between 0 and a max fee for a specified pool. | UnstakedFeeManager (Undeclared Multisig) |