Protocol (V2) Contract Architecture
The V2 Cellars infrastructure contains a number of smart contracts that contribute to the continued operation of each cellar and the Sommelier Cellars protocol.
The following four concepts categorize the scope of smart contract functionality in Cellars V2:
- Registry - contains global configuration applicable across Cellars, including references to other smart contracts and permissioning layers for adaptors and Cellars.
- PriceRouter - global Cellars pricing contract, using Chainlink. Built to price complex multi-token Cellar positions (e.g. LP positions). Price Router upgrades will allow for alternative price sources other than Chainlink where deemed appropriate.
- Cellars - user-facing ERC4626 vaults where strategists deploy assets to earn yield. Each Cellar is run by a single strategist, implementing various independent methods of earning yield. Each Strategist can make adjustments to the respective Cellar they're managing, via Sommelier governance and Gravity Bridge-approved strategist funcions.
- Adaptors - integration contracts that allow Cellars to interact with an external DeFi protocol. Adaptors should be use case-specific, and a Cellar may use multiple adaptors in the course of complex DeFi operations.
The Registry is owned by Sommelier governance and is responsible for providing on-chain pointers to canonical addresses used by Cellars and other components of the protocol.
Cellars and other smart contracts ask the Registry for the address pointers to other resources in the Sommelier ecosystem. The Registry supports the following flows:
- New canonical address pointers can be registered by governance via
register
. Every registered contract is stored in an append-only array. Therefore, integrating contracts must be aware of any needed resources' slot within thegetAddress
array. Failing to use the correct slot may result in getting the wrong address pointer, potentially leading to bugs. - The fee collector address on the Sommelier chain can be changed by governance via
setFeesDistributor
. Changing the fee collector address means that fees earned by Cellars will be sent to a new address when bridged back onto the native Sommelier Chain. The Fee Collector address on the Sommelier chain is responsible for redistributing fees to Sommelier protocol validators. See Fees And Reserves for more information. - Adapters (new external token or protocol integrations) can be added to the registry by governance via
trustAdaptor
. Once an adapter has been trusted in the registry, the Registry can also trust positions that use the adaptor (seetrustPosition
). - Positions (specific market positions that can be taken through the use of adapters) can be added to the Registry by governance via
trustPosition
. Theadaptor
argument intrustPosition
should represent a smart contract that allows Cellar integration with an external token or protocol (see "Adapters"). Compared to adaptors, positions are registered with specific payloads (seeadaptorData
), that specify position-specific data, such as assets used. Once a position has been trusted in the registry, Cellars can add the position to their own list of tracked positions and integrations according to risk rules. Adaptor and Cellar risks must be coordinated as outlined before. The same goes with risk alignment amongst positions and Cellars. Each position is tagged with anassetRisk
and aprotocolRisk
, and cellars are deployed with their own upper bounds on allowed risk levels (based on the stated purpose of the cellar). A cellar can add a trusted position as long as the cellar's risk levels are equal to or below the position's risk levels. When a cellar registers a new position,cellarAddPosition
is called to check these bounds.
- 1.First, an Adaptor contract must be built that contains all smart contract logic for integrating with the Uniswap V3 protocol. Once complete, that adaptor should be deployed on-chain, to an address - for example:
0xF00
. - 2.Once the adaptor is deployed to
0xF00
, and proper off-chain controls have taken place (audits, security review, governance votes), governance must trust the adaptor using thetrustAdaptor
function in the registry. - 3.Once the Uniswap adaptor is deployed and trusted, governance can start allowing Cellars to take specific positions that use that adapter. To enable the DAI/USDC pool, governance must call the
trustPosition
function in the registry, using the0xF00
adaptor address, and encode the DAI/USDC pool inadaptorData
. At this time, governance should also take care to setassetRisk
andprotocolRisk
appropriately. Once the position is trusted in the Registry, it will be assigned a uniquepositionId
. - 4.Once the Registry has trusted the UniswapV3Adaptor and the DAI/USDC pool, Cellars with appropriate risk tolerances can start using the adaptor. First, for each target Cellar, either governance or the strategist should call
setupAdaptor
, passing the0xF00
address in as an argument. In its execution logic, the Cellar will callcellarSetupAdaptor
in the registry. - 5.Once the adaptor is set up in each Cellar, the strategist can set up their DAI/USDC position by calling
addPosition
. The strategist should specify theindex
(see "Position Ordering" in the Cellars section), thepositionId
generated in Step 3, and should specify with theinDebtArray
boolean whether the position is a debt position (in this example case, the value should befalse
). For some positions, the strategist may also need to supplyconfigurationData
, but in the case of Uniswap V3, this is not used. In its execution logic, the Cellar will callcellarAddPosition
in the registry to perform risk tolerance checks. - 6.Once the position is added to the Cellar, the strategist can add/remove liquidity from the DAI/USDC Uniswap V3 pool, and perform other protocol-specific operations allowed by the adaptor contract, via invocations to
callOnAdaptor
.
Cellars rely on efficient and reliable pricing of any assets and positions held by the strategist. Pricing is used not only to determine Cellar performance and earned fees for the protocol and strategist, but it is also a critical part of the Cellar security model: pricing is used to make sure that strategist rebalances do not change the Cellar's TVL outside a defined envelope, preventing both mistakes and malicious operations by strategists.
Sommelier Cellars currently use Chainlink Data Feeds for pricing (See "Oracle Usage" for more information). The PriceRouter contract provides a pricing frontend to all other components of the Sommelier Cellars protocol. Each position that a Cellar takes must have a supported pricing mechanism: the Registry validates this during a
trustPosition
call by checking PriceRouter#isSupported
. Given the universal nature of the Price Router (used by all Cellars), the Price Router is owned by governance.The principal governance workflow for the Price Router is the calling of
addAsset
, in order to set up pricing for new adaptors and positions. Each Adapter specifies an underlying asset (see BaseAdaptor#assetOf
), and before any adapter is trusted by the registry, pricing for the underlying asset must be registered by calling addAsset
. When adding an asset, different token types follow different pricing logic defined by _settings.derivative.
For instance, for LP tokens, pricing is set up such that each token is broken down into its component ERC20 parts, and the prices of each token are fetched separately and summed to create a total pricing for the LP token.In addition to providing logical instructions on how to price the asset, the
addAsset
call requires the caller to provide an _expectedAnswer
, which will be checked against the price calculated on-chain. Assets whose reported on-chain pricing do not match the expected answer will fail when being added: this may mean that the pricing logic is incorrect and needs to be changed, the settings parameters are incorrect, or the expected answer was calculated incorrectly.Once an asset is added via
addAsset
, any of the view
functions that represent pricing operations can be called:getPriceInUSD
getValue
getValuesDelta
getValues
getExchangeRate
getExchangeRates
In order to reduce the gas costs of Cellar operations, the Price Router does not fetch pricing data from its underlying source on each pricing call - instead, it attempts to use an external cache. Cached price values are maintained by automation: the
checkUpkeep
function reports if current reported prices are outside the virtual price bound of cached prices, and if true, the performUpkeep
function allows keepers to update the cached prices in return for a reward. This approach reduces the gas overhead of updating pricing and cache values during user-facing Cellar operations, and in terms moves this gas overhead to the automation market, which has a well-defined incentivization model for gas-heavy operations. For Sommelier Cellars, keepers are incentivized to keep cached prices as close as possible to real-time oracle prices (within the "virtual price bound").In the case where there is no cached price value (either the asset was just added, or automation has not priced it), the pricing calls will fetch the on-chain oracle price and store it in the cache, consuming more gas.
Lastly, governance retains the power to configure automation by:
- Setting the gas price oracle address using
setGasFeed
- Setting the maximum gas price for automation via
setGasConstant
- Updating settings on an existing address using
updateVirtualPriceBound
- Changing the allowed keeper registry via
setAutomationRegistry
Cellars are the core user-facing smart contract of the Sommelier Cellars protocol. Cellars represent smart vaults that users can deposit into, earning yield and rewards with deposited funds, and withdraw from, based on the actions of the strategist (see "Roles & Permissions" for more info). To make this user-facing functionality possible, Cellars hew close to the ERC4626 standard.
Note: Sommelier Cellars depart from the ERC4626 standard in their implementation of
totalAssets
. According to the standard, totalAssets
must not revert. For Sommelier Cellars, totalAssets
may revert. Integrators should take care to note and handle this difference.A Sommelier Cellar implements an API for users, strategists, and governance (see "Roles & Permissions"). Users can deposit tokens to the Cellar, mint Cellar shares, and earn yield. Strategists can use
callOnAdaptor
to change the allocation of funds in the cellar (these operations can be termed "rebalances"). Governance has a number of safety mechanisms available (see "Roles & Permissions" for a detailed list).When a user deposits tokens to a Cellar, tokens are locked for a number of seconds specified by the
shareLockPeriod
state variable. The default shareLockPeriod
is 2 days. Once a user has deposited, their tokens cannot be withdrawn until the share-lock period has passed: this prevents opportunistic sandwiching flows around rebalances, and protects earned yield for the benefit of honest depositors.Sommelier Cellars are meant for steady growth, yield accumulation, and asset appreciation: therefore, one restriction of a Cellar is that strategist rebalances should not significantly change the TVL. This not only prevents the emergence of sandwiching opportunities, but it also protects honest depositors against malicious strategists that may try to drain cellar assets through adapter calls.
This restriction is enforced via the
totalAssets
check at the end of callOnAdaptor
. The totalAssets
function is designed such that it can always price the TVL of a Cellar in terms of the Cellar's base asset (the holdingPosition
). Before a series of rebalances (or "adaptor calls"), the starting amount of totalAssets
is stored. After the calls are complete, totalAssets
is recalculated and compared against the pre-rebalance value. If the new TVL differs from the old TVL by a ratio greater than the allowedRebalanceDeviation
, the transaction will revert. In addition, adapter calls cannot mint or destroy cellar shares: the totalShares
check ensures that strategists cannot find attack vectors that keep the TVL constant but redirect shares toward bad actors.The
totalAssets
and totalShares
checks are the principal security mechanisms that prevent abuse of adaptor calls - Cellars cannot and do not introspect the contents of these calls. As long as an adaptor and position have been trusted by governance and set up on the Cellar, the Cellar allows any calldata payload to be sent to that adaptor. For Cellars with specific use cases or high-reputation strategists, governance can increase the TVL envelope by calling setRebalanceDeviation
.The rebalance deviation is enforced in both directions, for unexpected increases as well as decreases in
totalAssets
. For rebalance operations for which the strategist expects to generate a substantial profit, outside the allowed deviation, a Vesting Contract should be used (with a Vesting Adaptor). Earned profit can be redirected to vesting contracts during rebalance operations, allowing them to slowly re-accumulate into the Cellar's TVL.Within a cellar, each position is stored in ordered lists, separated into
creditPositions
and debtPositions
. creditPositions
represent balances that contribute to TVL (for instance, holding an ERC20 token outright).debtPositions
represent debt incurred by the Cellar, which decreases the TVL (for instance, an open borrow from AAVE).- The
holdingPosition
represents a specially-designated position, where user deposits are immediately routed.
The values of these lists are
uint256
values that represent the positionId
as specified in the Registry (see "Registry" for more details). Cellars are deployed with a preset number of creditPositions
, debtPositions
, and a holdingPosition
. The holding position must also be a member of the creditPositions
array.Strategists manipulate position ordering in the following ways:
- Strategists may add a new position with
addPosition
(see "Registry" for a discussion of restrictions and dependencies around this call). - Strategists may remove a position with
removePosition
. When removing a position, the TVL of that particular position must be 0 or the transaction will revert. In addition, the holding position cannot be removed. - Strategists may swap position ordering with
swapPositions
. Positions cannot be moved from the credit to debt arrays or vice versa - they can only be swapped within their own array.
For credit positions, position ordering impacts withdrawal priority - when users withdraw, funds will always be sourced from each
creditPosition
in order. If the first creditPosition
does not have enough liquid funds to meet the withdrawal, each successive position will be withdrawn from in order until the required amount is met. Therefore, it is recommended that strategists keep a liquid position - such as the holdingPosition
- in the first slot in the creditPositions
array, to minimize effects on Cellar allocations and reduce gas costs. For debt positions, position order has no effect.In addition, each Cellar is restricted to have a number of credit positions and debt positions less than or equal to the Cellar's
MAX_POSITIONS
value. The strict upper bound on the number of positions prevents unbounded gas costs during the totalAssets
logic.Adaptors are the mechanisms by which strategists can interact with external protocols and allocate Cellar assets. All adaptors inherit from BaseAdaptor, which spells out a number of requirements for an adaptor to be used with Cellars:
- Each adaptor must have a unique
identifier
. Updated adaptors using the same protocol should increment a version number in their identifier. - Each adaptor should report its base asset, via
assetOf
, and any other assets used, withassetsUsed
. - An adaptor should report whether or not it allows the Cellar to take on debt, via
isDebt
. - An adaptor should be able to report its own balance in terms of the
assetOf
asset, via the invocation ofbalanceOf
. - An adaptor should contain
deposit
,withdraw
, andwithdrawableFrom
functions that allow Cellars to move assets into and out of the position in response to user deposit/withdrawal requests. For adaptors that do not represent liquid positions (such as adaptors for debt positions, like the AaveDebtTokenAdaptor), the implementor can havewithdrawableFrom
return0
.
For more information on adaptor architecture, see "Building Adaptors".
Sommelier Cellars are built on Foundry. Please see the Foundry documentation for instructions around building contracts from source, running tests, and running deployment scripts.
Last modified 12d ago