Introduction

This Gitbook provides a line-by-line walkthrough of the Cooler Loan codebase, including the Clearing House designed for use by Olympus. It is broken into two sections: the Borrower Flow, and the Lender/ClearingHouse Flow. Every line of every function in the code base is laid out here, with brief explanations of functionality and some comments on design choices. This is meant to be used as a resource for code auditors and reviewers. On this page we will review some general information.

A suggestion to the reader: go through this walkthrough with the codebase open next to it. Each code block will tell you where it is found, but many functions are broken into chunks for readability. Viewing them side-by-side will give you the best picture of the whole thing!

Cooler Contract Immutables

The owner of a Cooler, the debt token to borrow, and the collateral token used, are all immutable and set when the factory creates the contract.

Cooler.sol, lines 44-51
// This address owns the collateral in escrow.
address private immutable owner;
// This token is borrowed against.
ERC20 public immutable collateral;
// This token is lent.
ERC20 public immutable debt;
// This contract created the Cooler
CoolerFactory public immutable factory;

These are all set in the constructor.

Cooler.sol, lines 58-63
constructor (address o, ERC20 c, ERC20 d) {
    owner = o;
    collateral = c;
    debt = d;
    factory = CoolerFactory(msg.sender)
}

The Factory passes these in when it creates a new Cooler.

Factory.sol, line 32
cooler = address(new Cooler(msg.sender, collateral, debt));

Events

The Cooler Factory has a function, newEvent(), that emits events when a created Cooler: creates a loan request, rescinds a loan request, or clears a loan request.

Factory.sol, lines 39-54
enum Events {Request, Rescind, Clear}

/// @notice emit an event each time a request is interacted with on a Cooler
function newEvent (uint256 id, Events ev) external {
    require (created[msg.sender], "Only Created");

    if (ev == Events.Clear) emit Clear(msg.sender, id);
    else if (ev == Events.Rescind) emit Rescind(msg.sender, id);  
    else if (ev == Events.Request)
        emit Request(
            msg.sender, 
            address(Cooler(msg.sender).collateral()), 
            address(Cooler(msg.sender).debt()), 
            id
        );
}

Consolidating this information onto the Factory makes it easy for subgraphs to pick up and aggregate. Calls to this function occur on the request(), rescind(), and clear() functions in Cooler.sol. For example, the call on request() looks like this:

Cooler.sol, line 81
factory.newEvent(reqID, CoolerFactory.Events.Request);

mininterfaces.sol

This file, found in the /lib/ folder, contains two small interfaces. The first is the delegation function for delegatable ERC20's, which extends the standard ERC20 interface. The other is the manage function from the Olympus treasury, which allows the ClearingHouse to interact with and pull tokens (given it has been permissioned to do so).

mininterfaces.sol, lines 6-12
interface IDelegateERC20 is IERC20 {
    function delegate(address to) external;
}

interface ITreasury {
    function manage(address token, uint256 amount) external;
}

The four other interfaces in /lib/ are easily verified as standard OpenZeppelin contracts. They are: context.sol, ERC20.sol, IERC20.sol, and IERC20Metadata.sol.

Cooler Lender Transfers

The Cooler allows a lender to transfer their ownership rights over a loan to a different address. This is accomplished with a approve/transfer scheme, akin to a push/pull model. First, the lender will approve an address to transfer the loan rights by calling approve() and passing in the new address and ID of the loan. This can only be called by the lender.

Cooler.sol, lines 203-211
/// @notice approve transfer of loan ownership to new address
function approve (address to, uint256 loanID) external {
    Loan memory loan = loans[loanID];

    if (msg.sender != loan.lender)
        revert OnlyApproved();

    approvals[loanID] = to;
}

After an approval has been executed, the approved address can now transfer the lending rights. They do this by calling the transfer() function and passing in the ID of the loan.

Cooler.sol, lines 213-220
/// @notice execute approved transfer of loan ownership
function transfer (uint256 loanID) external {
    if (msg.sender != approvals[loanID])
        revert OnlyApproved();

    approvals[loanID] = address(0);
    loans[loanID].lender = msg.sender;
}

ClearingHouse Role Transfers

The Clearing House has two roles: Operator and Overseer. Each can transfer their role to a new address (for, i.e., security purposes). This process is handled with a standard push/pull model to ensure safety.

Either role can call the push() function, passing in the intended new address for their role. That address will be assigned a pending role (but not yet set to the actual role!).

ClearingHouse.sol, lines 118-124
function push (address newAddress) external {
    if (msg.sender == overseer) 
        pendingOverseer = newAddress;
    else if (msg.sender == operator) 
        pendingOperator = newAddress;
    else revert OnlyApproved();
}

Once an address is set as pending, it can call the pull() function to be set as the actual role.

ClearingHouse.sol, lines 127-135
function pull () external {
    if (msg.sender == pendingOverseer) {
        overseer = pendingOverseer;
        pendingOverseer = address(0);
    } else if (msg.sender == pendingOperator) {
        operator = pendingOperator;
        pendingOperator = address(0);
    } else revert OnlyApproved();
}

Note that, because both roles share the same functions, small issues (though easily surmountable) may arise if the same address is set as both Overseer and Operator. However, this should not be done in the first place. Thus, because it is unlikely to occur, and it is recoverable even should it occur, this is not an area of concern.

Last updated