Lender/ClearingHouse Flow
This page breaks down the code interaction flow for Cooler lenders in depth.
Clearing a Loan
The clearance flow begins with a pending loan request. This can be found on a given Cooler, and is stored in a Request struct, which looks like this:
struct Request { // A loan begins with a borrow request. It specifies:
uint256 amount; // the amount they want to borrow,
uint256 interest; // the percentage they will pay as interest,
uint256 loanToCollateral; // the loan-to-collateral ratio they want,
uint256 duration; // and the length of time until the loan defaults.
bool active; // Any lender can clear an active loan request.
}
The operator first checks the amount of debt tokens requested to borrow by looking at the 'amount' field. The number of collateral tokens required is checked with simple arithmetic 'amount' divided by 'loanToCollateral' -- this can be computed with the collateralFor() view function, or by viewing the number of collateral tokens transferred to the Cooler when the loan request was made. The loan tenure is viewable, in seconds, on the 'duration' field. The APR of the loan is viewable in the 'interest' field. The loan can only be cleared if 'active' is true.
Given loan terms are amicable, the Operator will call the clear() method on the ClearingHouse, specifying the address of the Cooler and the ID of the request.
function clear (Cooler cooler, uint256 id) external returns (uint256) {}
The first check done by this function ensures that the address calling it is, in fact, the Operator. If it is not, the call will revert immediately.
if (msg.sender != operator)
revert OnlyApproved();
Next, the validity of the Cooler is checked. The Cooler must be generated by the Cooler Factory to ensure there is no unexpected and/or malicious logic in the lending contract. It also checks that the collateral token is gOHM, and the debt token is DAI. Either of these conditions being untrue will revert execution.
// Validate escrow
if (!factory.created(address(cooler)))
revert OnlyFromFactory();
if (cooler.collateral() != gOHM || cooler.debt() != dai)
revert BadEscrow();
Next, the contract queries the terms of the request. This is a Request struct, as discussed at the top of this page. Note that we ignore the 'active' field, because the function will simply revert if the loan request is not active.
(
uint256 amount,
uint256 interest,
uint256 ltc,
uint256 duration,
) = cooler.requests(id);
Now that we have the requested terms, we can validate them. We first check that interest is greater than the minimum interest that can be charged; next, that the loanToCollateral is less than the maximum that can be lent; and finally, that the duration is less than the longest possible tenure.
// Validate terms
if (interest < minimumInterest)
revert InterestMinimum();
if (ltc > maxLTC)
revert LTCMaximum();
if (duration > maxDuration)
revert DurationMaximum();
Note that each of these bounds ('minimumInterest', 'maxLTC', and 'maxDuration') are immutable and set on the contract level (i.e. not in the constructor). The following are not finalized, but show what this looks like.
// Parameter Bounds
uint256 public constant minimumInterest = 2e16; // 2%
uint256 public constant maxLTC = 2_500 * 1e18; // 2,500
uint256 public constant maxDuration = 365 days; // 1 year
Now that the terms have been validated as within bounds, we can clear the loan. Execution from this point moves to the Cooler.sol contract. First, the cooler is approved to transfer the loan amount, then the clear() function is called for the request ID. The ID of the newly created loan is returned.
// Clear loan
dai.approve(address(cooler), amount);
return cooler.clear(id);
Calling cooler.clear(id) executes logic within the Cooler itself.
function clear (uint256 reqID) external returns (uint256 loanID) {}
First, the Request data is queried and pulled into storage. Note that because this is storage and not memory, any mutated data will persist on the EVM. So, we need to be extra aware of what data is being changed. The only data that is changed is the req.active variable, which is set to false now that the request is being filled. Failing to do so would allow multiple loans against the same collateral, or allow the borrower to take back their collateral after the loan has been given by calling rescind(). We do not want either of these to happen and thus gate against it. We also emit a 'clear' event on the factory.
Request storage req = requests[reqID];
factory.newEvent(reqID, CoolerFactory.Events.Clear);
if (!req.active)
revert Deactivated();
else req.active = false;
Next, we will calculate the interest due upon repayment of the loan. We pass in the loan amount, interest rate, and loan duration.
uint256 interest = interestFor(req.amount, req.interest, req.duration);
This takes us to a view function that does the actual math. First, we convert the annualized interest rate to the percentage paid for the loan duration. Next, we calculate the nominal interest for the loan. As an example, if 1000 DAI is being borrowed at 4% for 182.5 days: the 'interest' variable will be assigned 4% * 182.5 / 365 = 2%; the return value will be 1000 * 2% = 20 (with decimal adjustment -- an EVM thing).
/// @notice compute interest cost on amount for duration at given annualized rate
function interestFor(uint256 amount, uint256 rate, uint256 duration) public pure returns (uint256) {
uint256 interest = rate * duration / 365 days;
return amount * interest / decimals;
}
Next, we will calculate the collateral needed for the loan. Again, this takes us to a view function, which we pass the loan amount and loan to collateral ratio.
uint256 collat = collateralFor(req.amount, req.loanToCollateral);
This function is quite simple, and just divides amount by loan to collateral (adjusting for decimals). For example, if loan amount is 1000 and loan to collateral is 2000 DAI per gOHM, the math is: 1000 / 2000 = 0.5.
/// @notice compute collateral needed for loan amount at given loan to collateral ratio
function collateralFor(uint256 amount, uint256 loanToCollateral) public pure returns (uint256) {
return amount * decimals / loanToCollateral;
}
Finally, we compute the expiration timestamp, which is as simple as adding the loan duration to the current time.
uint256 expiration = block.timestamp + req.duration;
Now that we have all the data we need to construct the loan, we will do just that. First, we store the current length of the user's loans array as our loanID, since this is the index that the new loan will populate. Next, we push the loan data to that array. This data is: the request info, the loan amount plus interest (which is what they now owe), the collateral they added, the expiration timestamp, whether the loan can be rolled (defaulted to true), and who the lender is (the person calling the function aka the message sender).
loanID = loans.length;
loans.push(
Loan(req, req.amount + interest, collat, expiration, true, msg.sender)
);
Finally, we transfer the debt tokens from the lender (the message sender) to the borrower (the contract owner). Note that only req.amount is sent, but req.amount + interest is what is stored as owed. This is because interest is added to the debt when it comes to repayment.
debt.transferFrom(msg.sender, owner, req.amount);
That concludes the loan clearance flow for the clearing house!
Default Scenario
In the case of a default, anyone can send the borrower's collateral to the lender. This is done by calling the defaulted() function and passing in the ID of the loan in default. Let's see what happens:
function defaulted (uint256 loanID) external returns (uint256) {}
First, the loan is pulled into memory. We don't need to edit any of this info, so we use memory to save some gas. We immediately delete the loan from its array, since it is no longer active.
Loan memory loan = loans[loanID];
delete loans[loanID];
Next, we check that the expiry timestamp is not greater than the current time (aka expiration is in the future). If it is, that means the loan has not defaulted yet and we should revert!
if (block.timestamp <= loan.expiry)
revert NoDefault();
Finally, we transfer the loan collateral to the lender. We return the collateral amount because it might be useful to a smart contract automating this process.
collateral.transfer(loan.lender, loan.collateral);
return loan.collateral;
Rollover
Loans can be rolled by default, but this can be prevented by the lender. Let's see how:
First, the Operator calls toggleRoll() on the clearing house, passing in the cooler where the loan exists, and the ID of the loan.
/// @notice toggle 'rollable' status of a loan
function toggleRoll(Cooler cooler, uint256 loanID) external {
if (msg.sender != operator)
revert OnlyApproved();
cooler.toggleRoll(loanID);
}
We first check that the address calling the function is the operator, and revert if they are not. Next, we call the toggleRoll() function of the cooler itself, passing in the ID of the loan.
function toggleRoll(uint256 loanID) external returns (bool) {
Loan storage loan = loans[loanID];
if (msg.sender != loan.lender)
revert OnlyApproved();
loan.rollable = !loan.rollable;
return loan.rollable;
}
On the Cooler, we first pull the loan data into storage (again, we use storage here because we will mutate some data). Next, we check that it is the lender of the loan calling the function; if not, we will revert. Next, we will set the rollable status to the opposite of its previous value (if it was true, its now false; if it was false, its now true). Finally, we return the new value.
Funding, Defunding
Funding can only be done by the Overseer, and only after the ClearingHouse has been given the manager permission on the treasury. Thus, there are two security measures in place to safeguard treasury funds and ensure only the allocated amount can be lent.
This function simply checks that the caller is the overseer, then transfers the alloted amount in from the treasury using the manage() function.
/// @notice pull funding from treasury
function fund (uint256 amount) external {
if (msg.sender != overseer)
revert OnlyApproved();
ITreasury(treasury).manage(address(dai), amount);
}
The contract can be defunded by the Operator or Overseer. This function sends funds back to the treasury. First, it checks to ensure the caller is one of these addresses, then sends the passed in token with given amount to the treasury. Note that it can only transfer to the treasury. There is no trust relationship about where funds are sent.
/// @notice return funds to treasury
/// @param token to transfer
/// @param amount to transfer
function defund (ERC20 token, uint256 amount) external {
if (msg.sender != operator && msg.sender != overseer)
revert OnlyApproved();
token.transfer(treasury, amount);
}
This concludes the interaction flow for the Clearing House.
Last updated