Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Complete test accounts (SC-3610) #6

Merged
merged 3 commits into from
Sep 2, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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();
deluca-mike marked this conversation as resolved.
Show resolved Hide resolved

// 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);

deluca-mike marked this conversation as resolved.
Show resolved Hide resolved
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));
}

}