Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions src/EscrowSupplierNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
return Offer({
supplier: stored.supplier,
available: stored.available,
duration: stored.duration,
minDuration: stored.minDuration,
maxDuration: stored.maxDuration,
interestAPR: stored.interestAPR,
gracePeriod: stored.gracePeriod,
lateFeeAPR: stored.lateFeeAPR,
Expand All @@ -106,7 +107,7 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
escrowed: stored.escrowed,
gracePeriod: offer.gracePeriod,
lateFeeAPR: offer.lateFeeAPR,
duration: offer.duration,
duration: stored.duration,
expiration: stored.expiration,
feesHeld: stored.feesHeld,
released: stored.released,
Expand All @@ -119,16 +120,17 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
* Partially refunded depending on the time of escrow release.
* @param offerId The offer Id to use for calculations
* @param escrowed The escrowed amount
* @param duration The duration in seconds for the escrow
* @return total The calculated total fees (interest and late fee)
* @return interestFee The calculated interest fee to hold
* @return lateFee The calculated late fee to hold
*/
function upfrontFees(uint offerId, uint escrowed)
function upfrontFees(uint offerId, uint escrowed, uint duration)
public
view
returns (uint total, uint interestFee, uint lateFee)
{
(interestFee, lateFee) = _upfrontFees(offerId, escrowed);
(interestFee, lateFee) = _upfrontFees(offerId, escrowed, duration);
total = interestFee + lateFee;
}

Expand Down Expand Up @@ -172,7 +174,8 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
/**
* @notice Creates a new escrow offer
* @param amount The offered amount
* @param duration The offer duration in seconds
* @param minDuration The minimum duration in seconds for the escrow
* @param maxDuration The maximum duration in seconds for the escrow
* @param interestAPR The annual interest rate in basis points. At most MAX_FEE_REFUND_BIPS
* of the upfront interest can be refunded on cancellation. If interestAPR is 0, this
* will have no effect, and allow free cancellations.
Expand All @@ -183,7 +186,8 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
*/
function createOffer(
uint amount,
uint duration,
uint minDuration,
uint maxDuration,
uint interestAPR,
uint gracePeriod,
uint lateFeeAPR,
Expand All @@ -198,7 +202,8 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
offerId = nextOfferId++;
offers[offerId] = OfferStored({
supplier: msg.sender,
duration: SafeCast.toUint32(duration),
minDuration: SafeCast.toUint32(minDuration),
maxDuration: SafeCast.toUint32(maxDuration),
Comment thread
kutoft marked this conversation as resolved.
gracePeriod: SafeCast.toUint32(gracePeriod),
interestAPR: SafeCast.toUint24(interestAPR),
lateFeeAPR: SafeCast.toUint24(lateFeeAPR),
Expand All @@ -207,7 +212,7 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
});
asset.safeTransferFrom(msg.sender, address(this), amount);
emit OfferCreated(
msg.sender, interestAPR, duration, gracePeriod, lateFeeAPR, amount, offerId, minEscrow
Comment thread
kutoft marked this conversation as resolved.
msg.sender, interestAPR, maxDuration, gracePeriod, lateFeeAPR, amount, offerId, minEscrow
);
}

Expand Down Expand Up @@ -258,12 +263,12 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
* @param loanId The associated loan ID
* @return escrowId The ID of the created escrow
*/
function startEscrow(uint offerId, uint escrowed, uint fees, uint loanId)
function startEscrow(uint offerId, uint escrowed, uint fees, uint loanId, uint duration)
external
returns (uint escrowId)
{
// @dev msg.sender auth is checked vs. canOpenPair in _startEscrow
escrowId = _startEscrow(offerId, escrowed, fees, loanId);
escrowId = _startEscrow(offerId, escrowed, fees, loanId, duration);

// @dev despite the fact that they partially cancel out, so can be done as just fee transfer,
// these transfers are the whole point of this contract from product point of view.
Expand Down Expand Up @@ -308,11 +313,12 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
* @param offerId The ID of the new offer
* @param newFees The new interest fee amount
* @param newLoanId The new loan ID
* @param newDuration The new duration in seconds for the escrow
* @return newEscrowId The ID of the new escrow
* @return feesRefund The refunded fee amount from old escrow's upfront held interest fee
* and late fee
*/
function switchEscrow(uint releaseEscrowId, uint offerId, uint newFees, uint newLoanId)
function switchEscrow(uint releaseEscrowId, uint offerId, uint newFees, uint newLoanId, uint newDuration)
external
returns (uint newEscrowId, uint feesRefund)
{
Expand Down Expand Up @@ -340,7 +346,7 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
// The escrow funds are funds that have been escrowed in the ID being released ("O").
// The offer is reduced (which is used to repay the previous supplier)
// A new escrow ID is minted.
newEscrowId = _startEscrow(offerId, previousEscrow.escrowed, newFees, newLoanId);
newEscrowId = _startEscrow(offerId, previousEscrow.escrowed, newFees, newLoanId, newDuration);

// fee transfers
asset.safeTransferFrom(msg.sender, address(this), newFees);
Expand Down Expand Up @@ -403,7 +409,7 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {

// ----- INTERNAL MUTATIVE ----- //

function _startEscrow(uint offerId, uint escrowed, uint fees, uint loanId)
function _startEscrow(uint offerId, uint escrowed, uint fees, uint loanId, uint duration)
internal
returns (uint escrowId)
{
Expand All @@ -417,11 +423,14 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {

Offer memory offer = getOffer(offerId);
require(offer.supplier != address(0), "escrow: invalid offer"); // revert here for clarity
// Check if duration is within the offer's min and max duration
require(duration >= offer.minDuration, "escrow: duration below offer's min duration");
require(duration <= offer.maxDuration, "escrow: duration exceeds offer's max duration");

// check params are supported
require(configHub.isValidCollarDuration(offer.duration), "escrow: unsupported duration");
require(configHub.isValidCollarDuration(duration), "escrow: unsupported duration");

(uint expectedFees,,) = upfrontFees(offerId, escrowed);
(uint expectedFees,,) = upfrontFees(offerId, escrowed, duration);
// we don't check equality to avoid revert due to minor inaccuracies to the upside,
// even though exact value should be used from the view.
// The overpayment is refunded when escrow is properly released (but not when seized).
Expand All @@ -439,7 +448,8 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
escrows[escrowId] = EscrowStored({
offerId: SafeCast.toUint64(offerId),
loanId: SafeCast.toUint64(loanId),
expiration: SafeCast.toUint32(block.timestamp + offer.duration),
duration: SafeCast.toUint32(duration),
expiration: SafeCast.toUint32(block.timestamp + duration),
released: false, // unset until release
loans: msg.sender,
escrowed: escrowed,
Expand Down Expand Up @@ -502,14 +512,14 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
toLoans = available - withdrawal;
}

function _upfrontFees(uint offerId, uint escrowed)
function _upfrontFees(uint offerId, uint escrowed, uint duration)
internal
view
returns (uint interestFee, uint lateFee)
{
Offer memory offer = getOffer(offerId);
// rounds up against borrower
interestFee = Math.ceilDiv(escrowed * offer.interestAPR * offer.duration, BIPS_BASE * YEAR);
interestFee = Math.ceilDiv(escrowed * offer.interestAPR * duration, BIPS_BASE * YEAR);
lateFee = Math.ceilDiv(escrowed * offer.lateFeeAPR * offer.gracePeriod, BIPS_BASE * YEAR);
}

Expand All @@ -519,7 +529,7 @@ contract EscrowSupplierNFT is IEscrowSupplierNFT, BaseNFT {
returns (uint total, uint interestRefund, uint lateFeeRefund, uint overpaymentRefund)
{
// amounts held for interest and late fees. Assumes offer and size are immutable.
(uint interestHeld, uint lateFeeHeld) = _upfrontFees(escrow.offerId, escrow.escrowed);
(uint interestHeld, uint lateFeeHeld) = _upfrontFees(escrow.offerId, escrow.escrowed, escrow.duration);

interestRefund = _interestRefund(escrow, interestHeld);
lateFeeRefund = _lateFeeRefund(escrow, lateFeeHeld);
Expand Down
27 changes: 19 additions & 8 deletions src/LoansNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ contract LoansNFT is ILoansNFT, BaseNFT {

// handle optional escrow, must be done first, to use "supplier's" underlying in swap
(EscrowSupplierNFT escrowNFT, uint escrowId) =
_conditionalOpenEscrow(usesEscrow, underlyingAmount, escrowOffer, escrowFees);
_conditionalOpenEscrow(usesEscrow, underlyingAmount, escrowOffer, escrowFees, providerOffer);
Comment thread
kutoft marked this conversation as resolved.

// stack too deep
{
Expand Down Expand Up @@ -637,10 +637,13 @@ contract LoansNFT is ILoansNFT, BaseNFT {

// ----- Conditional escrow mutative methods ----- //

function _conditionalOpenEscrow(bool usesEscrow, uint escrowed, EscrowOffer memory offer, uint fees)
internal
returns (EscrowSupplierNFT escrowNFT, uint escrowId)
{
function _conditionalOpenEscrow(
bool usesEscrow,
uint escrowed,
EscrowOffer memory offer,
uint fees,
ProviderOffer memory providerOffer
) internal returns (EscrowSupplierNFT escrowNFT, uint escrowId) {
if (usesEscrow) {
escrowNFT = offer.escrowNFT;
// check asset matches
Expand All @@ -650,14 +653,18 @@ contract LoansNFT is ILoansNFT, BaseNFT {
configHub.canOpenSingle(address(underlying), address(escrowNFT)), "loans: unsupported escrow"
);

// get the duration from the provider offer
uint duration = providerOffer.providerNFT.getOffer(providerOffer.id).duration;

// @dev underlyingAmount and fee were pulled already before calling this method
underlying.forceApprove(address(escrowNFT), escrowed + fees);
escrowId = escrowNFT.startEscrow({
offerId: offer.id,
escrowed: escrowed,
fees: fees,
loanId: takerNFT.nextPositionId() // @dev checked later in _escrowValidations
});
loanId: takerNFT.nextPositionId(), // @dev checked later in _escrowValidations
duration: duration
});
// @dev no balance checks because contract holds no funds, mismatch will cause reverts
} else {
// returns default empty values
Expand All @@ -676,14 +683,18 @@ contract LoansNFT is ILoansNFT, BaseNFT {
configHub.canOpenSingle(address(underlying), address(escrowNFT)), "loans: unsupported escrow"
);

uint takerId = _takerId(newLoanId);
uint duration = takerNFT.getPosition(takerId).duration;

underlying.safeTransferFrom(msg.sender, address(this), newFees);
underlying.forceApprove(address(escrowNFT), newFees);
uint feesRefund;
(newEscrowId, feesRefund) = escrowNFT.switchEscrow({
releaseEscrowId: prevLoan.escrowId,
offerId: offerId,
newFees: newFees,
newLoanId: newLoanId
newLoanId: newLoanId,
newDuration: duration
});

// check escrow and loan have matching fields
Expand Down
9 changes: 6 additions & 3 deletions src/interfaces/IEscrowSupplierNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { ConfigHub } from "../ConfigHub.sol";
interface IEscrowSupplierNFT {
struct OfferStored {
// packed first slot
uint32 duration;
uint32 minDuration;
uint32 maxDuration;
uint32 gracePeriod;
uint24 interestAPR; // allows up to 167,772%, must allow MAX_INTEREST_APR_BIPS
uint24 lateFeeAPR; // allows up to 167,772%, must allow MAX_LATE_FEE_APR_BIPS
Expand All @@ -25,7 +26,8 @@ interface IEscrowSupplierNFT {
address supplier;
uint available;
// terms
uint duration;
uint minDuration;
uint maxDuration;
uint interestAPR;
uint gracePeriod;
uint lateFeeAPR;
Expand All @@ -38,6 +40,7 @@ interface IEscrowSupplierNFT {
uint64 loanId; // assumes sequential IDs
uint32 expiration;
bool released;
uint32 duration; // duration for this escrow
// second slot
address loans;
// rest of slots
Expand Down Expand Up @@ -68,7 +71,7 @@ interface IEscrowSupplierNFT {
event OfferCreated(
address indexed supplier,
uint indexed interestAPR,
uint indexed duration,
Comment thread
kutoft marked this conversation as resolved.
uint indexed maxDuration,
uint gracePeriod,
uint lateFeeAPR,
uint available,
Expand Down
Loading
Loading