Borrower Flow
This page breaks down the code interaction flow for Cooler borrowers in depth.
Creating a Cooler
The borrow flow begins by creating a Cooler. This is the only time that the Cooler Factory is interacted with by a user. To do this, we simply call the generate() function and pass in the token we desire to borrow, and the collateral we intend to use.
(Quick author's note before we continue: this next paragraph is probably the worst written of the whole walkthrough. If it has you confused, I'd suggest just moving on and coming back to it later!)
First, we assign the return address to the current value of our internal mapping keeping track of all coolers for user, collateral, and debt token. If a cooler exists, it will be assigned here, and the following if statement will not be triggered. If a cooler does not exist, this means we need to create one! We do this by generating a new Cooler for the user (message sender), collateral and debt tokens. Next, we will store this in our mapping (so that next time, the if statement will come back false). Finally, we push the cooler address to our 'coolersFor' public mapping (used for front end purposes), and mark down that the factory created this address (used for verification purposes). Now we have our Cooler and are ready to go!
/// @notice creates a new Escrow contract for collateral and debt tokens
function generate (ERC20 collateral, ERC20 debt) external returns (address cooler) {
// Return address if cooler exists
cooler = coolerFor[msg.sender][collateral][debt];
// Otherwise generate new cooler
if (cooler == address(0)) {
cooler = address(new Cooler(msg.sender, collateral, debt));
coolerFor[msg.sender][collateral][debt] = cooler;
coolersFor[collateral][debt].push(cooler);
created[cooler] = true;
}
}
Requesting a Loan
Creating a loan request is quite simple. We pass in the following information to the request() function: the amount of debt tokens to borrow, the annualized interest rate to pay, the loan to collateral ratio to collateralize at, and the duration of the loan in seconds.
function request (
uint256 amount,
uint256 interest,
uint256 loanToCollateral,
uint256 duration
) external returns (uint256 reqID) {}
The function first assigns our request ID return variable to the next available index in the requests[] array. It then emits a 'request' event on the factory and pushes a new Request to the requests[] array with the data we passed in, marking the 'active' status of the request as true.
reqID = requests.length;
factory.newEvent(reqID, CoolerFactory.Events.Request);
requests.push(
Request(amount, interest, loanToCollateral, duration, true)
);
Finally, we transfer the collateral needed for the loan from the message sender into the Cooler. Note that anyone can call this function. This is by design, and because the only addresses that can reclaim that collateral are the Cooler owner, or the lender of a loan (in case of default). This means there is no window for malicious action here. We can assume that a third party creating a request for another borrower is either due to affiliation or philanthropy. Both are just fine.
collateral.transferFrom(msg.sender, address(this), collateralFor(amount, loanToCollateral));
To calculate the amount of collateral needed, a public view function is called.
/// @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;
}
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.
Rescinding a Request
Let's say you've made a loan request, but have changed your mind. So long as the loan has not been cleared by a lender, you can simply take it back by calling the rescind() function and passing in the ID of the request.
function rescind (uint256 reqID) external {}
This will first check that you are the owner of the Cooler. Then, given you are, emit a 'rescind' event on the factory and pull the request data into storage (because we will be mutating it).
if (msg.sender != owner)
revert OnlyApproved();
factory.newEvent(reqID, CoolerFactory.Events.Rescind);
Request storage req = requests[reqID];
Next, we check that the loan request is active. This is very important. When a loan is cleared or rescinded, the active status is set to false. Checking this, and reverting if false, ensures the borrower cannot withdraw collateral for an active loan, or double-spend collateral from an already-rescinded request.
if (!req.active)
revert Deactivated();
Finally, we will set that 'active' status to false (to prevent that aforementioned double-spend!), then return the collateral to the borrower.
req.active = false;
collateral.transfer(owner, collateralFor(req.amount, req.loanToCollateral));
Repaying a Loan
Repaying a loan is as simple as calling the repay() function and passing in the ID of the loan and the amount of debt tokens to repay. Note that anyone can call this function. If you're not the borrower, and just want to do some charity, so be it! At the end, you will see that released collateral gets sent to the Cooler owner and not the message sender; thus, this presents no security concern.
function repay (uint256 loanID, uint256 repaid) external {}
First, we pull the loan data into storage (because we will be changing some data. That said, using storage means we need to be extra diligent with how we use this data! That's a common mistake devs make, and we will not be making it here!). Next, we validate that the loan has not expired (by checking to ensure the current time is before the expiration time).
Loan storage loan = loans[loanID];
if (block.timestamp > loan.expiry)
revert Default();
Next, we need to calculate how much collateral to return to the borrower. We do this by multiplying the total collateral by the percent of debt repaid. For example, if the loan has 1 gOHM collateral, 2000 DAI debt and 1000 DAI is repaid, the computation is: 1 * 1000 / 2000 = 0.5. Half the debt is repaid and half the collateral is returned, so this makes sense.
uint256 decollateralized = loan.collateral * repaid / loan.amount;
Next, we need to edit the loan information. If the entire debt was repaid, we can delete the loan altogether. If not, we will subtract the repaid amount from outstanding debt, and the returned collateral from the loan collateral.
if (repaid == loan.amount) delete loans[loanID];
else {
loan.amount -= repaid;
loan.collateral -= decollateralized;
}
Finally, we will send the repaid debt tokens to the lender, and returned collateral tokens to the Cooler owner. You can review the comments at the beginning of this section if you are confused about why the address transferring debt tokens, and the address receiving collateral tokens, are different.
debt.transferFrom(msg.sender, loan.lender, repaid);
collateral.transfer(owner, decollateralized);
Rolling a Loan
Given the lender has not prevented it, the borrower can roll over their loan with previous terms for another tenure. They do this by calling the roll() function, passing in the ID of the loan. Note that no amount is given: if a borrower only wants to roll a portion of the debt, they should pay down the loan to that portion before calling this function.
function roll (uint256 loanID) external {}
First, we will pull the loan data into storage, and the request data that originated the loan into memory. We only pull the loan data into storage because this is the only one with data that will be mutated, and using storage is more expensive.
Loan storage loan = loans[loanID];
Request memory req = loan.request;
Next, we need to check that the loan has not defaulted, and that the loan can be rolled.
if (block.timestamp > loan.expiry)
revert Default();
if (!loan.rollable)
revert NotRollable();
Next, we will compute the new collateral that must be added, and the new interest accrued. We do this using the previously discussed collateralFor() function to calculate the total collateral needed for the current debt (which includes the accrued interest that was not accounted for when the loan was first cleared), and subtracting the current collateral balance. Next, we calculate the new interest added using the interestFor() function (which is reviewed in the Lender Flow page).
uint256 newCollateral = collateralFor(loan.amount, req.loanToCollateral) - loan.collateral;
uint256 newDebt = interestFor(loan.amount, req.interest, req.duration);
Next, we add newDebt to our debt field, newCollateral to our collateral field, and loan duration to the expiry timestamp field. Recall that the loan data is being used in storage, so this directly mutates it on chain.
loan.amount += newDebt;
loan.expiry += req.duration;
loan.collateral += newCollateral;
Our final step is to transfer new collateral into the Cooler, ensuring the loan is properly collateralized for the remainder of its tenure.
collateral.transferFrom(msg.sender, address(this), newCollateral);
Delegation
A Cooler owner can delegate votes if their collateral conforms to the Compound/OpenZeppelin delegation interface. They do this by simply calling the delegate() function and passing in the address they wish to delegate to. The function will check that the message sender is the Cooler owner, then call the same delegate() function on the collateral token, passing in the same address.
function delegate (address to) external {
if (msg.sender != owner)
revert OnlyApproved();
IDelegateERC20(address(collateral)).delegate(to);
}
It is very important to note that the collateral token could have a callback here that presents a security vulnerability! In the case of most tokens, including gOHM, this is not the case; however the Cooler protocol is permissionless and extensible. It is up to the lender to ensure that they are writing loans against good collateral. A simple suggestion is to check that the delegate() function on the collateral token does not, for example, transfer the tokens to the 'to' address.
Last updated