Building Adaptors
Adaptors are the building blocks of the Sommelier Cellars protocol. Adaptors are used to connect Cellars to the larger DeFi ecosystem: for each external DeFi protocol Sommelier Cellars support, there is a corresponding adaptor smart contract.
Adaptors are standalone contracts that should enable single actions on single protocols (for example, providing LP liquidity on Uniswap V3). There are multiple different motivations for building adaptors:
- Smart Contract developers contributing to the Sommelier ecosystem may build adaptors to new or high-TVL protocols to enable new design possibilities for Cellars.
- Strategists may build adaptors if their proprietary strategies require integrations with previously-unsupported protocols. For instance, a strategist who wants to build an NFT trading cellar may need to build a Seaport adaptor.
- DeFi protocol developers may build adaptors for their own protocols, in order to use Sommelier Cellars as another funnel through which to grow TVL. This can be very effective for protocols that also build corresponding strategies centered around their protocol's utility.
While every adaptor will contain logic unique to its specific integration, adaptors require certain interface guarantees in order to work with Sommelier Cellars. The required interfaces are specified in BaseAdaptor.sol. It is recommended that any adaptors inherit from
BaseAdaptor
.In order to work with a Cellar, an adaptor must satisfy the following interface guarantees:
- Each deployment of an adaptor must report a globally-unique bytes32 identifier when the
identifier
function is called. Different versions of the same adaptor should report different identifiers. If an adaptor is updated due to bug fixes or new features, a new identifier should be used. - Each adaptor should report the "base asset" in which the holdings associated with the adaptor's positions should be priced. This base asset is reported using the
assetOf
function. The adaptor's asset must always be supported by the Price Router (see "Oracle Usage and Asset Pricing" for more information). - Each adaptor should report all assets used in any adaptor-based position using the
assetsUsed
function. Some adaptors may report multiple assets (for instance, an adaptor for Uniswap V3 LP positions should report both assets in the relevant pool). These assets may be specified byadaptorData
(see "adaptorData and configData" for more information). - Each adaptor should accurately report its managed TVL for a given cellar, via the
balanceOf
function. This specified balance is always in terms of the base asset. For adaptors whose positions may manage multiple assets, thebalanceOf
implementation should translate balances in one asset into corresponding balances of the base asset, and return the accumulated sum. - Every adaptor should implement a
deposit
function, which handles how direct deposits from users into the adaptor's managed positions should be handled. Usually, this involves depositing those user funds into the specified position. This function will be used in the case where the position managed by the adaptor is the Cellar's holding position. If there is no clear way to handle user deposits (for example, debt token adaptors which represent liabilities and not assets),deposit
may revert. Ifdeposit
is set up to revert, it may not be used as a Cellar's holding position. - Every adaptor should implement a
withdrawableFrom
function, which reports how many of the position's assets are withdrawable by a given cellar at the time of execution. This value is read when the position is used to service user withdrawals. If assets cannot be withdrawn from the position to service user deposits (for example, debt token adaptors which represent liabilities and not assets),withdrawableFrom
may return 0.withdrawableFrom
should never revert. - Every adaptor should implement a
withdraw
function, which removes assets from the adaptor-managed position to be sent to a user. Any withdrawal amount which is less than or equal to the amount reported bywithdrawableFrom
should not revert. Any withdrawal logic should not affect the adaptor-managed position except to the extent of theamount
of tokens specified in the function's parameters. - Every adaptor should report whether it represents a debt position, by returning a boolean when
isDebt
is called. Debt positions represent liabilities for the Cellar, as opposed to assets (for instance, open borrows from AAVE). Please refer to "Using Adaptors" for more information on debt positions.
The protocol-specific functionality that should be built into any given adaptor will depend on the set of functionality offered by the protocol. In general, an adaptor will contain "wrapper" functions for each relevant entry/exit point to the underlying protocol. Adaptors should follow the following general best practices:
- Every position that can be entered with an adaptor should be able to be unwound. Adaptors that do not implement any required "withdrawal" functionality increases the risk that a strategist may rebalance into a given position they cannot withdraw from, leading to stuck assets.
- Any "exit" functionality should take care to keep in mind protocol-specific requirements that may hurt the Cellar's performance. For instance, the AAVE adaptor's withdrawal functionality does not allow withdrawals that would lower the Cellar's health factor into the liquidation range.
- Adaptors should be purpose-specific, as opposed to protocol-specific. Most effective adaptors will not wrap an entire protocol's functionality, but will only provide access to the subset of functions relevant to position management.
- Adaptor developers must take care to keep in mind the Cellar's
totalAssets
check and allowed rebalance deviation. Position management functionality and thebalanceOf
implementation must be designed such that funds remain accounted for and do not lead to large swings in accounted-for TVL. - Adaptors should specify the underlying protocol's relevant smart contracts in an immutable manner. These address references should be expressed in code, and not in registry-level configuration (see "Adaptor Configuration").
- Any adaptor call for position management which requires external approval should revoke any leftover approval at the end of its execution flow. The function
revokeApproval
is provided as part ofBaseAdaptor
. - For adaptor calls that may withdraw tokens from an underlying protocol, often the underlying protocols allow the caller to decide which address will receive withdrawn funds. Adaptor developers should leverage the
_externalReceiverCheck
function inBaseAdaptor
to ensure that any received funds conform to the calling Cellar's requirements.
Each Cellar can use a given adaptor with a specific configuration, as specified by the
adaptorData
and configurationData
fields specified in the Registry. When a set of adaptor functionality is paired with a specific configuration, the underlying functionality is termed a position.When Cellars are configured to use certain adaptors, their usage is tied to a specific configuration. In most cases, this configuration specifies the tokens a given Cellar can interact with. For instance, a Cellar that wishes to LP WETH/USDC can be approved for a position that uses the Uniswap V3 adaptor, with WETH and USDC specified in
adaptorData
.Where
adaptorData
specifies a position in terms of the underlying assets interacted with, configurationData
can specify certain Cellar-specific parameters that affect the operational constraints of the position. For instance, configurationData
is used with the AAVE adaptor to enforce cellar-specific minimum health factors.Setting proper
adaptorData
and configurationData
is the responsibility of governance when approving new positions - see "Roles & Permissions" for more information. Strategists do not have the ability to change or override these configured values - to do so, new positions with the same adaptor and different configurations must be approved for the Cellar by governance.
All adaptors are designed such that calls to their protocol-specific functionality run in a
delegatecall
context. Therefore, adaptor developers must consider that at execution time, adaptor functionality will run within the context of the calling cellar. Some of the consequences of this design that should be considered include:- During adaptor calls,
address(this)
will return the Cellar's address, andmsg.sender
will return the caller of thecallOnAdaptor
function (the Gravity Bridge). - Adaptor contract code should not use contract storage or expect to be able to access adaptor storage. Adaptor calls should access Cellar storage with care.
- Any funds "managed" by positions belonging to the adaptor will be owned by the Cellar on-chain.
- Any approvals set by the adaptor will be approvals to spend from the Cellar itself, and leftover approvals may be used by other adaptors. This is another context in which zeroing out leftover approvals during adaptor calls is extremely important for the adaptor developer.
Another consequence of adaptor design is that there is no per-function permissioning granularity. Since adaptor calls flow through the Cellar's
callOnAdaptor
function, any function on a Cellar's approved adaptors are eligible to be called by the strategist. Adaptor developers should also take care to make sure to only expose functions that cannot be exploited by malicious strategists.The main entry point to adaptor calls,
callOnAdaptor
, has a re-entrancy guard attached. Adaptor calls cannot re-enter the Cellar to make new calls to callOnAdaptor
, or mint or burn new Cellar shares. This protects against strategist-initiated attack vectors which rely on minting or burning shares while retaining constant TVL.Sommelier Cellars can incur leverage by borrowing assets from any lending protocol supported by an adaptor - to do this, a debt adaptor can be built. Debt adaptors are subject to the following guidelines:
isDebt
must return true.- User deposits and withdrawals from the debt position should not be supported.
balanceOf
should report the total liability outstanding.- Any function which incurs additional debt should only borrow up to liquidation thresholds and not cause a strategist to be able to liquidate their own Cellar.