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
Aerodrome Finance is a next-generation AMM designed to serve as Base's central liquidity hub, combining a powerful liquidity incentive engine, vote-lock governance model, and friendly user experience. Aerodrome inherits the latest features from Velodrome V2.
Ratings
Chain
Aerodrome is deployed on the Base 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, AERO. 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
Aerodrome 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
Aerodrome provides multiple access points for users, including both centralized interfaces (aerodrome.finance, alt.aerodrome.finance) and decentralized interfaces on IPFS (aero.drome.eth, aero.drome.eth.limo, aero.drome.eth.link). This diversity in user interfaces ensures redundancy, allowing users to access the protocol even if one interface becomes unavailable.
Accessibility score: Low
Conclusion
The Aerodrome 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
The killing of a gauge
After each period, AERO 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 AERO 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
The permissions in Aerodrome are held by the multisigs highlighted below.
Security Council
| Name | Account | Type | ≥ 7 signers | ≥ 51% threshold | ≥ 50% non-insider | Signers public |
|---|---|---|---|---|---|---|
| Aerodrome Foundation and Incentives | 0xBDE0c70BdC242577c52dFAD53389F82fd149EA5a | Multisig 3/5 | ❌ | ❌ | ❌ | ❌ |
| Public Goods Fund | 0x834C0DA026d5F933C2c18Fa9F8Ba7f1f792fDa52 | Multisig 3/5 | ❌ | ❌ | ❌ | ❌ |
| Emergency Council | 0x99249b10593fCa1Ae9DAE6D4819F1A6dae5C013D | Multisig 3/5 | ❌ | ❌ | ❌ | ❌ |
| Undeclared Multisig | 0xE6A41fE61E7a1996B59d508661e3f524d6A32075 | Multisig 3/7 | ✅ | ❌ | ❌ | ❌ |
Info sourced from here: https://aerodrome.finance/security#emergency.
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 |
|---|---|---|
| Aerodrome Foundation and Incentives | 0xBDE0c70BdC242577c52dFAD53389F82fd149EA5a | Multisig 3/5 |
| Public Goods Fund | 0x834C0DA026d5F933C2c18Fa9F8Ba7f1f792fDa52 | Multisig 3/5 |
| Emergency Council | 0x99249b10593fCa1Ae9DAE6D4819F1A6dae5C013D | Multisig 3/5 |
| Undeclared Multisig | 0xE6A41fE61E7a1996B59d508661e3f524d6A32075 | Multisig 3/7 |
Permissions
| Contract | Function | Impact | Owner |
|---|---|---|---|
| AERO | setMinter | Sets the minter for the AERO token. | Minter contract 0xeB018363F0a9Af8f91F06FEe6613a751b2A33FE5 (immutable) |
| AERO | mint | Allows the permission owner to mint new AERO tokens into existence. | Minter contract 0xeB018363F0a9Af8f91F06FEe6613a751b2A33FE5 |
| AirdropDistributor | renounceOwnership | Renounces the ownership and disables all functions gated by onlyOwner modifier. | 0x0 (renounced) |
| AirdropDistributor | transferOwnership | Transfers ownership to another address that is not the 0-address. | 0x0 (renounced) |
| AirdropDistributor | distributeTokens | Distributes permanently locked NFTs to the desired addresses (airdrop). | 0x0 (renounced) |
| FactoryRegistry | renounceOwnership | Renounces ownership and disables all functions gated by onlyOwner modifier. | Undeclared Multisig |
| FactoryRegistry | transferOwnership | Transfers ownership to another address that is not the 0-address. | Undeclared Multisig |
| FactoryRegistry | approve | This function allows the permission owner to approve a set of factories used in Aerodrome Protocol. The Router contract (0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43) 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 0x420DD381b31aEf6683db6B902084cB0FFECe40Da | Undeclared Multisig |
| FactoryRegistry | unapprove | This function allows permission owner to unapprove a set of factories used in the Aerodrome Porotocol. As a consequence pools from these unapproved factories cannot be used for swaps called on the main router (0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43). | 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 | setTeam | Assigns the team role to a specific address which can call setTeamRate. | Aerodrome Foundation and Incentives multisig |
| Minter | acceptTeam | Transfers the team address to the role pendingTeam. | Currently, no address has the role pendingTeam |
| Minter | setTeamRate | Changes the team's percentage, capped to a maximum of 500 basis points (5%). | Aerodrome Foundation and Incentives 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 all pools deployed after that. | Contract voter 0x16613524e02ad97eDfeF371bC883F2F5d6C480A5 (immutable) |
| 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, capped at 3%. | Undeclared Multisig |
| 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 Multisig |
| 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 token ID for voting in the last hour prior to epoch flip. | 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 Multisig |
| Voter | reviveGauge | Revives a killed gauge, allowing it to receive emissions and deposits again (see "Notes regarding the killing of a gauge"). | Emergency Council Multisig |
| VotingEscrow | createManagedLockFor | Manager can create a managed ve position for a user (a permanent lock). | AllowedManager or Governor Undeclared Multisig |
| VotingEscrow | depositManaged | Delegates balance to a managed NFT, re-locking it to the maximum lock time on withdrawal. | Voter contract 0x16613524e02ad97eDfeF371bC883F2F5d6C480A5 |
| VotingEscrow | withdrawManaged | Retrieves locked rewards and withdraws balance from managed NFT. | Voter contract 0x16613524e02ad97eDfeF371bC883F2F5d6C480A5 |
| VotingEscrow | setAllowedManager | Permits one address to call createManagedLockFor() that is not the governor. | Governor (Undeclared Multisig) |
| VotingEscrow | setManagedState | Sets Managed NFT state. Inactive NFTs cannot be deposited into. | Emergency Council Multisig or Governor (Undeclared Multisig) |
| VotingEscrow | setVoterAndDistributor | Sets Voter and distributor reference in this contract. | Voter contract 0x16613524e02ad97eDfeF371bC883F2F5d6C480A5 |
| VotingEscrow | voting | Sets voted for _tokenId to true or false. | Voter contract 0x16613524e02ad97eDfeF371bC883F2F5d6C480A5 |
| 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 | The 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. | SwapFeeManager (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. | UnstakeFeeManager (Undeclared Multisig) |
| CLFactory | setDefaultUnstakedFee | Updates the default unstaked fee of the factory. | UnstakeFeeManager (Undeclared Multisig) |
| CLFactory | enableTickSpacing | Enables a certain tickSpacing in the pools deployed from the factory. | Undeclared Multisig |
| CustomSwapFeeModule | setCustomFee | Sets a custom fee between 0 and a max fee for a specified pool. The CLFactory gets the fee via calling getFee on this contract. | SwapFeeManager (Undeclared Multisig) |
| CustomUnstakedFeeModule | setCustomFee | Sets a custom fee between 0 and a max fee for a specified pool. The CLFactory gets the fee via calling getFee on this contract. | UnstakeFeeManager (Undeclared Multisig) |