Ante Code Tour: Ribbon Finance - Ante Finance - Medium

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!

Background

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.

Getting Started

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):

  1. Open up your command line (Terminal in MacOS or just a CLI)
  2. Clone the RibbonV2 GitHub repo to your local machine (e.g. git clone https://github.com/ribbon-finance/ribbon-v2.git )
  3. Navigate to the contracts folder
  4. Run grep -R someRibbonFunction . to see where else in the codebase someRibbonFunction pops up!

Directory Structure

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.

interfaces

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.

libraries

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.

storage

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.

tests

This subdirectory contains scaffolding and mock data structures for off-chain tests.

utils

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.

vaults

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.

vendor

Some custom code (including what appears to be a Ribbon-written proxy) for upgrading, including a math helper (used in some libraries code).

Theta Vault Design & Walkthrough

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).

Design pattern: proxy upgradeable contracts

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.

Privileged roles

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

Storage variables

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:

  1. Add storage to the base contract only
  2. Add storage to the final contract in the inheritance chain
  3. Add “gaps” in the preceding inherited contracts just in case you need to fill these in with storage variables later on

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 walkthrough

VaultLifecycle.sol is a key library file that governs the interaction of Ribbon Vaults with all other contracts/protocols including:

  • OpynMarginPool contains all the collateral used for minting oTokens (options tokens), while Controller governs all interactions with MarginPool — any minting/redemption of oTokens goes through the Controller. This is called the GAMMA_CONTROLLER in ribbon’s Vault contracts.
  • StrikeSelection — Ribbon’s contract which selects the next options strike price
  • Gnosis — interacts with Gnosis EasyAuction protocol through Ribbon’s own GnosisAuction.sol smart contract. GnosisAuction also picks the option price premium by querying Ribbon’s OptionsPremiumPricer contract

A couple interesting notes on interacting with Opyn:

  • All controller actions sent to the Opyn GammaController are run through the operate method. This method runs an array of actions then checks whether or not the resulting vault state is valid with _verifyFinalState
  • Interestingly enough, Opyn doesn’t publicize its v2 interface and the one available online (v2.opyn.co) has no ability to create vaults

Notable functions in VaultLifecycle:

  • (Lines 47–99) the somewhat confusingly-named 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)
  • (Lines 165–269) 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
  • (Lines 279–371) 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 options
  • (Lines 381–427) settleShort 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 than optionState.nextOptionReadyAt .

RibbonThetaVault.sol brief walkthrough

RibbonThetaVault.sol puts everything together and implements the unique logic for Theta Vaults.

  • (Lines 1–17) Lots of imports! You’ll recognize some files mentioned earlier like Vault.sol and VaultLifecycle.sol.
  • (Lines 25–203) Defining events and data structures. Note the responsible warning for developers noting that all storage variable updates need to happen in 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)
  • Depending on usage by other contracts, the events could also be defined in a separate file and inherited by RibbonThetaVault
  • (Lines 205–291): Various parameter setters — note that onlyOwner and onlyKeeper modifiers are applied to various functions, meaning that not just any anon can set these params!
  • (Lines 292–494) Various vault operations. A few key things to note about the operations:
  • The onlyKeeper modifier is used to restrict access for some operations (e.g. rollToNextOption()) to only Keepers
  • Line 300 function 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.

Conclusion

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!

About Ante

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

GitHub: https://github.com/ribbon-finance/ribbon-v2

Subscribe to Ante Finance
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.