// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"@solmate/auth/Owned.sol";
import"@solmate/utils/ReentrancyGuard.sol";
import"./InputChecker.sol";
/// @title AddressManager/// @notice A contract that handles a whitelist of addresses and their indexes./// @dev We assume no more than 65535 addresses will be added to the directory.contractAddressManagerisOwned, ReentrancyGuard{
usingInputCheckerforaddress;
eventAddressAdded(address address_added);
eventAddressRemovedFromWhitelist(address address_removed);
eventAddressWhitelisted(address address_whitelisted);
errorAddressAlreadyAddedError(address _address);
errorAddressNotAddedError(address _address);
mapping(address=>uint16) private _directory;
mapping(uint16=>address) private _inverseDirectory;
mapping(address=>bool) private _whitelist;
uint16private _lastAdded;
constructor(address[] memory _original) Owned(tx.origin) {
uint256 total = _original.length;
for (uint256 i; i < total;) {
_add(_original[i]);
unchecked {
++i;
}
}
}
/// @notice Adds an address to the directory. If it already exists,/// reverts. It assumes it's whitelisted./// @param _entry The address to add./// @return The index of the address in the directory.functionadd(address _entry) externalpayableonlyOwnerreturns (uint16) {
return _add(_entry);
}
/// @notice Whitelist an address that's already part of the directory./// @param _entry The address to whitelist.functionaddToWhitelist(address _entry) externalpayableonlyOwner{
if (_directory[_entry] ==0) {
revert AddressNotAddedError(_entry);
}
_whitelist[_entry] =true;
emit AddressWhitelisted(_entry);
}
/// @notice Removes an address from the whitelist. We still keep it/// in the directory since this mapping is relevant across time./// @param _entry The address to remove from the whitelist.functionremoveFromWhitelist(address _entry) externalpayableonlyOwner{
_whitelist[_entry] =false;
emit AddressRemovedFromWhitelist(_entry);
}
/// @param _address The address to get the index for./// @return The index for a given address.functionaddressToIndex(address _address) externalviewreturns (uint16) {
return _directory[_address];
}
/// @param _index The index to get the address for./// @return The address for a given index.functionindexToAddress(uint16 _index) externalviewreturns (address) {
return _inverseDirectory[_index];
}
/// @param _entry The address to check if it's whitelisted./// @return Whether the address is whitelisted or not.functionisWhitelisted(address _entry) externalviewreturns (bool) {
return _whitelist[_entry];
}
function_add(address _entry) privatereturns (uint16) {
_entry.checkNotZero();
if (_directory[_entry] !=0) {
revert AddressAlreadyAddedError(_entry);
}
unchecked {
++_lastAdded;
}
_directory[_entry] = _lastAdded;
_inverseDirectory[_lastAdded] = _entry;
_whitelist[_entry] =true;
emit AddressAdded(_entry);
return _lastAdded;
}
}
Contract Source Code
File 2 of 35: BaseLoan.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"@openzeppelin/utils/cryptography/MessageHashUtils.sol";
import"@openzeppelin/interfaces/IERC1271.sol";
import"@solmate/auth/Owned.sol";
import"@solmate/tokens/ERC721.sol";
import"@solmate/utils/FixedPointMathLib.sol";
import"../../interfaces/loans/IBaseLoan.sol";
import"../utils/Hash.sol";
import"../AddressManager.sol";
import"../LiquidationHandler.sol";
/// @title BaseLoan/// @author Florida St/// @notice Base implementation that we expect all loans to share. Offers can either be/// for new loans or renegotiating existing ones./// Offers are signed off-chain./// Offers have a nonce associated that is used for cancelling and/// marking as executed.abstractcontractBaseLoanisERC721TokenReceiver, IBaseLoan, LiquidationHandler{
usingFixedPointMathLibforuint256;
usingInputCheckerforaddress;
usingMessageHashUtilsforbytes32;
/// @notice Used in compliance with EIP712uint256internalimmutable INITIAL_CHAIN_ID;
bytes32publicimmutable INITIAL_DOMAIN_SEPARATOR;
bytes4internalconstant MAGICVALUE_1271 =0x1626ba7e;
/// @notice Precision used for calculating interests.uint256internalconstant _PRECISION =10000;
bytespublicconstant VERSION ="3";
/// @notice Minimum improvement (in BPS) required for a strict improvement.uint256internal _minImprovementApr =1000;
stringpublic name;
/// @notice Total number of loans issued. Given it's a serial value, we use it/// as loan id.uint256publicoverride getTotalLoansIssued;
/// @notice Offer capacitymapping(address user =>mapping(uint256 offerId =>uint256 used)) internal _used;
/// @notice Used for validate off chain maker offers / canceling onemapping(address user =>mapping(uint256 offerId =>bool notActive)) public isOfferCancelled;
/// @notice Used for validating off chain maker offers / canceling allmapping(address user =>uint256 minOfferId) public minOfferId;
/// @notice Used in a similar way as `isOfferCancelled` to handle renegotiations.mapping(address user =>mapping(uint256 renegotiationIf =>bool notActive)) public isRenegotiationOfferCancelled;
/// @notice Loans are only denominated in whitelisted addresses. Within each struct,/// we save those as their `uint` representation.
AddressManager internalimmutable _currencyManager;
/// @notice Only whilteslited collections are accepted as collateral. Within each struct,/// we save those as their `uint` representation.
AddressManager internalimmutable _collectionManager;
eventOfferCancelled(address lender, uint256 offerId);
eventAllOffersCancelled(address lender, uint256 minOfferId);
eventRenegotiationOfferCancelled(address lender, uint256 renegotiationId);
eventMinAprImprovementUpdated(uint256 _minimum);
errorCancelledOrExecutedOfferError(address _lender, uint256 _offerId);
errorExpiredOfferError(uint256 _expirationTime);
errorLowOfferIdError(address _lender, uint256 _newMinOfferId, uint256 _minOfferId);
errorLowRenegotiationOfferIdError(address _lender, uint256 _newMinRenegotiationOfferId, uint256 _minOfferId);
errorZeroInterestError();
errorInvalidSignatureError();
errorCurrencyNotWhitelistedError();
errorCollectionNotWhitelistedError();
errorMaxCapacityExceededError();
errorInvalidLoanError(uint256 _loanId);
errorNotStrictlyImprovedError();
errorInvalidAmountError(uint256 _amount, uint256 _principalAmount);
/// @notice Constructor/// @param _name The name of the loan contract/// @param currencyManager The address of the currency manager/// @param collectionManager The address of the collection manager/// @param protocolFee The protocol fee/// @param loanLiquidator The liquidator contract/// @param owner The owner of the contract/// @param minWaitTime The time to wait before a new owner can be setconstructor(stringmemory _name,
address currencyManager,
address collectionManager,
ProtocolFee memory protocolFee,
address loanLiquidator,
address owner,
uint256 minWaitTime
) LiquidationHandler(owner, minWaitTime, loanLiquidator, protocolFee) {
name = _name;
currencyManager.checkNotZero();
collectionManager.checkNotZero();
_currencyManager = AddressManager(currencyManager);
_collectionManager = AddressManager(collectionManager);
INITIAL_CHAIN_ID =block.chainid;
INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator();
}
/// @return The minimum improvement for a loan to be considered strictly better.functiongetMinImprovementApr() externalviewreturns (uint256) {
return _minImprovementApr;
}
/// @notice Updates the minimum improvement for a loan to be considered strictly better./// Only the owner can call this function./// @param _newMinimum The new minimum improvement.functionupdateMinImprovementApr(uint256 _newMinimum) externalonlyOwner{
_minImprovementApr = _newMinimum;
emit MinAprImprovementUpdated(_minImprovementApr);
}
/// @return Address of the currency manager.functiongetCurrencyManager() externalviewreturns (address) {
returnaddress(_currencyManager);
}
/// @return Address of the collection manager.functiongetCollectionManager() externalviewreturns (address) {
returnaddress(_collectionManager);
}
/// @inheritdoc IBaseLoanfunctioncancelOffer(uint256 _offerId) external{
address user =msg.sender;
isOfferCancelled[user][_offerId] =true;
emit OfferCancelled(user, _offerId);
}
/// @inheritdoc IBaseLoanfunctioncancelAllOffers(uint256 _minOfferId) externalvirtual{
address user =msg.sender;
uint256 currentMinOfferId = minOfferId[user];
if (currentMinOfferId >= _minOfferId) {
revert LowOfferIdError(user, _minOfferId, currentMinOfferId);
}
minOfferId[user] = _minOfferId;
emit AllOffersCancelled(user, _minOfferId);
}
/// @inheritdoc IBaseLoanfunctioncancelRenegotiationOffer(uint256 _renegotiationId) externalvirtual{
address lender =msg.sender;
isRenegotiationOfferCancelled[lender][_renegotiationId] =true;
emit RenegotiationOfferCancelled(lender, _renegotiationId);
}
/// @notice Returns the remaining capacity for a given loan offer./// @param _lender The address of the lender./// @param _offerId The id of the offer./// @return The amount lent out.functiongetUsedCapacity(address _lender, uint256 _offerId) externalviewreturns (uint256) {
return _used[_lender][_offerId];
}
/// @notice Get the domain separator requried to comply with EIP-712.functionDOMAIN_SEPARATOR() publicviewreturns (bytes32) {
returnblock.chainid== INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator();
}
/// @notice Call when issuing a new loan to get/set a unique serial id./// @dev This id should never be 0./// @return The new loan id.function_getAndSetNewLoanId() internalreturns (uint256) {
unchecked {
return++getTotalLoansIssued;
}
}
/// @notice Compute domain separator for EIP-712./// @return The domain separator.function_computeDomainSeparator() privateviewreturns (bytes32) {
returnkeccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(VERSION),
block.chainid,
address(this)
)
);
}
}
Contract Source Code
File 3 of 35: CallbackHandler.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"../utils/TwoStepOwned.sol";
import"../InputChecker.sol";
import"../utils/WithProtocolFee.sol";
import"../../interfaces/callbacks/ILoanCallback.sol";
/// @title CallbackHandler/// @author Florida St/// @notice Handle callbacks from the MultiSourceLoan contract.abstractcontractCallbackHandlerisWithProtocolFee{
usingInputCheckerforaddress;
/// @notice For security reasons we only allow a whitelisted set of callback contracts.mapping(address callbackContract =>bool isWhitelisted) internal _isWhitelistedCallbackContract;
addressprivateimmutable _multiSourceLoan;
eventWhitelistedCallbackContractAdded(address contractAdded);
eventWhitelistedCallbackContractRemoved(address contractRemoved);
constructor(address __owner, uint256 _minWaitTime, ProtocolFee memory __protocolFee)
WithProtocolFee(__owner, _minWaitTime, __protocolFee)
{}
/// @notice Add a whitelisted callback contract./// @param _contract Address of the contract.functionaddWhitelistedCallbackContract(address _contract) externalonlyOwner{
_contract.checkNotZero();
_isWhitelistedCallbackContract[_contract] =true;
emit WhitelistedCallbackContractAdded(_contract);
}
/// @notice Remove a whitelisted callback contract./// @param _contract Address of the contract.functionremoveWhitelistedCallbackContract(address _contract) externalonlyOwner{
_isWhitelistedCallbackContract[_contract] =false;
emit WhitelistedCallbackContractRemoved(_contract);
}
/// @return Whether a callback contract is whitelistedfunctionisWhitelistedCallbackContract(address _contract) externalviewreturns (bool) {
return _isWhitelistedCallbackContract[_contract];
}
/// @notice Handle the afterPrincipalTransfer callback./// @param _loan Loan./// @param _callbackAddress Callback address./// @param _callbackData Callback data./// @param _fee Fee.functionhandleAfterPrincipalTransferCallback(
IMultiSourceLoan.Loan memory _loan,
address _callbackAddress,
bytesmemory _callbackData,
uint256 _fee
) internal{
if (
!_isWhitelistedCallbackContract[_callbackAddress]
|| ILoanCallback(_callbackAddress).afterPrincipalTransfer(_loan, _fee, _callbackData)
!= ILoanCallback.afterPrincipalTransfer.selector
) {
revert ILoanCallback.InvalidCallbackError();
}
}
/// @notice Handle the afterNFTTransfer callback./// @param _loan Loan./// @param _callbackAddress Callback address./// @param _callbackData Callback data.functionhandleAfterNFTTransferCallback(
IMultiSourceLoan.Loan memory _loan,
address _callbackAddress,
bytescalldata _callbackData
) internal{
if (
!_isWhitelistedCallbackContract[_callbackAddress]
|| ILoanCallback(_callbackAddress).afterNFTTransfer(_loan, _callbackData)
!= ILoanCallback.afterNFTTransfer.selector
) {
revert ILoanCallback.InvalidCallbackError();
}
}
}
Contract Source Code
File 4 of 35: ECDSA.sol
// SPDX-License-Identifier: MIT// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol)pragmasolidity ^0.8.20;/**
* @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
*
* These functions can be used to verify that a message was signed by the holder
* of the private keys of a given address.
*/libraryECDSA{
enumRecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS
}
/**
* @dev The signature derives the `address(0)`.
*/errorECDSAInvalidSignature();
/**
* @dev The signature has an invalid length.
*/errorECDSAInvalidSignatureLength(uint256 length);
/**
* @dev The signature has an S value that is in the upper half order.
*/errorECDSAInvalidSignatureS(bytes32 s);
/**
* @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not
* return address(0) without also returning an error description. Errors are documented using an enum (error type)
* and a bytes32 providing additional information about the error.
*
* If no error is returned, then the address can be used for verification purposes.
*
* The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
*
* Documentation for signature generation:
* - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
* - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
*/functiontryRecover(bytes32 hash, bytesmemory signature) internalpurereturns (address, RecoverError, bytes32) {
if (signature.length==65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them// currently is to use assembly./// @solidity memory-safe-assemblyassembly {
r :=mload(add(signature, 0x20))
s :=mload(add(signature, 0x40))
v :=byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature`. This address can then be used for verification purposes.
*
* The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
*/functionrecover(bytes32 hash, bytesmemory signature) internalpurereturns (address) {
(address recovered, RecoverError error, bytes32errorArg) = tryRecover(hash, signature);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
*
* See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures]
*/functiontryRecover(bytes32 hash, bytes32 r, bytes32 vs) internalpurereturns (address, RecoverError, bytes32) {
unchecked {
bytes32 s = vs &bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
// We do not check for an overflow here since the shift operation results in 0 or 1.uint8 v =uint8((uint256(vs) >>255) +27);
return tryRecover(hash, v, r, s);
}
}
/**
* @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
*/functionrecover(bytes32 hash, bytes32 r, bytes32 vs) internalpurereturns (address) {
(address recovered, RecoverError error, bytes32errorArg) = tryRecover(hash, r, vs);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `v`,
* `r` and `s` signature fields separately.
*/functiontryRecover(bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internalpurereturns (address, RecoverError, bytes32) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most// signatures from current libraries generate a unique signature with an s-value in the lower half order.//// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept// these malleable signatures as well.if (uint256(s) >0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS, s);
}
// If the signature is valid (and not malleable), return the signer addressaddress signer =ecrecover(hash, v, r, s);
if (signer ==address(0)) {
return (address(0), RecoverError.InvalidSignature, bytes32(0));
}
return (signer, RecoverError.NoError, bytes32(0));
}
/**
* @dev Overload of {ECDSA-recover} that receives the `v`,
* `r` and `s` signature fields separately.
*/functionrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internalpurereturns (address) {
(address recovered, RecoverError error, bytes32errorArg) = tryRecover(hash, v, r, s);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.
*/function_throwError(RecoverError error, bytes32 errorArg) privatepure{
if (error == RecoverError.NoError) {
return; // no error: do nothing
} elseif (error == RecoverError.InvalidSignature) {
revert ECDSAInvalidSignature();
} elseif (error == RecoverError.InvalidSignatureLength) {
revert ECDSAInvalidSignatureLength(uint256(errorArg));
} elseif (error == RecoverError.InvalidSignatureS) {
revert ECDSAInvalidSignatureS(errorArg);
}
}
}
Contract Source Code
File 5 of 35: ERC20.sol
// SPDX-License-Identifier: AGPL-3.0-onlypragmasolidity >=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.abstractcontractERC20{
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/eventTransfer(addressindexedfrom, addressindexed to, uint256 amount);
eventApproval(addressindexed owner, addressindexed spender, uint256 amount);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/stringpublic name;
stringpublic symbol;
uint8publicimmutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/uint256public totalSupply;
mapping(address=>uint256) public balanceOf;
mapping(address=>mapping(address=>uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/uint256internalimmutable INITIAL_CHAIN_ID;
bytes32internalimmutable INITIAL_DOMAIN_SEPARATOR;
mapping(address=>uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/constructor(stringmemory _name,
stringmemory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID =block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/functionapprove(address spender, uint256 amount) publicvirtualreturns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
returntrue;
}
functiontransfer(address to, uint256 amount) publicvirtualreturns (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);
returntrue;
}
functiontransferFrom(addressfrom,
address to,
uint256 amount
) publicvirtualreturns (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);
returntrue;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/functionpermit(address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) publicvirtual{
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);
}
functionDOMAIN_SEPARATOR() publicviewvirtualreturns (bytes32) {
returnblock.chainid== INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
functioncomputeDomainSeparator() internalviewvirtualreturns (bytes32) {
returnkeccak256(
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) internalvirtual{
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(addressfrom, uint256 amount) internalvirtual{
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);
}
}
Contract Source Code
File 6 of 35: ERC721.sol
// SPDX-License-Identifier: AGPL-3.0-onlypragmasolidity >=0.8.0;/// @notice Modern, minimalist, and gas efficient ERC-721 implementation./// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol)abstractcontractERC721{
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/eventTransfer(addressindexedfrom, addressindexed to, uint256indexed id);
eventApproval(addressindexed owner, addressindexed spender, uint256indexed id);
eventApprovalForAll(addressindexed owner, addressindexed operator, bool approved);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE/LOGIC
//////////////////////////////////////////////////////////////*/stringpublic name;
stringpublic symbol;
functiontokenURI(uint256 id) publicviewvirtualreturns (stringmemory);
/*//////////////////////////////////////////////////////////////
ERC721 BALANCE/OWNER STORAGE
//////////////////////////////////////////////////////////////*/mapping(uint256=>address) internal _ownerOf;
mapping(address=>uint256) internal _balanceOf;
functionownerOf(uint256 id) publicviewvirtualreturns (address owner) {
require((owner = _ownerOf[id]) !=address(0), "NOT_MINTED");
}
functionbalanceOf(address owner) publicviewvirtualreturns (uint256) {
require(owner !=address(0), "ZERO_ADDRESS");
return _balanceOf[owner];
}
/*//////////////////////////////////////////////////////////////
ERC721 APPROVAL STORAGE
//////////////////////////////////////////////////////////////*/mapping(uint256=>address) public getApproved;
mapping(address=>mapping(address=>bool)) public isApprovedForAll;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/constructor(stringmemory _name, stringmemory _symbol) {
name = _name;
symbol = _symbol;
}
/*//////////////////////////////////////////////////////////////
ERC721 LOGIC
//////////////////////////////////////////////////////////////*/functionapprove(address spender, uint256 id) publicvirtual{
address owner = _ownerOf[id];
require(msg.sender== owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED");
getApproved[id] = spender;
emit Approval(owner, spender, id);
}
functionsetApprovalForAll(address operator, bool approved) publicvirtual{
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
functiontransferFrom(addressfrom,
address to,
uint256 id
) publicvirtual{
require(from== _ownerOf[id], "WRONG_FROM");
require(to !=address(0), "INVALID_RECIPIENT");
require(
msg.sender==from|| isApprovedForAll[from][msg.sender] ||msg.sender== getApproved[id],
"NOT_AUTHORIZED"
);
// Underflow of the sender's balance is impossible because we check for// ownership above and the recipient's balance can't realistically overflow.unchecked {
_balanceOf[from]--;
_balanceOf[to]++;
}
_ownerOf[id] = to;
delete getApproved[id];
emit Transfer(from, to, id);
}
functionsafeTransferFrom(addressfrom,
address to,
uint256 id
) publicvirtual{
transferFrom(from, to, id);
require(
to.code.length==0||
ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
functionsafeTransferFrom(addressfrom,
address to,
uint256 id,
bytescalldata data
) publicvirtual{
transferFrom(from, to, id);
require(
to.code.length==0||
ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
/*//////////////////////////////////////////////////////////////
ERC165 LOGIC
//////////////////////////////////////////////////////////////*/functionsupportsInterface(bytes4 interfaceId) publicviewvirtualreturns (bool) {
return
interfaceId ==0x01ffc9a7||// ERC165 Interface ID for ERC165
interfaceId ==0x80ac58cd||// ERC165 Interface ID for ERC721
interfaceId ==0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
}
/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/function_mint(address to, uint256 id) internalvirtual{
require(to !=address(0), "INVALID_RECIPIENT");
require(_ownerOf[id] ==address(0), "ALREADY_MINTED");
// Counter overflow is incredibly unrealistic.unchecked {
_balanceOf[to]++;
}
_ownerOf[id] = to;
emit Transfer(address(0), to, id);
}
function_burn(uint256 id) internalvirtual{
address owner = _ownerOf[id];
require(owner !=address(0), "NOT_MINTED");
// Ownership check above ensures no underflow.unchecked {
_balanceOf[owner]--;
}
delete _ownerOf[id];
delete getApproved[id];
emit Transfer(owner, address(0), id);
}
/*//////////////////////////////////////////////////////////////
INTERNAL SAFE MINT LOGIC
//////////////////////////////////////////////////////////////*/function_safeMint(address to, uint256 id) internalvirtual{
_mint(to, id);
require(
to.code.length==0||
ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
function_safeMint(address to,
uint256 id,
bytesmemory data
) internalvirtual{
_mint(to, id);
require(
to.code.length==0||
ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) ==
ERC721TokenReceiver.onERC721Received.selector,
"UNSAFE_RECIPIENT"
);
}
}
/// @notice A generic interface for a contract which properly accepts ERC721 tokens./// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol)abstractcontractERC721TokenReceiver{
functiononERC721Received(address,
address,
uint256,
bytescalldata) externalvirtualreturns (bytes4) {
return ERC721TokenReceiver.onERC721Received.selector;
}
}
Contract Source Code
File 7 of 35: FixedPointMathLib.sol
// SPDX-License-Identifier: AGPL-3.0-onlypragmasolidity >=0.8.0;/// @notice Arithmetic library with operations for fixed-point numbers./// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol)/// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol)libraryFixedPointMathLib{
/*//////////////////////////////////////////////////////////////
SIMPLIFIED FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/uint256internalconstant MAX_UINT256 =2**256-1;
uint256internalconstant WAD =1e18; // The scalar of ETH and most ERC20s.functionmulWadDown(uint256 x, uint256 y) internalpurereturns (uint256) {
return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down.
}
functionmulWadUp(uint256 x, uint256 y) internalpurereturns (uint256) {
return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up.
}
functiondivWadDown(uint256 x, uint256 y) internalpurereturns (uint256) {
return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down.
}
functiondivWadUp(uint256 x, uint256 y) internalpurereturns (uint256) {
return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up.
}
/*//////////////////////////////////////////////////////////////
LOW LEVEL FIXED POINT OPERATIONS
//////////////////////////////////////////////////////////////*/functionmulDivDown(uint256 x,
uint256 y,
uint256 denominator
) internalpurereturns (uint256 z) {
/// @solidity memory-safe-assemblyassembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))ifiszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// Divide x * y by the denominator.
z :=div(mul(x, y), denominator)
}
}
functionmulDivUp(uint256 x,
uint256 y,
uint256 denominator
) internalpurereturns (uint256 z) {
/// @solidity memory-safe-assemblyassembly {
// Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))ifiszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) {
revert(0, 0)
}
// If x * y modulo the denominator is strictly greater than 0,// 1 is added to round up the division of x * y by the denominator.
z :=add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator))
}
}
functionrpow(uint256 x,
uint256 n,
uint256 scalar
) internalpurereturns (uint256 z) {
/// @solidity memory-safe-assemblyassembly {
switch x
case0 {
switch n
case0 {
// 0 ** 0 = 1
z := scalar
}
default {
// 0 ** n = 0
z :=0
}
}
default {
switchmod(n, 2)
case0 {
// If n is even, store scalar in z for now.
z := scalar
}
default {
// If n is odd, store x in z for now.
z := x
}
// Shifting right by 1 is like dividing by 2.let half :=shr(1, scalar)
for {
// Shift n right by 1 before looping to halve it.
n :=shr(1, n)
} n {
// Shift n right by 1 each iteration to halve it.
n :=shr(1, n)
} {
// Revert immediately if x ** 2 would overflow.// Equivalent to iszero(eq(div(xx, x), x)) here.ifshr(128, x) {
revert(0, 0)
}
// Store x squared.let xx :=mul(x, x)
// Round to the nearest number.let xxRound :=add(xx, half)
// Revert if xx + half overflowed.iflt(xxRound, xx) {
revert(0, 0)
}
// Set x to scaled xxRound.
x :=div(xxRound, scalar)
// If n is even:ifmod(n, 2) {
// Compute z * x.let zx :=mul(z, x)
// If z * x overflowed:ifiszero(eq(div(zx, x), z)) {
// Revert if x is non-zero.ifiszero(iszero(x)) {
revert(0, 0)
}
}
// Round to the nearest number.let zxRound :=add(zx, half)
// Revert if zx + half overflowed.iflt(zxRound, zx) {
revert(0, 0)
}
// Return properly scaled zxRound.
z :=div(zxRound, scalar)
}
}
}
}
}
/*//////////////////////////////////////////////////////////////
GENERAL NUMBER UTILITIES
//////////////////////////////////////////////////////////////*/functionsqrt(uint256 x) internalpurereturns (uint256 z) {
/// @solidity memory-safe-assemblyassembly {
let y := x // We start y at x, which will help us make our initial estimate.
z :=181// The "correct" value is 1, but this saves a multiplication later.// This segment is to get a reasonable initial estimate for the Babylonian method. With a bad// start, the correct # of bits increases ~linearly each iteration instead of ~quadratically.// We check y >= 2^(k + 8) but shift right by k bits// each branch to ensure that if x >= 256, then y >= 256.ifiszero(lt(y, 0x10000000000000000000000000000000000)) {
y :=shr(128, y)
z :=shl(64, z)
}
ifiszero(lt(y, 0x1000000000000000000)) {
y :=shr(64, y)
z :=shl(32, z)
}
ifiszero(lt(y, 0x10000000000)) {
y :=shr(32, y)
z :=shl(16, z)
}
ifiszero(lt(y, 0x1000000)) {
y :=shr(16, y)
z :=shl(8, z)
}
// Goal was to get z*z*y within a small factor of x. More iterations could// get y in a tighter range. Currently, we will have y in [256, 256*2^16).// We ensured y >= 256 so that the relative difference between y and y+1 is small.// That's not possible if x < 256 but we can just verify those cases exhaustively.// Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256.// Correctness can be checked exhaustively for x < 256, so we assume y >= 256.// Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps.// For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range// (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256.// Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate// sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18.// There is no overflow risk here since y < 2^136 after the first branch above.
z :=shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181.// Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough.
z :=shr(1, add(z, div(x, z)))
z :=shr(1, add(z, div(x, z)))
z :=shr(1, add(z, div(x, z)))
z :=shr(1, add(z, div(x, z)))
z :=shr(1, add(z, div(x, z)))
z :=shr(1, add(z, div(x, z)))
z :=shr(1, add(z, div(x, z)))
// If x+1 is a perfect square, the Babylonian method cycles between// floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor.// See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division// Since the ceil is rare, we save gas on the assignment and repeat division in the rare case.// If you don't care whether the floor or ceil square root is returned, you can remove this statement.
z :=sub(z, lt(div(x, z), z))
}
}
functionunsafeMod(uint256 x, uint256 y) internalpurereturns (uint256 z) {
/// @solidity memory-safe-assemblyassembly {
// Mod x by y. Note this will return// 0 instead of reverting if y is zero.
z :=mod(x, y)
}
}
functionunsafeDiv(uint256 x, uint256 y) internalpurereturns (uint256 r) {
/// @solidity memory-safe-assemblyassembly {
// Divide x by y. Note this will return// 0 instead of reverting if y is zero.
r :=div(x, y)
}
}
functionunsafeDivUp(uint256 x, uint256 y) internalpurereturns (uint256 z) {
/// @solidity memory-safe-assemblyassembly {
// Add 1 to x * y if x % y > 0. Note this will// return 0 instead of reverting if y is zero.
z :=add(gt(mod(x, y), 0), div(x, y))
}
}
}
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;import"./loans/IMultiSourceLoan.sol";
/// @title Liquidates Collateral for Defaulted Loans using English Auctions./// @author Florida St/// @notice It liquidates collateral corresponding to defaulted loans/// and sends back the proceeds to the loan contract for distribution.interfaceIAuctionLoanLiquidator{
/// @notice The auction struct./// @param loanAddress The loan contract address./// @param loanId The loan id./// @param highestBid The highest bid./// @param triggerFee The trigger fee./// @param minBid The minimum bid./// @param highestBidder The highest bidder./// @param duration The auction duration./// @param asset The asset address./// @param startTime The auction start time./// @param originator The address that triggered the liquidation./// @param lastBidTime The last bid time.structAuction {
address loanAddress;
uint256 loanId;
uint256 highestBid;
uint256 triggerFee;
uint256 minBid;
address highestBidder;
uint96 duration;
address asset;
uint96 startTime;
address originator;
uint96 lastBidTime;
}
/// @notice Add a loan contract to the list of accepted contracts./// @param _loanContract The loan contract to be added.functionaddLoanContract(address _loanContract) external;
/// @notice Remove a loan contract from the list of accepted contracts./// @param _loanContract The loan contract to be removed.functionremoveLoanContract(address _loanContract) external;
/// @return The loan contracts that are accepted by this liquidator.functiongetValidLoanContracts() externalviewreturns (address[] memory);
/// @notice Update liquidation distributor./// @param _liquidationDistributor The new liquidation distributor.functionupdateLiquidationDistributor(address _liquidationDistributor) external;
/// @return liquidationDistributor The liquidation distributor address.functiongetLiquidationDistributor() externalviewreturns (address);
/// @notice Called by the owner to update the trigger fee./// @param triggerFee The new trigger fee.functionupdateTriggerFee(uint256 triggerFee) external;
/// @return triggerFee The trigger fee.functiongetTriggerFee() externalviewreturns (uint256);
/// @notice When a bid is placed, the contract takes possesion of the bid, and/// if there was a previous bid, it returns that capital to the original/// bidder./// @param _contract The nft contract address./// @param _tokenId The nft id./// @param _auction The auction struct./// @param _bid The bid amount./// @return auction The updated auction struct.functionplaceBid(address _contract, uint256 _tokenId, Auction memory _auction, uint256 _bid)
externalreturns (Auction memory);
/// @notice On settlement, the NFT is sent to the highest bidder./// Calls loan liquidated for accounting purposes./// @param _auction The auction struct./// @param _loan The loan struct.functionsettleAuction(Auction calldata _auction, IMultiSourceLoan.Loan calldata _loan) external;
/// @notice The contract has hashes of all auctions to save space (not the actual struct)/// @param _contract The nft contract address./// @param _tokenId The nft id./// @return auctionHash The auction hash.functiongetAuctionHash(address _contract, uint256 _tokenId) externalviewreturns (bytes32);
}
Contract Source Code
File 10 of 35: IBaseLoan.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;import"../../interfaces/ILoanLiquidator.sol";
/// @title Interface for Loans./// @author Florida St/// @notice Basic LoaninterfaceIBaseLoan{
/// @notice Minimum improvement (in BPS) required for a strict improvement./// @param principalAmount Minimum delta of principal amount./// @param interest Minimum delta of interest./// @param duration Minimum delta of duration.structImprovementMinimum {
uint256 principalAmount;
uint256 interest;
uint256 duration;
}
/// @notice Arbitrary contract to validate offers implementing `IBaseOfferValidator`./// @param validator Address of the validator contract./// @param arguments Arguments to pass to the validator.structOfferValidator {
address validator;
bytes arguments;
}
/// @notice Total number of loans issued by this contract.functiongetTotalLoansIssued() externalviewreturns (uint256);
/// @notice Cancel offer for `msg.sender`. Each lender has unique offerIds./// @param _offerId Offer ID.functioncancelOffer(uint256 _offerId) external;
/// @notice Cancell all offers with offerId < _minOfferId/// @param _minOfferId Minimum offer ID.functioncancelAllOffers(uint256 _minOfferId) external;
/// @notice Cancel renegotiation offer. Similar to offers./// @param _renegotiationId Renegotiation offer ID.functioncancelRenegotiationOffer(uint256 _renegotiationId) external;
}
Contract Source Code
File 11 of 35: IDelegateRegistry.sol
// SPDX-License-Identifier: CC0-1.0pragmasolidity >=0.8.13;/**
* @title IDelegateRegistry
* @custom:version 2.0
* @custom:author foobar (0xfoobar)
* @notice A standalone immutable registry storing delegated permissions from one address to another
*/interfaceIDelegateRegistry{
/// @notice Delegation type, NONE is used when a delegation does not exist or is revokedenumDelegationType {
NONE,
ALL,
CONTRACT,
ERC721,
ERC20,
ERC1155
}
/// @notice Struct for returning delegationsstructDelegation {
DelegationType type_;
address to;
addressfrom;
bytes32 rights;
address contract_;
uint256 tokenId;
uint256 amount;
}
/// @notice Emitted when an address delegates or revokes rights for their entire walleteventDelegateAll(addressindexedfrom, addressindexed to, bytes32 rights, bool enable);
/// @notice Emitted when an address delegates or revokes rights for a contract addresseventDelegateContract(addressindexedfrom, addressindexed to, addressindexed contract_, bytes32 rights, bool enable);
/// @notice Emitted when an address delegates or revokes rights for an ERC721 tokenIdeventDelegateERC721(addressindexedfrom, addressindexed to, addressindexed contract_, uint256 tokenId, bytes32 rights, bool enable);
/// @notice Emitted when an address delegates or revokes rights for an amount of ERC20 tokenseventDelegateERC20(addressindexedfrom, addressindexed to, addressindexed contract_, bytes32 rights, uint256 amount);
/// @notice Emitted when an address delegates or revokes rights for an amount of an ERC1155 tokenIdeventDelegateERC1155(addressindexedfrom, addressindexed to, addressindexed contract_, uint256 tokenId, bytes32 rights, uint256 amount);
/// @notice Thrown if multicall calldata is malformederrorMulticallFailed();
/**
* ----------- WRITE -----------
*//**
* @notice Call multiple functions in the current contract and return the data from all of them if they all succeed
* @param data The encoded function data for each of the calls to make to this contract
* @return results The results from each of the calls passed in via data
*/functionmulticall(bytes[] calldata data) externalpayablereturns (bytes[] memory results);
/**
* @notice Allow the delegate to act on behalf of `msg.sender` for all contracts
* @param to The address to act as delegate
* @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
* @param enable Whether to enable or disable this delegation, true delegates and false revokes
* @return delegationHash The unique identifier of the delegation
*/functiondelegateAll(address to, bytes32 rights, bool enable) externalpayablereturns (bytes32 delegationHash);
/**
* @notice Allow the delegate to act on behalf of `msg.sender` for a specific contract
* @param to The address to act as delegate
* @param contract_ The contract whose rights are being delegated
* @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
* @param enable Whether to enable or disable this delegation, true delegates and false revokes
* @return delegationHash The unique identifier of the delegation
*/functiondelegateContract(address to, address contract_, bytes32 rights, bool enable) externalpayablereturns (bytes32 delegationHash);
/**
* @notice Allow the delegate to act on behalf of `msg.sender` for a specific ERC721 token
* @param to The address to act as delegate
* @param contract_ The contract whose rights are being delegated
* @param tokenId The token id to delegate
* @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
* @param enable Whether to enable or disable this delegation, true delegates and false revokes
* @return delegationHash The unique identifier of the delegation
*/functiondelegateERC721(address to, address contract_, uint256 tokenId, bytes32 rights, bool enable) externalpayablereturns (bytes32 delegationHash);
/**
* @notice Allow the delegate to act on behalf of `msg.sender` for a specific amount of ERC20 tokens
* @dev The actual amount is not encoded in the hash, just the existence of a amount (since it is an upper bound)
* @param to The address to act as delegate
* @param contract_ The address for the fungible token contract
* @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
* @param amount The amount to delegate, > 0 delegates and 0 revokes
* @return delegationHash The unique identifier of the delegation
*/functiondelegateERC20(address to, address contract_, bytes32 rights, uint256 amount) externalpayablereturns (bytes32 delegationHash);
/**
* @notice Allow the delegate to act on behalf of `msg.sender` for a specific amount of ERC1155 tokens
* @dev The actual amount is not encoded in the hash, just the existence of a amount (since it is an upper bound)
* @param to The address to act as delegate
* @param contract_ The address of the contract that holds the token
* @param tokenId The token id to delegate
* @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights
* @param amount The amount of that token id to delegate, > 0 delegates and 0 revokes
* @return delegationHash The unique identifier of the delegation
*/functiondelegateERC1155(address to, address contract_, uint256 tokenId, bytes32 rights, uint256 amount) externalpayablereturns (bytes32 delegationHash);
/**
* ----------- CHECKS -----------
*//**
* @notice Check if `to` is a delegate of `from` for the entire wallet
* @param to The potential delegate address
* @param from The potential address who delegated rights
* @param rights Specific rights to check for, pass the zero value to ignore subdelegations and check full delegations only
* @return valid Whether delegate is granted to act on the from's behalf
*/functioncheckDelegateForAll(address to, addressfrom, bytes32 rights) externalviewreturns (bool);
/**
* @notice Check if `to` is a delegate of `from` for the specified `contract_` or the entire wallet
* @param to The delegated address to check
* @param contract_ The specific contract address being checked
* @param from The cold wallet who issued the delegation
* @param rights Specific rights to check for, pass the zero value to ignore subdelegations and check full delegations only
* @return valid Whether delegate is granted to act on from's behalf for entire wallet or that specific contract
*/functioncheckDelegateForContract(address to, addressfrom, address contract_, bytes32 rights) externalviewreturns (bool);
/**
* @notice Check if `to` is a delegate of `from` for the specific `contract` and `tokenId`, the entire `contract_`, or the entire wallet
* @param to The delegated address to check
* @param contract_ The specific contract address being checked
* @param tokenId The token id for the token to delegating
* @param from The wallet that issued the delegation
* @param rights Specific rights to check for, pass the zero value to ignore subdelegations and check full delegations only
* @return valid Whether delegate is granted to act on from's behalf for entire wallet, that contract, or that specific tokenId
*/functioncheckDelegateForERC721(address to, addressfrom, address contract_, uint256 tokenId, bytes32 rights) externalviewreturns (bool);
/**
* @notice Returns the amount of ERC20 tokens the delegate is granted rights to act on the behalf of
* @param to The delegated address to check
* @param contract_ The address of the token contract
* @param from The cold wallet who issued the delegation
* @param rights Specific rights to check for, pass the zero value to ignore subdelegations and check full delegations only
* @return balance The delegated balance, which will be 0 if the delegation does not exist
*/functioncheckDelegateForERC20(address to, addressfrom, address contract_, bytes32 rights) externalviewreturns (uint256);
/**
* @notice Returns the amount of a ERC1155 tokens the delegate is granted rights to act on the behalf of
* @param to The delegated address to check
* @param contract_ The address of the token contract
* @param tokenId The token id to check the delegated amount of
* @param from The cold wallet who issued the delegation
* @param rights Specific rights to check for, pass the zero value to ignore subdelegations and check full delegations only
* @return balance The delegated balance, which will be 0 if the delegation does not exist
*/functioncheckDelegateForERC1155(address to, addressfrom, address contract_, uint256 tokenId, bytes32 rights) externalviewreturns (uint256);
/**
* ----------- ENUMERATIONS -----------
*//**
* @notice Returns all enabled delegations a given delegate has received
* @param to The address to retrieve delegations for
* @return delegations Array of Delegation structs
*/functiongetIncomingDelegations(address to) externalviewreturns (Delegation[] memory delegations);
/**
* @notice Returns all enabled delegations an address has given out
* @param from The address to retrieve delegations for
* @return delegations Array of Delegation structs
*/functiongetOutgoingDelegations(addressfrom) externalviewreturns (Delegation[] memory delegations);
/**
* @notice Returns all hashes associated with enabled delegations an address has received
* @param to The address to retrieve incoming delegation hashes for
* @return delegationHashes Array of delegation hashes
*/functiongetIncomingDelegationHashes(address to) externalviewreturns (bytes32[] memory delegationHashes);
/**
* @notice Returns all hashes associated with enabled delegations an address has given out
* @param from The address to retrieve outgoing delegation hashes for
* @return delegationHashes Array of delegation hashes
*/functiongetOutgoingDelegationHashes(addressfrom) externalviewreturns (bytes32[] memory delegationHashes);
/**
* @notice Returns the delegations for a given array of delegation hashes
* @param delegationHashes is an array of hashes that correspond to delegations
* @return delegations Array of Delegation structs, return empty structs for nonexistent or revoked delegations
*/functiongetDelegationsFromHashes(bytes32[] calldata delegationHashes) externalviewreturns (Delegation[] memory delegations);
/**
* ----------- STORAGE ACCESS -----------
*//**
* @notice Allows external contracts to read arbitrary storage slots
*/functionreadSlot(bytes32 location) externalviewreturns (bytes32);
/**
* @notice Allows external contracts to read an arbitrary array of storage slots
*/functionreadSlots(bytes32[] calldata locations) externalviewreturns (bytes32[] memory);
}
Contract Source Code
File 12 of 35: IERC1271.sol
// SPDX-License-Identifier: MIT// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1271.sol)pragmasolidity ^0.8.20;/**
* @dev Interface of the ERC-1271 standard signature validation method for
* contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271].
*/interfaceIERC1271{
/**
* @dev Should return whether the signature provided is valid for the provided data
* @param hash Hash of the data to be signed
* @param signature Signature byte array associated with _data
*/functionisValidSignature(bytes32 hash, bytesmemory signature) externalviewreturns (bytes4 magicValue);
}
Contract Source Code
File 13 of 35: ILiquidationHandler.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"./loans/IMultiSourceLoan.sol";
/// @title Interface for liquidation handlers./// @author Florida St/// @notice Liquidation HandlerinterfaceILiquidationHandler{
/// @return Liquidator contract addressfunctiongetLiquidator() externalreturns (address);
/// @notice Updates the liquidation contract./// @param loanLiquidator New liquidation contract.functionupdateLiquidationContract(address loanLiquidator) external;
/// @notice Updates the auction duration for liquidations./// @param _newDuration New auction duration.functionupdateLiquidationAuctionDuration(uint48 _newDuration) external;
/// @return auctionDuration Returns the auction's duration for liquidations.functiongetLiquidationAuctionDuration() externalreturns (uint48);
}
Contract Source Code
File 14 of 35: ILoanCallback.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;import"../loans/IMultiSourceLoan.sol";
interfaceILoanCallback{
errorInvalidCallbackError();
/// @notice Called by the MSL contract after the principal of loan has been tranfered (when a loan is initiated)/// but before it tries to transfer the NFT into escrow./// @param _loan The loan./// @param _fee The origination fee./// @param _executionData Execution data for purchase./// @return The bytes4 magic value.functionafterPrincipalTransfer(IMultiSourceLoan.Loan memory _loan, uint256 _fee, bytescalldata _executionData)
externalreturns (bytes4);
/// @notice Call by the MSL contract after the NFT has been transfered to the borrower repaying the loan, but before/// transfering the principal to the lender./// @param _loan The loan./// @param _executionData Execution data for the offer./// @return The bytes4 magic value.functionafterNFTTransfer(IMultiSourceLoan.Loan memory _loan, bytescalldata _executionData)
externalreturns (bytes4);
}
Contract Source Code
File 15 of 35: ILoanLiquidator.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;import"../interfaces/loans/IMultiSourceLoan.sol";
/// @title Liquidates Collateral for Defaulted Loans/// @author Florida St/// @notice It liquidates collateral corresponding to defaulted loans/// and sends back the proceeds to the loan contract for distribution.interfaceILoanLiquidator{
/// @notice Given a loan, it takes posession of the NFT and liquidates it./// @param _loanId The loan id./// @param _contract The loan contract address./// @param _tokenId The NFT id./// @param _asset The asset address./// @param _duration The liquidation duration./// @param _minBid The minimum bid./// @param _originator The address that trigger the liquidation./// @return encodedAuction Encoded struct.functionliquidateLoan(uint256 _loanId,
address _contract,
uint256 _tokenId,
address _asset,
uint96 _duration,
uint256 _minBid,
address _originator
) externalreturns (bytesmemory);
}
Contract Source Code
File 16 of 35: ILoanManager.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;/// @title Multi Source Loan Interface/// @author Florida St/// @notice A multi source loan is one with multiple tranches.interfaceILoanManager{
structProposedCaller {
address caller;
bool isLoanContract;
}
/// @notice Validate an offer. Can only be called by an accepted caller./// @param _tokenId The token id./// @param _offer The offer to validate./// @param _protocolFee The protocol fee.functionvalidateOffer(uint256 _tokenId, bytescalldata _offer, uint256 _protocolFee) external;
/// @notice Update the offer handler./// @param _offerHandler The new offer handler.functionupdateOfferHandler(address _offerHandler) external;
/// @notice Get the offer handler setter./// @dev Had to take this out from the contract because of size issues./// @return The offer handler setter.functiongetParameterSetter() externalviewreturns (address);
/// @notice Add allowed callers./// @param _callers The callers to add.functionaddCallers(ProposedCaller[] calldata _callers) external;
/// @notice Called on loan repayment./// @param _loanId The loan id./// @param _principalAmount The principal amount./// @param _apr The APR./// @param _accruedInterest The accrued interest./// @param _protocolFee The protocol fee./// @param _startTime The start time.functionloanRepayment(uint256 _loanId,
uint256 _principalAmount,
uint256 _apr,
uint256 _accruedInterest,
uint256 _protocolFee,
uint256 _startTime
) external;
/// @notice Called on loan liquidation./// @param _loanAddress The address of the loan contract since this might be called by a liquidator./// @param _loanId The loan id./// @param _principalAmount The principal amount./// @param _apr The APR./// @param _accruedInterest The accrued interest./// @param _protocolFee The protocol fee./// @param _received The received amount (from liquidation proceeds)/// @param _startTime The start time.functionloanLiquidation(address _loanAddress,
uint256 _loanId,
uint256 _principalAmount,
uint256 _apr,
uint256 _accruedInterest,
uint256 _protocolFee,
uint256 _received,
uint256 _startTime
) external;
}
Contract Source Code
File 17 of 35: ILoanManagerRegistry.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;/// @title Interface Loan Manager Registry/// @author Florida St/// @notice Interface for a Loan Manager Registry.interfaceILoanManagerRegistry{
/// @notice Add a loan manager to the registry/// @param _loanManager Address of the loan managerfunctionaddLoanManager(address _loanManager) external;
/// @notice Remove a loan manager from the registry/// @param _loanManager Address of the loan managerfunctionremoveLoanManager(address _loanManager) external;
/// @notice Check if a loan manager is registered/// @param _loanManager Address of the loan manager/// @return True if the loan manager is registeredfunctionisLoanManager(address _loanManager) externalviewreturns (bool);
}
Contract Source Code
File 18 of 35: IMultiSourceLoan.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"./IBaseLoan.sol";
/// @title Multi Source Loan Interface/// @author Florida St/// @notice A multi source loan is one with multiple tranches.interfaceIMultiSourceLoan{
/// @notice Borrowers receive offers that are then validated./// @dev Setting the nftCollateralTokenId to 0 triggers validation through `validators`./// @param offerId Offer ID. Used for canceling/setting as executed./// @param lender Lender of the offer./// @param fee Origination fee./// @param capacity Capacity of the offer./// @param nftCollateralAddress Address of the NFT collateral./// @param nftCollateralTokenId NFT collateral token ID./// @param principalAddress Address of the principal./// @param principalAmount Principal amount of the loan./// @param aprBps APR in BPS./// @param expirationTime Expiration time of the offer./// @param duration Duration of the loan in seconds./// @param maxSeniorRepayment Max amount of senior capital ahead (principal + interest)./// @param validators Arbitrary contract to validate offers implementing `IBaseOfferValidator`.structLoanOffer {
uint256 offerId;
address lender;
uint256 fee;
uint256 capacity;
address nftCollateralAddress;
uint256 nftCollateralTokenId;
address principalAddress;
uint256 principalAmount;
uint256 aprBps;
uint256 expirationTime;
uint256 duration;
uint256 maxSeniorRepayment;
IBaseLoan.OfferValidator[] validators;
}
/// @notice Offer + how much will be filled (always <= principalAmount)./// @param offer Offer./// @param amount Amount to be filled.structOfferExecution {
LoanOffer offer;
uint256 amount;
bytes lenderOfferSignature;
}
/// @notice Offer + necessary fields to execute a specific loan. This has a separate expirationTime to avoid/// someone holding an offer and executing much later, without the borrower's awareness./// @dev It's advised that borrowers only set an expirationTime close to the actual time they will execute the loan/// to avoid replays./// @param offerExecution List of offers to be filled and amount for each./// @param tokenId NFT collateral token ID./// @param amount The amount the borrower is willing to take (must be <= _loanOffer principalAmount)/// @param expirationTime Expiration time of the signed offer by the borrower./// @param callbackData Data to pass to the callback.structExecutionData {
OfferExecution[] offerExecution;
uint256 tokenId;
uint256 duration;
uint256 expirationTime;
address principalReceiver;
bytes callbackData;
}
/// @param executionData Execution data./// @param borrower Address that owns the NFT and will take over the loan./// @param borrowerOfferSignature Signature of the offer (signed by borrower)./// @param callbackData Whether to call the afterPrincipalTransfer callbackstructLoanExecutionData {
ExecutionData executionData;
address borrower;
bytes borrowerOfferSignature;
}
/// @param loanId Loan ID./// @param callbackData Whether to call the afterNFTTransfer callback/// @param shouldDelegate Whether to delegate ownership of the NFT (avoid seaport flags).structSignableRepaymentData {
uint256 loanId;
bytes callbackData;
bool shouldDelegate;
}
/// @param loan Loan./// @param borrowerLoanSignature Signature of the loan (signed by borrower).structLoanRepaymentData {
SignableRepaymentData data;
Loan loan;
bytes borrowerSignature;
}
/// @notice Tranches have different seniority levels./// @param loanId Loan ID./// @param floor Amount of principal more senior to this tranche./// @param principalAmount Total principal in this tranche./// @param lender Lender for this given tranche./// @param accruedInterest Accrued Interest./// @param startTime Start Time. Either the time at which the loan initiated / was refinanced./// @param aprBps APR in basis points.structTranche {
uint256 loanId;
uint256 floor;
uint256 principalAmount;
address lender;
uint256 accruedInterest;
uint256 startTime;
uint256 aprBps;
}
/// @dev Principal Amount is equal to the sum of all tranches principalAmount./// We keep it for caching purposes. Since we are not saving this on chain but the hash,/// it does not have a huge impact on gas./// @param borrower Borrower./// @param nftCollateralTokenId NFT Collateral Token ID./// @param nftCollateralAddress NFT Collateral Address./// @param principalAddress Principal Address./// @param principalAmount Principal Amount./// @param startTime Start Time./// @param duration Duration./// @param tranche Tranches./// @param protocolFee Protocol Fee.structLoan {
address borrower;
uint256 nftCollateralTokenId;
address nftCollateralAddress;
address principalAddress;
uint256 principalAmount;
uint256 startTime;
uint256 duration;
Tranche[] tranche;
uint256 protocolFee;
}
/// @notice Renegotiation offer./// @param renegotiationId Renegotiation ID./// @param loanId Loan ID./// @param lender Lender./// @param fee Fee./// @param trancheIndex Tranche Indexes to be refinanced./// @param principalAmount Principal Amount. If more than one tranche, it must be the sum./// @param aprBps APR in basis points./// @param expirationTime Expiration Time./// @param duration Duration.structRenegotiationOffer {
uint256 renegotiationId;
uint256 loanId;
address lender;
uint256 fee;
uint256[] trancheIndex;
uint256 principalAmount;
uint256 aprBps;
uint256 expirationTime;
uint256 duration;
}
eventLoanLiquidated(uint256 loanId);
eventLoanEmitted(uint256 loanId, uint256[] offerId, Loan loan, uint256 fee);
eventLoanRefinanced(uint256 renegotiationId, uint256 oldLoanId, uint256 newLoanId, Loan loan, uint256 fee);
eventLoanRepaid(uint256 loanId, uint256 totalRepayment, uint256 fee);
eventLoanRefinancedFromNewOffers(uint256 loanId, uint256 newLoanId, Loan loan, uint256[] offerIds, uint256 totalFee
);
eventDelegated(uint256 loanId, address delegate, bytes32 _rights, bool value);
eventFlashActionContractUpdated(address newFlashActionContract);
eventFlashActionExecuted(uint256 loanId, address target, bytes data);
eventRevokeDelegate(address delegate, address collection, uint256 tokenId, bytes32 _rights);
eventMinLockPeriodUpdated(uint256 minLockPeriod);
/// @notice Call by the borrower when emiting a new loan./// @param _loanExecutionData Loan execution data./// @return loanId Loan ID./// @return loan Loan.functionemitLoan(LoanExecutionData calldata _loanExecutionData) externalreturns (uint256, Loan memory);
/// @notice Refinance whole loan (leaving just one tranche)./// @param _renegotiationOffer Offer to refinance a loan./// @param _loan Current loan./// @param _renegotiationOfferSignature Signature of the offer./// @return loanId New Loan Id, New Loan.functionrefinanceFull(
RenegotiationOffer calldata _renegotiationOffer,
Loan memory _loan,
bytescalldata _renegotiationOfferSignature
) externalreturns (uint256, Loan memory);
/// @notice Add a new tranche to a loan./// @param _renegotiationOffer Offer for new tranche./// @param _loan Current loan./// @param _renegotiationOfferSignature Signature of the offer./// @return loanId New Loan Id/// @return loan New Loan.functionaddNewTranche(
RenegotiationOffer calldata _renegotiationOffer,
Loan memory _loan,
bytescalldata _renegotiationOfferSignature
) externalreturns (uint256, Loan memory);
/// @notice Refinance a loan partially. It can only be called by the new lender/// (they are always a strict improvement on apr)./// @param _renegotiationOffer Offer to refinance a loan partially./// @param _loan Current loan./// @return loanId New Loan Id, New Loan./// @return loan New Loan.functionrefinancePartial(RenegotiationOffer calldata _renegotiationOffer, Loan memory _loan)
externalreturns (uint256, Loan memory);
/// @notice Refinance a loan from LoanExecutionData. We let borrowers use outstanding offers for new loans/// to refinance their current loan./// @param _loanId Loan ID./// @param _loan Current loan./// @param _loanExecutionData Loan Execution Data./// @return loanId New Loan Id./// @return loan New Loan.functionrefinanceFromLoanExecutionData(uint256 _loanId,
Loan calldata _loan,
LoanExecutionData calldata _loanExecutionData
) externalreturns (uint256, Loan memory);
/// @notice Repay loan. Interest is calculated pro-rata based on time. Lender is defined by nft ownership./// @param _repaymentData Repayment data.functionrepayLoan(LoanRepaymentData calldata _repaymentData) external;
/// @notice Call when a loan is past its due date./// @param _loanId Loan ID./// @param _loan Loan./// @return Liquidation Struct of the liquidation.functionliquidateLoan(uint256 _loanId, Loan calldata _loan) externalreturns (bytesmemory);
/// @return getMaxTranches Maximum number of tranches per loan.functiongetMaxTranches() externalviewreturns (uint256);
/// @notice Set min lock period (in BPS)./// @param _minLockPeriod Min lock period.functionsetMinLockPeriod(uint256 _minLockPeriod) external;
/// @notice Get min lock period (in BPS)./// @return minLockPeriod Min lock period.functiongetMinLockPeriod() externalviewreturns (uint256);
/// @notice Get delegation registry./// @return delegateRegistry Delegate registry.functiongetDelegateRegistry() externalviewreturns (address);
/// @notice Delegate ownership./// @param _loanId Loan ID./// @param _loan Loan./// @param _rights Delegation Rights. Empty for all./// @param _delegate Delegate address./// @param _value True if delegate, false if undelegate.functiondelegate(uint256 _loanId, Loan calldata _loan, address _delegate, bytes32 _rights, bool _value) external;
/// @notice Anyone can reveke a delegation on an NFT that's no longer in escrow./// @param _delegate Delegate address./// @param _collection Collection address./// @param _tokenId Token ID./// @param _rights Delegation Rights. Empty for all.functionrevokeDelegate(address _delegate, address _collection, uint256 _tokenId, bytes32 _rights) external;
/// @notice Get Flash Action Contract./// @return flashActionContract Flash Action Contract.functiongetFlashActionContract() externalviewreturns (address);
/// @notice Update Flash Action Contract./// @param _newFlashActionContract Flash Action Contract.functionsetFlashActionContract(address _newFlashActionContract) external;
/// @notice Get Loan Hash./// @param _loanId Loan ID./// @return loanHash Loan Hash.functiongetLoanHash(uint256 _loanId) externalviewreturns (bytes32);
/// @notice Transfer NFT to the flash action contract (expected use cases here are for airdrops and similar scenarios)./// The flash action contract would implement specific interactions with given contracts./// Only the the borrower can call this function for a given loan. By the end of the transaction, the NFT must have/// been returned to escrow./// @param _loanId Loan ID./// @param _loan Loan./// @param _target Target address for the flash action contract to interact with./// @param _data Data to be passed to be passed to the ultimate contract.functionexecuteFlashAction(uint256 _loanId, Loan calldata _loan, address _target, bytescalldata _data) external;
/// @notice Called by the liquidator for accounting purposes./// @param _loanId The id of the loan./// @param _loan The loan object.functionloanLiquidated(uint256 _loanId, Loan calldata _loan) external;
}
Contract Source Code
File 19 of 35: IMulticall.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;interfaceIMulticall{
errorMulticallFailed(uint256 i, bytes returndata);
/// @notice Call multiple functions in the contract. Revert if one of them fails, return results otherwise./// @param data Encoded function calls./// @return results The results of the function calls.functionmulticall(bytes[] calldata data) externalpayablereturns (bytes[] memory results);
}
Contract Source Code
File 20 of 35: INFTFlashAction.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;/// @title NFT Flash Action Interface/// @author Florida St/// @notice Interface for Flash Actions on NFTs in outstanding loans.interfaceINFTFlashAction{
errorInvalidOwnerError();
/// @notice Execute an arbitrary flash action on a given NFT. This contract owns it and must return it./// @param _collection The NFT collection./// @param _tokenId The NFT token ID./// @param _target The target contract./// @param _data The data to send to the target.functionexecute(address _collection, uint256 _tokenId, address _target, bytescalldata _data) external;
}
Contract Source Code
File 21 of 35: IOfferValidator.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.20;import"../loans/IMultiSourceLoan.sol";
/// @title Interface for Loan Offer Validators./// @author Florida St/// @notice Verify the given `_offer` is valid for `_tokenId` and `_validatorData`.interfaceIOfferValidator{
/// @notice Validate a loan offer.functionvalidateOffer(IMultiSourceLoan.LoanOffer calldata _offer, uint256 _tokenId, bytescalldata _validatorData)
externalview;
}
// SPDX-License-Identifier: MIT// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol)pragmasolidity ^0.8.20;/**
* @dev Standard math utilities missing in the Solidity language.
*/libraryMath{
/**
* @dev Muldiv operation overflow.
*/errorMathOverflowedMulDiv();
enumRounding {
Floor, // Toward negative infinity
Ceil, // Toward positive infinity
Trunc, // Toward zero
Expand // Away from zero
}
/**
* @dev Returns the addition of two unsigned integers, with an success flag (no overflow).
*/functiontryAdd(uint256 a, uint256 b) internalpurereturns (bool success, uint256 result) {
unchecked {
uint256 c = a + b;
if (c < a) return (false, 0);
return (true, c);
}
}
/**
* @dev Returns the subtraction of two unsigned integers, with an success flag (no overflow).
*/functiontrySub(uint256 a, uint256 b) internalpurereturns (bool success, uint256 result) {
unchecked {
if (b > a) return (false, 0);
return (true, a - b);
}
}
/**
* @dev Returns the multiplication of two unsigned integers, with an success flag (no overflow).
*/functiontryMul(uint256 a, uint256 b) internalpurereturns (bool success, uint256 result) {
unchecked {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the// benefit is lost if 'b' is also tested.// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522if (a ==0) return (true, 0);
uint256 c = a * b;
if (c / a != b) return (false, 0);
return (true, c);
}
}
/**
* @dev Returns the division of two unsigned integers, with a success flag (no division by zero).
*/functiontryDiv(uint256 a, uint256 b) internalpurereturns (bool success, uint256 result) {
unchecked {
if (b ==0) return (false, 0);
return (true, a / b);
}
}
/**
* @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).
*/functiontryMod(uint256 a, uint256 b) internalpurereturns (bool success, uint256 result) {
unchecked {
if (b ==0) return (false, 0);
return (true, a % b);
}
}
/**
* @dev Returns the largest of two numbers.
*/functionmax(uint256 a, uint256 b) internalpurereturns (uint256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two numbers.
*/functionmin(uint256 a, uint256 b) internalpurereturns (uint256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two numbers. The result is rounded towards
* zero.
*/functionaverage(uint256 a, uint256 b) internalpurereturns (uint256) {
// (a + b) / 2 can overflow.return (a & b) + (a ^ b) /2;
}
/**
* @dev Returns the ceiling of the division of two numbers.
*
* This differs from standard division with `/` in that it rounds towards infinity instead
* of rounding towards zero.
*/functionceilDiv(uint256 a, uint256 b) internalpurereturns (uint256) {
if (b ==0) {
// Guarantee the same behavior as in a regular Solidity division.return a / b;
}
// The following calculation ensures accurate ceiling division without overflow.// Since a is non-zero, (a - 1) / b will not overflow.// The largest possible result occurs when (a - 1) / b is type(uint256).max,// but the largest value we can obtain is type(uint256).max - 1, which happens// when a = type(uint256).max and b = 1.unchecked {
return a ==0 ? 0 : (a -1) / b +1;
}
}
/**
* @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or
* denominator == 0.
* @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by
* Uniswap Labs also under MIT license.
*/functionmulDiv(uint256 x, uint256 y, uint256 denominator) internalpurereturns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use// use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256// variables such that product = prod1 * 2^256 + prod0.uint256 prod0 = x * y; // Least significant 256 bits of the productuint256 prod1; // Most significant 256 bits of the productassembly {
let mm :=mulmod(x, y, not(0))
prod1 :=sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division.if (prod1 ==0) {
// Solidity will revert if denominator == 0, unlike the div opcode on its own.// The surrounding unchecked block does not change this fact.// See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.return prod0 / denominator;
}
// Make sure the result is less than 2^256. Also prevents denominator == 0.if (denominator <= prod1) {
revert MathOverflowedMulDiv();
}
///////////////////////////////////////////////// 512 by 256 division.///////////////////////////////////////////////// Make division exact by subtracting the remainder from [prod1 prod0].uint256 remainder;
assembly {
// Compute remainder using mulmod.
remainder :=mulmod(x, y, denominator)
// Subtract 256 bit number from 512 bit number.
prod1 :=sub(prod1, gt(remainder, prod0))
prod0 :=sub(prod0, remainder)
}
// Factor powers of two out of denominator and compute largest power of two divisor of denominator.// Always >= 1. See https://cs.stackexchange.com/q/138556/92363.uint256 twos = denominator & (0- denominator);
assembly {
// Divide denominator by twos.
denominator :=div(denominator, twos)
// Divide [prod1 prod0] by twos.
prod0 :=div(prod0, twos)
// Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
twos :=add(div(sub(0, twos), twos), 1)
}
// Shift in bits from prod1 into prod0.
prod0 |= prod1 * twos;
// Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such// that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for// four bits. That is, denominator * inv = 1 mod 2^4.uint256 inverse = (3* denominator) ^2;
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also// works in modular arithmetic, doubling the correct bits in each step.
inverse *=2- denominator * inverse; // inverse mod 2^8
inverse *=2- denominator * inverse; // inverse mod 2^16
inverse *=2- denominator * inverse; // inverse mod 2^32
inverse *=2- denominator * inverse; // inverse mod 2^64
inverse *=2- denominator * inverse; // inverse mod 2^128
inverse *=2- denominator * inverse; // inverse mod 2^256// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.// This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is// less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1// is no longer required.
result = prod0 * inverse;
return result;
}
}
/**
* @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
*/functionmulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internalpurereturns (uint256) {
uint256 result = mulDiv(x, y, denominator);
if (unsignedRoundsUp(rounding) &&mulmod(x, y, denominator) >0) {
result +=1;
}
return result;
}
/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded
* towards zero.
*
* Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
*/functionsqrt(uint256 a) internalpurereturns (uint256) {
if (a ==0) {
return0;
}
// For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.//// We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have// `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.//// This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`// → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`// → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`//// Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.uint256 result =1<< (log2(a) >>1);
// At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,// since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at// every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision// into the expected uint128 result.unchecked {
result = (result + a / result) >>1;
result = (result + a / result) >>1;
result = (result + a / result) >>1;
result = (result + a / result) >>1;
result = (result + a / result) >>1;
result = (result + a / result) >>1;
result = (result + a / result) >>1;
return min(result, a / result);
}
}
/**
* @notice Calculates sqrt(a), following the selected rounding direction.
*/functionsqrt(uint256 a, Rounding rounding) internalpurereturns (uint256) {
unchecked {
uint256 result = sqrt(a);
return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0);
}
}
/**
* @dev Return the log in base 2 of a positive value rounded towards zero.
* Returns 0 if given 0.
*/functionlog2(uint256 value) internalpurereturns (uint256) {
uint256 result =0;
unchecked {
if (value >>128>0) {
value >>=128;
result +=128;
}
if (value >>64>0) {
value >>=64;
result +=64;
}
if (value >>32>0) {
value >>=32;
result +=32;
}
if (value >>16>0) {
value >>=16;
result +=16;
}
if (value >>8>0) {
value >>=8;
result +=8;
}
if (value >>4>0) {
value >>=4;
result +=4;
}
if (value >>2>0) {
value >>=2;
result +=2;
}
if (value >>1>0) {
result +=1;
}
}
return result;
}
/**
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/functionlog2(uint256 value, Rounding rounding) internalpurereturns (uint256) {
unchecked {
uint256 result =log2(value);
return result + (unsignedRoundsUp(rounding) &&1<< result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 10 of a positive value rounded towards zero.
* Returns 0 if given 0.
*/functionlog10(uint256 value) internalpurereturns (uint256) {
uint256 result =0;
unchecked {
if (value >=10**64) {
value /=10**64;
result +=64;
}
if (value >=10**32) {
value /=10**32;
result +=32;
}
if (value >=10**16) {
value /=10**16;
result +=16;
}
if (value >=10**8) {
value /=10**8;
result +=8;
}
if (value >=10**4) {
value /=10**4;
result +=4;
}
if (value >=10**2) {
value /=10**2;
result +=2;
}
if (value >=10**1) {
result +=1;
}
}
return result;
}
/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/functionlog10(uint256 value, Rounding rounding) internalpurereturns (uint256) {
unchecked {
uint256 result = log10(value);
return result + (unsignedRoundsUp(rounding) &&10** result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 256 of a positive value rounded towards zero.
* Returns 0 if given 0.
*
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
*/functionlog256(uint256 value) internalpurereturns (uint256) {
uint256 result =0;
unchecked {
if (value >>128>0) {
value >>=128;
result +=16;
}
if (value >>64>0) {
value >>=64;
result +=8;
}
if (value >>32>0) {
value >>=32;
result +=4;
}
if (value >>16>0) {
value >>=16;
result +=2;
}
if (value >>8>0) {
result +=1;
}
}
return result;
}
/**
* @dev Return the log in base 256, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/functionlog256(uint256 value, Rounding rounding) internalpurereturns (uint256) {
unchecked {
uint256 result = log256(value);
return result + (unsignedRoundsUp(rounding) &&1<< (result <<3) < value ? 1 : 0);
}
}
/**
* @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.
*/functionunsignedRoundsUp(Rounding rounding) internalpurereturns (bool) {
returnuint8(rounding) %2==1;
}
}
Contract Source Code
File 26 of 35: MessageHashUtils.sol
// SPDX-License-Identifier: MIT// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MessageHashUtils.sol)pragmasolidity ^0.8.20;import {Strings} from"../Strings.sol";
/**
* @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing.
*
* The library provides methods for generating a hash of a message that conforms to the
* https://eips.ethereum.org/EIPS/eip-191[ERC-191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712]
* specifications.
*/libraryMessageHashUtils{
/**
* @dev Returns the keccak256 digest of an ERC-191 signed data with version
* `0x45` (`personal_sign` messages).
*
* The digest is calculated by prefixing a bytes32 `messageHash` with
* `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the
* hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method.
*
* NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with
* keccak256, although any bytes32 value can be safely used because the final digest will
* be re-hashed.
*
* See {ECDSA-recover}.
*/functiontoEthSignedMessageHash(bytes32 messageHash) internalpurereturns (bytes32 digest) {
/// @solidity memory-safe-assemblyassembly {
mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHashmstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix
digest :=keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20)
}
}
/**
* @dev Returns the keccak256 digest of an ERC-191 signed data with version
* `0x45` (`personal_sign` messages).
*
* The digest is calculated by prefixing an arbitrary `message` with
* `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the
* hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method.
*
* See {ECDSA-recover}.
*/functiontoEthSignedMessageHash(bytesmemory message) internalpurereturns (bytes32) {
returnkeccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message));
}
/**
* @dev Returns the keccak256 digest of an ERC-191 signed data with version
* `0x00` (data with intended validator).
*
* The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended
* `validator` address. Then hashing the result.
*
* See {ECDSA-recover}.
*/functiontoDataWithIntendedValidatorHash(address validator, bytesmemory data) internalpurereturns (bytes32) {
returnkeccak256(abi.encodePacked(hex"19_00", validator, data));
}
/**
* @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`).
*
* The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with
* `\x19\x01` and hashing the result. It corresponds to the hash signed by the
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712.
*
* See {ECDSA-recover}.
*/functiontoTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internalpurereturns (bytes32 digest) {
/// @solidity memory-safe-assemblyassembly {
let ptr :=mload(0x40)
mstore(ptr, hex"19_01")
mstore(add(ptr, 0x02), domainSeparator)
mstore(add(ptr, 0x22), structHash)
digest :=keccak256(ptr, 0x42)
}
}
}
Contract Source Code
File 27 of 35: MultiSourceLoan.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"@delegate/IDelegateRegistry.sol";
import"@openzeppelin/utils/cryptography/ECDSA.sol";
import"@solmate/tokens/ERC20.sol";
import"@solmate/tokens/ERC721.sol";
import"@solmate/utils/FixedPointMathLib.sol";
import"@solmate/utils/ReentrancyGuard.sol";
import"@solmate/utils/SafeTransferLib.sol";
import"../../interfaces/validators/IOfferValidator.sol";
import"../../interfaces/INFTFlashAction.sol";
import"../../interfaces/loans/ILoanManager.sol";
import"../../interfaces/loans/ILoanManagerRegistry.sol";
import"../../interfaces/loans/IMultiSourceLoan.sol";
import"../utils/Hash.sol";
import"../utils/Interest.sol";
import"../Multicall.sol";
import"./BaseLoan.sol";
/// @title MultiSourceLoan (v3)/// @author Florida St/// @notice Loan contract that allows for multiple tranches with different/// seniorities. Each loan is collateralized by an NFT. Loans have a duration,/// principal, and APR. Loans can be refinanced automatically by lenders (if terms/// are improved). Borrowers can also get renegotiation offers which they can then/// accept. If a loan is not repaid by its end time, it's considered to have defaulted./// If it had only one lender behind it, then the lender (unless it's a pool), can claim/// the collateral. If there are multiple lenders or the sole lender is a pool, then there's/// a liquidation process (run by an instance of `ILoanLiquidator`).contractMultiSourceLoanisIMultiSourceLoan, Multicall, ReentrancyGuard, BaseLoan{
usingFixedPointMathLibforuint256;
usingHashforExecutionData;
usingHashforLoan;
usingHashforLoanOffer;
usingHashforSignableRepaymentData;
usingHashforRenegotiationOffer;
usingInputCheckerforaddress;
usingInterestforuint256;
usingECDSAforbytes32;
usingMessageHashUtilsforbytes32;
usingSafeTransferLibforERC20;
/// @notice Loan Id to hashmapping(uint256 loanId =>bytes32 loanHash) private _loans;
/// This is used in _getMinTranchePrincipal.uint256privateconstant _MAX_RATIO_TRANCHE_MIN_PRINCIPAL =2;
/// @notice Maximum number of tranches per loanuint256publicimmutable getMaxTranches;
/// @notice delegate registryaddresspublicimmutable getDelegateRegistry;
/// @notice Contract to execute flash actions.addresspublic getFlashActionContract;
/// @notice Loan manager registry (we currently have Gondi's pools)
ILoanManagerRegistry publicimmutable getLoanManagerRegistry;
/// @notice Min lock period for a trancheuint256private _minLockPeriod;
errorInvalidParametersError();
errorMismatchError();
errorInvalidCollateralIdError();
errorInvalidMethodError();
errorInvalidAddressesError();
errorInvalidCallerError();
errorInvalidTrancheError();
errorInvalidRenegotiationOfferError();
errorTooManyTranchesError();
errorLoanExpiredError();
errorNFTNotReturnedError();
errorTrancheCannotBeRefinancedError(uint256 minTimestamp);
errorLoanLockedError();
/// @param loanLiquidator Address of the liquidator contract./// @param protocolFee Protocol fee charged on gains./// @param currencyManager Address of the currency manager./// @param collectionManager Address of the collection manager./// @param maxTranches Maximum number of tranches per loan./// @param minLockPeriod Minimum lock period for a tranche/loan./// @param delegateRegistry Address of the delegate registry (Delegate.xyz)./// @param loanManagerRegistry Address of the loan manager registry./// @param flashActionContract Address of the flash action contract./// @param minWaitTime The time to wait before a new owner can be set.constructor(address loanLiquidator,
ProtocolFee memory protocolFee,
address currencyManager,
address collectionManager,
uint256 maxTranches,
uint256 minLockPeriod,
address delegateRegistry,
address loanManagerRegistry,
address flashActionContract,
uint256 minWaitTime
)
BaseLoan("GONDI_MULTI_SOURCE_LOAN",
currencyManager,
collectionManager,
protocolFee,
loanLiquidator,
tx.origin,
minWaitTime
)
{
loanLiquidator.checkNotZero();
_minLockPeriod = minLockPeriod;
getMaxTranches = maxTranches;
getDelegateRegistry = delegateRegistry;
getFlashActionContract = flashActionContract;
getLoanManagerRegistry = ILoanManagerRegistry(loanManagerRegistry);
}
/// @inheritdoc IMultiSourceLoanfunctionemitLoan(LoanExecutionData calldata _loanExecutionData)
externalnonReentrantreturns (uint256, Loan memory)
{
address borrower = _loanExecutionData.borrower;
ExecutionData calldata executionData = _loanExecutionData.executionData;
(address principalAddress, address nftCollateralAddress) = _getAddressesFromExecutionData(executionData);
OfferExecution[] calldata offerExecution = executionData.offerExecution;
_validateExecutionData(_loanExecutionData, borrower);
_checkWhitelists(principalAddress, nftCollateralAddress);
(uint256 loanId, uint256[] memory offerIds, Loan memory loan, uint256 totalFee) =
_processOffersFromExecutionData(
borrower,
executionData.principalReceiver,
principalAddress,
nftCollateralAddress,
executionData.tokenId,
executionData.duration,
offerExecution
);
if (_hasCallback(executionData.callbackData)) {
handleAfterPrincipalTransferCallback(loan, msg.sender, executionData.callbackData, totalFee);
}
ERC721(nftCollateralAddress).transferFrom(borrower, address(this), executionData.tokenId);
_loans[loanId] = loan.hash();
emit LoanEmitted(loanId, offerIds, loan, totalFee);
return (loanId, loan);
}
/// @inheritdoc IMultiSourceLoanfunctionrefinanceFull(
RenegotiationOffer calldata _renegotiationOffer,
Loan memory _loan,
bytescalldata _renegotiationOfferSignature
) externalnonReentrantreturns (uint256, Loan memory) {
_baseLoanChecks(_renegotiationOffer.loanId, _loan);
_baseRenegotiationChecks(_renegotiationOffer, _loan);
if (_renegotiationOffer.trancheIndex.length!= _loan.tranche.length) {
revert InvalidRenegotiationOfferError();
}
bool lenderInitiated =msg.sender== _renegotiationOffer.lender;
uint256 netNewLender = _renegotiationOffer.principalAmount - _renegotiationOffer.fee;
uint256 totalAccruedInterest;
uint256 totalAnnualInterest;
/// @dev If it's lender initiated, needs to be strictly better.if (lenderInitiated) {
(totalAccruedInterest, totalAnnualInterest,) =
_processOldTranchesFull(_renegotiationOffer, _loan, lenderInitiated, 0);
if (_isLoanLocked(_loan.startTime, _loan.duration)) {
revert LoanLockedError();
}
_checkStrictlyBetter(
_renegotiationOffer.principalAmount,
_loan.principalAmount,
_renegotiationOffer.duration +block.timestamp,
_loan.duration + _loan.startTime,
_renegotiationOffer.aprBps,
totalAnnualInterest / _loan.principalAmount,
_renegotiationOffer.fee
);
if (_renegotiationOffer.principalAmount > _loan.principalAmount) {
ERC20(_loan.principalAddress).safeTransferFrom(
_renegotiationOffer.lender,
_loan.borrower,
_renegotiationOffer.principalAmount - _loan.principalAmount
);
}
} elseif (msg.sender!= _loan.borrower) {
revert InvalidCallerError();
} else {
(totalAccruedInterest, totalAnnualInterest, netNewLender) =
_processOldTranchesFull(_renegotiationOffer, _loan, lenderInitiated, netNewLender);
/// @notice Borrowers clears interest
_checkSignature(_renegotiationOffer.lender, _renegotiationOffer.hash(), _renegotiationOfferSignature);
if (netNewLender >0) {
ERC20(_loan.principalAddress).safeTransferFrom(_renegotiationOffer.lender, _loan.borrower, netNewLender);
}
totalAccruedInterest =0;
}
uint256 newLoanId = _getAndSetNewLoanId();
Tranche[] memory newTranche =new Tranche[](1);
newTranche[0] = Tranche(
newLoanId,
0,
_renegotiationOffer.principalAmount,
_renegotiationOffer.lender,
totalAccruedInterest,
block.timestamp,
_renegotiationOffer.aprBps
);
_loan.tranche = newTranche;
_loan.startTime =block.timestamp;
_loan.duration = _renegotiationOffer.duration;
_loan.principalAmount = _renegotiationOffer.principalAmount;
_loans[newLoanId] = _loan.hash();
delete _loans[_renegotiationOffer.loanId];
emit LoanRefinanced(
_renegotiationOffer.renegotiationId, _renegotiationOffer.loanId, newLoanId, _loan, _renegotiationOffer.fee
);
return (newLoanId, _loan);
}
/// @inheritdoc IMultiSourceLoanfunctionrefinancePartial(RenegotiationOffer calldata _renegotiationOffer, Loan memory _loan)
externalnonReentrantreturns (uint256, Loan memory)
{
if (msg.sender!= _renegotiationOffer.lender) {
revert InvalidCallerError();
}
if (_isLoanLocked(_loan.startTime, _loan.duration)) {
revert LoanLockedError();
}
if (_renegotiationOffer.trancheIndex.length==0) {
revert InvalidRenegotiationOfferError();
}
uint256 loanId = _renegotiationOffer.loanId;
_baseLoanChecks(loanId, _loan);
_baseRenegotiationChecks(_renegotiationOffer, _loan);
uint256 newLoanId = _getAndSetNewLoanId();
uint256 totalProtocolFee;
uint256 totalAnnualInterest;
uint256 totalRefinanced;
/// @dev bring to memuint256 minImprovementApr = _minImprovementApr;
/// @dev We iterate over all tranches to execute repayments.uint256 totalTranchesRenegotiated = _renegotiationOffer.trancheIndex.length;
for (uint256 i; i < totalTranchesRenegotiated;) {
uint256 index = _renegotiationOffer.trancheIndex[i];
if (index >= _loan.tranche.length) {
revert InvalidRenegotiationOfferError();
}
Tranche memory tranche = _loan.tranche[index];
_checkTrancheStrictly(true, tranche.aprBps, _renegotiationOffer.aprBps, minImprovementApr);
(uint256 accruedInterest, uint256 thisProtocolFee,) = _processOldTranche(
_renegotiationOffer.lender,
_loan.borrower,
_loan.principalAddress,
tranche,
_loan.startTime + _loan.duration,
_loan.protocolFee,
type(uint256).max
);
unchecked {
totalRefinanced += tranche.principalAmount;
totalAnnualInterest += tranche.principalAmount * tranche.aprBps;
totalProtocolFee += thisProtocolFee;
}
tranche.loanId = newLoanId;
tranche.lender = _renegotiationOffer.lender;
tranche.accruedInterest = accruedInterest;
tranche.startTime =block.timestamp;
tranche.aprBps = _renegotiationOffer.aprBps;
unchecked {
++i;
}
}
if (_renegotiationOffer.principalAmount != totalRefinanced) {
revert InvalidRenegotiationOfferError();
}
_handleProtocolFeeForFee(
_loan.principalAddress, _renegotiationOffer.lender, totalProtocolFee, _protocolFee.recipient
);
_loans[newLoanId] = _loan.hash();
delete _loans[loanId];
/// @dev Here reneg fee is always 0emit LoanRefinanced(_renegotiationOffer.renegotiationId, loanId, newLoanId, _loan, 0);
return (newLoanId, _loan);
}
/// @inheritdoc IMultiSourceLoanfunctionrefinanceFromLoanExecutionData(uint256 _loanId,
Loan calldata _loan,
LoanExecutionData calldata _loanExecutionData
) externalnonReentrantreturns (uint256, Loan memory) {
if (msg.sender!= _loan.borrower) {
revert InvalidCallerError();
}
_baseLoanChecks(_loanId, _loan);
ExecutionData calldata executionData = _loanExecutionData.executionData;
/// @dev We ignore the borrower in executionData, and used existing one.address borrower = _loan.borrower;
(address principalAddress, address nftCollateralAddress) = _getAddressesFromExecutionData(executionData);
OfferExecution[] calldata offerExecution = executionData.offerExecution;
_validateExecutionData(_loanExecutionData, borrower);
_checkWhitelists(principalAddress, nftCollateralAddress);
if (_loan.principalAddress != principalAddress || _loan.nftCollateralAddress != nftCollateralAddress) {
revert InvalidAddressesError();
}
if (_loan.nftCollateralTokenId != executionData.tokenId) {
revert InvalidCollateralIdError();
}
/// @dev We first process the incoming offers so borrower gets the capital. After that, we process repayments./// NFT doesn't need to be transfered (it was already in escrow)
(uint256 newLoanId, uint256[] memory offerIds, Loan memory loan, uint256 totalFee) =
_processOffersFromExecutionData(
borrower,
executionData.principalReceiver,
principalAddress,
nftCollateralAddress,
executionData.tokenId,
executionData.duration,
offerExecution
);
_processRepayments(_loan);
emit LoanRefinancedFromNewOffers(_loanId, newLoanId, loan, offerIds, totalFee);
_loans[newLoanId] = loan.hash();
delete _loans[_loanId];
return (newLoanId, loan);
}
/// @inheritdoc IMultiSourceLoanfunctionaddNewTranche(
RenegotiationOffer calldata _renegotiationOffer,
Loan memory _loan,
bytescalldata _renegotiationOfferSignature
) externalnonReentrantreturns (uint256, Loan memory) {
if (msg.sender!= _loan.borrower) {
revert InvalidCallerError();
}
uint256 loanId = _renegotiationOffer.loanId;
_baseLoanChecks(loanId, _loan);
_baseRenegotiationChecks(_renegotiationOffer, _loan);
_checkSignature(_renegotiationOffer.lender, _renegotiationOffer.hash(), _renegotiationOfferSignature);
if (_renegotiationOffer.trancheIndex.length!=1|| _renegotiationOffer.trancheIndex[0] != _loan.tranche.length)
{
revert InvalidRenegotiationOfferError();
}
if (_loan.tranche.length== getMaxTranches) {
revert TooManyTranchesError();
}
uint256 newLoanId = _getAndSetNewLoanId();
Loan memory loanWithTranche = _addNewTranche(newLoanId, _loan, _renegotiationOffer);
_loans[newLoanId] = loanWithTranche.hash();
delete _loans[loanId];
ERC20(_loan.principalAddress).safeTransferFrom(
_renegotiationOffer.lender, _loan.borrower, _renegotiationOffer.principalAmount - _renegotiationOffer.fee
);
if (_renegotiationOffer.fee !=0) {
/// @dev Cached
ERC20(_loan.principalAddress).safeTransferFrom(
_renegotiationOffer.lender,
_protocolFee.recipient,
_renegotiationOffer.fee.mulDivUp(_loan.protocolFee, _PRECISION)
);
}
emit LoanRefinanced(
_renegotiationOffer.renegotiationId, loanId, newLoanId, loanWithTranche, _renegotiationOffer.fee
);
return (newLoanId, loanWithTranche);
}
/// @inheritdoc IMultiSourceLoanfunctionrepayLoan(LoanRepaymentData calldata _repaymentData) externaloverridenonReentrant{
uint256 loanId = _repaymentData.data.loanId;
Loan calldata loan = _repaymentData.loan;
/// @dev If the caller is not the borrower itself, check the signature to avoid someone else forcing an unwanted repayment.if (msg.sender!= loan.borrower) {
_checkSignature(loan.borrower, _repaymentData.data.hash(), _repaymentData.borrowerSignature);
}
_baseLoanChecks(loanId, loan);
/// @dev Unlikely this is used outside of the callback with a seaport sell, but leaving here in case that's not correct.if (_repaymentData.data.shouldDelegate) {
IDelegateRegistry(getDelegateRegistry).delegateERC721(
loan.borrower, loan.nftCollateralAddress, loan.nftCollateralTokenId, bytes32(""), true
);
}
ERC721(loan.nftCollateralAddress).transferFrom(address(this), loan.borrower, loan.nftCollateralTokenId);
/// @dev After returning the NFT to the borrower, check if there's an action to be taken (eg: sell it to cover repayment).if (_hasCallback(_repaymentData.data.callbackData)) {
handleAfterNFTTransferCallback(loan, msg.sender, _repaymentData.data.callbackData);
}
(uint256 totalRepayment, uint256 totalProtocolFee) = _processRepayments(loan);
emit LoanRepaid(loanId, totalRepayment, totalProtocolFee);
/// @dev Reclaim space.delete _loans[loanId];
}
/// @inheritdoc IMultiSourceLoanfunctionliquidateLoan(uint256 _loanId, Loan calldata _loan)
externaloverridenonReentrantreturns (bytesmemory)
{
if (_loan.hash() != _loans[_loanId]) {
revert InvalidLoanError(_loanId);
}
(bool liquidated, bytesmemory liquidation) = _liquidateLoan(
_loanId, _loan, _loan.tranche.length==1&&!getLoanManagerRegistry.isLoanManager(_loan.tranche[0].lender)
);
if (liquidated) {
delete _loans[_loanId];
}
return liquidation;
}
/// @inheritdoc IMultiSourceLoanfunctionloanLiquidated(uint256 _loanId, Loan calldata _loan) externaloverrideonlyLiquidator{
if (_loan.hash() != _loans[_loanId]) {
revert InvalidLoanError(_loanId);
}
emit LoanLiquidated(_loanId);
/// @dev Reclaim space.delete _loans[_loanId];
}
/// @inheritdoc IMultiSourceLoanfunctiondelegate(uint256 _loanId, Loan calldata loan, address _delegate, bytes32 _rights, bool _value) external{
if (loan.hash() != _loans[_loanId]) {
revert InvalidLoanError(_loanId);
}
if (msg.sender!= loan.borrower) {
revert InvalidCallerError();
}
IDelegateRegistry(getDelegateRegistry).delegateERC721(
_delegate, loan.nftCollateralAddress, loan.nftCollateralTokenId, _rights, _value
);
emit Delegated(_loanId, _delegate, _rights, _value);
}
/// @inheritdoc IMultiSourceLoanfunctionrevokeDelegate(address _delegate, address _collection, uint256 _tokenId, bytes32 _rights) external{
if (ERC721(_collection).ownerOf(_tokenId) ==address(this)) {
revert InvalidMethodError();
}
IDelegateRegistry(getDelegateRegistry).delegateERC721(_delegate, _collection, _tokenId, _rights, false);
emit RevokeDelegate(_delegate, _collection, _tokenId, _rights);
}
/// @inheritdoc IMultiSourceLoanfunctiongetMinLockPeriod() externalviewreturns (uint256) {
return _minLockPeriod;
}
/// @inheritdoc IMultiSourceLoanfunctionsetMinLockPeriod(uint256 __minLockPeriod) externalonlyOwner{
_minLockPeriod = __minLockPeriod;
emit MinLockPeriodUpdated(__minLockPeriod);
}
/// @inheritdoc IMultiSourceLoanfunctiongetLoanHash(uint256 _loanId) externalviewreturns (bytes32) {
return _loans[_loanId];
}
/// @inheritdoc IMultiSourceLoanfunctionexecuteFlashAction(uint256 _loanId, Loan calldata _loan, address _target, bytescalldata _data)
externalnonReentrant{
if (_loan.hash() != _loans[_loanId]) {
revert InvalidLoanError(_loanId);
}
if (msg.sender!= _loan.borrower) {
revert InvalidCallerError();
}
address flashActionContract = getFlashActionContract;
ERC721(_loan.nftCollateralAddress).transferFrom(address(this), flashActionContract, _loan.nftCollateralTokenId);
INFTFlashAction(flashActionContract).execute(
_loan.nftCollateralAddress, _loan.nftCollateralTokenId, _target, _data
);
if (ERC721(_loan.nftCollateralAddress).ownerOf(_loan.nftCollateralTokenId) !=address(this)) {
revert NFTNotReturnedError();
}
emit FlashActionExecuted(_loanId, _target, _data);
}
/// @inheritdoc IMultiSourceLoanfunctionsetFlashActionContract(address _newFlashActionContract) externalonlyOwner{
getFlashActionContract = _newFlashActionContract;
emit FlashActionContractUpdated(_newFlashActionContract);
}
/// @notice Process repayments for tranches upon a full renegotiation./// @param _renegotiationOffer The renegotiation offer./// @param _loan The loan to be processed./// @param _isStrictlyBetter Whether the new tranche needs to be strictly better than all previous ones./// @param _remainingNewLender Amount left for new lender to payfunction_processOldTranchesFull(
RenegotiationOffer calldata _renegotiationOffer,
Loan memory _loan,
bool _isStrictlyBetter,
uint256 _remainingNewLender
) privatereturns (uint256 totalAccruedInterest, uint256 totalAnnualInterest, uint256 remainingNewLender) {
uint256 totalProtocolFee = _renegotiationOffer.fee.mulDivUp(_loan.protocolFee, _PRECISION);
unchecked {
_remainingNewLender += totalProtocolFee;
/// @dev bring to memuint256 minImprovementApr = _minImprovementApr;
remainingNewLender = _isStrictlyBetter ? type(uint256).max : _remainingNewLender;
// We iterate first for the new lender and then for the rest.// This way if he is owed some principal,// it's discounted before transfering to the other lendersfor (uint256 i =0; i < _loan.tranche.length<<1;) {
Tranche memory tranche = _loan.tranche[i % _loan.tranche.length];
bool onlyNewLenderPass = i < _loan.tranche.length;
bool isNewLender = tranche.lender == _renegotiationOffer.lender;
++i;
if (onlyNewLenderPass != isNewLender) continue;
uint256 accruedInterest;
uint256 thisProtocolFee;
(accruedInterest, thisProtocolFee, remainingNewLender) = _processOldTranche(
_renegotiationOffer.lender,
_loan.borrower,
_loan.principalAddress,
tranche,
_loan.startTime + _loan.duration,
_loan.protocolFee,
remainingNewLender
);
_checkTrancheStrictly(_isStrictlyBetter, tranche.aprBps, _renegotiationOffer.aprBps, minImprovementApr);
totalAnnualInterest += tranche.principalAmount * tranche.aprBps;
totalAccruedInterest += accruedInterest;
totalProtocolFee += thisProtocolFee;
}
uint256 lenderFee = remainingNewLender > totalProtocolFee ? totalProtocolFee : remainingNewLender;
uint256 borrowerFee = totalProtocolFee - lenderFee;
_handleProtocolFeeForFee(
_loan.principalAddress, _renegotiationOffer.lender, lenderFee, _protocolFee.recipient
);
_handleProtocolFeeForFee(_loan.principalAddress, _loan.borrower, borrowerFee, _protocolFee.recipient);
remainingNewLender -= lenderFee;
}
}
/// @notice Process the current source tranche during a renegotiation./// @param _lender The new lender./// @param _borrower The borrower of the loan./// @param _principalAddress The principal address of the loan./// @param _tranche The tranche to be processed./// @param _endTime The end time of the loan./// @param _protocolFeeFraction The protocol fee fraction./// @param _remainingNewLender The amount left for the new lender to pay./// @return accruedInterest The accrued interest paid./// @return thisProtocolFee The protocol fee paid for this tranche./// @return remainingNewLender The amount left for the new lender to pay.function_processOldTranche(address _lender,
address _borrower,
address _principalAddress,
Tranche memory _tranche,
uint256 _endTime,
uint256 _protocolFeeFraction,
uint256 _remainingNewLender
) privatereturns (uint256 accruedInterest, uint256 thisProtocolFee, uint256 remainingNewLender) {
uint256 unlockTime = _getUnlockedTime(_tranche.startTime, _endTime);
if (unlockTime >block.timestamp) {
revert TrancheCannotBeRefinancedError(unlockTime);
}
unchecked {
accruedInterest =
_tranche.principalAmount.getInterest(_tranche.aprBps, block.timestamp- _tranche.startTime);
thisProtocolFee = accruedInterest.mulDivUp(_protocolFeeFraction, _PRECISION);
accruedInterest += _tranche.accruedInterest;
}
if (getLoanManagerRegistry.isLoanManager(_tranche.lender)) {
ILoanManager(_tranche.lender).loanRepayment(
_tranche.loanId,
_tranche.principalAmount,
_tranche.aprBps,
_tranche.accruedInterest,
_protocolFeeFraction,
_tranche.startTime
);
}
uint256 oldLenderDebt;
unchecked {
oldLenderDebt = _tranche.principalAmount + accruedInterest - thisProtocolFee;
}
ERC20 asset = ERC20(_principalAddress);
if (oldLenderDebt > _remainingNewLender) {
/// @dev already checked in the condition
asset.safeTransferFrom(_borrower, _tranche.lender, oldLenderDebt - _remainingNewLender);
oldLenderDebt = _remainingNewLender;
}
if (oldLenderDebt >0) {
if (_lender != _tranche.lender) {
asset.safeTransferFrom(_lender, _tranche.lender, oldLenderDebt);
}
/// @dev oldLenderDebt < _remainingNewLender because it would enter previous condition if not and set to _remainingNewLenderunchecked {
_remainingNewLender -= oldLenderDebt;
}
}
remainingNewLender = _remainingNewLender;
}
/// @notice Basic loan checks (check if the hash is correct) + whether loan is still active./// @param _loanId The loan ID./// @param _loan The loan to be checked.function_baseLoanChecks(uint256 _loanId, Loan memory _loan) privateview{
if (_loan.hash() != _loans[_loanId]) {
revert InvalidLoanError(_loanId);
}
if (_loan.startTime + _loan.duration <=block.timestamp) {
revert LoanExpiredError();
}
}
/// @notice Basic renegotiation checks. Check basic parameters + expiration + whether the offer is active.function_baseRenegotiationChecks(RenegotiationOffer calldata _renegotiationOffer, Loan memory _loan)
privateview{
if (
(_renegotiationOffer.principalAmount ==0)
|| (_loan.tranche.length< _renegotiationOffer.trancheIndex.length)
) {
revert InvalidRenegotiationOfferError();
}
if (block.timestamp> _renegotiationOffer.expirationTime) {
revert ExpiredOfferError(_renegotiationOffer.expirationTime);
}
uint256 renegotiationId = _renegotiationOffer.renegotiationId;
address lender = _renegotiationOffer.lender;
if (isRenegotiationOfferCancelled[lender][renegotiationId]) {
revert CancelledOrExecutedOfferError(lender, renegotiationId);
}
}
/// @notice Protocol fee for fees charged on offers/renegotationOffers./// @param _principalAddress The principal address of the loan./// @param _lender The lender of the loan./// @param _fee The fee to be charged./// @param _feeRecipient The protocol fee recipient.function_handleProtocolFeeForFee(address _principalAddress, address _lender, uint256 _fee, address _feeRecipient)
private{
if (_fee !=0) {
ERC20(_principalAddress).safeTransferFrom(_lender, _feeRecipient, _fee);
}
}
/// @notice Check condition for strictly better tranches/// @param _isStrictlyBetter Whether the new tranche needs to be strictly better than the old one./// @param _currentAprBps The current apr of the tranche./// @param _targetAprBps The target apr of the tranche./// @param __minImprovementApr The minimum improvement in APR.function_checkTrancheStrictly(bool _isStrictlyBetter,
uint256 _currentAprBps,
uint256 _targetAprBps,
uint256 __minImprovementApr
) privatepure{
/// @dev If _isStrictlyBetter is set, and the new apr is higher, then it'll underflow.if (
_isStrictlyBetter
&& ((_currentAprBps - _targetAprBps).mulDivDown(_PRECISION, _currentAprBps) < __minImprovementApr)
) {
revert InvalidRenegotiationOfferError();
}
}
/// @dev Tranches are locked from any refi after they are initiated for some time.function_getUnlockedTime(uint256 _trancheStartTime, uint256 _loanEndTime) privateviewreturns (uint256) {
uint256 delta;
unchecked {
delta = _loanEndTime - _trancheStartTime;
}
return _trancheStartTime + delta.mulDivUp(_minLockPeriod, _PRECISION);
}
/// @dev Loans are locked from lender initiated refis in the end.function_isLoanLocked(uint256 _loanStartTime, uint256 _loanDuration) privateviewreturns (bool) {
unchecked {
/// @dev doesn't overflow because _minLockPeriod should be < 1returnblock.timestamp> _loanStartTime + _loanDuration - _loanDuration.mulDivUp(_minLockPeriod, _PRECISION);
}
}
/// @notice Base ExecutionData Checks/// @dev Note that we do not validate fee < principalAmount since this is done in the child class in this case./// @param _offerExecution The offer execution./// @param _tokenId The token ID./// @param _lender The lender./// @param _duration The duration./// @param _lenderOfferSignature The signature of the lender of LoanOffer./// @param _feeFraction The protocol fee fraction./// @param _totalAmount The total amount ahead.function_validateOfferExecution(
OfferExecution calldata _offerExecution,
uint256 _tokenId,
address _lender,
uint256 _duration,
bytescalldata _lenderOfferSignature,
uint256 _feeFraction,
uint256 _totalAmount
) private{
LoanOffer calldata offer = _offerExecution.offer;
address lender = offer.lender;
uint256 offerId = offer.offerId;
uint256 totalAmountAfterExecution = _offerExecution.amount + _totalAmount;
if (lender.code.length!=0&& getLoanManagerRegistry.isLoanManager(lender)) {
ILoanManager(lender).validateOffer(_tokenId, abi.encode(_offerExecution), _feeFraction);
} else {
_checkSignature(lender, offer.hash(), _lenderOfferSignature);
}
if (block.timestamp> offer.expirationTime) {
revert ExpiredOfferError(offer.expirationTime);
}
if (isOfferCancelled[_lender][offerId] || (offerId <= minOfferId[_lender])) {
revert CancelledOrExecutedOfferError(_lender, offerId);
}
if (totalAmountAfterExecution > offer.principalAmount) {
revert InvalidAmountError(totalAmountAfterExecution, offer.principalAmount);
}
if (offer.duration ==0|| _duration > offer.duration) {
revert InvalidDurationError();
}
if (offer.aprBps ==0) {
revert ZeroInterestError();
}
if ((offer.capacity !=0) && (_used[_lender][offer.offerId] + _offerExecution.amount > offer.capacity)) {
revert MaxCapacityExceededError();
}
_checkValidators(_offerExecution.offer, _tokenId);
}
/// @notice Basic checks (expiration / signature if diff than borrower) for execution data.function_validateExecutionData(LoanExecutionData calldata _executionData, address _borrower) privateview{
if (msg.sender!= _borrower) {
_checkSignature(_borrower, _executionData.executionData.hash(), _executionData.borrowerOfferSignature);
}
if (block.timestamp> _executionData.executionData.expirationTime) {
revert ExpiredOfferError(_executionData.executionData.expirationTime);
}
if (_executionData.executionData.offerExecution.length> getMaxTranches) {
revert TooManyTranchesError();
}
}
/// @notice Extract addresses from first offer. Used for validations./// @param _executionData Execution data./// @return principalAddress Address of the principal token./// @return nftCollateralAddress Address of the NFT collateral.function_getAddressesFromExecutionData(ExecutionData calldata _executionData)
privatepurereturns (address, address)
{
LoanOffer calldata one = _executionData.offerExecution[0].offer;
return (one.principalAddress, one.nftCollateralAddress);
}
/// @notice Check addresses are whitelisted./// @param _principalAddress Address of the principal token./// @param _nftCollateralAddress Address of the NFT collateral.function_checkWhitelists(address _principalAddress, address _nftCollateralAddress) privateview{
if (!_currencyManager.isWhitelisted(_principalAddress)) {
revert CurrencyNotWhitelistedError();
}
if (!_collectionManager.isWhitelisted(_nftCollateralAddress)) {
revert CollectionNotWhitelistedError();
}
}
/// @notice Check principal/collateral addresses match./// @param _offer The offer to check./// @param _principalAddress Address of the principal token./// @param _nftCollateralAddress Address of the NFT collateral./// @param _amountWithInterestAhead Amount of more senior principal + max accrued interest ahead.function_checkOffer(
LoanOffer calldata _offer,
address _principalAddress,
address _nftCollateralAddress,
uint256 _amountWithInterestAhead
) privatepure{
if (_offer.principalAddress != _principalAddress || _offer.nftCollateralAddress != _nftCollateralAddress) {
revert InvalidAddressesError();
}
if (_amountWithInterestAhead > _offer.maxSeniorRepayment) {
revert InvalidTrancheError();
}
}
/// @notice Check generic offer validators for a given offer or/// an exact match if no validators are given. The validators/// check is performed only if tokenId is set to 0./// Having one empty validator is used for collection offers (all IDs match)./// @param _loanOffer The loan offer to check./// @param _tokenId The token ID to check.function_checkValidators(LoanOffer calldata _loanOffer, uint256 _tokenId) privateview{
uint256 offerTokenId = _loanOffer.nftCollateralTokenId;
if (_loanOffer.nftCollateralTokenId !=0) {
if (offerTokenId != _tokenId) {
revert InvalidCollateralIdError();
}
} else {
uint256 totalValidators = _loanOffer.validators.length;
if (totalValidators ==0&& _tokenId !=0) {
revert InvalidCollateralIdError();
} elseif ((totalValidators ==1) && _loanOffer.validators[0].validator ==address(0)) {
return;
}
for (uint256 i =0; i < totalValidators;) {
IBaseLoan.OfferValidator memory thisValidator = _loanOffer.validators[i];
IOfferValidator(thisValidator.validator).validateOffer(_loanOffer, _tokenId, thisValidator.arguments);
unchecked {
++i;
}
}
}
}
/// @dev Check new trnches are at least this big.function_getMinTranchePrincipal(uint256 _loanPrincipal) privateviewreturns (uint256) {
return _loanPrincipal / (_MAX_RATIO_TRANCHE_MIN_PRINCIPAL * getMaxTranches);
}
function_hasCallback(bytescalldata _callbackData) privatepurereturns (bool) {
return _callbackData.length!=0;
}
function_processRepayments(Loan calldata loan) privatereturns (uint256, uint256) {
bool withProtocolFee = loan.protocolFee !=0;
uint256 totalRepayment =0;
uint256 totalProtocolFee =0;
ERC20 asset = ERC20(loan.principalAddress);
uint256 totalTranches = loan.tranche.length;
for (uint256 i; i < totalTranches;) {
Tranche memory tranche = loan.tranche[i];
uint256 newInterest =
tranche.principalAmount.getInterest(tranche.aprBps, block.timestamp- tranche.startTime);
uint256 thisProtocolFee =0;
if (withProtocolFee) {
thisProtocolFee = newInterest.mulDivUp(loan.protocolFee, _PRECISION);
unchecked {
totalProtocolFee += thisProtocolFee;
}
}
uint256 repayment = tranche.principalAmount + tranche.accruedInterest + newInterest - thisProtocolFee;
asset.safeTransferFrom(loan.borrower, tranche.lender, repayment);
unchecked {
totalRepayment += repayment;
}
if (getLoanManagerRegistry.isLoanManager(tranche.lender)) {
ILoanManager(tranche.lender).loanRepayment(
tranche.loanId,
tranche.principalAmount,
tranche.aprBps,
tranche.accruedInterest,
loan.protocolFee,
tranche.startTime
);
}
unchecked {
++i;
}
}
if (withProtocolFee) {
asset.safeTransferFrom(loan.borrower, _protocolFee.recipient, totalProtocolFee);
}
return (totalRepayment, totalProtocolFee);
}
/// @notice Process a series of offers and return the loan ID, offer IDs, loan (built from such offers) and total fee./// @param _borrower The borrower of the loan./// @param _principalReceiver The receiver of the principal./// @param _principalAddress The principal address of the loan./// @param _nftCollateralAddress The NFT collateral address of the loan./// @param _tokenId The token ID of the loan./// @param _duration The duration of the loan./// @param _offerExecution The offer execution./// @return loanId The loan ID./// @return offerIds The offer IDs./// @return loan The loan./// @return totalFee The total fee.function_processOffersFromExecutionData(address _borrower,
address _principalReceiver,
address _principalAddress,
address _nftCollateralAddress,
uint256 _tokenId,
uint256 _duration,
OfferExecution[] calldata _offerExecution
) privatereturns (uint256, uint256[] memory, Loan memory, uint256) {
Tranche[] memory tranche =new Tranche[](_offerExecution.length);
uint256[] memory offerIds =newuint256[](_offerExecution.length);
uint256 totalAmount;
uint256 loanId = _getAndSetNewLoanId();
ProtocolFee memory protocolFee = _protocolFee;
LoanOffer calldata offer;
uint256 totalFee;
uint256 totalAmountWithMaxInterest;
uint256 minAmount =type(uint256).max;
uint256 totalOffers = _offerExecution.length;
for (uint256 i =0; i < totalOffers;) {
OfferExecution calldata thisOfferExecution = _offerExecution[i];
offer = thisOfferExecution.offer;
_validateOfferExecution(
thisOfferExecution,
_tokenId,
offer.lender,
_duration,
thisOfferExecution.lenderOfferSignature,
protocolFee.fraction,
totalAmount
);
uint256 amount = thisOfferExecution.amount;
if (amount < minAmount) {
minAmount = amount;
}
address lender = offer.lender;
_checkOffer(offer, _principalAddress, _nftCollateralAddress, totalAmountWithMaxInterest);
/// @dev Please note that we can now have many tranches with same `loanId`.
tranche[i] = Tranche(loanId, totalAmount, amount, lender, 0, block.timestamp, offer.aprBps);
unchecked {
totalAmount += amount;
totalAmountWithMaxInterest += amount + amount.getInterest(offer.aprBps, _duration);
}
uint256 fee;
unchecked {
fee = offer.fee.mulDivUp(amount, offer.principalAmount);
totalFee += fee;
}
_handleProtocolFeeForFee(
offer.principalAddress, lender, fee.mulDivUp(protocolFee.fraction, _PRECISION), protocolFee.recipient
);
ERC20(offer.principalAddress).safeTransferFrom(lender, _principalReceiver, amount - fee);
if (offer.capacity !=0) {
unchecked {
_used[lender][offer.offerId] += amount;
}
} else {
isOfferCancelled[lender][offer.offerId] =true;
}
offerIds[i] = offer.offerId;
unchecked {
++i;
}
}
if (minAmount < _getMinTranchePrincipal(totalAmount)) {
revert InvalidTrancheError();
}
Loan memory loan = Loan(
_borrower,
_tokenId,
_nftCollateralAddress,
_principalAddress,
totalAmount,
block.timestamp,
_duration,
tranche,
protocolFee.fraction
);
return (loanId, offerIds, loan, totalFee);
}
function_addNewTranche(uint256 _newLoanId,
IMultiSourceLoan.Loan memory _loan,
IMultiSourceLoan.RenegotiationOffer calldata _renegotiationOffer
) privateviewreturns (IMultiSourceLoan.Loan memory) {
if (_renegotiationOffer.principalAmount < _getMinTranchePrincipal(_loan.principalAmount)) {
revert InvalidTrancheError();
}
uint256 newTrancheIndex = _loan.tranche.length;
IMultiSourceLoan.Tranche[] memory tranches =new IMultiSourceLoan.Tranche[](newTrancheIndex +1);
/// @dev Copy old tranchesfor (uint256 i =0; i < newTrancheIndex;) {
tranches[i] = _loan.tranche[i];
unchecked {
++i;
}
}
tranches[newTrancheIndex] = IMultiSourceLoan.Tranche(
_newLoanId,
_loan.principalAmount,
_renegotiationOffer.principalAmount,
_renegotiationOffer.lender,
0,
block.timestamp,
_renegotiationOffer.aprBps
);
_loan.tranche = tranches;
unchecked {
_loan.principalAmount += _renegotiationOffer.principalAmount;
}
return _loan;
}
/// @notice Check a signature is valid given a hash and signer./// @dev Comply with IERC1271 and EIP-712.function_checkSignature(address _signer, bytes32 _hash, bytescalldata _signature) privateview{
bytes32 typedDataHash = DOMAIN_SEPARATOR().toTypedDataHash(_hash);
if (_signer.code.length!=0) {
if (IERC1271(_signer).isValidSignature(typedDataHash, _signature) != MAGICVALUE_1271) {
revert InvalidSignatureError();
}
} else {
address recovered = typedDataHash.recover(_signature);
if (_signer != recovered) {
revert InvalidSignatureError();
}
}
}
/// @dev Check whether an offer is strictly better than a tranchefunction_checkStrictlyBetter(uint256 _offerPrincipalAmount,
uint256 _loanPrincipalAmount,
uint256 _offerEndTime,
uint256 _loanEndTime,
uint256 _offerAprBps,
uint256 _loanAprBps,
uint256 _offerFee
) internalview{
uint256 minImprovementApr = _minImprovementApr;
/// @dev If principal is increased, then we need to check net daily interest is better./// interestDelta = (_loanAprBps * _loanPrincipalAmount - _offerAprBps * _offerPrincipalAmount)/// We already checked that all tranches are strictly better./// We check that the duration is not decreased or the offer charges a fee.if (
(
(_offerPrincipalAmount - _loanPrincipalAmount !=0)
&& (
(_loanAprBps * _loanPrincipalAmount - _offerAprBps * _offerPrincipalAmount).mulDivDown(
_PRECISION, _loanAprBps * _loanPrincipalAmount
) < minImprovementApr
)
) || (_offerFee !=0) || (_offerEndTime < _loanEndTime)
) {
revert NotStrictlyImprovedError();
}
}
}
Contract Source Code
File 28 of 35: Multicall.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"../interfaces/IMulticall.sol";
/// @title Multicall/// @author Florida St/// @notice Base implementation for multicall.abstractcontractMulticallisIMulticall{
functionmulticall(bytes[] calldata data) externalpayableoverridereturns (bytes[] memory results) {
results =newbytes[](data.length);
bool success;
uint256 totalCalls = data.length;
for (uint256 i =0; i < totalCalls;) {
//slither-disable-next-line calls-loop,delegatecall-loop
(success, results[i]) =address(this).delegatecall(data[i]);
if (!success) {
revert MulticallFailed(i, results[i]);
}
unchecked {
++i;
}
}
}
}
// SPDX-License-Identifier: AGPL-3.0-onlypragmasolidity >=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.librarySafeTransferLib{
/*//////////////////////////////////////////////////////////////
ETH OPERATIONS
//////////////////////////////////////////////////////////////*/functionsafeTransferETH(address to, uint256 amount) internal{
bool success;
/// @solidity memory-safe-assemblyassembly {
// 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
//////////////////////////////////////////////////////////////*/functionsafeTransferFrom(
ERC20 token,
addressfrom,
address to,
uint256 amount
) internal{
bool success;
/// @solidity memory-safe-assemblyassembly {
// 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");
}
functionsafeTransfer(
ERC20 token,
address to,
uint256 amount
) internal{
bool success;
/// @solidity memory-safe-assemblyassembly {
// 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");
}
functionsafeApprove(
ERC20 token,
address to,
uint256 amount
) internal{
bool success;
/// @solidity memory-safe-assemblyassembly {
// 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");
}
}
Contract Source Code
File 32 of 35: SignedMath.sol
// SPDX-License-Identifier: MIT// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SignedMath.sol)pragmasolidity ^0.8.20;/**
* @dev Standard signed math utilities missing in the Solidity language.
*/librarySignedMath{
/**
* @dev Returns the largest of two signed numbers.
*/functionmax(int256 a, int256 b) internalpurereturns (int256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two signed numbers.
*/functionmin(int256 a, int256 b) internalpurereturns (int256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two signed numbers without overflow.
* The result is rounded towards zero.
*/functionaverage(int256 a, int256 b) internalpurereturns (int256) {
// Formula from the book "Hacker's Delight"int256 x = (a & b) + ((a ^ b) >>1);
return x + (int256(uint256(x) >>255) & (a ^ b));
}
/**
* @dev Returns the absolute unsigned value of a signed value.
*/functionabs(int256 n) internalpurereturns (uint256) {
unchecked {
// Formula from the "Bit Twiddling Hacks" by Sean Eron Anderson.// Since `n` is a signed integer, the generated bytecode will use the SAR opcode to perform the right shift,// taking advantage of the most significant (or "sign" bit) in two's complement representation.// This opcode adds new most significant bits set to the value of the previous most significant bit. As a result,// the mask will either be `bytes(0)` (if n is positive) or `~bytes32(0)` (if n is negative).int256 mask = n >>255;
// A `bytes(0)` mask leaves the input unchanged, while a `~bytes32(0)` mask complements it.returnuint256((n + mask) ^ mask);
}
}
}
Contract Source Code
File 33 of 35: Strings.sol
// SPDX-License-Identifier: MIT// OpenZeppelin Contracts (last updated v5.0.0) (utils/Strings.sol)pragmasolidity ^0.8.20;import {Math} from"./math/Math.sol";
import {SignedMath} from"./math/SignedMath.sol";
/**
* @dev String operations.
*/libraryStrings{
bytes16privateconstant HEX_DIGITS ="0123456789abcdef";
uint8privateconstant ADDRESS_LENGTH =20;
/**
* @dev The `value` string doesn't fit in the specified `length`.
*/errorStringsInsufficientHexLength(uint256 value, uint256 length);
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/functiontoString(uint256 value) internalpurereturns (stringmemory) {
unchecked {
uint256 length = Math.log10(value) +1;
stringmemory buffer =newstring(length);
uint256 ptr;
/// @solidity memory-safe-assemblyassembly {
ptr :=add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assemblyassembly {
mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
}
value /=10;
if (value ==0) break;
}
return buffer;
}
}
/**
* @dev Converts a `int256` to its ASCII `string` decimal representation.
*/functiontoStringSigned(int256 value) internalpurereturns (stringmemory) {
returnstring.concat(value <0 ? "-" : "", toString(SignedMath.abs(value)));
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/functiontoHexString(uint256 value) internalpurereturns (stringmemory) {
unchecked {
return toHexString(value, Math.log256(value) +1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/functiontoHexString(uint256 value, uint256 length) internalpurereturns (stringmemory) {
uint256 localValue = value;
bytesmemory buffer =newbytes(2* length +2);
buffer[0] ="0";
buffer[1] ="x";
for (uint256 i =2* length +1; i >1; --i) {
buffer[i] = HEX_DIGITS[localValue &0xf];
localValue >>=4;
}
if (localValue !=0) {
revert StringsInsufficientHexLength(value, length);
}
returnstring(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal
* representation.
*/functiontoHexString(address addr) internalpurereturns (stringmemory) {
return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
}
/**
* @dev Returns true if the two strings are equal.
*/functionequal(stringmemory a, stringmemory b) internalpurereturns (bool) {
returnbytes(a).length==bytes(b).length&&keccak256(bytes(a)) ==keccak256(bytes(b));
}
}
Contract Source Code
File 34 of 35: TwoStepOwned.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"@solmate/auth/Owned.sol";
/// @title TwoStepOwned/// @author Florida St/// @notice This contract is used to transfer ownership of a contract in two steps.abstractcontractTwoStepOwnedisOwned{
eventTransferOwnerRequested(address newOwner);
errorTooSoonError();
errorInvalidInputError();
uint256publicimmutable MIN_WAIT_TIME;
addresspublic pendingOwner;
uint256public pendingOwnerTime;
constructor(address _owner, uint256 _minWaitTime) Owned(_owner) {
pendingOwnerTime =type(uint256).max;
MIN_WAIT_TIME = _minWaitTime;
}
/// @notice First step transferring ownership to the new owner./// @param _newOwner The address of the new owner.functionrequestTransferOwner(address _newOwner) externalonlyOwner{
pendingOwner = _newOwner;
pendingOwnerTime =block.timestamp;
emit TransferOwnerRequested(_newOwner);
}
/// @notice Second step transferring ownership to the new owner.functiontransferOwnership() public{
address newOwner =msg.sender;
if (pendingOwnerTime + MIN_WAIT_TIME >block.timestamp) {
revert TooSoonError();
}
if (newOwner != pendingOwner) {
revert InvalidInputError();
}
owner = newOwner;
pendingOwner =address(0);
pendingOwnerTime =type(uint256).max;
emit OwnershipTransferred(owner, newOwner);
}
}
Contract Source Code
File 35 of 35: WithProtocolFee.sol
// SPDX-License-Identifier: AGPL-3.0pragmasolidity ^0.8.21;import"./TwoStepOwned.sol";
import"../InputChecker.sol";
abstractcontractWithProtocolFeeisTwoStepOwned{
usingInputCheckerforaddress;
/// @notice Recipient address and fraction of gains charged by the protocol.structProtocolFee {
address recipient;
uint256 fraction;
}
uint256publicconstant FEE_UPDATE_NOTICE =30days;
/// @notice Protocol fee charged on gains.
ProtocolFee internal _protocolFee;
/// @notice Set as the target new protocol fee.
ProtocolFee internal _pendingProtocolFee;
/// @notice Set when the protocol fee updating mechanisms starts.uint256internal _pendingProtocolFeeSetTime;
eventProtocolFeeUpdated(ProtocolFee fee);
eventProtocolFeePendingUpdate(ProtocolFee fee);
errorTooEarlyError(uint256 _pendingProtocolFeeSetTime);
/// @notice Constructor/// @param _owner The owner of the contract/// @param _minWaitTime The time to wait before a new owner can be set/// @param __protocolFee The protocol feeconstructor(address _owner, uint256 _minWaitTime, ProtocolFee memory __protocolFee)
TwoStepOwned(_owner, _minWaitTime)
{
_protocolFee = __protocolFee;
_pendingProtocolFeeSetTime =type(uint256).max;
}
/// @return protocolFee The Protocol fee.functiongetProtocolFee() externalviewreturns (ProtocolFee memory) {
return _protocolFee;
}
/// @return pendingProtocolFee The pending protocol fee.functiongetPendingProtocolFee() externalviewreturns (ProtocolFee memory) {
return _pendingProtocolFee;
}
/// @return protocolFeeSetTime Time when the protocol fee was set to be changed.functiongetPendingProtocolFeeSetTime() externalviewreturns (uint256) {
return _pendingProtocolFeeSetTime;
}
/// @notice Kicks off the process to update the protocol fee./// @param _newProtocolFee New protocol fee.functionupdateProtocolFee(ProtocolFee calldata _newProtocolFee) externalonlyOwner{
_newProtocolFee.recipient.checkNotZero();
_pendingProtocolFee = _newProtocolFee;
_pendingProtocolFeeSetTime =block.timestamp;
emit ProtocolFeePendingUpdate(_pendingProtocolFee);
}
/// @notice Set the protocol fee if enough notice has been given.functionsetProtocolFee() externalvirtual{
if (block.timestamp< _pendingProtocolFeeSetTime + FEE_UPDATE_NOTICE) {
revert TooSoonError();
}
ProtocolFee memory protocolFee = _pendingProtocolFee;
_protocolFee = protocolFee;
emit ProtocolFeeUpdated(protocolFee);
}
}