Security Audit
Aug 4, 2023
Version 1.0.0
Presented by 0xMacro
This document includes the results of the security audit for Sommelier's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from July 31, 2023 to August 4, 2023.
The purpose of this audit is to review the source code of certain Sommelier Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.
Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.
The following is an aggregation of issues found by the Macro Audit team:
Severity | Count | Acknowledged | Won't Do | Addressed |
---|---|---|---|---|
High | 3 | - | - | 3 |
Medium | 1 | - | - | 1 |
Low | 2 | - | - | 2 |
Code Quality | 4 | - | - | 4 |
Informational | 1 | 1 | - | - |
Sommelier was quick to respond to these issues.
Our understanding of the specification was based on the following sources:
The following source code was reviewed during the audit:
e10e66952ba637c432f2aa681b8d2a86d05239ff
Specifically, we audited the following contracts within this repository for the initial part of the audit:
Source Code | SHA256 |
---|---|
src/Deployer.sol |
|
src/Registry.sol |
|
src/base/Cellar.sol |
|
src/base/ERC4626SharePriceOracle.sol |
|
src/base/permutations/CellarWithAaveFlashLoans.sol |
|
src/base/permutations/CellarWithBalancerFlashLoans.sol |
|
src/base/permutations/CellarWithOracle.sol |
|
src/base/permutations/CellarWithOracleWithAaveFlashLoans.sol |
|
src/base/permutations/CellarWithOracleWithBalancerFlashLoans.sol |
|
src/base/permutations/CellarWithShareLockPeriod.sol |
|
src/modules/adaptors/Balancer/BalancerPoolAdaptor.sol |
|
src/modules/adaptors/Sommelier/LegacyCellarAdaptor.sol |
|
src/modules/price-router/Extensions/Lido/StEthExtension.sol |
|
Click on an issue to jump to it, or scroll down to see them all.
automationActions
setAutomationActions
missing event
distrustPosition
missing event
callOnAdaptor()
positionCount
no longer used
We quantify issues in three parts:
This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:
Severity | Description |
---|---|
(C-x) Critical |
We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost. |
(H-x) High |
We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec. |
(M-x) Medium |
We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner. |
(L-x) Low |
The risk is small, unlikely, or may not relevant to the project in a meaningful way. Whether or not the project wants to develop a fix is up to the goals and needs of the project. |
(Q-x) Code Quality |
The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design. |
(I-x) Informational |
Warnings and things to keep in mind when operating the protocol. No immediate action required. |
(G-x) Gas Optimizations |
The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it. |
Currently the trust model is setup so that both governance and the multi-sig need to be compromised before anyone can act maliciously. This requires that positions and trusted addresses that governance may want to use are set in the registry contract by it’s multi-sig owner. However, CellarWithOracle.sol
’s setSharePriceOracle()
allows governance to set the cellars sharePriceOracle
to any address as it is not verified by the registry contract.
function setSharePriceOracle(ERC4626SharePriceOracle _sharePriceOracle) external onlyOwner {
if (_sharePriceOracle.ORACLE_DECIMALS() != ORACLE_DECIMALS) revert Cellar__OracleFailure();
sharePriceOracle = _sharePriceOracle;
emit SharePriceOracleUpdated(address(_sharePriceOracle));
}
Reference: CellarWithOracle.sol#L63-L67
This could allow governance to set the sharePriceOracle
to be a malicious contract that could return favourable pricing, allowing minting and redeeming of shares to drain the cellar of its assets.
Remediations to Consider
Instead of passing in the address of the new sharePriceOracle
, use a registry id and call getAddress()
on the registry with that id to receive an address that has been approved. This adds an additional layer of protection for shareholders as it would require both the multisig and governance to act maliciously to set an invalid oracle. There should also be a check to ensure the returned address from the registry is the one expected, doing so prevents the registry’s multisig from front-running the transaction and setting the address to an unintended address, as is done in Cellar.sol
’s setAutomationActions()
.
automationActions
As discussed in issue H-1, addresses set should be trusted by the registry beforehand. In Cellar.sol
’s setAutomationActions()
it seems like the address being set has been verified by the registry.
function setAutomationActions(uint256 _registryId, address _expectedAutomationActions) external onlyOwner {
address registryAutomationActions = registry.getAddress(_registryId);
if (registryAutomationActions != _expectedAutomationActions) revert Cellar__ExpectedAddressDoesNotMatchActual();
automationActions = registryAutomationActions;
}
Reference: Cellar.sol#L1206-L1210
However, in Registry.sol
’s setAddress()
, if the id
being set is 0
, and the current address set as the zero id
is the caller, they can change the address.
function setAddress(uint256 id, address newAddress) external {
if (id > 0) {
_checkOwner();
if (id >= nextId) revert Registry__ContractNotRegistered(id);
} else {
if (msg.sender != getAddress[0]) revert Registry__OnlyCallableByZeroId();
}
emit AddressChanged(id, getAddress[id], newAddress);
getAddress[id] = newAddress;
}
Reference: Registry.sol#L60-L71
Since the address of id zero is the gravityBridge
address, which is controlled by governance, this allows governance to set the id
zero to be any address. Doing so allows governance to set a malicious address as the cellar’s automationActions
, allowing this address to call callOnAdaptor()
, which could be used to make many rebalances and drain assets from the cellar.
Remediations to Consider
When using the Registry.sol
’s getAddress()
ensure that the _registryId
is not zero, preventing governance from setting an unverified automationActions
address.
Cellar.sol
’s forcePositionOut()
allows the strategist to remove a position from being tracked while it still has a balance. This is supposed to only be used in cases where there is an issue with the position, such as it reverting when calculating share price. If the strategist were to force out a position that the cellar has a large balance in, the share price would drop. If shares were purchased, then governance added the position back, it would increase the share price, allowing bought shares to be sold for a profit and drain assets from the cellar.
Remediations to Consider
Check to ensure that the position being forced out is set to be untrusted in the registry. Doing so prevents the position from being added back by governance maliciously.
In ERC4626SharePriceOracle.sol
’s performUpkeep()
there are multiple checks to ensure the upkeep condition has been met, and the data’s timestamp is newer than the previous observation. However, it assumes that the data provided is honest, and not malicious. If chainlink automation were to act maliciously, performUpKeep()
could accept either a very large new share price, and/or a timestamp that is larger than the current time. A sufficiently large share price would effect anything directly using the latest price, and would slowly effect the TWAA. If the provided timestamp is larger than the current time, then the price could have an immediate effect on the TWAA as well.
Remediations to Consider
currentTime
is not greater than the current timestamp.setAutomationActions
missing event
Cellar.sol
’s setAutomationActions()
is missing an event that would indicate it’s been called.
It may be useful to watch for calls to this function, especially if they are not expected.
Remediations to Consider
Add an event to indicate what the automationActions
address is set to.
distrustPosition
missing event
In Registry.sol
’s distrustPosition()
does not emit an event. It would be beneficial to emit an event here so that call to this function can be more easily watched and reacted to in case a position is unexpectedly distrusted by the registry’s owner as well as to more easily determine which positions are trusted by the registry.
Remediations to Consider
Add an event that emits the information of the position when it is distrusted.
In StEthExtension.sol
there are multiple mentions of using curve as a secondary pricing source, however uniswapV3 is used for pricing instead.
Remediations to Consider
Replace all mentions of curve with uniswap.
callOnAdaptor()
In Cellar.sol
’s callOnAdaptor()
a change was made to allow a set automationActions
address to call it as well as the strategist, however the comments only mention that its callable by the strategist.
* @dev Callable by Sommelier Strategist.
Reference: Cellar.sol#L1311
Remediations to Consider
Mention that callOnAdaptor()
is also callable by the automationActions
address.
There is a typo haas
in Cellar.sol
// After making every external call, check that the totalAssets haas not deviated significantly, and that totalShares is the same.
Reference: Cellar.sol#L1333
Remediations to Consider
Replace haas
with has
positionCount
no longer used
In Registry.sol
the public variable positionCount
is not longer used or referenced by anything and displays an inaccurate position count.
/**
* @notice Stores the number of positions that have been added to the registry.
* Starts at 101.
*/
uint32 public positionCount = 100;
Reference: Registry.sol#L387-L391
Remediations to Consider
Remove this variable as it is no longer used and displays inaccurate information about the registry.
In Cellar.sol
, there is an addition to allow an automationActions
contract to be able to call callOnAdaptor()
. The intention is for this to be a contract that strategists can set actions to be called if certain conditions are met, with the intent to use chainlink automation to verify and trigger these calls. This contract has yet to be developed, and should be audited before cellars use it, but this functionality was added so that new cellars would be able to take advantage of it in the future once it is ready.
all Automation Action contracts will be audited BEFORE
automationActions
is set to a non zero value in newly deployed Cellars.
Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.
Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.
The scope of this report and review is limited to a review of only the code presented by the Sommelier team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.