Skip to content

Commit

Permalink
feat: Complete Test Accounts
Browse files Browse the repository at this point in the history
- DebtLockerOwner is better named as a Pool
- remove interfaces from interfaces
  • Loading branch information
deluca-mike committed Aug 31, 2021
1 parent 54ce89d commit 1019004
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 50 deletions.
42 changes: 21 additions & 21 deletions contracts/DebtLocker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ contract DebtLocker is IDebtLocker {

uint256 constant WAD = 10 ** 18;

ILoanLike public override immutable loan;
IERC20 public override immutable liquidityAsset;
address public override immutable pool;
address public override immutable liquidityAsset;
address public override immutable loan;
address public override immutable pool;

uint256 public override lastPrincipalPaid;
uint256 public override lastInterestPaid;
Expand All @@ -35,9 +35,9 @@ contract DebtLocker is IDebtLocker {
}

constructor(address _loan, address _pool) public {
loan = ILoanLike(_loan);
loan = _loan;
pool = _pool;
liquidityAsset = IERC20(ILoanLike(_loan).liquidityAsset());
liquidityAsset = ILoanLike(_loan).liquidityAsset();
}

// Note: If newAmt > 0, totalNewAmt will always be greater than zero.
Expand All @@ -48,46 +48,46 @@ contract DebtLocker is IDebtLocker {
function claim() external override isPool returns (uint256[7] memory) {

uint256 newDefaultSuffered = uint256(0);
uint256 loan_defaultSuffered = loan.defaultSuffered();
uint256 loan_defaultSuffered = ILoanLike(loan).defaultSuffered();

// If a default has occurred, update storage variable and update memory variable from zero for return.
// `newDefaultSuffered` represents the proportional loss that the DebtLocker registers based on its balance
// of LoanFDTs in comparison to the total supply of LoanFDTs.
// Default will occur only once, so below statement will only be `true` once.
if (lastDefaultSuffered == uint256(0) && loan_defaultSuffered > uint256(0)) {
newDefaultSuffered = lastDefaultSuffered = _calcAllotment(loan.balanceOf(address(this)), loan_defaultSuffered, loan.totalSupply());
newDefaultSuffered = lastDefaultSuffered = _calcAllotment(ILoanLike(loan).balanceOf(address(this)), loan_defaultSuffered, ILoanLike(loan).totalSupply());
}

// Account for any transfers into Loan that have occurred since last call.
loan.updateFundsReceived();
ILoanLike(loan).updateFundsReceived();

// Handles case where no claimable funds are present but a default must be registered (zero-collateralized loans defaulting).
if (loan.withdrawableFundsOf(address(this)) == uint256(0)) return([0, 0, 0, 0, 0, 0, newDefaultSuffered]);
if (ILoanLike(loan).withdrawableFundsOf(address(this)) == uint256(0)) return([0, 0, 0, 0, 0, 0, newDefaultSuffered]);

// If there are claimable funds, calculate portions and claim using LoanFDT.

// Calculate payment deltas.
uint256 newInterest = loan.interestPaid() - lastInterestPaid; // `loan.interestPaid` updated in `loan._makePayment()`
uint256 newPrincipal = loan.principalPaid() - lastPrincipalPaid; // `loan.principalPaid` updated in `loan._makePayment()`
uint256 newInterest = ILoanLike(loan).interestPaid() - lastInterestPaid; // `loan.interestPaid` updated in `loan._makePayment()`
uint256 newPrincipal = ILoanLike(loan).principalPaid() - lastPrincipalPaid; // `loan.principalPaid` updated in `loan._makePayment()`

// Update storage variables for next delta calculation.
lastInterestPaid = loan.interestPaid();
lastPrincipalPaid = loan.principalPaid();
lastInterestPaid = ILoanLike(loan).interestPaid();
lastPrincipalPaid = ILoanLike(loan).principalPaid();

// Calculate one-time deltas if storage variables have not yet been updated.
uint256 newFee = lastFeePaid == uint256(0) ? loan.feePaid() : uint256(0); // `loan.feePaid` updated in `loan.drawdown()`
uint256 newExcess = lastExcessReturned == uint256(0) ? loan.excessReturned() : uint256(0); // `loan.excessReturned` updated in `loan.unwind()` OR `loan.drawdown()` if `amt < fundingLockerBal`
uint256 newAmountRecovered = lastAmountRecovered == uint256(0) ? loan.amountRecovered() : uint256(0); // `loan.amountRecovered` updated in `loan.triggerDefault()`
uint256 newFee = lastFeePaid == uint256(0) ? ILoanLike(loan).feePaid() : uint256(0); // `loan.feePaid` updated in `loan.drawdown()`
uint256 newExcess = lastExcessReturned == uint256(0) ? ILoanLike(loan).excessReturned() : uint256(0); // `loan.excessReturned` updated in `loan.unwind()` OR `loan.drawdown()` if `amt < fundingLockerBal`
uint256 newAmountRecovered = lastAmountRecovered == uint256(0) ? ILoanLike(loan).amountRecovered() : uint256(0); // `loan.amountRecovered` updated in `loan.triggerDefault()`

// Update DebtLocker storage variables if Loan storage variables has been updated since last claim.
if (newFee > 0) lastFeePaid = newFee;
if (newExcess > 0) lastExcessReturned = newExcess;
if (newAmountRecovered > 0) lastAmountRecovered = newAmountRecovered;

// Withdraw all claimable funds via LoanFDT.
uint256 beforeBal = liquidityAsset.balanceOf(address(this)); // Current balance of DebtLocker (accounts for direct inflows).
loan.withdrawFunds(); // Transfer funds from Loan to DebtLocker.
uint256 claimBal = liquidityAsset.balanceOf(address(this)).sub(beforeBal); // Amount claimed from Loan using LoanFDT.
uint256 beforeBal = IERC20(liquidityAsset).balanceOf(address(this)); // Current balance of DebtLocker (accounts for direct inflows).
ILoanLike(loan).withdrawFunds(); // Transfer funds from Loan to DebtLocker.
uint256 claimBal = IERC20(liquidityAsset).balanceOf(address(this)).sub(beforeBal); // Amount claimed from Loan using LoanFDT.

// Calculate sum of all deltas, to be used to calculate portions for metadata.
uint256 sum = newInterest.add(newPrincipal).add(newFee).add(newExcess).add(newAmountRecovered);
Expand All @@ -101,7 +101,7 @@ contract DebtLocker is IDebtLocker {
newExcess = _calcAllotment(newExcess, claimBal, sum);
newAmountRecovered = _calcAllotment(newAmountRecovered, claimBal, sum);

liquidityAsset.safeTransfer(pool, claimBal); // Transfer entire amount claimed using LoanFDT.
IERC20(liquidityAsset).safeTransfer(pool, claimBal); // Transfer entire amount claimed using LoanFDT.

// Return claim amount plus all relevant metadata, to be used by Pool for further claim logic.
// Note: newInterest + newPrincipal + newFee + newExcess + newAmountRecovered = claimBal - dust
Expand All @@ -110,7 +110,7 @@ contract DebtLocker is IDebtLocker {
}

function triggerDefault() external override isPool {
loan.triggerDefault();
ILoanLike(loan).triggerDefault();
}

}
8 changes: 2 additions & 6 deletions contracts/interfaces/IDebtLocker.sol
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity 0.6.11;

import { IERC20 } from "../../modules/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

import { ILoanLike } from "./ILoanLike.sol";

/// @title DebtLocker holds custody of LoanFDT tokens.
interface IDebtLocker {

/**
@dev The Loan contract this locker is holding tokens for.
*/
function loan() external view returns (ILoanLike);
function loan() external view returns (address);

/**
@dev The Liquidity Asset this locker can claim.
*/
function liquidityAsset() external view returns (IERC20);
function liquidityAsset() external view returns (address);

/**
@dev The owner of this Locker (the Pool).
Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/ILoanLike.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface ILoanLike is IERC20 {

function interestPaid() external view returns (uint256);

function liquidityAsset() external view returns (IERC20);
function liquidityAsset() external view returns (address);

function principalPaid() external view returns (uint256);

Expand Down
25 changes: 13 additions & 12 deletions contracts/test/DebtLockerFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IDebtLocker } from "../interfaces/IDebtLocker.sol";

import { DebtLockerFactory } from "../DebtLockerFactory.sol";

import { DebtLockerOwner } from "./accounts/DebtLockerOwner.sol";
import { Pool } from "./accounts/Pool.sol";

contract MockToken is ERC20 {

Expand Down Expand Up @@ -37,26 +37,27 @@ contract MockLoan {
contract DebtLockerFactoryTest is DSTest {

function test_newLocker() external {
DebtLockerFactory factory = new DebtLockerFactory();
MockToken token = new MockToken("TKN", "TKN");
DebtLockerOwner owner = new DebtLockerOwner();
DebtLockerOwner nonOwner = new DebtLockerOwner();
MockLoan loan = new MockLoan(address(token));
DebtLockerFactory factory = new DebtLockerFactory();
MockToken token = new MockToken("TKN", "TKN");
Pool pool = new Pool();
Pool notPool = new Pool();

IDebtLocker locker = IDebtLocker(owner.debtLockerFactory_newLocker(address(factory), address(loan)));
MockLoan loan = new MockLoan(address(token));

IDebtLocker locker = IDebtLocker(pool.debtLockerFactory_newLocker(address(factory), address(loan)));

// Validate the storage of factory.
assertEq(factory.owner(address(locker)), address(owner), "Invalid owner");
assertEq(factory.owner(address(locker)), address(pool), "Invalid owner");
assertTrue(factory.isLocker(address(locker)), "Invalid isLocker");

// Validate the storage of locker.
assertEq(address(locker.loan()), address(loan), "Incorrect loan address");
assertEq(locker.pool(), address(owner), "Incorrect pool address");
assertEq(locker.pool(), address(pool), "Incorrect pool address");
assertEq(address(locker.liquidityAsset()), address(token), "Incorrect address of liquidity asset");

// Assert that only the DebtLocker owner can trigger default
assertTrue(!nonOwner.try_debtLocker_triggerDefault(address(locker)), "Trigger Default succeeded from nonOwner");
assertTrue( owner.try_debtLocker_triggerDefault(address(locker)), "Trigger Default failed from owner");
// Assert that only the DebtLocker owner (pool) can trigger default
assertTrue(!notPool.try_debtLocker_triggerDefault(address(locker)), "Trigger Default succeeded from notPool");
assertTrue( pool.try_debtLocker_triggerDefault(address(locker)), "Trigger Default failed from pool");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,38 @@ pragma solidity 0.6.11;
import { IDebtLocker } from "../../interfaces/IDebtLocker.sol";
import { IDebtLockerFactory } from "../../interfaces/IDebtLockerFactory.sol";

contract DebtLockerOwner {
contract Pool {

/************************/
/*** Direct Functions ***/
/************************/

function debtLockerFactory_newLocker(address factory, address loan) external returns (address) {
return IDebtLockerFactory(factory).newLocker(loan);
}

function try_debtLockerFactory_newLocker(address factory, address loan) external returns (bool ok) {
(ok,) = factory.call(abi.encodeWithSignature("newLocker(address)", loan));
}

function debtLocker_claim(address locker) external {
IDebtLocker(locker).claim();
}

function try_debtLocker_claim(address locker) external returns (bool ok) {
(ok,) = locker.call(abi.encodeWithSignature("claim()"));
}

function debtLocker_triggerDefault(address locker) external {
IDebtLocker(locker).triggerDefault();
}

/*********************/
/*** Try Functions ***/
/*********************/

function try_debtLockerFactory_newLocker(address factory, address loan) external returns (bool ok) {
(ok,) = factory.call(abi.encodeWithSelector(IDebtLockerFactory.newLocker.selector, loan));
}

function try_debtLocker_claim(address locker) external returns (bool ok) {
(ok,) = locker.call(abi.encodeWithSelector(IDebtLocker.claim.selector));
}

function try_debtLocker_triggerDefault(address locker) external returns (bool ok) {
(ok,) = locker.call(abi.encodeWithSignature("triggerDefault()"));
(ok,) = locker.call(abi.encodeWithSelector(IDebtLocker.triggerDefault.selector));
}

}

0 comments on commit 1019004

Please sign in to comment.