This code tour is intended to give technical or tech-adjacent people (who may be newer to Solidity or to DeFi) a casual walkthrough of the Ribbon V2 codebase so they can improve their familiarity with and understanding of Ribbon V2, with a focus on the core Solidity contracts. We’ve also added some thoughts on specific design choices and object interactions.
We’re excited to tour the Ribbon V2 code base together!
Ribbon is a decentralized finance (DeFi) protocol that creates algorithmic structured products for users to earn yield on digital assets. Their most popular structured products are vaults that automatically sell “covered call” options on ETH and wrapped Bitcoin (WBTC).
Ribbon works by hosting vaults that users can use to generate yield on their digital assets. The most popular V2 vaults generate yield by selling out-of-the-money options using Gnosis auctions.
One thing to note is that Ribbon’s code is very modular — modularity is great for future iterations and improvements (any module can be easily “swapped out” for another and tested in a relatively self-contained and logically easy-to-understand way). However, this does mean that reading through the code base can be quite daunting (due to the number of apparent “moving parts”).
While we’ve skimmed them, we haven’t studied every single file in all the folders listed in depth line-by-line, so this is intended more as a 10,000-meter “view” of RibbonV2.
Note: this walkthrough is NOT AN AUDIT and is meant for educational purposes only. Please do NOT take anything here as a security recommendation or an investment recommendation. Not financial advice. Comments are based on the 2022–02–03 commit
735f076376edef3230771efc32f81bbd2dcecda5
on ribbon-v2 master.
Quick Tip: when browsing alongside us through the codebase, if you’re curious to see how someRibbonFunction()
is used in the codebase, you can (if on a *nix machine):
git clone https://github.com/ribbon-finance/ribbon-v2.git
)contracts
foldergrep -R someRibbonFunction .
to see where else in the codebase someRibbonFunction
pops up!There are a few folders in the contracts repo for Ribbon, but we’ll focus the more detailed part of the walkthrough on just a small number of files within the libraries and vaults folder, where most of the “body” of Ribbon sits.
For those new to Solidity, interfaces (typically denoted with an ‘I’ prefix) allow a developer to define how their code will interact with other pieces of code. When writing a smart contract that interacts with another smart contract, then, the simplest way to bring in those “rules of engagement” is by importing the interface.
Ribbon defines a number of interfaces here for purposes including interacting with external systems (e.g. IGnosisAuction.sol, ISwapRouter.sol) and dealing with specific tokens (IWETH.sol, IERC20Detailed.sol). Notably, IRibbonThetaVault.sol is the interface to access instances of Ribbon’s Theta Vault. This is used, for example, when the code to auction off options needs to access a given Theta Vault, or in Ribbon’s more complex Delta Vaults.
Lots of files here! It’s a good practice for any project to abstract repeated calculations into library functions or to grab those files from a trusted source. Here, in addition to libraries you’d expect for various helpers, there is also preliminary functionality for vaults (and their periodic behavior) sketched out in Vault.sol and VaultLifecycle.sol.
This directory contains the data structures used by various Ribbon vaults. These storage objects are written in a way that allows them to be upgraded alongside the Ribbon protocol and seamlessly reused across minor version releases without interrupting the end user experience. We go into a little more detail below.
This subdirectory contains scaffolding and mock data structures for off-chain tests.
utils contains helper files (e.g. for selecting appropriate params) not referenced directly by other Solidity files in the contracts folder but used in TypeScript test scaffolding in the rest of the ribbon-v2 repository.
This subdirectory hosts a lot of the core vault code (both the base classes that get inherited, as well as the implementations) of various vaults. Note that there are several vault types here that are not yet deployed on Mainnet, including a Delta Vault that can potentially buy the options produced by a Theta Vault.
Some custom code (including what appears to be a Ribbon-written proxy) for upgrading, including a math helper (used in some libraries code).
In this section we’ll go a little deeper into one of the key files that help define the Ribbon Theta Vaults!
What is a Theta Vault?
A Ribbon Theta Vault is a vault that generates yield by algorithmically selling options on the digital assets deposited via Gnosis auctions (Learn more about Gnosis Auctions).
Any highly innovative DeFi protocol like Ribbon faces the challenge of balancing shipping speed with security. It can be easier to rapidly innovate when you can push upgrades to an existing contract. However, upgradeability can introduce additional security risks if not managed properly.
Ribbon uses an upgradeable proxy pattern in order to allow for protocol upgrades — that is, it uses a proxy contract to point to the current “correct” implemented version of a smart contract. There are some implementation wrinkles with proxy upgradable contracts (e.g. upgradeable contracts cannot be initialized using constructors, but there are workarounds), but from a “vault security” standpoint, there are two things we wanted to point out in Ribbon V2 before the actual walkthrough: privileged roles and storage variables.
Ribbon V2 has two important “trusted” roles: Admin and Owner. Admins can upgrade the proxies for Theta Vaults, and Owners can update vault parameters. Whenever privileged roles exist, the potential for an attack through those roles exists. A compromised multisig role could theoretically cause unforeseen problems!
The Ribbon team has stated they are moving toward greater decentralization (as seen already in the Ribbon V1 → V2 feature set), but it is currently an open-source system with both trusted (e.g. upgradeable proxy pattern) and trustless components.
Note: there is a third role, Keeper, but it is used for operating vaults rather than upgrading code
Upgradeability means that additional functionality can be added to Ribbon Theta Vaults over time without requiring users to “exit and re-enter” the Ribbon system after major version updates. However, if there is important data saved in storage, it is important that the inheritance chain is carefully updated otherwise the data could be corrupted/lost.
Due to the way storage works in Solidity, storage can only be safely appended to the end of the existing storage slots for a contract. In practice, this implies 3 main approaches to adding storage variables in upgradeable contracts:
Ribbon (as well as Compound) uses the second method of updating storage variables by creating a new version of RibbonThetaVaultStorage
in the inheritance chain whenever new storage variables are needed. These versions of the storage contract (using the naming convention of affixing V1, V2, V3, etc. after the class name) are then all inherited by the base contract (abstract contract RibbonThetaVaultStorage is RibbonThetaVaultStorageV1, RibbonThetaVaultStorageV2, ...
).
Doing this inheritance properly means that “wonky” (yes, that’s a very rigorous and non-hand-wavy description) things don’t happen to data in storage and that the seamless transition of user data is maintained!
VaultLifecycle.sol is a key library file that governs the interaction of Ribbon Vaults with all other contracts/protocols including:
GAMMA_CONTROLLER
in ribbon’s Vault contracts.A couple interesting notes on interacting with Opyn:
operate
method. This method runs an array of actions then checks whether or not the resulting vault state is valid with _verifyFinalState
Notable functions in VaultLifecycle:
commitAndClose
calculates the parameters of the next option (strike, premium, address, etc.) but does not actually close the current option. Instead, these parameters are passed to a function in the parent Vault contract — this parent function closes the existing option (by calling VaultLifecycle’s settleShort
in the case of the Theta Vault)rollover
is a view function that calculates the new Vault share price, vault fees, new collateral amount, pending withdraw amount based on existing vault state and rollover params. Called by RibbonVault.sol’s _rollToNextOption
createShort
mints options based on the vault’s current locked balance. Called by parent Theta Vault’s rollToNextOption
function which then subsequently starts a Gnosis auction for the minted optionssettleShort
closes the current short and retrieves collateral from Opyn, called by parent Theta Vault’s commitAndClose
Note: during our limited walkthrough, we could not find where the logic that enforces that
commitAndClose
can only be called after option expiry is located... it’s possible that something will revert somewhere if called before option expiry (likely in Controller). On the other hand,rollToNextOption
in the Vault contract does require that the current block timestamp be greater thanoptionState.nextOptionReadyAt
.
RibbonThetaVault.sol puts everything together and implements the unique logic for Theta Vaults.
RibbonThetaVaultStorage
and that RibbonThetaVault
should not inherit from any contracts aside from RibbonVault
and RibbonThetaVaultStorage
(recall our earlier discussion about storage variables in upgradable contracts)onlyOwner
and onlyKeeper
modifiers are applied to various functions, meaning that not just any anon can set these params!onlyKeeper
modifier is used to restrict access for some operations (e.g. rollToNextOption()
) to only Keepersfunction withdrawInstantly(uint256 amount)
is a good example of the Checks-Effects-Interactions (C-E-I) pattern used in practice! First, the requirements around a user’s deposits and balances are checked, followed by state variable changes, finally followed by transfer of value. Note the nonReentrant
modifier is also used, presumably to reduce the attack surface area.We hope you’ve learned a bit about Ribbon and better understand some of the design decisions and workings of the protocol. If there’s anything you’d like us to dig more into with the Ribbon codebase, let us know in the comments!
Ante hosts real-time incentivized on-chain smart contract tests (”Ante Tests”) that pay out when code fails. This aligns teams and users and makes code trust explicit, resulting in a safer, more composable web3 ecosystem. Responsible teams like Ribbon🎀 can explicitly put “skin in the game” by staking their Ante Tests (which anyone can also challenge or stake), boosting Ribbon’s “Decentralized Trust Score”. In turn, developers can reference Ante trust scores to build more safely on top of Ribbon, and users can more clearly see community trust in Ribbon real-time.
If you would like to learn more about how Ante could be useful for your protocol team, please reach out to us at signup.ante.xyz, or follow the team on Twitter and join our Discord for more web3 security discussion!
Documentation: https://docs.ribbon.finance/developers/ribbon-v2