// SPDX-License-Identifier: BSD
pragma solidity ^0.8.4;
/// @title Clone
/// @author zefram.eth
/// @notice Provides helper functions for reading immutable args from calldata
contract Clone {
/// @notice Reads an immutable arg with type address
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgAddress(uint256 argOffset)
internal
pure
returns (address arg)
{
uint256 offset = _getImmutableArgsOffset();
assembly {
arg := shr(0x60, calldataload(add(offset, argOffset)))
}
}
/// @notice Reads an immutable arg with type uint256
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgUint256(uint256 argOffset)
internal
pure
returns (uint256 arg)
{
uint256 offset = _getImmutableArgsOffset();
// solhint-disable-next-line no-inline-assembly
assembly {
arg := calldataload(add(offset, argOffset))
}
}
/// @notice Reads an immutable arg with type uint64
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgUint64(uint256 argOffset)
internal
pure
returns (uint64 arg)
{
uint256 offset = _getImmutableArgsOffset();
// solhint-disable-next-line no-inline-assembly
assembly {
arg := shr(0xc0, calldataload(add(offset, argOffset)))
}
}
/// @notice Reads an immutable arg with type uint8
/// @param argOffset The offset of the arg in the packed data
/// @return arg The arg value
function _getArgUint8(uint256 argOffset) internal pure returns (uint8 arg) {
uint256 offset = _getImmutableArgsOffset();
// solhint-disable-next-line no-inline-assembly
assembly {
arg := shr(0xf8, calldataload(add(offset, argOffset)))
}
}
/// @return offset The offset of the packed immutable args in calldata
function _getImmutableArgsOffset() internal pure returns (uint256 offset) {
// solhint-disable-next-line no-inline-assembly
assembly {
offset := sub(
calldatasize(),
add(shr(240, calldataload(sub(calldatasize(), 2))), 2)
)
}
}
}
// SPDX-License-Identifier: BSD
pragma solidity ^0.8.4;
/// @title ClonesWithImmutableArgs
/// @author wighawag, zefram.eth
/// @notice Enables creating clone contracts with immutable args
library ClonesWithImmutableArgs {
error CreateFail();
/// @notice Creates a clone proxy of the implementation contract, with immutable args
/// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length
/// @param implementation The implementation contract to clone
/// @param data Encoded immutable args
/// @return instance The address of the created clone
function clone(address implementation, bytes memory data)
internal
returns (address instance)
{
// unrealistic for memory ptr or data length to exceed 256 bits
unchecked {
uint256 extraLength = data.length + 2; // +2 bytes for telling how much data there is appended to the call
uint256 creationSize = 0x43 + extraLength;
uint256 runSize = creationSize - 11;
uint256 dataPtr;
uint256 ptr;
// solhint-disable-next-line no-inline-assembly
assembly {
ptr := mload(0x40)
// -------------------------------------------------------------------------------------------------------------
// CREATION (11 bytes)
// -------------------------------------------------------------------------------------------------------------
// 3d | RETURNDATASIZE | 0 | –
// 61 runtime | PUSH2 runtime (r) | r 0 | –
mstore(
ptr,
0x3d61000000000000000000000000000000000000000000000000000000000000
)
mstore(add(ptr, 0x02), shl(240, runSize)) // size of the contract running bytecode (16 bits)
// creation size = 0b
// 80 | DUP1 | r r 0 | –
// 60 creation | PUSH1 creation (c) | c r r 0 | –
// 3d | RETURNDATASIZE | 0 c r r 0 | –
// 39 | CODECOPY | r 0 | [0-2d]: runtime code
// 81 | DUP2 | 0 c 0 | [0-2d]: runtime code
// f3 | RETURN | 0 | [0-2d]: runtime code
mstore(
add(ptr, 0x04),
0x80600b3d3981f300000000000000000000000000000000000000000000000000
)
// -------------------------------------------------------------------------------------------------------------
// RUNTIME
// -------------------------------------------------------------------------------------------------------------
// 36 | CALLDATASIZE | cds | –
// 3d | RETURNDATASIZE | 0 cds | –
// 3d | RETURNDATASIZE | 0 0 cds | –
// 37 | CALLDATACOPY | – | [0, cds] = calldata
// 61 | PUSH2 extra | extra | [0, cds] = calldata
mstore(
add(ptr, 0x0b),
0x363d3d3761000000000000000000000000000000000000000000000000000000
)
mstore(add(ptr, 0x10), shl(240, extraLength))
// 60 0x38 | PUSH1 0x38 | 0x38 extra | [0, cds] = calldata // 0x38 (56) is runtime size - data
// 36 | CALLDATASIZE | cds 0x38 extra | [0, cds] = calldata
// 39 | CODECOPY | _ | [0, cds] = calldata
// 3d | RETURNDATASIZE | 0 | [0, cds] = calldata
// 3d | RETURNDATASIZE | 0 0 | [0, cds] = calldata
// 3d | RETURNDATASIZE | 0 0 0 | [0, cds] = calldata
// 36 | CALLDATASIZE | cds 0 0 0 | [0, cds] = calldata
// 61 extra | PUSH2 extra | extra cds 0 0 0 | [0, cds] = calldata
mstore(
add(ptr, 0x12),
0x603836393d3d3d36610000000000000000000000000000000000000000000000
)
mstore(add(ptr, 0x1b), shl(240, extraLength))
// 01 | ADD | cds+extra 0 0 0 | [0, cds] = calldata
// 3d | RETURNDATASIZE | 0 cds 0 0 0 | [0, cds] = calldata
// 73 addr | PUSH20 0x123… | addr 0 cds 0 0 0 | [0, cds] = calldata
mstore(
add(ptr, 0x1d),
0x013d730000000000000000000000000000000000000000000000000000000000
)
mstore(add(ptr, 0x20), shl(0x60, implementation))
// 5a | GAS | gas addr 0 cds 0 0 0 | [0, cds] = calldata
// f4 | DELEGATECALL | success 0 | [0, cds] = calldata
// 3d | RETURNDATASIZE | rds success 0 | [0, cds] = calldata
// 82 | DUP3 | 0 rds success 0 | [0, cds] = calldata
// 80 | DUP1 | 0 0 rds success 0 | [0, cds] = calldata
// 3e | RETURNDATACOPY | success 0 | [0, rds] = return data (there might be some irrelevant leftovers in memory [rds, cds] when rds < cds)
// 90 | SWAP1 | 0 success | [0, rds] = return data
// 3d | RETURNDATASIZE | rds 0 success | [0, rds] = return data
// 91 | SWAP2 | success 0 rds | [0, rds] = return data
// 60 0x36 | PUSH1 0x36 | 0x36 sucess 0 rds | [0, rds] = return data
// 57 | JUMPI | 0 rds | [0, rds] = return data
// fd | REVERT | – | [0, rds] = return data
// 5b | JUMPDEST | 0 rds | [0, rds] = return data
// f3 | RETURN | – | [0, rds] = return data
mstore(
add(ptr, 0x34),
0x5af43d82803e903d91603657fd5bf30000000000000000000000000000000000
)
}
// -------------------------------------------------------------------------------------------------------------
// APPENDED DATA (Accessible from extcodecopy)
// (but also send as appended data to the delegatecall)
// -------------------------------------------------------------------------------------------------------------
extraLength -= 2;
uint256 counter = extraLength;
uint256 copyPtr = ptr + 0x43;
// solhint-disable-next-line no-inline-assembly
assembly {
dataPtr := add(data, 32)
}
for (; counter >= 32; counter -= 32) {
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(copyPtr, mload(dataPtr))
}
copyPtr += 32;
dataPtr += 32;
}
uint256 mask = ~(256**(32 - counter) - 1);
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(copyPtr, and(mload(dataPtr), mask))
}
copyPtr += counter;
// solhint-disable-next-line no-inline-assembly
assembly {
mstore(copyPtr, shl(240, extraLength))
}
// solhint-disable-next-line no-inline-assembly
assembly {
instance := create(0, ptr, creationSize)
}
if (instance == address(0)) {
revert CreateFail();
}
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {Clone} from "clones/Clone.sol";
import {CoolerFactory} from "./CoolerFactory.sol";
import {CoolerCallback} from "./CoolerCallback.sol";
// Function sig taken from gOHM contract
interface IDelegate { function delegate(address to_) external; }
/// @title Cooler Loans.
/// @notice A Cooler is a smart contract escrow that facilitates fixed-duration, peer-to-peer
/// loans for a user-defined debt-collateral pair.
/// @dev This contract uses Clones (https://github.com/wighawag/clones-with-immutable-args)
/// to save gas on deployment.
contract Cooler is Clone {
using SafeTransferLib for ERC20;
// --- ERRORS ----------------------------------------------------
error OnlyApproved();
error Deactivated();
error Default();
error NotExpired();
error NotCoolerCallback();
// --- DATA STRUCTURES -------------------------------------------
/// @notice A loan begins with a borrow request.
struct Request {
uint256 amount; // Amount to be borrowed.
uint256 interest; // Annualized percentage to be paid as interest.
uint256 loanToCollateral; // Requested loan-to-collateral ratio.
uint256 duration; // Time to repay the loan before it defaults.
bool active; // Any lender can clear an active loan request.
address requester; // The address that created the request.
}
/// @notice A request is converted to a loan when a lender clears it.
struct Loan {
Request request; // Loan terms specified in the request.
uint256 principal; // Amount of principal debt owed to the lender.
uint256 interestDue; // Interest owed to the lender.
uint256 collateral; // Amount of collateral pledged.
uint256 expiry; // Time when the loan defaults.
address lender; // Lender's address.
address recipient; // Recipient of repayments.
bool callback; // If this is true, the lender must inherit CoolerCallback.
}
// --- IMMUTABLES ------------------------------------------------
// This makes the code look prettier.
uint256 private constant DECIMALS_INTEREST = 1e18;
/// @notice This address owns the collateral in escrow.
function owner() public pure returns (address _owner) {
return _getArgAddress(0x0);
}
/// @notice This token is borrowed against.
function collateral() public pure returns (ERC20 _collateral) {
return ERC20(_getArgAddress(0x14));
}
/// @notice This token is lent.
function debt() public pure returns (ERC20 _debt) {
return ERC20(_getArgAddress(0x28));
}
/// @notice This contract created the Cooler
function factory() public pure returns (CoolerFactory _factory) {
return CoolerFactory(_getArgAddress(0x3c));
}
// --- STATE VARIABLES -------------------------------------------
/// @notice Arrays stores all the loan requests.
Request[] public requests;
/// @notice Arrays stores all the granted loans.
Loan[] public loans;
/// @notice Facilitates transfer of lender ownership to new addresses
mapping(uint256 => address) public approvals;
// --- BORROWER --------------------------------------------------
/// @notice Request a loan with given parameters.
/// Collateral is taken at time of request.
/// @param amount_ of debt tokens to borrow.
/// @param interest_ to pay (annualized % of 'amount_'). Expressed in DECIMALS_INTEREST.
/// @param loanToCollateral_ debt tokens per collateral token pledged. Expressed in 10**collateral().decimals().
/// @param duration_ of loan tenure in seconds.
/// @return reqID of the created request. Equivalent to the index of request in requests[].
function requestLoan(
uint256 amount_,
uint256 interest_,
uint256 loanToCollateral_,
uint256 duration_
) external returns (uint256 reqID) {
reqID = requests.length;
requests.push(
Request({
amount: amount_,
interest: interest_,
loanToCollateral: loanToCollateral_,
duration: duration_,
active: true,
requester: msg.sender
})
);
// The collateral is taken upfront. Will be escrowed
// until the loan is repaid or defaulted.
collateral().safeTransferFrom(
msg.sender,
address(this),
collateralFor(amount_, loanToCollateral_)
);
// Log the event.
factory().logRequestLoan(reqID);
}
/// @notice Cancel a loan request and get the collateral back.
/// @param reqID_ index of request in requests[].
function rescindRequest(uint256 reqID_) external {
if (msg.sender != owner()) revert OnlyApproved();
Request storage req = requests[reqID_];
if (!req.active) revert Deactivated();
// Update storage and send collateral back to the owner.
req.active = false;
collateral().safeTransfer(owner(), collateralFor(req.amount, req.loanToCollateral));
// Log the event.
factory().logRescindRequest(reqID_);
}
/// @notice Repay a loan to get the collateral back.
/// @dev Despite a malicious lender could reenter with the callback, the
/// usage of `msg.sender` prevents any economical benefit to the
/// attacker, since they would be repaying the loan themselves.
/// @param loanID_ index of loan in loans[].
/// @param repayment_ debt tokens to be repaid.
/// @return collateral given back to the borrower.
function repayLoan(uint256 loanID_, uint256 repayment_) external returns (uint256) {
Loan memory loan = loans[loanID_];
if (block.timestamp > loan.expiry) revert Default();
// Cap the repayment to the total debt of the loan
uint256 totalDebt = loan.principal + loan.interestDue;
if (repayment_ > totalDebt) repayment_ = totalDebt;
// Need to repay interest first, then any extra goes to paying down principal.
uint256 interestPaid;
uint256 remainder;
if (repayment_ >= loan.interestDue) {
remainder = repayment_ - loan.interestDue;
interestPaid = loan.interestDue;
loan.interestDue = 0;
} else {
loan.interestDue -= repayment_;
interestPaid = repayment_;
}
// We pay back only if user has paid back principal. This can be 0.
uint256 decollateralized;
if (remainder > 0) {
decollateralized = (loan.collateral * remainder) / loan.principal;
loan.principal -= remainder;
loan.collateral -= decollateralized;
}
// Save updated loan info in storage.
loans[loanID_] = loan;
// Transfer repaid debt back to the lender and collateral back to the owner if applicable
debt().safeTransferFrom(msg.sender, loan.recipient, repayment_);
if (decollateralized > 0) collateral().safeTransfer(owner(), decollateralized);
// Log the event.
factory().logRepayLoan(loanID_, repayment_);
// If necessary, trigger lender callback.
if (loan.callback) {
CoolerCallback(loan.lender).onRepay(loanID_, remainder, interestPaid);
}
return decollateralized;
}
/// @notice Delegate voting power on collateral.
/// @param to_ address to delegate.
function delegateVoting(address to_) external {
if (msg.sender != owner()) revert OnlyApproved();
IDelegate(address(collateral())).delegate(to_);
}
// --- LENDER ----------------------------------------------------
/// @notice Fill a requested loan as a lender.
/// @param reqID_ index of request in requests[].
/// @param recipient_ address to repay the loan to.
/// @param isCallback_ true if the lender implements the CoolerCallback abstract. False otherwise.
/// @return loanID of the granted loan. Equivalent to the index of loan in loans[].
function clearRequest(
uint256 reqID_,
address recipient_,
bool isCallback_
) external returns (uint256 loanID) {
Request memory req = requests[reqID_];
// Loan callbacks are only allowed if:
// 1. The loan request has been created via a trusted lender.
// 2. The lender signals that it implements the CoolerCallback Abstract.
bool callback = (isCallback_ && msg.sender == req.requester);
// If necessary, ensure lender implements the CoolerCallback abstract.
if (callback && !CoolerCallback(msg.sender).isCoolerCallback()) revert NotCoolerCallback();
// Ensure loan request is active.
if (!req.active) revert Deactivated();
// Clear the loan request in memory.
req.active = false;
// Calculate and store loan terms.
uint256 interest = interestFor(req.amount, req.interest, req.duration);
uint256 collat = collateralFor(req.amount, req.loanToCollateral);
loanID = loans.length;
loans.push(
Loan({
request: req,
principal: req.amount,
interestDue: interest,
collateral: collat,
expiry: block.timestamp + req.duration,
lender: msg.sender,
recipient: recipient_,
callback: callback
})
);
// Clear the loan request storage.
requests[reqID_].active = false;
// Transfer debt tokens to the owner of the request.
debt().safeTransferFrom(msg.sender, owner(), req.amount);
// Log the event.
factory().logClearRequest(reqID_, loanID);
}
/// @notice Allow lender to extend a loan for the borrower. Doesn't require
/// borrower permission because it doesn't have a negative impact for them.
/// @dev Since this function solely impacts the expiration day, the lender
/// should ensure that extension interest payments are done beforehand.
/// @param loanID_ index of loan in loans[].
/// @param times_ that the fixed-term loan duration is extended.
function extendLoanTerms(uint256 loanID_, uint8 times_) external {
Loan memory loan = loans[loanID_];
if (msg.sender != loan.lender) revert OnlyApproved();
if (block.timestamp > loan.expiry) revert Default();
// Update loan terms to reflect the extension.
loan.expiry += loan.request.duration * times_;
// Save updated loan info in storage.
loans[loanID_] = loan;
// Log the event.
factory().logExtendLoan(loanID_, times_);
}
/// @notice Claim collateral upon loan default.
/// @param loanID_ index of loan in loans[].
/// @return defaulted debt by the borrower, collateral kept by the lender, elapsed time since expiry.
function claimDefaulted(uint256 loanID_) external returns (uint256, uint256, uint256, uint256) {
Loan memory loan = loans[loanID_];
if (block.timestamp <= loan.expiry) revert NotExpired();
loans[loanID_].principal = 0;
loans[loanID_].interestDue = 0;
loans[loanID_].collateral = 0;
// Transfer defaulted collateral to the lender.
collateral().safeTransfer(loan.lender, loan.collateral);
// Log the event.
factory().logDefaultLoan(loanID_, loan.collateral);
// If necessary, trigger lender callback.
if (loan.callback) {
CoolerCallback(loan.lender).onDefault(loanID_, loan.principal, loan.interestDue, loan.collateral);
}
return (loan.principal, loan.interestDue, loan.collateral, block.timestamp - loan.expiry);
}
/// @notice Approve transfer of loan ownership rights to a new address.
/// @param to_ address to be approved.
/// @param loanID_ index of loan in loans[].
function approveTransfer(address to_, uint256 loanID_) external {
if (msg.sender != loans[loanID_].lender) revert OnlyApproved();
// Update transfer approvals.
approvals[loanID_] = to_;
}
/// @notice Execute loan ownership transfer. Must be previously approved by the lender.
/// @param loanID_ index of loan in loans[].
function transferOwnership(uint256 loanID_) external {
if (msg.sender != approvals[loanID_]) revert OnlyApproved();
// Update the load lender and the recipient.
loans[loanID_].lender = msg.sender;
loans[loanID_].recipient = msg.sender;
// Callbacks are disabled when transferring ownership.
loans[loanID_].callback = false;
// Clear transfer approvals.
approvals[loanID_] = address(0);
}
/// @notice Allow lender to set repayment recipient of a given loan.
/// @param loanID_ of lender's loan.
/// @param recipient_ reciever of repayments
function setRepaymentAddress(uint256 loanID_, address recipient_) external {
if (msg.sender != loans[loanID_].lender) revert OnlyApproved();
// Update the repayment method.
loans[loanID_].recipient = recipient_;
}
// --- AUX FUNCTIONS ---------------------------------------------
/// @notice Compute collateral needed for a desired loan amount at given loan to collateral ratio.
/// @param principal_ amount of debt tokens.
/// @param loanToCollateral_ ratio for loan. Expressed in 10**collateral().decimals().
function collateralFor(uint256 principal_, uint256 loanToCollateral_) public view returns (uint256) {
return (principal_ * (10 ** collateral().decimals())) / loanToCollateral_;
}
/// @notice Compute interest cost on amount for duration at given annualized rate.
/// @param principal_ amount of debt tokens.
/// @param rate_ of interest (annualized).
/// @param duration_ of the loan in seconds.
/// @return Interest in debt token terms.
function interestFor(uint256 principal_, uint256 rate_, uint256 duration_) public pure returns (uint256) {
uint256 interest = (rate_ * duration_) / 365 days;
return (principal_ * interest) / DECIMALS_INTEREST;
}
/// @notice Check if given loan has expired.
/// @param loanID_ index of loan in loans[].
/// @return Expiration status.
function hasExpired(uint256 loanID_) external view returns (bool) {
return block.timestamp > loans[loanID_].expiry;
}
/// @notice Check if a given request is active.
/// @param reqID_ index of request in requests[].
/// @return Active status.
function isActive(uint256 reqID_) external view returns (bool) {
return requests[reqID_].active;
}
/// @notice Getter for Request data as a struct.
/// @param reqID_ index of request in requests[].
/// @return Request struct.
function getRequest(uint256 reqID_) external view returns (Request memory) {
return requests[reqID_];
}
/// @notice Getter for Loan data as a struct.
/// @param loanID_ index of loan in loans[].
/// @return Loan struct.
function getLoan(uint256 loanID_) external view returns (Loan memory) {
return loans[loanID_];
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {CoolerFactory} from "./CoolerFactory.sol";
/// @notice Allows for debt issuers to execute logic when a loan is repaid, rolled, or defaulted.
/// @dev The three callback functions must be implemented if `isCoolerCallback()` is set to true.
abstract contract CoolerCallback {
// --- ERRORS ----------------------------------------------------
error OnlyFromFactory();
// --- INITIALIZATION --------------------------------------------
CoolerFactory public immutable factory;
constructor(address coolerFactory_) {
factory = CoolerFactory(coolerFactory_);
}
// --- EXTERNAL FUNCTIONS ------------------------------------------------
/// @notice Informs to Cooler that this contract can handle its callbacks.
function isCoolerCallback() external pure returns (bool) {
return true;
}
/// @notice Callback function that handles repayments.
function onRepay(uint256 loanID_, uint256 principlePaid_, uint256 interestPaid_) external {
if (!factory.created(msg.sender)) revert OnlyFromFactory();
_onRepay(loanID_, principlePaid_, interestPaid_);
}
/// @notice Callback function that handles defaults.
function onDefault(
uint256 loanID_,
uint256 principle,
uint256 interest,
uint256 collateral
) external {
if (!factory.created(msg.sender)) revert OnlyFromFactory();
_onDefault(loanID_, principle, interest, collateral);
}
// --- INTERNAL FUNCTIONS ------------------------------------------------
/// @notice Callback function that handles repayments. Override for custom logic.
function _onRepay(
uint256 loanID_,
uint256 principlePaid_,
uint256 interestPaid_
) internal virtual;
/// @notice Callback function that handles defaults.
function _onDefault(
uint256 loanID_,
uint256 principle_,
uint256 interestDue_,
uint256 collateral
) internal virtual;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import {ERC20} from "solmate/tokens/ERC20.sol";
import {ClonesWithImmutableArgs} from "clones/ClonesWithImmutableArgs.sol";
import {Cooler} from "./Cooler.sol";
/// @title Cooler Loans Factory.
/// @notice The Cooler Factory creates new Cooler escrow contracts.
/// @dev This contract uses Clones (https://github.com/wighawag/clones-with-immutable-args)
/// to save gas on deployment.
contract CoolerFactory {
using ClonesWithImmutableArgs for address;
// --- ERRORS ----------------------------------------------------
error NotFromFactory();
error DecimalsNot18();
// --- EVENTS ----------------------------------------------------
/// @notice A global event when a new loan request is created.
event RequestLoan(address indexed cooler, address collateral, address debt, uint256 reqID);
/// @notice A global event when a loan request is rescinded.
event RescindRequest(address indexed cooler, uint256 reqID);
/// @notice A global event when a loan request is fulfilled.
event ClearRequest(address indexed cooler, uint256 reqID, uint256 loanID);
/// @notice A global event when a loan is repaid.
event RepayLoan(address indexed cooler, uint256 loanID, uint256 amount);
/// @notice A global event when a loan is extended.
event ExtendLoan(address indexed cooler, uint256 loanID, uint8 times);
/// @notice A global event when the collateral of defaulted loan is claimed.
event DefaultLoan(address indexed cooler, uint256 loanID, uint256 amount);
// -- STATE VARIABLES --------------------------------------------
/// @notice Cooler reference implementation (deployed on creation to clone from).
Cooler public immutable coolerImplementation;
/// @notice Mapping to validate deployed coolers.
mapping(address => bool) public created;
/// @notice Mapping to prevent duplicate coolers.
mapping(address => mapping(ERC20 => mapping(ERC20 => address))) private coolerFor;
/// @notice Mapping to query Coolers for Collateral-Debt pair.
mapping(ERC20 => mapping(ERC20 => address[])) public coolersFor;
// --- INITIALIZATION --------------------------------------------
constructor() {
coolerImplementation = new Cooler();
}
// --- DEPLOY NEW COOLERS ----------------------------------------
/// @notice creates a new Escrow contract for collateral and debt tokens.
/// @param collateral_ the token given as collateral.
/// @param debt_ the token to be lent. Interest is denominated in debt tokens.
/// @return cooler address of the contract.
function generateCooler(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)) {
if (collateral_.decimals() != 18 || debt_.decimals() != 18) revert DecimalsNot18();
// Clone the cooler implementation.
bytes memory coolerData = abi.encodePacked(
msg.sender, // owner
address(collateral_), // collateral
address(debt_), // debt
address(this) // factory
);
cooler = address(coolerImplementation).clone(coolerData);
// Update storage accordingly.
coolerFor[msg.sender][collateral_][debt_] = cooler;
coolersFor[collateral_][debt_].push(cooler);
created[cooler] = true;
}
}
// --- EMIT EVENTS -----------------------------------------------
/// @notice Ensure that the called is a Cooler.
modifier onlyFromFactory {
if (!created[msg.sender]) revert NotFromFactory();
_;
}
/// @notice Emit a global event when a new loan request is created.
function logRequestLoan(uint256 reqID_) external onlyFromFactory {
emit RequestLoan(msg.sender, address(Cooler(msg.sender).collateral()), address(Cooler(msg.sender).debt()), reqID_);
}
/// @notice Emit a global event when a loan request is rescinded.
function logRescindRequest(uint256 reqID_) external onlyFromFactory {
emit RescindRequest(msg.sender, reqID_);
}
/// @notice Emit a global event when a loan request is fulfilled.
function logClearRequest(uint256 reqID_, uint256 loanID_) external onlyFromFactory {
emit ClearRequest(msg.sender, reqID_, loanID_);
}
/// @notice Emit a global event when a loan is repaid.
function logRepayLoan(uint256 loanID_, uint256 repayment_) external onlyFromFactory {
emit RepayLoan(msg.sender, loanID_, repayment_);
}
/// @notice Emit a global event when a loan is extended.
function logExtendLoan(uint256 loanID_, uint8 times_) external onlyFromFactory {
emit ExtendLoan(msg.sender, loanID_, times_);
}
/// @notice Emit a global event when the collateral of defaulted loan is claimed.
function logDefaultLoan(uint256 loanID_, uint256 collateral_) external onlyFromFactory {
emit DefaultLoan(msg.sender, loanID_, collateral_);
}
// --- AUX FUNCTIONS ---------------------------------------------
/// @notice Getter function to get an existing cooler for a given user <> collateral <> debt combination.
function getCoolerFor(address user_, address collateral_, address debt_) public view returns (address) {
return coolerFor[user_][ERC20(collateral_)][ERC20(debt_)];
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "../tokens/ERC20.sol";
/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol)
/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer.
/// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller.
library SafeTransferLib {
/*//////////////////////////////////////////////////////////////
ETH OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferETH(address to, uint256 amount) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Transfer the ETH and store if it succeeded or not.
success := call(gas(), to, amount, 0, 0, 0, 0)
}
require(success, "ETH_TRANSFER_FAILED");
}
/*//////////////////////////////////////////////////////////////
ERC20 OPERATIONS
//////////////////////////////////////////////////////////////*/
function safeTransferFrom(
ERC20 token,
address from,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument.
mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 100, 0, 32)
)
}
require(success, "TRANSFER_FROM_FAILED");
}
function safeTransfer(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "TRANSFER_FAILED");
}
function safeApprove(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "APPROVE_FAILED");
}
}
{
"compilationTarget": {
"lib/Cooler/src/CoolerFactory.sol": "CoolerFactory"
},
"evmVersion": "london",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 10
},
"remappings": [
":@openzeppelin/=lib/openzeppelin-contracts/",
":Cooler/=lib/Cooler/src/",
":balancer-v2/=lib/balancer-v2/",
":bonds/=lib/bonds/src/",
":clones-with-immutable-args/=lib/clones-with-immutable-args/src/",
":clones/=lib/clones-with-immutable-args/src/",
":cooler/=lib/Cooler/src/",
":ds-test/=lib/ds-test/src/",
":forge-std/=lib/forge-std/src/",
":interfaces/=src/interfaces/",
":layer-zero/=lib/solidity-examples/contracts/",
":libraries/=src/libraries/",
":modules/=src/modules/",
":olympus-v3/=lib/Cooler/lib/olympus-v3/src/",
":openzeppelin-contracts/=lib/openzeppelin-contracts/",
":policies/=src/policies/",
":solidity-examples/=lib/solidity-examples/contracts/",
":solmate/=lib/solmate/src/",
":test/=src/test/"
]
}
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"CreateFail","type":"error"},{"inputs":[],"name":"DecimalsNot18","type":"error"},{"inputs":[],"name":"NotFromFactory","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"cooler","type":"address"},{"indexed":false,"internalType":"uint256","name":"reqID","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"loanID","type":"uint256"}],"name":"ClearRequest","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"cooler","type":"address"},{"indexed":false,"internalType":"uint256","name":"loanID","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DefaultLoan","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"cooler","type":"address"},{"indexed":false,"internalType":"uint256","name":"loanID","type":"uint256"},{"indexed":false,"internalType":"uint8","name":"times","type":"uint8"}],"name":"ExtendLoan","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"cooler","type":"address"},{"indexed":false,"internalType":"uint256","name":"loanID","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"RepayLoan","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"cooler","type":"address"},{"indexed":false,"internalType":"address","name":"collateral","type":"address"},{"indexed":false,"internalType":"address","name":"debt","type":"address"},{"indexed":false,"internalType":"uint256","name":"reqID","type":"uint256"}],"name":"RequestLoan","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"cooler","type":"address"},{"indexed":false,"internalType":"uint256","name":"reqID","type":"uint256"}],"name":"RescindRequest","type":"event"},{"inputs":[],"name":"coolerImplementation","outputs":[{"internalType":"contract Cooler","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract ERC20","name":"","type":"address"},{"internalType":"contract ERC20","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"coolersFor","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"created","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract ERC20","name":"collateral_","type":"address"},{"internalType":"contract ERC20","name":"debt_","type":"address"}],"name":"generateCooler","outputs":[{"internalType":"address","name":"cooler","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user_","type":"address"},{"internalType":"address","name":"collateral_","type":"address"},{"internalType":"address","name":"debt_","type":"address"}],"name":"getCoolerFor","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"reqID_","type":"uint256"},{"internalType":"uint256","name":"loanID_","type":"uint256"}],"name":"logClearRequest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"loanID_","type":"uint256"},{"internalType":"uint256","name":"collateral_","type":"uint256"}],"name":"logDefaultLoan","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"loanID_","type":"uint256"},{"internalType":"uint8","name":"times_","type":"uint8"}],"name":"logExtendLoan","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"loanID_","type":"uint256"},{"internalType":"uint256","name":"repayment_","type":"uint256"}],"name":"logRepayLoan","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"reqID_","type":"uint256"}],"name":"logRequestLoan","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"reqID_","type":"uint256"}],"name":"logRescindRequest","outputs":[],"stateMutability":"nonpayable","type":"function"}]