Cygnus-Core Smart Contracts Walkthrough — Infinite Lending Pools

CygnusDAO
23 min readAug 11, 2022

--

This article will detail the architecture of Cygnus Core contracts. All the code is open source and currently hosted on Github at the following address:
https://github.com/CygnusDAO/cygnusdao-core/tree/main/contracts/cygnus-core

CygnusDAO consists of:

  • 12 core contracts
  • 2 deployer contracts (1 for LP Tokens, 1 for DAI)
  • 1 LP price oracle
  • 1 factory
Overview of Cygnus lending pools

The core contracts are fairly straight forward to understand and provide the basis for anyone to deploy a lending pool based on any LP Token supported by our oracle. We hope this walk-through helps everyone understand how Cygnus contracts work. Even if you are completely new to smart contract development, don’t be scared. We subscribe to the KISS philosophy (“Keep It Simple, Stupid!”) and the contracts were written with this philosophy in mind. Also, the core contracts make good use of the Solidity language and cover many features of it, including custom errors, abi encode/decode, create2, inheritance, etc. so even if you are not fully comfortable with Solidity yet, you will be able to see many of its features implemented here.

Furthermore, the core contracts are a fork of Impermax, with some important differences. Impermax developed a clever way to leverage LP Tokens using volatile assets to increase the yield for leveraged positions, and their system has been proven to work fairly well. Cygnus instead relies on DAI as its lending asset. We chose DAI since it is the most decentralized and we believe to be the safest stablecoin in DeFi due to its over-collateralization model.

Our approach to leverage is more akin to the one found across Centralized Exchanges, but with no fees. Currently most CEX’s charge a fee for margin trading, where as Cygnus charges nothing to deposit, redeem or leverage. Furthermore with Cygnus users can leverage x13 their collateral value (see the conclusion). The idea is to create a leverage yield farming protocol which is decentralized, completely on-chain and with $0 fees for lenders or borrowers.

Core Architecture

The core contracts consist of 8 Borrow contracts, and 7 Collateral contracts. Each contract inherits from the previous one, finishing at CygnusBorrow.soland CygnusCollateral.sol respectively. Thus, we are in reality just deploying these 2 contracts, but each contains the bytecode of the parent contract. We will refer to CygnusBorrow.sol as the borrowable arm and CygnusCollateral.sol as the collateral arm.

Complete Cygnus-Core architecture

The CygnusFactory (the bottom red) is in charge of deploying the borrowable and collateral arms (1 lending pool) as such we will begin here.

1. Cygnus Factory, Orbiters and Oracle

Factory
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusFactory.sol

Borrow Orbiter
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/AlbireoOrbiter.sol

Collateral Orbiter
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/DenebOrbiter.sol

Cygnus LP Oracle
https://github.com/CygnusDAO/cygnusdao-price-oracle/blob/main/contracts/ChainlinkNebulaOracle.sol

Since Cygnus is to be deployed across multiple chains the factory acts as the control center of each chain. It contains the information of all lending pools deployed on each chain and acts as the main security system. Instead of setting important addresses in the lending pools, we set them in the factory, and then all lending pools call the factory to confirm the address they are looking for. Specifically, each lending pool calls the factory (denoted as hangar18) to check for:

  • admin — This is the address of the admin, who is the only one which may adjust certain collateral/borrowable settings in each lending pool.
CygnusTerminal.sol — Internal function which calls the factory (`hangar18`) to check the current admin. It then reverts if the msg.sender is not the factory admin.
  • daoReserves — The address where reserves get minted (it is a smart contract).
CygnusBorrow.sol — The protocol accumulates DAI reserves. This internal function mints DAO reserves, calling the factory to check for the current DAO Reserves address (a contract).
  • cygnusNebulaOracle — The address of the latest Cygnus Oracle.
CygnusCollateralControl.sol — The collateral arm relies on an oracle to be active to check for the price of the LP Token. When the CygnusCollateral gets deployed, it calls the factory to check for the current oracle (`cygnusNebulaOracle`)

These 3 addresses can be removed or added back, but only by the current admin and only at the factory. Therefore the factory is the nucleus that keeps all lending pools deployed in sync.

Since most EVMs have a maximum limit to how big a contract’s bytecode can be (https://github.com/ethereum/EIPs/issues/170), the factory contract uses external contracts called orbiters to deploy lending pools (one for borrowable contracts and one for collateral contracts). These Orbiters are not part of the core contracts, but each import the bytecode of the corresponding contracts.

These orbiters are deployed first onto the chain in a stand alone fashion and have 1 purpose and 1 function only: To deploy borrowable and collateral contracts.

  • AlbireoOrbiter: Imports the bytecode of the Borrow arm (line 28) and is in charge of deploying borrowable contracts.
AlbireoOrbiter: Imports CygnusBorrow bytecode
  • DenebOrbiter: Imports the bytecode of the Collateral arm (line 28) and is in charge of deploying collateral contracts.
DenebOrbiter: Imports CygnusCollateral bytecode

After deploying the orbiters, we add them to the factory with the initializeOrbiters function and pass the addresses of both orbiters. The factory creates a unique ID to identify each pair of them. We will share why we may need different orbiters later, but for now let’s just assume we have deployed them and added them to the factory. Since they are the only orbiters, this pair has the ID of 0 (so 1 borrow deployer and 1 collateral deployer).

Once we have orbiters set in the factory we can start deploying lending pools. To create a lending pool, the factory has a deployShuttle function which anyone may call. It takes 4 parameters:

  • LP Token address
  • Orbiter ID
  • Interest rate parameter 1: Base Rate
  • Interest rate parameter 2: Multiplier

Let’s deploy a lending pool with the LP Token for JOE/AVAX: https://snowtrace.io/token/0x454e67025631c065d3cfad6d71e6892f74487a15

deployShuttle(“0x454e67025631c065d3cfad6d71e6892f74487a15”, 0, 0.03e18, 0.1e18)

(the address of the LP Token, orbiter ID 0, a 3% base rate and 10% multiplier)

The deployShuttle function found at the factory used to deploy lending pools.

The deployShuttle function has 5 phases to make sure the lending pools are deployed correctly. The first 4 phases can revert the deployment (causing the transaction to fail), the 5th phase, if reached, stores all the information necessary to identify a lending pool in the factory. The first 4 phases are:

Phase 1: Orbiter Check — The orbiters are active and usable.

Reverts transaction if orbiters don’t exist or are set as inactive (Orbiters can be disabled withswitchOrbiterStatus function)

Phase 2: Lending Pool Check — Calls boardShuttle function to check that no lending pool exists with the same LP Token and orbiter ID. If it’s not yet registered, the factory creates a lending pool with a unique ID (shuttleId) and passes it to the deployShuttle function.

Reverts transaction if a lending pool with the same LP Token and same Orbiter ID already exists.

Phase 3: Collateral Address Check — Since each borrowable and collateral arm should be linked with each other to perform loans and liquidations, we need to find a way to store the address of the collateral contract in the borrowable contract and vice versa. To avoid having to manually set these after deployment, we use the create2 method (https://docs.openzeppelin.com/cli/2.8/deploying-with-create2).

Since we know the address of the orbiters and we know that orbiters store the init code hash of each contract, we can calculate what the future address of a collateral will be given an LP Token, a msg.sender and an orbiter (to get the init code hash of CygnusCollateral.sol).

CygnusPoolAddress.sol library (https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/libraries/CygnusPoolAddress.sol)

The getCollateralContract is part of our library which calculates the future address of a collateral contract given an LP Token, a factory address and an orbiter address. Once calculated, the deployShuttle function tells the borrowable orbiter to:

  1. Deploy a CygnusBorrow contract with the calculated collateral address as a parameter to store in the borrowable contract constructor.
  2. Deploy a CygnusCollateral contract with the deployed CygnusBorrow address as a parameter to store in the collateral contract constructor.
  3. Check if the deployed collateral address matches the previously calculated collateral address.

Reverts transaction if the calculated collateral address is different to the deployed collateral address.

Phase 4: Checks that the oracle is initialized to retrieve the price of 1 LP Token in DAI. Before deploying a lending pool we must initialize the LP Token pair in the oracle. The oracle calculates the price of 1 LP Token of the underlying in DAI, and thus is needed for the collateral arm to know how much a user can borrow from the borrowable arm.

Reverts transaction if the oracle for this LP Token is not initialized.

Phase 5: No reverts, stores the information of the lending pool in the Shuttles struct to easily retrieve information of any lending pool from the factory whenever we need

With the optimizer enabled at 200 runs, it costs around 5,786,987 gas to deploy a lending pool (1 collateral and 1 borrow contract).

Cost deploying a lending pool

The cost may seem high, but remember we are deploying 2 contracts, each with all the data stored and no need to initialize variables anymore. As a reference, UniswapV3 pools cost ~4,5M per deployment to only deploy a single contract (uniswap factory contract): https://etherscan.io/address/0x1f98431c8ad98523631ae4a59f267346ea31f984).

Once deployed, the contracts can run by themselves with no interference, unless we do a specific strategy (more on this later).

If all checks are successful, we successfully deployed a lending pool and all its information is stored in the factory contract!

Since we just deployed the first lending pool, we can find the borrowable or collateral contracts in 2 ways: allShuttles or getShuttles from the factory. The allShuttles variable is an array containing all lending pools by ID, and getShuttles is a mapping to find if a lending pool exists for a given LP Token.

We used the LP Token for JOE/AVAX (0x454e67025631c065d3cfad6d71e6892f74487a15) and the orbiter with ID 0, so we call getShuttles("0x454e67025631c065d3cfad6d71e6892f74487a15", 0) to get all the information of the lending pool:

The Shuttle struct contains the information of the lending pools along with the Orbiter Struct within it .

— Shuttle —
launched : Whether or not a lending pool with this LP Token exists
shuttleId: Unique ID of each lending pool (also found in deployed contracts)
cygnusDai: The address of the CygnusBorrow contract
collateral: The address of the CygnusCollateral contract
borrowToken: The address of the underlying borrow asset (DAI)
lpTokenPair: The address of the underlying collateral asset (LP Token)

— Orbiter —
status: Whether or not the orbiters are active
orbiter: The ID for the orbiters used to deploy this lending pool.
orbiterName: A custom name to easily identify what this orbiter was used for (can be left blank)
albireoOrbiter: The address of the borrowable deployer
denebOrbiter: The address of the collateral deployer

Now that we have the addresses of the borrowable (0x468000A6E1533197f26132ACF6ad53be1967DF62) and the collateral (t0x0AfFf300a21673cAbebB384f1Fb645A8A706573c), we can start to look at the deployed contracts.

Since the collateral contract has less contracts, let’s start with CygnusCollateral.sol.

2. Cygnus Collateral

CygnusCollateral.sol

The collateral contract is responsible for safely handling LP Tokens. It accepts LP Token deposits and mints a vault token called CygLP to the user. It contains the collateralization model used by Cygnus and the address of the price oracle which calculates how much a user’s deposited assets are worth (in DAI).

The first functions of this contract are the deposit and redeem functions, which are ERC-4626 compatible (https://ethereum-magicians.org/t/eip-4626-yield-bearing-vault-standard/7900) but these are the only 2 functions from the standard we implemented to save on byte code size. These can be found at CygnusTerminal.sol contract so let’s start here (we won’t cover ERC20 or ERC20Permit in this article).

1. CygnusTerminal
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusTerminal.sol

The CygnusTerminal contract is the contract where users mint vault collateral tokens (CygLP) and it inherits from the ERC20 standard. The only public storage variables this contract holds are:

  • totalBalance — The total amount of LP Tokens this contract holds.
  • underlying — The address of the LP Token.
  • hangar18 — The address of the factory.
  • shuttleId — The lending pool ID to identify in the factory.
Storage variables at CygnusTerminal.

The initial exchange rate represents the exchange rate between 1 CygLP and 1 LP Token.

Aside from the deposit and redeem functions, there is an extra function to recover ERC20 tokens sent to this contract by accident. It sweeps any token passed EXCEPT the underlying.

Function to sweep any token accidentally sent to the collateral contract.

2. CygnusCollateralControl
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusCollateralControl.sol

The CygnusCollateralControl inherits from the Cygnus Terminal and it is the control contract for the collateral. It is the only contract in the collateral arm that the Cygnus admin has write access to.

Remember how we deployed the collateral contract with the LP token pair and borrowable address (check deployShuttle above)? Well, the Collateral Control contract reads this data in its constructor at the time of deployment and it stores the factory address (msg.sender of orbiter), the LP Token address and the shuttle ID. It also introduces and stores a new variable cygnusDai which is the address of the borrowable contract.

The oracle is needed only in the collateral arm (as it is the only one which holds LP Tokens), as such it is read from the factory and gets assigned at the time of deployment as cygnusNebulaOracle.

Constructor in CygnusCollateralControl

It contains 3 functions which can only be accessed by the factory admin. These can only be adjusted through community proposals. These 3 functions are:

  • setDebtRatio — This function takes a new debt ratio as a parameter and updates the debt ratio for this pool. The default debt ratio of all lending pools is 95%, meaning a user may borrow up to 95% of their collateral value, taking into account liquidation incentive and liquidation fee.

Default: 95%
Min Debt Ratio: 80%
Max Debt Ratio: 100%

  • setLiquidationIncentive — This function takes a new liquidation incentive as parameter and updates the profit liquidators receive from each liquidation. The default liquidation incentive is 2.5%.

Default: 2.5%
Min Liquidation Incentive: 0%
Max Liquidation Incentive: 10%

  • setLiquidationFee — This function takes a new liquidation fee as parameter and updates the fee the protocol keeps from each liquidation. The default value is 0, so there is no liquidation fee the protocol keeps.

Default: 0%
Min Liquidation Fee: 0%
Max Liquidation Fee: 10%

These 3 variables set the foundation for the amount users can borrow. Since we know that a user may borrow up to 95% of their deposited value and there is a 2.5% liquidation incentive, the maximum a user can borrow becomes a function of these 2 variables. More on this in the CygnusCollateralModel section below.

3. CygnusCollateralVoid
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusCollateralVoid.sol

The CygnusVoid inherits from the control contract and it contains only private and external functions, meaning no other contract in the collateral arm relies on it. It can be left completely empty if no strategy is in place (except for the constructor), making it very easy for anyone to customize different strategies before deploying orbiters while keeping the same core functionality of Cygnus. If a strategy is employed, all of it must be contained within this contract.

For example, the strategy on our Github is for TraderJoe MasterchefV3 contracts. The CygnusCollateralVoid contract has a reinvest function, which connects to their masterchef and harvests the rewards. It then uses the rewards to buy more LP Tokens to then deposit them again in the masterchef. This in turn lowers the debt ratio of borrowers (as now their CygLP can be redeemed for more LP Tokens than they deposited), allowing them to borrow more or lower their risk of liquidation. Anyone may call this function and there is a 2.5% reinvest reward for anyone who calls the function (paid in the rewards token). A script showing this is shown in the next section.

Harvest function of CollateralVoid for TraderJoe’s MasterChefV3. It withdraws 0 staked LP Tokens, triggering the masterchef to send all accumulated rewards to the CollateralVoid.
Reinvest function of CollateralVoid. Harvests the rewards first and proceeds to convert the rewards into more LP Tokens.

4. CygnusCollateralModel
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusCollateralModel.sol

The CygnusCollateralModel contract inherits from the Cygnus Void and it contains the collateralization model used by Cygnus. The model is as simple as it can be, and it calculates the collateral needed for a loan X given an amount of collateral Y, and returns a user’s liquidity or shortfall.

Takes into account the exchange rate of CygLP to LP Tokens of this contract before calling collateralNeededInternal to calculate how much the collateral is worth in DAI.
Collateral Model

The first variable lpTokenPrice is the price of 1 LP Token denominated in DAI which is taken from our oracle. It then calculates the user’s deposited collateral and times it by the price of 1 LP Token to get the price of the collateral (in DAI), adjusting it with the debt ratio set (default at 95%). It then gets the DAI borrowed amount and multiplies it by the liquidation incentive. To find the max borrow amount we thus calculate:

(priceLPToken * depositedLP * debtRatio) / (max(debt, 1) * 1.025)

For example, a user deposits 100 LP Tokens of JOE/AVAX and receives 100 CygLP in return. The price of 1 LP Token is $8.29696 and the user has $0 previous borrows. The maximum they can borrow is equal to:

borrowerPosition = 8.29696 * 100 LP Tokens = $829.696

maxBorrow = (borrowerPosition * 0.95) / 1.025 = $768.9867

Below is an example of a script found on our Github (https://github.com/CygnusDAO/cygnusdao-core/blob/main/scripts/Borrow.js) which shows a simple borrow interaction where a user deposits 100 LP Tokens of JOE/AVAX and maxes their borrow limit, reaching 100% debt ratio (any downward price movement in the LP Token and their position becomes liquidatable). Another user then reinvests the rewards after 3 days lowering the debt ratio of all CygLP holders:

Ran on a forked instance of Avalanche C-Chain, a sample of a user borrowing the max amount (no leverage).

All the functions in this contract are view functions, which means they only read but do not alter the state variables (in other words, there is no change to the blockchain after calling these functions). The 4 view functions of this contract which anyone may call are:

canBorrow — This is called by the borrowable contract, and queries a user address against the model to see if the user can borrow X amount of DAI from the borrowable arm. Anyone can call this after depositing LP to see if they can borrow X amount also.

getDebtRatio — Returns the percentage of a user’s loan to assets position, allowing anyone to check their position to see if they are in a liquidatable state or not (anything over 100% is liquidatable). If a user deposits but doesn’t borrow, it returns 0%. If a user max borrows, it returns 100%.

getAccountLiquidity — Returns the max amount of DAI they can borrow given their deposited amount. It returns the liquidity of their position (how much dai they can borrow) or their shortfall (they can be liquidated). Ir is the same as getDebtRatio but returns their position’s health in DAI instead of a percentage.

getLPTokenPrice — Checks the price of 1 unit of the underlying LP Token in DAI, called by getDebtRatio and getAccountLiquidity. It calls the oracle address (we deployed it before the factory) which in turn calls the LP Token address to get the reserves and calculates the price using chainlink price feeds.

5. CygnusCollateral
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusCollateral.sol

The CygnusCollateral contract is the final collateral contract and inherits from collateral model. It contains 4 key functions, 3 of which are external.

ERC20 Internal Transfer override

TransferInternal — The core contracts inherit from Erc20.sol and this contract overrides the internal transfer , not allowing users to withdraw their collateral tokens if it puts their position in shortfall. Once a user borrows DAI, the collateral amount equal to the loan amount is locked until they repay the corresponding amount.

Function to check how much a user can redeem

canRedeem — Returns a bool value of whether or not redeeming a certain amount puts a user’s position in shortfall, called by transferInternal.

seizeCygLP — Seize collateral from a user when they are in a liquidatable position

seizeCygLP — Function which seizes collateral from a borrower if their position is in liquidatable state. Since loans are repaid in DAI, the function queries the oracle with getLPTokenPrice to calculate how much collateral to seize (equal to repayAmount * liquidation incentive), adding this CygLP amount to the liquidator and decreasing it from the borrower (lines 131 and 134).

Flash redeem. Called during the deleverage function. Transfers redeemAmount of LP Token to the redeemer, and at the end does sanity check if the CygLP was repaid back to this contract.

flashRedeemAltair — Function to flash redeem collateral, allowing users to deleverage their position, making use of the before withdraw hook found in the Cygnus Terminal. It allows anyone to withdraw LP Tokens from the contract without CygLP, but does a sanity check at the end to make sure that the equivalent amount of LP Tokens in CygLP is returned to the contract and burns the CygLP (similar to AAVE’s flash loan mechanism).

And that’s it! The CygnusCollateral contract has all the functions necessary for users to deposit LP Tokens and receive the vault token CygLP, allowing them to borrow DAI and/or claim back their LP Tokens. The CygnusCollateral contract consists of a control contract for admins, a strategy contract for the deposited LP, the collateralization model and the ability to seize collateral from users who have a position in shortfall.

There is no fee to deposit or withdraw, so users may even choose to deposit their LP Tokens here for the auto-compounding without borrowing. However, the rewards paid out in CYG token (more on this in the next section) are a reserved right for borrowers only.

Now that we covered the collateral arm let’s go onto the borrowable arm.

3. CygnusBorrow

The borrowable arm follows a similar structure to the collateral, but instead of having a strategy contract and a collateral model contract, it has the interest rate model contract and a borrow tracker contract. Both collateral and borrowable arms share the same CygnusTerminal contract, but the state variables hold different addresses.

CygnusBorrow.sol

In contrast to the collateral arm, the borrowable arm is responsible for safely handling DAI. It accepts DAI deposits and mints a vault token called CygDAI to the user. Like the collateral contract, the deposit and redeem functions are ERC-4626 compatible and can be found at CygnusTerminal.sol.

1. CygnusTerminal
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusTerminal.sol

The CygnusTerminal contract is the same as the collateral arm but mints CygDAI instead of CygLP. The variables thus represent:

  • totalBalance — The total amount of DAI this contract holds.
  • underlying — The address of DAI.
  • hangar18 — The address of the factory (same as collateral)
  • shuttleId — The lending pool ID to identify in the factory (same as collateral)
Storage variables for both borrow and collateral contracts

The initial exchange rate represents the exchange rate between 1 CygDAI and 1 DAI token.

Like the Collateral’s CygnusTerminal, it has the same sweepToken function to recover ERC20 tokens accidentally sent to this contract.

2. CygnusBorrowControl
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusBorrowControl.sol

Similar to the collateral arm, there is also a control contract for the borrowable arm called CygnusBorrowControl and is the only contract in the borrowable arm that the Cygnus admin has write access to. Again, the same as the collateral control contract, it reads the data passed from the orbiter and stores the factory address as hangar18(msg.sender of orbiter), the DAI address as underlying, the collateral address as collateral and the lending pool ID as shuttleId. The extra two parameters baseRate and multiplier get read in the next child contract.

Constructor in CygnusBorrow.sol

It contains 3 functions which can only be accessed by the admin to adjust important borrowable parameters. The first function assigns the cygnusBorrowTracker contract, which is the contract that rewards borrowers in CYG relative to their borrowed amount. The other 2 functions are to be accessed only through community proposals and these are:

  • setReserveFactor — This function takes a new reserve factor as a parameter and updates the percentage from loans that is kept as DAO reserves. The default value is 5%, and this is the amount of DAI that CygnusDAO keeps in case loans go underwater and to act as a safety mechanism for lenders.

Default: 5%
Min Reserve Factor: 0%
Max Reserve Factor: 10%

  • setKinkUtilizationRate — This function takes a new kink rate as a parameter and updates the kink utilization rate. This parameter reflects the point at which the slope of the interest rate goes from “gradual” to “steep”. The default value is 85%, meaning that once borrows/cash exceeds 85% the interest rate increases at a faster rate to discourage users from borrowing more.

Default: 85%
Min Kink Utilization Rate: 50%
Max Kink Utilization Rate: 95%

The CygnusBorrowControl splits into 2 child contracts, each which handle a unique part of the borrow mechanism: CygnusBorrowApprove and CygnusBorrowInterest.

3. CygnusBorrowApprove
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusBorrowApprove.sol

The CygnusBorrowApprove contract inherits from the control contract and its main function is to keep a record of borrow allowances.

Cygnus has core contracts which should be accessed with a periphery contract (similar to Uniswap). By separating between core and periphery contracts, it allows the core contracts to prioritize safety and the periphery to prioritize a better UX for users.

We will detail the router contract in a future article, but for those who want to take a look it is currently hosted on our Github under the Periphery repository (https://github.com/CygnusDAO/cygnusdao-periphery/blob/main/contracts/CygnusAltairX.sol)

Before a user borrows any amount of DAI held by this contract, he or she must first approve a periphery contract to borrow on their behalf. For example, when using the borrow function found in CygnusBorrow.sol, the call checks the allowance of the msg.sender and reverts if the borrow amount is higher than the allowance.

1. User calls borrow function in CygnusBorrow.sol and the contract checks CygnusBorrowApprove.sol for allowances (line 136).
2. Approve function on CygnusBorrowApprove called by the the main borrow function (reverts on line 83 if amount is higher than allowance)

This contract allows users to increase or decrease allowances as they want and is the first contract CygLP holders should interact with to create a leverage position.

4. CygnusBorrowInterest
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusBorrowInterest.sol

The CygnusBorrowInterest contract also inherits from the control contract, and it displays the interest rate model used by this lending pool.

The interest rate model is the same as the one used by Compound Finance, and it is set at the time of deployment. If you remember when we deployed a lending pool at the factory, we passed 2 variables at the end: baseRate and multiplier.

deployShuttle([lpTokenAddress], [orbiterId], baseRate, multiplier)

These two parameters are what constructs the interest rate model at Cygnus.

  • Base rate per year, the minimum borrowing rate. For example, at 0% utilization rate, a user must pay this base rate if they wish to borrow.
  • Multiplier per year, the rate of increase in interest rate with respect to utilization.

When deploying the lending pool, the constructor reads these variables passed from the factory, and constructs the interest rate model:

Constructor and storage variables in CygnusBorrowInterest

It stores the per-second variables of the parameters to accrue interest rates every second.

With the base rate, multiplier and a reserve factor (explained earlier to be default at 5%) we can extrapolate the interest rate for borrowing and for lending:

Interest rate model

A user can deploy a lending pool with their own interest rate, even set base rate and multiplier as 0 if they wish. This design was intentional to give users maximum flexibility at the time of deployment. However, the DAO owned pools (those deployed by us) follow the pattern described on our docs here: https://docs.cygnusdao.finance/how-cygnus-works/interest-rates-model

For example, someone deposits $1000 worth of the LP Token ETH/BTC and borrow $3000 worth of DAI to buy more LP Tokens. Cygnus converts $3000 DAI into LP Tokens and puts $4000 worth of LP Tokens back in the pool (and farm). If the LP Token underlying assets (ETH and BTC) start increasing in value, this means that their debt ratio starts lowering and they can borrow more, or sell off a smaller quantity of assets to pay back their loan in DAI. This strategy is the maximum profit strategy for anyone who is bullish on their assets as they owe a static debt (in DAI) against their assets which can rise in value. This is also not taking into account the earnings from being a liquidity provider plus any liquidity mining incentive earned. As they are leveraged 4x they are earning 4 times more than they would without leverage, meaning higher earnings from trading fees and farming rewards.

Note: It is also not taking into account the compounding effect from CygnusCollateralVoid which reinvests rewards to buy more LP Tokens, so in reality their position is even more valuable.

5. CygnusBorrowTracker
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusBorrowTracker.sol

The CygnusBorrowTracker contract inherits from both the interest rate model and the borrow approval contract, and it is responsible for:

  • Accruing interests to borrows and reserves.
  • Keeping track of individual borrow positions.

The borrow tracker is optimized to use 2 memory slots per accrual, and accrues interest on every deposit, redeem, borrow or liquidation. With every accrual, the borrow interest rate gets updated to reflect the interest rate paid by borrowers and earned by lenders.

It is here where the total reserves variable is initialized. Whenever a borrow happens, the CygnusBorrow.sol contract calls the borrow tracker to record the borrower’s position. Users who wish to know how much they owe can query the getBorrowBalance function in this contract to see their borrow balance.

6. CygnusBorrow
https://github.com/CygnusDAO/cygnusdao-core/blob/main/contracts/cygnus-core/CygnusBorrow.sol

The CygnusBorrow contract is the final borrow contract of the borrowable arm and it inherits from the borrow tracker. Like the CygnusCollateral contract, it contains 4 key functions, 3 of which are external:

mintReservesInternal — This function is called by the exchange rate function to mint reserves (if any). The way the DAO accrues reserves is by the minting of CygDAI, and not DAI itself. By accruing CygDAI, the DAO has the rights to redeem DAI from the pool only in case loans go underwater and cannot be repaid.

exchangeRate — The exchange rate in CygnusBorrow overrides the exchangeRate function at the Terminal and it triggers interest rate accruals. Since the exchange rate is called on all redeems and mints, interests are accrued on basically any interaction with the borrowable contract. By overriding it, it takes into account borrows to calculate the latest exchange rate equal to:

(totalBalance + totalBorrows - totalReserves) / totalSupply

borrow — The main borrow function used by Cygnus, which should only be called by the periphery contract. It can be used without, but using our periphery contract is advised for ease of use. It triggers interest rate accruals.

liquidate — The main liquidate function used by Cygnus, which again should only be called by the periphery contract. The liquidate function checks the borrow balance of a borrower and calls the seizeCygLP function in CygnusCollateral. If the user’s position is not in shortfall, it reverts the whole transaction. It triggers interest rate accruals.

And that’s it! These are all the Cygnus Core contracts, nothing more, nothing less. We hope to have clearly explained how the contracts interact with one another. To see the leverage function in action (where users borrow more DAI than their collateral) you can check the leverage function in our periphery contract. With a 95% debt ratio and 5% liquidation incentive and 0% liquidation fee, users can leverage up to 13x, but a small price drop in the LP Token would put the user at liquidation levels.

Example of a user depositing 100 LP Tokens of JOE/AVAX, and leveraging their position 13x using the LeverageAndDeleverage.js script found on our Github: https://github.com/CygnusDAO/cygnusdao-core/blob/main/scripts/LeverageAndDeleverage

1 LP Token is worth 8.40 DAI. With 100 LP Tokens deposited, their collateral is valued at 840 DAI and they borrow 10,128 DAI.

To run this on your end:

  1. Clone the repository

git clone https://github.com/CygnusDAO/cygnusdao-core

2. Install dependencies

npm install

3. Fork Avalanche C-Chain

npx hardhat node --fork https://api.avax.network/ext/bc/C/rpc

4. Open a new terminal and run the LeverageAndDeleverage.js script in the scripts folder

npx hardhat run ./scripts/LeverageAndDeleverage.js

Recap

  1. When we call deployShuttle, the Factory creates a lending pool object with the LP Token address and Orbiters and stores a unique ID. It then passes the LP Token address, the interest rate parameters and the lending pool ID to the Orbiters.
  2. The Borrowable Orbiter takes the interest rate parameters and deploys a borrowable contract with the interest rate parameters. The Collateral Orbiter takes the LP Token address and deploys 1 collateral contract with the LP Token address.
  3. Once deployed, the borrowable and collateral arms are linked and the collateral and cygnusDai addresses are immutable, meaning they can only be used by one another forever.
  4. The collateral arm accepts and holds LP deposits only, minting CygLP to borrowers.
  5. The borrowable arm accepts and holds DAI deposits only, minting CygDAI to lenders.
  6. To borrow, a user deposits LP Tokens in the collateral arm and calls borrow on the borrowable contract.
  7. The borrowable contract checks the borrow balance of the borrower and passes this info back to the collateral.
  8. The collateral asks the price oracle to check how much the user’s LP deposits are worth in DAI. Once the collateral knows the LP Price, it calculates the user’s liquidity or shortfall, and passes a true or false back to the borrowable.
  9. If the user has liquidity, the borrowable accepts the borrow function call and transfers DAI to the borrower.

--

--

CygnusDAO

Decentralized stablecoin lending and leverage trading for Liquidity Providers.