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 theLibLoans
storage functions()
which gets ourloan
data back after we pass ourloanID
getLoanEscrowValue
directly calls theLibEscrow
library functioncalculateTotalValue
to calculate the loan escrow value usingloanID
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 stringThe
store()
function in theMarketStorageLib
returns ourMarketStorage
at the position we initialized beforeThe
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 theloanID
as a parameter and returns a struct of typeLoan
(defined inmarket.sol
) with the help of ours()
functionthe
s()
function calls ourMarketStorageLib.store()
, which if you remember it returns ourMarketStorage
struct stored at our hashed slotNow that our
MarketStorage
is returned vias()
, 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
Was this helpful?