Teller Protocol Diamond

Structure

This is the main diamond contract where all facets of the protocol can talk to each other through a shared storage interface. The ITellerDiamond contract itself works by importing all of it's facets as dependencies. See below:

abstract contract ITellerDiamond is
    SettingsFacet,
    PlatformSettingsFacet,
    AssetSettingsDataFacet,
    AssetSettingsFacet,
    PausableFacet,
    PriceAggFacet,
    ChainlinkAggFacet,
    LendingFacet,
    CollateralFacet,
    CreateLoanFacet,
    LoanDataFacet,
    RepayFacet,
    SignersFacet,
    NFTFacet,
    EscrowClaimTokensFacet,
    CompoundFacet,
    UniswapFacet,
    IDiamondCut,
    IDiamondLoupe
{}

In the Teller Protocol, each Facet contains function helpers that come from its respective Library or libraries. Each Library contains not only function helpers to assist the facets, but also a storage function caller. Here's an example of our LoanDataFacet

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Libraries
import { LibLoans } from "./libraries/LibLoans.sol";

// Storage
import { Loan, LoanDebt, LoanTerms } from "../storage/market.sol";

contract LoanDataFacet {

    function getLoan(uint256 loanID) external view returns (Loan memory loan_) {
        loan_ = LibLoans.s().loans[loanID];
    }

    function getLoanEscrowValue(uint256 loanID)
        external
        view
        returns (uint256)
    {
        return LibEscrow.calculateTotalValue(loanID);
    }
}

Note: this example does not resemble our actual file on the Teller protocol, but rather a simplified version.

In this example, our LoanDataFacet uses 2 functions that call from the LibLoans and the LibEscrow library, respectively:

  • getLoan directly calls the LibLoans storage function s() which gets our loan data back after we pass our loanID

  • getLoanEscrowValue directly calls the LibEscrow library function calculateTotalValue to calculate the loan escrow value using loanID

Next, we'll look at how storage works.

Storage

The way diamond storage works is that we add or read data to a struct that is stored in a hashed position slot by calling a function. Let's call this function store(). We shall take a look at our market.sol storage file, since it's the most popular one in our Teller protocol.

struct MarketStorage {
    // Holds the index for the next loan ID
    Counters.Counter loanIDCounter;
    // Maps loanIDs to loan data
    mapping(uint256 => Loan) loans;
    // Maps loanID to loan debt (total owed left)
    mapping(uint256 => LoanDebt) loanDebt;
    // Maps loanID to loan terms
    mapping(uint256 => LoanTerms) _loanTerms; // DEPRECATED: DO NOT REMOVE
    // Maps loanIDs to escrow address to list of held tokens
    mapping(uint256 => ILoansEscrow) loanEscrows;
    // Maps loanIDs to list of tokens owned by a loan escrow
    mapping(uint256 => EnumerableSet.AddressSet) escrowTokens;
    // Maps collateral token address to a LoanCollateralEscrow that hold collateral funds
    mapping(address => ICollateralEscrow) collateralEscrows;
    // Maps accounts to owned loan IDs
    mapping(address => uint128[]) borrowerLoans;
    // Maps lending token to overall amount of interest collected from loans
    mapping(address => ITToken) tTokens;
    // Maps lending token to list of signer addresses who are only ones allowed to verify loan requests
    mapping(address => EnumerableSet.AddressSet) signers;
    // Maps lending token to list of allowed collateral tokens
    mapping(address => EnumerableSet.AddressSet) collateralTokens;
}

bytes32 constant MARKET_STORAGE_POS = keccak256("teller.market.storage");

library MarketStorageLib {
    function store() internal pure returns (MarketStorage storage s) {
        bytes32 pos = MARKET_STORAGE_POS;
        assembly {
            s.slot := pos
        }
    }
}

This file, like the previous file, has been compressed for ease of understanding.

  • Our MARKET_STORAGE_POS is the hash of our position string

  • The store() function in the MarketStorageLib returns our MarketStorage at the position we initialized before

  • The MarketStorage is a heavy struct with multiple mappings to interfaces, primitive data types and other structs

So, now that we understand this, how do we call this function to update or read data? Well, it's simple really! Let's head to a simple function in our LibLoans library called loan(), which simply returns our loan data:

function s() internal pure returns (MarketStorage storage) {
        return MarketStorageLib.store();
    }

		/**
     * @notice it returns the loan
     * @param loanID the ID of the respective loan
     * @return l_ the loan 
     */
    function loan(uint256 loanID) internal view returns (Loan storage l_) {
        l_ = s().loans[loanID];
    }

Let's go through this step by step

  • loan() function takes in the loanID as a parameter and returns a struct of type Loan (defined in market.sol) with the help of our s() function

  • the s() function calls our MarketStorageLib.store(), which if you remember it returns our MarketStorage struct stored at our hashed slot

  • Now that our MarketStorage is returned via s(), we also return the specific loan by adding .loans[loanID]

That's really our Diamond Structure and Storage in a nutshell! Since our Diamond pulls in all of its Facets via Inheritance, calling any of our facet is still calling our Diamond Contract's address.

Last updated