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

Introduce MaxLTVLoanOffer class #325

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4d71a0b
refactor: separate collateral from debt order params
saturnial Sep 28, 2018
94a60c5
feat: introduce LTVLoanOffer class
saturnial Sep 28, 2018
b09e0f7
feat: hash creditor commitment terms
saturnial Sep 28, 2018
3df8a89
feat: introduce collateralAmount getter and setter
chrismin Sep 28, 2018
24f5237
feat: introduce principalPrice and collateralPrice getters and setters
chrismin Sep 28, 2018
b8373bf
feat: introduce signAsDebtor method
chrismin Sep 28, 2018
313b236
feat: add placeholder for acceptAsDebtor
chrismin Sep 28, 2018
b4ac44f
refactor: remove unused interfaces
chrismin Sep 28, 2018
97556c7
feat: make signAsDebtor more robust
chrismin Sep 28, 2018
1bd585e
fix: add small fixes, TODOs
chrismin Oct 1, 2018
a4e840e
refactor: rename filename
chrismin Oct 1, 2018
2661299
refactor: rename variables
chrismin Oct 1, 2018
d4bcdc1
feat: add scaffolding for MaxLTVLoanOffer integration test
chrismin Oct 1, 2018
e573d31
feat: make relayer and relayer fee optional
chrismin Oct 1, 2018
5d41d64
feat: add isSignedByCreditor and isSignedByDebtor
chrismin Oct 1, 2018
62608e5
test: add specs for constructor
chrismin Oct 1, 2018
29ca740
feat: implement createAndSignAsCreditor
chrismin Oct 1, 2018
7b8c6d0
feat: mock out commitment hash getters
chrismin Oct 1, 2018
80e47c7
feat: add specs for createAndSignAsCreditor
chrismin Oct 1, 2018
12d8b39
fix: rename file
chrismin Oct 1, 2018
bdae591
test: add specs for signAsCreditor
chrismin Oct 1, 2018
a1ef119
test: add generateSignedPrice util
chrismin Oct 1, 2018
86d25c9
test: add spec for signAsDebtor
chrismin Oct 1, 2018
4d068d9
feat: improve signAsDebtor error handling
chrismin Oct 1, 2018
121aa04
test: improve signAsDebtor tests
chrismin Oct 1, 2018
181838c
refactor: implement async create function to set contract addresses
chrismin Oct 2, 2018
a0b52e7
test: fix failing tests
chrismin Oct 2, 2018
cf70836
feat: introduce debtorFee to DebtOrder base class
chrismin Oct 2, 2018
06feebd
refactor: make data interface more strict
chrismin Oct 2, 2018
1744163
feat: unmock hashing of debtor commitment and pack parameters
chrismin Oct 2, 2018
afc90ae
fix: use correct types for relayer and relayer fee
chrismin Oct 2, 2018
3789328
refactor: make packing of terms contract params sync
chrismin Oct 2, 2018
c224602
refactor: pack parameters when collateral amount is set
chrismin Oct 2, 2018
51a20c4
feat: add more error handling
chrismin Oct 2, 2018
7682fb7
refactor: rename to getDebtorCommitmentHash
chrismin Oct 2, 2018
7455713
feat: add documentation
chrismin Oct 2, 2018
e6d5465
refactor: import constant from simple interest loan terms
chrismin Oct 2, 2018
7d87c2c
fix: placate hound
chrismin Oct 2, 2018
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
33 changes: 33 additions & 0 deletions __test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Dharma, Web3 } from "../../../src";

jest.unmock("@dharmaprotocol/contracts");

import { VALID_MAX_LTV_LOAN_ORDER_PARAMS } from "./scenarios/valid_max_ltv_loan_order_params";

import { testConstructor } from "./runners/constructor";
import { testCreateAndSignAsCreditor } from "./runners/create_and_sign_as_creditor";
import { testSignAsCreditor } from "./runners/sign_as_creditor";
import { testSignAsDebtor } from "./runners/sign_as_debtor";

const provider = new Web3.providers.HttpProvider("http://localhost:8545");
const dharma = new Dharma(provider);

describe("Max LTV Loan Offer (Integration)", () => {
describe("constructor", () => {
testConstructor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS);
});

describe("createAndSignAsCreditor", async () => {
await testCreateAndSignAsCreditor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS);
});

describe("signAsCreditor", async () => {
await testSignAsCreditor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS);
});

describe("signAsDebtor", async () => {
await testSignAsDebtor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS);
});

describe("acceptAsDebtor", () => {});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block is empty

});
31 changes: 31 additions & 0 deletions __test__/integration/max_ltv_loan_offer/runners/constructor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Dharma } from "../../../../src";

import { MaxLTVLoanOffer } from "../../../../src/types";

import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv";

export function testConstructor(dharma: Dharma, params: MaxLTVParams) {
describe("passing valid params", () => {
let loanOffer: MaxLTVLoanOffer;

beforeEach(async () => {
loanOffer = await MaxLTVLoanOffer.create(dharma, params);
});

test("returns a MaxLTVLoanOffer", () => {
expect(loanOffer).toBeInstanceOf(MaxLTVLoanOffer);
});

test("not signed by the debtor", () => {
const isSignedByDebtor = loanOffer.isSignedByDebtor();

expect(isSignedByDebtor).toEqual(false);
});

test("not signed by the creditor", () => {
const isSignedByCreditor = loanOffer.isSignedByCreditor();

expect(isSignedByCreditor).toEqual(false);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Dharma } from "../../../../src";

import { MaxLTVLoanOffer } from "../../../../src/types";

import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv";

import { ACCOUNTS } from "../../../accounts";

export async function testCreateAndSignAsCreditor(dharma: Dharma, params: MaxLTVParams) {
describe("passing valid params", () => {
let loanOffer: MaxLTVLoanOffer;

beforeEach(async () => {
const creditor = ACCOUNTS[0].address;

loanOffer = await MaxLTVLoanOffer.createAndSignAsCreditor(dharma, params, creditor);
});

test("returns a MaxLTVLoanOffer", () => {
expect(loanOffer).toBeInstanceOf(MaxLTVLoanOffer);
});

test("not signed by the debtor", () => {
const isSignedByDebtor = loanOffer.isSignedByDebtor();

expect(isSignedByDebtor).toEqual(false);
});

test("is signed by the creditor", () => {
const isSignedByCreditor = loanOffer.isSignedByCreditor();

expect(isSignedByCreditor).toEqual(true);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Dharma } from "../../../../src";

import { MaxLTVLoanOffer } from "../../../../src/types";

import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv";

import { ACCOUNTS } from "../../../accounts";

export async function testSignAsCreditor(dharma: Dharma, params: MaxLTVParams) {
describe("passing valid params", () => {
const creditor = ACCOUNTS[0].address;

let loanOffer: MaxLTVLoanOffer;

beforeEach(async () => {
loanOffer = await MaxLTVLoanOffer.create(dharma, params);
});

test("signs the offer as the creditor", async () => {
const isSignedByCreditorBefore = loanOffer.isSignedByCreditor();
expect(isSignedByCreditorBefore).toBe(false);

await loanOffer.signAsCreditor(creditor);

const isSignedByCreditorAfter = loanOffer.isSignedByCreditor();
expect(isSignedByCreditorAfter).toBe(true);
});
});
}
132 changes: 132 additions & 0 deletions __test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Dharma } from "../../../../src";

import { MaxLTVLoanOffer } from "../../../../src/types";

import { MAX_LTV_LOAN_OFFER_ERRORS, MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv";

import { generateSignedPrice } from "../utils/generate_signed_price";

import { ACCOUNTS } from "../../../accounts";

export async function testSignAsDebtor(dharma: Dharma, params: MaxLTVParams) {
describe("passing valid params", () => {
const creditor = ACCOUNTS[0].address;
const priceProvider = params.priceProvider;
const debtor = ACCOUNTS[1].address;

let loanOffer: MaxLTVLoanOffer;

async function setPrices() {
const principalTokenAddress = await dharma.contracts.getTokenAddressBySymbolAsync(
params.principalToken,
);
const collateralTokenAddress = await dharma.contracts.getTokenAddressBySymbolAsync(
params.collateralToken,
);

const principalPrice = await generateSignedPrice(
dharma,
priceProvider,
principalTokenAddress,
10,
Math.round(Date.now() / 1000),
);
const collateralPrice = await generateSignedPrice(
dharma,
priceProvider,
collateralTokenAddress,
10,
Math.round(Date.now() / 1000),
);

loanOffer.setPrincipalPrice(principalPrice);
loanOffer.setCollateralPrice(collateralPrice);
}

beforeEach(async () => {
loanOffer = await MaxLTVLoanOffer.create(dharma, params);
});

test("signs the offer as the debtor if all prerequisites are met", async () => {
const isSignedByDebtorBefore = loanOffer.isSignedByDebtor();
expect(isSignedByDebtorBefore).toBe(false);

await loanOffer.signAsCreditor(creditor);

await setPrices();

loanOffer.setCollateralAmount(160);

await loanOffer.signAsDebtor(debtor);

const isSignedByDebtorAfter = loanOffer.isSignedByDebtor();
expect(isSignedByDebtorAfter).toBe(true);
});

describe("should throw", () => {
test("when the debtor has already signed", async () => {
const isSignedByDebtorBefore = loanOffer.isSignedByDebtor();
expect(isSignedByDebtorBefore).toBe(false);

await loanOffer.signAsCreditor(creditor);

await setPrices();

loanOffer.setCollateralAmount(160);

await loanOffer.signAsDebtor(debtor);

// second attempt to sign as debtor
await expect(loanOffer.signAsDebtor(debtor)).rejects.toThrow(
MAX_LTV_LOAN_OFFER_ERRORS.ALREADY_SIGNED_BY_DEBTOR(),
);
});

test("when prices are not set", async () => {
const isSignedByDebtorBefore = loanOffer.isSignedByDebtor();
expect(isSignedByDebtorBefore).toBe(false);

await loanOffer.signAsCreditor(creditor);

loanOffer.setCollateralAmount(160);

await expect(loanOffer.signAsDebtor(debtor)).rejects.toThrow(
MAX_LTV_LOAN_OFFER_ERRORS.PRICES_NOT_SET(),
);
});

test("when the collateral amount is not set", async () => {
const isSignedByDebtorBefore = loanOffer.isSignedByDebtor();
expect(isSignedByDebtorBefore).toBe(false);

await loanOffer.signAsCreditor(creditor);

await setPrices();

await expect(loanOffer.signAsDebtor(debtor)).rejects.toThrow(
MAX_LTV_LOAN_OFFER_ERRORS.COLLATERAL_AMOUNT_NOT_SET(),
);
});

test("when the collateral amount is insufficient", async () => {
const isSignedByDebtorBefore = loanOffer.isSignedByDebtor();
expect(isSignedByDebtorBefore).toBe(false);

await loanOffer.signAsCreditor(creditor);

const collateralAmount = 10;

loanOffer.setCollateralAmount(collateralAmount);

await setPrices();

await expect(loanOffer.signAsDebtor(debtor)).rejects.toThrow(
MAX_LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT(
collateralAmount,
params.collateralToken,
),
);
});
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv";

import { ACCOUNTS } from "../../../accounts";

export const VALID_MAX_LTV_LOAN_ORDER_PARAMS: MaxLTVParams = {
collateralToken: "MKR",
expiresInDuration: 5,
expiresInUnit: "days",
interestRate: 12.3,
maxLTV: 50,
priceProvider: ACCOUNTS[0].address,
principalAmount: 10,
principalToken: "REP",
termDuration: 6,
termUnit: "months",
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Dharma } from "../../../../src";

import { BigNumber } from "../../../../utils/bignumber";

import { SignedPrice } from "../../../../src/types/loan_offer/signed_price";

import { SignatureUtils } from "../../../../utils/signature_utils";

import { Web3Utils } from "../../../../utils/web3_utils";

export async function generateSignedPrice(
dharma: Dharma,
priceProvider: string,
tokenAddress: string,
tokenPriceAsNumber: number,
timestampAsNumber: number,
): Promise<SignedPrice> {
const tokenPrice = new BigNumber(tokenPriceAsNumber);
const timestamp = new BigNumber(timestampAsNumber);

const priceHash = Web3Utils.soliditySHA3(tokenAddress, tokenPrice, timestamp);

const operatorSignature = await dharma.sign.signPayloadWithAddress(
priceHash,
priceProvider,
false,
);

return { tokenAddress, tokenPrice, timestamp, operatorSignature };
}
2 changes: 1 addition & 1 deletion src/adapters/simple_interest_loan_terms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const MAX_PRINCIPAL_AMOUNT_HEX = "0xffffffffffffffffffffffff";
const MAX_TERM_LENGTH_VALUE_HEX = "0xffff";

const MAX_INTEREST_RATE_PRECISION = 4;
const FIXED_POINT_SCALING_FACTOR = 10 ** MAX_INTEREST_RATE_PRECISION;
export const FIXED_POINT_SCALING_FACTOR = 10 ** MAX_INTEREST_RATE_PRECISION;
const MAX_INTEREST_RATE = 2 ** 24 / FIXED_POINT_SCALING_FACTOR;

export class SimpleInterestLoanTerms {
Expand Down
18 changes: 15 additions & 3 deletions src/loan/debt_order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ export interface OrderData {
export interface DebtOrderParams {
principalAmount: number;
principalToken: string;
collateralAmount: number;
collateralToken: string;
interestRate: number;
termDuration: number;
termUnit: DurationUnit;
Expand All @@ -82,6 +80,12 @@ export interface DebtOrderParams {
relayerAddress?: string;
relayerFeeAmount?: number;
creditorFeeAmount?: number;
debtorFeeAmount?: number;
}

export interface CollateralizedDebtOrderParams extends DebtOrderParams {
collateralAmount: number;
collateralToken: string;
}

export interface DebtOrderTerms {
Expand All @@ -102,7 +106,7 @@ export class DebtOrder {

public static async create<T extends DebtOrder>(
dharma: Dharma,
params: DebtOrderParams,
params: CollateralizedDebtOrderParams,
): Promise<T> {
const {
principalAmount,
Expand All @@ -117,6 +121,7 @@ export class DebtOrder {
expiresInDuration,
expiresInUnit,
creditorFeeAmount,
debtorFeeAmount,
} = params;

const principal = new TokenAmount(principalAmount, principalToken);
Expand Down Expand Up @@ -172,6 +177,13 @@ export class DebtOrder {
data.creditorFee = creditorFee.rawAmount;
}

if (debtorFeeAmount && debtorFeeAmount > 0) {
const debtorFee = new TokenAmount(debtorFeeAmount, principalToken);

loanRequestConstructorParams.debtorFee = debtorFee;
data.debtorFee = debtorFee.rawAmount;
}

data.kernelVersion = debtKernel.address;
data.issuanceVersion = repaymentRouter.address;
data.salt = salt;
Expand Down
4 changes: 2 additions & 2 deletions src/loan/loan_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as singleLineString from "single-line-string";

import { Dharma } from "../types/dharma";

import { DEBT_ORDER_ERRORS, DebtOrder, DebtOrderParams } from "./debt_order";
import { CollateralizedDebtOrderParams, DEBT_ORDER_ERRORS, DebtOrder } from "./debt_order";

import { EthereumAddress } from "../types";

Expand Down Expand Up @@ -35,7 +35,7 @@ export class LoanRequest extends DebtOrder {
*/
public static async createAndSignAsDebtor(
dharma: Dharma,
params: DebtOrderParams,
params: CollateralizedDebtOrderParams,
debtor?: string,
): Promise<LoanRequest> {
const request = await LoanRequest.create<LoanRequest>(dharma, params);
Expand Down
2 changes: 1 addition & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export { TokenAmount } from "./token_amount";
export { InterestRate } from "./interest_rate";
export { TimeInterval } from "./time_interval";

export { LoanOffer } from "./loan_offer";
export { LoanOffer, MaxLTVLoanOffer } from "./loan_offer";
Loading