From 4d71a0b745f8b1f016de226656ae9773d8eddb49 Mon Sep 17 00:00:00 2001 From: Kayvon Tehranian Date: Thu, 27 Sep 2018 18:09:28 -0700 Subject: [PATCH 01/38] refactor: separate collateral from debt order params --- src/loan/debt_order.ts | 9 ++++++--- src/loan/loan_request.ts | 4 ++-- src/types/loan_offer/loan_offer.ts | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/loan/debt_order.ts b/src/loan/debt_order.ts index c98dfca6..599eb16a 100644 --- a/src/loan/debt_order.ts +++ b/src/loan/debt_order.ts @@ -72,8 +72,6 @@ export interface OrderData { export interface DebtOrderParams { principalAmount: number; principalToken: string; - collateralAmount: number; - collateralToken: string; interestRate: number; termDuration: number; termUnit: DurationUnit; @@ -84,6 +82,11 @@ export interface DebtOrderParams { creditorFeeAmount?: number; } +export interface CollateralizedDebtOrderParams extends DebtOrderParams { + collateralAmount: number; + collateralToken: string; +} + export interface DebtOrderTerms { principalAmount: number; principalTokenSymbol: string; @@ -102,7 +105,7 @@ export class DebtOrder { public static async create( dharma: Dharma, - params: DebtOrderParams, + params: CollateralizedDebtOrderParams, ): Promise { const { principalAmount, diff --git a/src/loan/loan_request.ts b/src/loan/loan_request.ts index 0eb96944..b587dae9 100644 --- a/src/loan/loan_request.ts +++ b/src/loan/loan_request.ts @@ -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 { DEBT_ORDER_ERRORS, DebtOrder, CollateralizedDebtOrderParams } from "./debt_order"; import { EthereumAddress } from "../types"; @@ -35,7 +35,7 @@ export class LoanRequest extends DebtOrder { */ public static async createAndSignAsDebtor( dharma: Dharma, - params: DebtOrderParams, + params: CollateralizedDebtOrderParams, debtor?: string, ): Promise { const request = await LoanRequest.create(dharma, params); diff --git a/src/types/loan_offer/loan_offer.ts b/src/types/loan_offer/loan_offer.ts index bd113a92..452a025b 100644 --- a/src/types/loan_offer/loan_offer.ts +++ b/src/types/loan_offer/loan_offer.ts @@ -1,6 +1,6 @@ import { Dharma } from "../dharma"; -import { DEBT_ORDER_ERRORS, DebtOrder, DebtOrderParams } from "../../loan/debt_order"; +import { DEBT_ORDER_ERRORS, DebtOrder, CollateralizedDebtOrderParams } from "../../loan/debt_order"; import { EthereumAddress } from "../../types"; @@ -12,7 +12,7 @@ import { SignatureUtils } from "../../../utils/signature_utils"; export class LoanOffer extends DebtOrder { public static async createAndSignAsCreditor( dharma: Dharma, - params: DebtOrderParams, + params: CollateralizedDebtOrderParams, creditor?: string, ): Promise { const offer = await LoanOffer.create(dharma, params); From 94a60c5f0bd85c9e76fc709b8e9afca459594050 Mon Sep 17 00:00:00 2001 From: Kayvon Tehranian Date: Thu, 27 Sep 2018 18:09:53 -0700 Subject: [PATCH 02/38] feat: introduce LTVLoanOffer class --- src/types/loan_offer/ltv.ts | 93 +++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/types/loan_offer/ltv.ts diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts new file mode 100644 index 00000000..e4c93a20 --- /dev/null +++ b/src/types/loan_offer/ltv.ts @@ -0,0 +1,93 @@ +import { DebtOrderParams } from "../../loan/debt_order"; + +import { Dharma } from "../dharma"; + +import { + DebtOrderData, + ECDSASignature, + EthereumAddress, + InterestRate, + TimeInterval, + TokenAmount, +} from "../"; + +import { BigNumber } from "../../../utils/bignumber"; + +export interface LTVData { + principal: TokenAmount; + interestRate: InterestRate; + termLength: TimeInterval; + expiresIn: TimeInterval; + ltv: BigNumber; + collateralTokenSymbol: string; + priceProvider: string; + relayer: EthereumAddress; + relayerFee: TokenAmount; +} + +export interface LTVParams extends DebtOrderParams { + ltv: number; + collateralTokenSymbol: string; + priceProvider: string; +} + +export interface CreditorCommmitmentTerms { + decisionEngineAddress: string; + decisionEngineParams: DecisionEngineParams; +} + +export interface DecisionEngineParams { + ltv: BigNumber; +} + +export class LTVLoanOffer { + private readonly data: LTVData; + + private creditorSignature?: ECDSASignature; + private debtorSignature?: ECDSASignature; + + constructor(private readonly dharma: Dharma, params: LTVParams) { + const { + ltv, + priceProvider, + collateralTokenSymbol, + principalAmount, + principalToken, + relayerAddress, + relayerFeeAmount, + interestRate, + termDuration, + termUnit, + expiresInDuration, + expiresInUnit, + } = params; + + this.data = { + principal: new TokenAmount(principalAmount, principalToken), + interestRate: new InterestRate(interestRate), + termLength: new TimeInterval(termDuration, termUnit), + expiresIn: new TimeInterval(expiresInDuration, expiresInUnit), + ltv: new BigNumber(ltv), + relayer: new EthereumAddress(relayerAddress), + relayerFee: new TokenAmount(relayerFeeAmount, principalToken), + collateralTokenSymbol, + priceProvider, + }; + } + + // private async toDebtOrderData: Promise { + // + // } + // + // public async acceptAsDebtor(): Promise { + // + // } + // + // public async signAsDebtor(): Promise { + // + // } + // + // public getLoanOfferHash(): string { + // + // } +} From b09e0f7666a36bb88527b1d663654a5b386ed930 Mon Sep 17 00:00:00 2001 From: Kayvon Tehranian Date: Fri, 28 Sep 2018 10:43:46 -0700 Subject: [PATCH 03/38] feat: hash creditor commitment terms --- src/types/loan_offer/ltv.ts | 66 ++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index e4c93a20..ca9d680d 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -1,3 +1,5 @@ +import { Web3Utils } from "../../../utils/web3_utils"; + import { DebtOrderParams } from "../../loan/debt_order"; import { Dharma } from "../dharma"; @@ -41,6 +43,8 @@ export interface DecisionEngineParams { } export class LTVLoanOffer { + public static decisionEngineAddress = "test"; + private readonly data: LTVData; private creditorSignature?: ECDSASignature; @@ -75,6 +79,38 @@ export class LTVLoanOffer { }; } + /** + * Eventually signs the loan offer as the creditor. + * + * @throws Throws if the loan offer is already signed by a creditor. + * + * @example + * loanOffer.signAsCreditor(); + * => Promise + * + * @return {Promise} + */ + public async signAsCreditor(creditorAddress?: string): Promise { + if (this.isSignedByCreditor()) { + throw new Error(DEBT_ORDER_ERRORS.ALREADY_SIGNED_BY_CREDITOR); + } + + this.data.creditor = await EthereumAddress.validAddressOrCurrentUser( + this.dharma, + creditorAddress, + ); + + const loanOfferHash = this.getCreditorCommitmentHash(); + + const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; + + this.data.creditorSignature = await this.dharma.sign.signPayloadWithAddress( + loanOfferHash, + this.data.creditor, + isMetaMask, + ); + } + // private async toDebtOrderData: Promise { // // } @@ -86,8 +122,30 @@ export class LTVLoanOffer { // public async signAsDebtor(): Promise { // // } - // - // public getLoanOfferHash(): string { - // - // } + + public getCreditorCommitmentTermsHash(): string { + return Web3Utils.soliditySHA3( + this.data.kernelVersion, + this.data.issuanceVersion, + this.data.termsContract, + this.data.principalAmount, + this.data.principalToken, + this.data.collateralToken, + this.data.ltv, + this.data.interestRate, + this.data.debtorFee, + this.data.creditorFee, + this.data.relayer, + this.data.relayerFee, + this.data.expirationTimestampInSec, + this.data.salt, + ); + } + + public getCreditorCommitmentHash(): string { + return Web3Utils.soliditySHA3( + LTVLoanOffer.decisionEngineAddress, + this.getCreditorCommitmentTermsHash(), + ); + } } From 3df8a89984e1f67991c7fac1ddd4f4bd9e638565 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Fri, 28 Sep 2018 13:20:20 -0700 Subject: [PATCH 04/38] feat: introduce collateralAmount getter and setter --- src/types/loan_offer/ltv.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index ca9d680d..b99c208e 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -49,6 +49,7 @@ export class LTVLoanOffer { private creditorSignature?: ECDSASignature; private debtorSignature?: ECDSASignature; + private collateralAmount?: number; constructor(private readonly dharma: Dharma, params: LTVParams) { const { @@ -111,17 +112,16 @@ export class LTVLoanOffer { ); } - // private async toDebtOrderData: Promise { - // - // } - // - // public async acceptAsDebtor(): Promise { - // - // } - // - // public async signAsDebtor(): Promise { - // - // } + public setCollateralAmount(collateralAmount: number) { + // TODO: assert prices are set + // TODO: assert collateralAmount sufficient + + this.collateralAmount = collateralAmount; + } + + public getCollateralAmount(): number { + return this.collateralAmount; + } public getCreditorCommitmentTermsHash(): string { return Web3Utils.soliditySHA3( From 24f52378e7d4b31464405455fa22f4062189784a Mon Sep 17 00:00:00 2001 From: Chris Min Date: Fri, 28 Sep 2018 13:26:38 -0700 Subject: [PATCH 05/38] feat: introduce principalPrice and collateralPrice getters and setters --- src/types/loan_offer/ltv.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index b99c208e..847fd814 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -13,6 +13,8 @@ import { TokenAmount, } from "../"; +import { SignedPrice } from "./signed_price"; + import { BigNumber } from "../../../utils/bignumber"; export interface LTVData { @@ -50,6 +52,8 @@ export class LTVLoanOffer { private creditorSignature?: ECDSASignature; private debtorSignature?: ECDSASignature; private collateralAmount?: number; + private principalPrice?: SignedPrice; + private collateralPrice?: SignedPrice; constructor(private readonly dharma: Dharma, params: LTVParams) { const { @@ -112,10 +116,31 @@ export class LTVLoanOffer { ); } + public setPrincipalPrice(principalPrice: SignedPrice) { + // TODO: assert signed address matches principal token address + // TODO: assert signed price feed provider address is the address we expect + // TODO: assert signed time is within some delta of current time (?) + this.principalPrice = principalPrice; + } + + public getPrincipalPrice(): SignedPrice { + return this.principalPrice; + } + + public setCollateralPrice(collateralPrice: SignedPrice) { + // TODO: assert signed address matches collateral token address + // TODO: assert signed price feed provider address is the address we expect + // TODO: assert signed time is within some delta of current time (?) + this.collateralPrice = collateralPrice; + } + + public getCollateralPrice(): SignedPrice { + return this.principalPrice; + } + public setCollateralAmount(collateralAmount: number) { // TODO: assert prices are set // TODO: assert collateralAmount sufficient - this.collateralAmount = collateralAmount; } From b8373bf0761ad45e0230d1eac821d6570d5d78c0 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Fri, 28 Sep 2018 13:41:35 -0700 Subject: [PATCH 06/38] feat: introduce signAsDebtor method --- src/types/loan_offer/ltv.ts | 59 ++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index 847fd814..35fc50e1 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -49,11 +49,13 @@ export class LTVLoanOffer { private readonly data: LTVData; + private creditor?: string; private creditorSignature?: ECDSASignature; private debtorSignature?: ECDSASignature; private collateralAmount?: number; private principalPrice?: SignedPrice; private collateralPrice?: SignedPrice; + private debtor?: string; constructor(private readonly dharma: Dharma, params: LTVParams) { const { @@ -96,11 +98,9 @@ export class LTVLoanOffer { * @return {Promise} */ public async signAsCreditor(creditorAddress?: string): Promise { - if (this.isSignedByCreditor()) { - throw new Error(DEBT_ORDER_ERRORS.ALREADY_SIGNED_BY_CREDITOR); - } + // TODO: check if already signed by creditor - this.data.creditor = await EthereumAddress.validAddressOrCurrentUser( + this.creditor = await EthereumAddress.validAddressOrCurrentUser( this.dharma, creditorAddress, ); @@ -109,9 +109,9 @@ export class LTVLoanOffer { const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; - this.data.creditorSignature = await this.dharma.sign.signPayloadWithAddress( + this.creditorSignature = await this.dharma.sign.signPayloadWithAddress( loanOfferHash, - this.data.creditor, + this.creditor, isMetaMask, ); } @@ -148,7 +148,23 @@ export class LTVLoanOffer { return this.collateralAmount; } - public getCreditorCommitmentTermsHash(): string { + public async signAsDebtor(debtorAddress?: string): Promise { + // TODO: check if already signed by debtor + + this.debtor = await EthereumAddress.validAddressOrCurrentUser(this.dharma, debtorAddress); + + const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; + + const debtorCommitmentHash = this.getDebtorCommitHash(); + + this.debtorSignature = await this.dharma.sign.signPayloadWithAddress( + debtorCommitmentHash, + this.debtor, + isMetaMask, + ); + } + + private getCreditorCommitmentTermsHash(): string { return Web3Utils.soliditySHA3( this.data.kernelVersion, this.data.issuanceVersion, @@ -167,10 +183,37 @@ export class LTVLoanOffer { ); } - public getCreditorCommitmentHash(): string { + private getCreditorCommitmentHash(): string { return Web3Utils.soliditySHA3( LTVLoanOffer.decisionEngineAddress, this.getCreditorCommitmentTermsHash(), ); } + + private getIssuanceCommitmentHash(): string { + return Web3Utils.soliditySHA3( + this.data.issuanceVersion, + this.debtor, + this.data.underwriter, + this.data.underwriterRiskRating, + this.data.termsContract, + this.data.termsContractParameters, + this.data.salt, + ); + } + + private getDebtorCommitHash(): string { + return Web3Utils.soliditySHA3( + this.data.kernelVersion, + this.getIssuanceCommitmentHash(), + this.data.underwriterFee, + this.data.principal.rawAmount, + this.data.principalToken, + this.data.debtorFee, + this.data.creditorFee, + this.data.relayer, + this.data.relayerFee, + this.data.expirationTimestampInSec, + ); + } } From 313b236e2aeb0eb4bdbb9ca5d5427ed0ac587e9a Mon Sep 17 00:00:00 2001 From: Chris Min Date: Fri, 28 Sep 2018 13:48:56 -0700 Subject: [PATCH 07/38] feat: add placeholder for acceptAsDebtor --- src/types/loan_offer/ltv.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index 35fc50e1..617589fa 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -164,6 +164,10 @@ export class LTVLoanOffer { ); } + public async acceptAsDebtor(): Promise { + // TODO: send transaction to CreditorProxtContract + } + private getCreditorCommitmentTermsHash(): string { return Web3Utils.soliditySHA3( this.data.kernelVersion, From b4ac44f25e23a01ef30909852f5b18741a779ac2 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Fri, 28 Sep 2018 14:03:30 -0700 Subject: [PATCH 08/38] refactor: remove unused interfaces --- src/types/loan_offer/ltv.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index 617589fa..ee833110 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -35,15 +35,6 @@ export interface LTVParams extends DebtOrderParams { priceProvider: string; } -export interface CreditorCommmitmentTerms { - decisionEngineAddress: string; - decisionEngineParams: DecisionEngineParams; -} - -export interface DecisionEngineParams { - ltv: BigNumber; -} - export class LTVLoanOffer { public static decisionEngineAddress = "test"; From 97556c756a833631d356b563e0f68005e2718db6 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Fri, 28 Sep 2018 14:31:04 -0700 Subject: [PATCH 09/38] feat: make signAsDebtor more robust --- src/types/loan_offer/ltv.ts | 46 +++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index ee833110..c9dc27a2 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -1,3 +1,5 @@ +import * as singleLineString from "single-line-string"; + import { Web3Utils } from "../../../utils/web3_utils"; import { DebtOrderParams } from "../../loan/debt_order"; @@ -17,6 +19,13 @@ import { SignedPrice } from "./signed_price"; import { BigNumber } from "../../../utils/bignumber"; +export const LTV_LOAN_OFFER_ERRORS = { + INSUFFICIENT_COLLATERAL_AMOUNT: (collateralAmount: number, collateralTokenSymbol: string) => + singleLineString`Collateral of ${collateralAmount} ${collateralTokenSymbol} is too high + for the maximum loan-to-value.`, + PRICES_NOT_SET: () => `The prices of the principal and collateral must be set first.`, +}; + export interface LTVData { principal: TokenAmount; interestRate: InterestRate; @@ -108,7 +117,6 @@ export class LTVLoanOffer { } public setPrincipalPrice(principalPrice: SignedPrice) { - // TODO: assert signed address matches principal token address // TODO: assert signed price feed provider address is the address we expect // TODO: assert signed time is within some delta of current time (?) this.principalPrice = principalPrice; @@ -119,7 +127,6 @@ export class LTVLoanOffer { } public setCollateralPrice(collateralPrice: SignedPrice) { - // TODO: assert signed address matches collateral token address // TODO: assert signed price feed provider address is the address we expect // TODO: assert signed time is within some delta of current time (?) this.collateralPrice = collateralPrice; @@ -142,6 +149,22 @@ export class LTVLoanOffer { public async signAsDebtor(debtorAddress?: string): Promise { // TODO: check if already signed by debtor + if (!this.principalPrice || !this.collateralPrice) { + throw new Error(LTV_LOAN_OFFER_ERRORS.PRICES_NOT_SET()); + } + + // TODO: assert signed address matches principal token address + // TODO: assert signed address matches collateral token address + + if (!this.collateralAmountIsSufficient()) { + throw new Error( + LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT( + this.collateralAmount, + this.data.collateralTokenSymbol, + ), + ); + } + this.debtor = await EthereumAddress.validAddressOrCurrentUser(this.dharma, debtorAddress); const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; @@ -211,4 +234,23 @@ export class LTVLoanOffer { this.data.expirationTimestampInSec, ); } + + private collateralAmountIsSufficient(): boolean { + if (!this.collateralAmount || !this.principalPrice || !this.collateralPrice) { + return false; + } + + // We do not use the TokenAmount's rawValue here because what matters is the "real world" amount + // of the principal and collateral, without regard for how many decimals are used in their + // blockchain representations. + const principalValue = new BigNumber(this.data.principal.decimalAmount).times( + this.principalPrice.tokenPrice, + ); + + const collateralValue = new BigNumber(this.collateralAmount).times( + this.collateralPrice.tokenPrice, + ); + + return principalValue.div(collateralValue).lte(this.data.ltv); + } } From 1bd585e9611e6a3148954feefbe5e0efa2ec91b0 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 12:35:00 -0700 Subject: [PATCH 10/38] fix: add small fixes, TODOs --- src/types/loan_offer/ltv.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/ltv.ts index c9dc27a2..2327e8a2 100644 --- a/src/types/loan_offer/ltv.ts +++ b/src/types/loan_offer/ltv.ts @@ -19,14 +19,14 @@ import { SignedPrice } from "./signed_price"; import { BigNumber } from "../../../utils/bignumber"; -export const LTV_LOAN_OFFER_ERRORS = { +export const MAX_LTV_LOAN_OFFER_ERRORS = { INSUFFICIENT_COLLATERAL_AMOUNT: (collateralAmount: number, collateralTokenSymbol: string) => - singleLineString`Collateral of ${collateralAmount} ${collateralTokenSymbol} is too high + singleLineString`Collateral of ${collateralAmount} ${collateralTokenSymbol} is too insufficient for the maximum loan-to-value.`, PRICES_NOT_SET: () => `The prices of the principal and collateral must be set first.`, }; -export interface LTVData { +export interface MaxLTVData { principal: TokenAmount; interestRate: InterestRate; termLength: TimeInterval; @@ -38,16 +38,17 @@ export interface LTVData { relayerFee: TokenAmount; } -export interface LTVParams extends DebtOrderParams { +export interface MaxLTVParams extends DebtOrderParams { ltv: number; collateralTokenSymbol: string; priceProvider: string; } -export class LTVLoanOffer { +export class MaxLTVLoanOffer { + // TODO: replace with decision engine address (async function?) public static decisionEngineAddress = "test"; - private readonly data: LTVData; + private readonly data: MaxLTVData; private creditor?: string; private creditorSignature?: ECDSASignature; @@ -57,7 +58,7 @@ export class LTVLoanOffer { private collateralPrice?: SignedPrice; private debtor?: string; - constructor(private readonly dharma: Dharma, params: LTVParams) { + constructor(private readonly dharma: Dharma, params: MaxLTVParams) { const { ltv, priceProvider, @@ -150,7 +151,7 @@ export class LTVLoanOffer { // TODO: check if already signed by debtor if (!this.principalPrice || !this.collateralPrice) { - throw new Error(LTV_LOAN_OFFER_ERRORS.PRICES_NOT_SET()); + throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.PRICES_NOT_SET()); } // TODO: assert signed address matches principal token address @@ -158,7 +159,7 @@ export class LTVLoanOffer { if (!this.collateralAmountIsSufficient()) { throw new Error( - LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT( + MAX_LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT( this.collateralAmount, this.data.collateralTokenSymbol, ), @@ -203,7 +204,7 @@ export class LTVLoanOffer { private getCreditorCommitmentHash(): string { return Web3Utils.soliditySHA3( - LTVLoanOffer.decisionEngineAddress, + MaxLTVLoanOffer.decisionEngineAddress, this.getCreditorCommitmentTermsHash(), ); } From a4e840ea867ce8cad35440599a4a43c8e34c8593 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 12:36:33 -0700 Subject: [PATCH 11/38] refactor: rename filename --- src/types/loan_offer/index.ts | 1 + src/types/loan_offer/{ltv.ts => max_ltv.ts} | 0 2 files changed, 1 insertion(+) rename src/types/loan_offer/{ltv.ts => max_ltv.ts} (100%) diff --git a/src/types/loan_offer/index.ts b/src/types/loan_offer/index.ts index cd4d0336..3c6ede53 100644 --- a/src/types/loan_offer/index.ts +++ b/src/types/loan_offer/index.ts @@ -1 +1,2 @@ export { LoanOffer } from "./loan_offer"; +export { MaxLTVLoanOffer } from "./max_ltv"; diff --git a/src/types/loan_offer/ltv.ts b/src/types/loan_offer/max_ltv.ts similarity index 100% rename from src/types/loan_offer/ltv.ts rename to src/types/loan_offer/max_ltv.ts From 2661299119adfd72d8c8ce7de7aea612bb49a34b Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 12:40:18 -0700 Subject: [PATCH 12/38] refactor: rename variables --- src/types/loan_offer/max_ltv.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 2327e8a2..c6e2c816 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -31,7 +31,7 @@ export interface MaxLTVData { interestRate: InterestRate; termLength: TimeInterval; expiresIn: TimeInterval; - ltv: BigNumber; + maxLTV: BigNumber; collateralTokenSymbol: string; priceProvider: string; relayer: EthereumAddress; @@ -39,8 +39,8 @@ export interface MaxLTVData { } export interface MaxLTVParams extends DebtOrderParams { - ltv: number; - collateralTokenSymbol: string; + maxLTV: number; + collateralToken: string; priceProvider: string; } @@ -60,9 +60,9 @@ export class MaxLTVLoanOffer { constructor(private readonly dharma: Dharma, params: MaxLTVParams) { const { - ltv, + maxLTV, priceProvider, - collateralTokenSymbol, + collateralToken, principalAmount, principalToken, relayerAddress, @@ -79,10 +79,10 @@ export class MaxLTVLoanOffer { interestRate: new InterestRate(interestRate), termLength: new TimeInterval(termDuration, termUnit), expiresIn: new TimeInterval(expiresInDuration, expiresInUnit), - ltv: new BigNumber(ltv), + maxLTV: new BigNumber(maxLTV), relayer: new EthereumAddress(relayerAddress), relayerFee: new TokenAmount(relayerFeeAmount, principalToken), - collateralTokenSymbol, + collateralTokenSymbol: collateralToken, priceProvider, }; } @@ -191,7 +191,7 @@ export class MaxLTVLoanOffer { this.data.principalAmount, this.data.principalToken, this.data.collateralToken, - this.data.ltv, + this.data.maxLTV, this.data.interestRate, this.data.debtorFee, this.data.creditorFee, @@ -252,6 +252,6 @@ export class MaxLTVLoanOffer { this.collateralPrice.tokenPrice, ); - return principalValue.div(collateralValue).lte(this.data.ltv); + return principalValue.div(collateralValue).lte(this.data.maxLTV); } } From d4bcdc12f49eb4a42cc13ff41cb47c02391b1e35 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 12:44:48 -0700 Subject: [PATCH 13/38] feat: add scaffolding for MaxLTVLoanOffer integration test --- .../max_ltv_loan_offer.spec.ts | 18 ++++++++++++++++++ .../valid_max_ltv_loan_order_params.ts | 14 ++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 __test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts create mode 100644 __test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts diff --git a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts new file mode 100644 index 00000000..733d3dd2 --- /dev/null +++ b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts @@ -0,0 +1,18 @@ +import { Dharma, Web3 } from "../../../src"; + +jest.unmock("@dharmaprotocol/contracts"); + +import { VALID_MAX_LTV_LOAN_ORDER_PARAMS } from "./scenarios/valid_max_ltv_loan_order_params"; + +const provider = new Web3.providers.HttpProvider("http://localhost:8545"); +const dharma = new Dharma(provider); + +describe("Max LTV Loan Offer (Integration)", () => { + describe("constructor", () => {}); + + describe("signAsCreditor", () => {}); + + describe("signAsDebtor", () => {}); + + describe("acceptAsDebtor", () => {}); +}); diff --git a/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts b/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts new file mode 100644 index 00000000..71f9a20e --- /dev/null +++ b/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts @@ -0,0 +1,14 @@ +import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv"; + +export const VALID_MAX_LTV_LOAN_ORDER_PARAMS: MaxLTVParams = { + collateralToken: "MKR", + expiresInDuration: 5, + expiresInUnit: "days", + interestRate: 12.3, + maxLTV: 50, + priceProvider: "test", + principalAmount: 5, + principalToken: "REP", + termDuration: 6, + termUnit: "months", +}; From e573d31fd484b5708828e2593780ab266abe0c0f Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 12:57:16 -0700 Subject: [PATCH 14/38] feat: make relayer and relayer fee optional --- .../scenarios/valid_max_ltv_loan_order_params.ts | 4 +++- src/types/index.ts | 2 +- src/types/loan_offer/max_ltv.ts | 15 ++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts b/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts index 71f9a20e..cd18d1fb 100644 --- a/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts +++ b/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts @@ -1,12 +1,14 @@ 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: "test", + priceProvider: ACCOUNTS[0].address, principalAmount: 5, principalToken: "REP", termDuration: 6, diff --git a/src/types/index.ts b/src/types/index.ts index ed231537..c68392e5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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"; diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index c6e2c816..4110f267 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -34,8 +34,8 @@ export interface MaxLTVData { maxLTV: BigNumber; collateralTokenSymbol: string; priceProvider: string; - relayer: EthereumAddress; - relayerFee: TokenAmount; + relayer?: EthereumAddress; + relayerFee?: TokenAmount; } export interface MaxLTVParams extends DebtOrderParams { @@ -74,17 +74,22 @@ export class MaxLTVLoanOffer { expiresInUnit, } = params; - this.data = { + const data = { principal: new TokenAmount(principalAmount, principalToken), interestRate: new InterestRate(interestRate), termLength: new TimeInterval(termDuration, termUnit), expiresIn: new TimeInterval(expiresInDuration, expiresInUnit), maxLTV: new BigNumber(maxLTV), - relayer: new EthereumAddress(relayerAddress), - relayerFee: new TokenAmount(relayerFeeAmount, principalToken), collateralTokenSymbol: collateralToken, priceProvider, }; + + if (relayerAddress && relayerFeeAmount) { + data.relayer = new EthereumAddress(relayerAddress); + data.relayerFee = new TokenAmount(relayerFeeAmount, principalToken); + } + + this.data = data; } /** From 5d41d64d6e912a002f3ecf2feeaf803658cbc5a6 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 13:11:32 -0700 Subject: [PATCH 15/38] feat: add isSignedByCreditor and isSignedByDebtor --- src/types/loan_offer/max_ltv.ts | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 4110f267..dd82fc1d 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -4,6 +4,8 @@ import { Web3Utils } from "../../../utils/web3_utils"; import { DebtOrderParams } from "../../loan/debt_order"; +import { SignatureUtils } from "../../../utils/signature_utils"; + import { Dharma } from "../dharma"; import { @@ -122,6 +124,30 @@ export class MaxLTVLoanOffer { ); } + /** + * Returns whether the loan offer has been signed by a creditor. + * + * @example + * loanOffer.isSignedByCreditor(); + * => true + * + * @return {boolean} + */ + public isSignedByCreditor(): boolean { + if ( + this.creditorSignature && + SignatureUtils.isValidSignature( + this.getCreditorCommitmentHash(), + this.data.creditorSignature, + this.data.creditor, + ) + ) { + return true; + } + + return false; + } + public setPrincipalPrice(principalPrice: SignedPrice) { // TODO: assert signed price feed provider address is the address we expect // TODO: assert signed time is within some delta of current time (?) @@ -184,6 +210,30 @@ export class MaxLTVLoanOffer { ); } + /** + * Returns whether the loan offer has been signed by a debtor. + * + * @example + * loanOffer.isSignedByDebtor(); + * => true + * + * @return {boolean} + */ + public isSignedByDebtor(): boolean { + if ( + this.debtorSignature && + SignatureUtils.isValidSignature( + this.getDebtorCommitHash(), + this.data.debtorSignature, + this.data.debtor, + ) + ) { + return true; + } + + return false; + } + public async acceptAsDebtor(): Promise { // TODO: send transaction to CreditorProxtContract } From 62608e5c8fb5696341dc977d6a64af1ec02cc8ef Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 13:12:47 -0700 Subject: [PATCH 16/38] test: add specs for constructor --- .../max_ltv_loan_offer.spec.ts | 6 +++- .../max_ltv_loan_offer/runners/constructor.ts | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 __test__/integration/max_ltv_loan_offer/runners/constructor.ts diff --git a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts index 733d3dd2..ff89e4c6 100644 --- a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts +++ b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts @@ -4,11 +4,15 @@ jest.unmock("@dharmaprotocol/contracts"); import { VALID_MAX_LTV_LOAN_ORDER_PARAMS } from "./scenarios/valid_max_ltv_loan_order_params"; +import { testConstructor } from "./runners/constructor"; + const provider = new Web3.providers.HttpProvider("http://localhost:8545"); const dharma = new Dharma(provider); describe("Max LTV Loan Offer (Integration)", () => { - describe("constructor", () => {}); + describe("constructor", () => { + testConstructor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS); + }); describe("signAsCreditor", () => {}); diff --git a/__test__/integration/max_ltv_loan_offer/runners/constructor.ts b/__test__/integration/max_ltv_loan_offer/runners/constructor.ts new file mode 100644 index 00000000..c3f8be48 --- /dev/null +++ b/__test__/integration/max_ltv_loan_offer/runners/constructor.ts @@ -0,0 +1,31 @@ +import { Dharma } from "../../../../src"; + +import { MaxLTVLoanOffer } from "../../../../src/types"; + +import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv"; + +export async function testConstructor(dharma: Dharma, params: MaxLTVParams) { + describe("passing valid params", () => { + let loanOffer: MaxLTVLoanOffer; + + beforeEach(() => { + loanOffer = new MaxLTVLoanOffer(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); + }); + }); +} From 29ca740406a92543544475b4bca0a8124fa271bf Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 13:40:07 -0700 Subject: [PATCH 17/38] feat: implement createAndSignAsCreditor --- src/types/loan_offer/max_ltv.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index dd82fc1d..d17ad9a1 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -50,6 +50,18 @@ export class MaxLTVLoanOffer { // TODO: replace with decision engine address (async function?) public static decisionEngineAddress = "test"; + public static async createAndSignAsCreditor( + dharma: Dharma, + params: MaxLTVParams, + creditor?: string, + ): Promise { + const offer = new MaxLTVLoanOffer(dharma, params); + + await offer.signAsCreditor(creditor); + + return offer; + } + private readonly data: MaxLTVData; private creditor?: string; @@ -138,8 +150,8 @@ export class MaxLTVLoanOffer { this.creditorSignature && SignatureUtils.isValidSignature( this.getCreditorCommitmentHash(), - this.data.creditorSignature, - this.data.creditor, + this.creditorSignature, + this.creditor, ) ) { return true; @@ -224,8 +236,8 @@ export class MaxLTVLoanOffer { this.debtorSignature && SignatureUtils.isValidSignature( this.getDebtorCommitHash(), - this.data.debtorSignature, - this.data.debtor, + this.debtorSignature, + this.debtor, ) ) { return true; From 7b8c6d03100bed7ed679089744fde6a37a13a758 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 13:40:56 -0700 Subject: [PATCH 18/38] feat: mock out commitment hash getters --- src/types/loan_offer/max_ltv.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index d17ad9a1..1923a873 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -270,6 +270,9 @@ export class MaxLTVLoanOffer { } private getCreditorCommitmentHash(): string { + // TODO: remove mock implementation + return Web3Utils.soliditySHA3("mockCreditorCommitmentHash"); + return Web3Utils.soliditySHA3( MaxLTVLoanOffer.decisionEngineAddress, this.getCreditorCommitmentTermsHash(), @@ -289,6 +292,9 @@ export class MaxLTVLoanOffer { } private getDebtorCommitHash(): string { + // TODO: remove mock implementation + return Web3Utils.soliditySHA3("mockDebtorCommitmentHash"); + return Web3Utils.soliditySHA3( this.data.kernelVersion, this.getIssuanceCommitmentHash(), From 80e47c72fc05710c24b63ca8c8da9aba7eb80cad Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 13:41:24 -0700 Subject: [PATCH 19/38] feat: add specs for createAndSignAsCreditor --- .../max_ltv_loan_offer.spec.ts | 5 +++ .../max_ltv_loan_offer/runners/constructor.ts | 2 +- .../runners/createAndSignAsCreditor.ts | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 __test__/integration/max_ltv_loan_offer/runners/createAndSignAsCreditor.ts diff --git a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts index ff89e4c6..d14e67de 100644 --- a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts +++ b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts @@ -5,6 +5,7 @@ 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/createAndSignAsCreditor"; const provider = new Web3.providers.HttpProvider("http://localhost:8545"); const dharma = new Dharma(provider); @@ -14,6 +15,10 @@ describe("Max LTV Loan Offer (Integration)", () => { testConstructor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS); }); + describe("createAndSignAsCreditor", async () => { + await testCreateAndSignAsCreditor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS); + }); + describe("signAsCreditor", () => {}); describe("signAsDebtor", () => {}); diff --git a/__test__/integration/max_ltv_loan_offer/runners/constructor.ts b/__test__/integration/max_ltv_loan_offer/runners/constructor.ts index c3f8be48..24cd265b 100644 --- a/__test__/integration/max_ltv_loan_offer/runners/constructor.ts +++ b/__test__/integration/max_ltv_loan_offer/runners/constructor.ts @@ -4,7 +4,7 @@ import { MaxLTVLoanOffer } from "../../../../src/types"; import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv"; -export async function testConstructor(dharma: Dharma, params: MaxLTVParams) { +export function testConstructor(dharma: Dharma, params: MaxLTVParams) { describe("passing valid params", () => { let loanOffer: MaxLTVLoanOffer; diff --git a/__test__/integration/max_ltv_loan_offer/runners/createAndSignAsCreditor.ts b/__test__/integration/max_ltv_loan_offer/runners/createAndSignAsCreditor.ts new file mode 100644 index 00000000..00bb8390 --- /dev/null +++ b/__test__/integration/max_ltv_loan_offer/runners/createAndSignAsCreditor.ts @@ -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); + }); + }); +} From 12d8b39566c1a682b3300d19a3f9997adbcf987e Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 13:44:12 -0700 Subject: [PATCH 20/38] fix: rename file --- .../integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts | 2 +- ...reateAndSignAsCreditor.ts => create_and_sign_as_creditor.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename __test__/integration/max_ltv_loan_offer/runners/{createAndSignAsCreditor.ts => create_and_sign_as_creditor.ts} (100%) diff --git a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts index d14e67de..729cc43b 100644 --- a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts +++ b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts @@ -5,7 +5,7 @@ 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/createAndSignAsCreditor"; +import { testCreateAndSignAsCreditor } from "./runners/create_and_sign_as_creditor"; const provider = new Web3.providers.HttpProvider("http://localhost:8545"); const dharma = new Dharma(provider); diff --git a/__test__/integration/max_ltv_loan_offer/runners/createAndSignAsCreditor.ts b/__test__/integration/max_ltv_loan_offer/runners/create_and_sign_as_creditor.ts similarity index 100% rename from __test__/integration/max_ltv_loan_offer/runners/createAndSignAsCreditor.ts rename to __test__/integration/max_ltv_loan_offer/runners/create_and_sign_as_creditor.ts From bdae59196ea051a3d73ea58435578b00544688ef Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 13:50:01 -0700 Subject: [PATCH 21/38] test: add specs for signAsCreditor --- .../max_ltv_loan_offer.spec.ts | 5 +++- .../runners/sign_as_creditor.ts | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 __test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts diff --git a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts index 729cc43b..300d5cfd 100644 --- a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts +++ b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts @@ -6,6 +6,7 @@ import { VALID_MAX_LTV_LOAN_ORDER_PARAMS } from "./scenarios/valid_max_ltv_loan_ import { testConstructor } from "./runners/constructor"; import { testCreateAndSignAsCreditor } from "./runners/create_and_sign_as_creditor"; +import { testSignAsCreditor } from "./runners/sign_as_creditor"; const provider = new Web3.providers.HttpProvider("http://localhost:8545"); const dharma = new Dharma(provider); @@ -19,7 +20,9 @@ describe("Max LTV Loan Offer (Integration)", () => { await testCreateAndSignAsCreditor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS); }); - describe("signAsCreditor", () => {}); + describe("signAsCreditor", async () => { + await testSignAsCreditor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS); + }); describe("signAsDebtor", () => {}); diff --git a/__test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts b/__test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts new file mode 100644 index 00000000..2dba4002 --- /dev/null +++ b/__test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts @@ -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(() => { + loanOffer = new MaxLTVLoanOffer(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); + }); + }); +} From a1ef119ab89cea3d5bebbc55d16fde4cb45b4726 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 14:25:01 -0700 Subject: [PATCH 22/38] test: add generateSignedPrice util --- .../utils/generate_signed_price.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 __test__/integration/max_ltv_loan_offer/utils/generate_signed_price.ts diff --git a/__test__/integration/max_ltv_loan_offer/utils/generate_signed_price.ts b/__test__/integration/max_ltv_loan_offer/utils/generate_signed_price.ts new file mode 100644 index 00000000..26ef4016 --- /dev/null +++ b/__test__/integration/max_ltv_loan_offer/utils/generate_signed_price.ts @@ -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 { + 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 }; +} From 86d25c96ac173c216699d0c6ee4dbf6d7bd6adbb Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 14:32:32 -0700 Subject: [PATCH 23/38] test: add spec for signAsDebtor --- .../max_ltv_loan_offer.spec.ts | 5 +- .../runners/sign_as_debtor.ts | 62 +++++++++++++++++++ .../valid_max_ltv_loan_order_params.ts | 2 +- src/types/loan_offer/max_ltv.ts | 4 +- 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 __test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts diff --git a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts index 300d5cfd..80fecd87 100644 --- a/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts +++ b/__test__/integration/max_ltv_loan_offer/max_ltv_loan_offer.spec.ts @@ -7,6 +7,7 @@ import { VALID_MAX_LTV_LOAN_ORDER_PARAMS } from "./scenarios/valid_max_ltv_loan_ 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); @@ -24,7 +25,9 @@ describe("Max LTV Loan Offer (Integration)", () => { await testSignAsCreditor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS); }); - describe("signAsDebtor", () => {}); + describe("signAsDebtor", async () => { + await testSignAsDebtor(dharma, VALID_MAX_LTV_LOAN_ORDER_PARAMS); + }); describe("acceptAsDebtor", () => {}); }); diff --git a/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts new file mode 100644 index 00000000..bd85d96e --- /dev/null +++ b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts @@ -0,0 +1,62 @@ +import { Dharma } from "../../../../src"; + +import { MaxLTVLoanOffer } from "../../../../src/types"; + +import { 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; + + beforeEach(() => { + loanOffer = new MaxLTVLoanOffer(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); + + 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); + + loanOffer.setCollateralAmount(160); + + await loanOffer.signAsDebtor(debtor); + + const isSignedByDebtorAfter = loanOffer.isSignedByDebtor(); + expect(isSignedByDebtorAfter).toBe(true); + }); + }); +} diff --git a/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts b/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts index cd18d1fb..017609d0 100644 --- a/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts +++ b/__test__/integration/max_ltv_loan_offer/scenarios/valid_max_ltv_loan_order_params.ts @@ -9,7 +9,7 @@ export const VALID_MAX_LTV_LOAN_ORDER_PARAMS: MaxLTVParams = { interestRate: 12.3, maxLTV: 50, priceProvider: ACCOUNTS[0].address, - principalAmount: 5, + principalAmount: 10, principalToken: "REP", termDuration: 6, termUnit: "months", diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 1923a873..fe1d57cd 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -23,7 +23,7 @@ import { BigNumber } from "../../../utils/bignumber"; export const MAX_LTV_LOAN_OFFER_ERRORS = { INSUFFICIENT_COLLATERAL_AMOUNT: (collateralAmount: number, collateralTokenSymbol: string) => - singleLineString`Collateral of ${collateralAmount} ${collateralTokenSymbol} is too insufficient + singleLineString`Collateral of ${collateralAmount} ${collateralTokenSymbol} is insufficient for the maximum loan-to-value.`, PRICES_NOT_SET: () => `The prices of the principal and collateral must be set first.`, }; @@ -325,6 +325,6 @@ export class MaxLTVLoanOffer { this.collateralPrice.tokenPrice, ); - return principalValue.div(collateralValue).lte(this.data.maxLTV); + return principalValue.div(collateralValue).lte(this.data.maxLTV.div(100)); } } From 4d068d9869e63fa5eab54c915abcdf55b3a49884 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 15:05:48 -0700 Subject: [PATCH 24/38] feat: improve signAsDebtor error handling --- src/types/loan_offer/max_ltv.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index fe1d57cd..b61c4ba7 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -22,6 +22,8 @@ import { SignedPrice } from "./signed_price"; import { BigNumber } from "../../../utils/bignumber"; export const MAX_LTV_LOAN_OFFER_ERRORS = { + ALREADY_SIGNED_BY_DEBTOR: () => `The debtor has already signed the loan offer.`, + COLLATERAL_AMOUNT_NOT_SET: () => `The collateral amount must be set first`, INSUFFICIENT_COLLATERAL_AMOUNT: (collateralAmount: number, collateralTokenSymbol: string) => singleLineString`Collateral of ${collateralAmount} ${collateralTokenSymbol} is insufficient for the maximum loan-to-value.`, @@ -191,12 +193,18 @@ export class MaxLTVLoanOffer { } public async signAsDebtor(debtorAddress?: string): Promise { - // TODO: check if already signed by debtor + if (this.isSignedByDebtor()) { + throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.ALREADY_SIGNED_BY_DEBTOR()); + } if (!this.principalPrice || !this.collateralPrice) { throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.PRICES_NOT_SET()); } + if (!this.collateralAmount) { + throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.COLLATERAL_AMOUNT_NOT_SET()); + } + // TODO: assert signed address matches principal token address // TODO: assert signed address matches collateral token address From 121aa044be7e1210e13a2267dc31497c884ce712 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 15:06:06 -0700 Subject: [PATCH 25/38] test: improve signAsDebtor tests --- .../runners/sign_as_debtor.ts | 92 ++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) diff --git a/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts index bd85d96e..eebce9cd 100644 --- a/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts +++ b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts @@ -2,7 +2,7 @@ import { Dharma } from "../../../../src"; import { MaxLTVLoanOffer } from "../../../../src/types"; -import { MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv"; +import { MAX_LTV_LOAN_OFFER_ERRORS, MaxLTVParams } from "../../../../src/types/loan_offer/max_ltv"; import { generateSignedPrice } from "../utils/generate_signed_price"; @@ -16,16 +16,7 @@ export async function testSignAsDebtor(dharma: Dharma, params: MaxLTVParams) { let loanOffer: MaxLTVLoanOffer; - beforeEach(() => { - loanOffer = new MaxLTVLoanOffer(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); - + async function setPrices() { const principalTokenAddress = await dharma.contracts.getTokenAddressBySymbolAsync( params.principalToken, ); @@ -50,6 +41,19 @@ export async function testSignAsDebtor(dharma: Dharma, params: MaxLTVParams) { loanOffer.setPrincipalPrice(principalPrice); loanOffer.setCollateralPrice(collateralPrice); + } + + beforeEach(() => { + loanOffer = new MaxLTVLoanOffer(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); @@ -58,5 +62,71 @@ export async function testSignAsDebtor(dharma: Dharma, params: MaxLTVParams) { 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); + + await setPrices(); + + const collateralAmount = 10; + + loanOffer.setCollateralAmount(collateralAmount); + + await expect(loanOffer.signAsDebtor(debtor)).rejects.toThrow( + MAX_LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT( + collateralAmount, + params.collateralToken, + ), + ); + }); + }); }); } From 181838c12f334d2b62e7889bcd329fe9a803713c Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 17:19:23 -0700 Subject: [PATCH 26/38] refactor: implement async create function to set contract addresses --- src/types/loan_offer/loan_offer.ts | 2 +- src/types/loan_offer/max_ltv.ts | 148 +++++++++++++++++++---------- 2 files changed, 100 insertions(+), 50 deletions(-) diff --git a/src/types/loan_offer/loan_offer.ts b/src/types/loan_offer/loan_offer.ts index 452a025b..8c76bc98 100644 --- a/src/types/loan_offer/loan_offer.ts +++ b/src/types/loan_offer/loan_offer.ts @@ -1,6 +1,6 @@ import { Dharma } from "../dharma"; -import { DEBT_ORDER_ERRORS, DebtOrder, CollateralizedDebtOrderParams } from "../../loan/debt_order"; +import { CollateralizedDebtOrderParams, DEBT_ORDER_ERRORS, DebtOrder } from "../../loan/debt_order"; import { EthereumAddress } from "../../types"; diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index b61c4ba7..0b7b2070 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -1,3 +1,5 @@ +import { RepaymentRouter } from "@dharmaprotocol/contracts"; + import * as singleLineString from "single-line-string"; import { Web3Utils } from "../../../utils/web3_utils"; @@ -6,6 +8,8 @@ import { DebtOrderParams } from "../../loan/debt_order"; import { SignatureUtils } from "../../../utils/signature_utils"; +import { NULL_ECDSA_SIGNATURE, SALT_DECIMALS } from "../../../utils/constants"; + import { Dharma } from "../dharma"; import { @@ -31,73 +35,78 @@ export const MAX_LTV_LOAN_OFFER_ERRORS = { }; export interface MaxLTVData { - principal: TokenAmount; - interestRate: InterestRate; - termLength: TimeInterval; + collateralTokenAddress: string; + collateralTokenSymbol: string; + creditorFee?: BigNumber; + debtorFee?: BigNumber; expiresIn: TimeInterval; + interestRate: InterestRate; + issuanceVersion: string; + kernelVersion: string; maxLTV: BigNumber; - collateralTokenSymbol: string; priceProvider: string; + principal: TokenAmount; + principalTokenAddress: string; relayer?: EthereumAddress; relayerFee?: TokenAmount; + termLength: TimeInterval; + termsContract: string; } export interface MaxLTVParams extends DebtOrderParams { maxLTV: number; collateralToken: string; priceProvider: string; + debtorFeeAmount?: number; } export class MaxLTVLoanOffer { // TODO: replace with decision engine address (async function?) public static decisionEngineAddress = "test"; - public static async createAndSignAsCreditor( - dharma: Dharma, - params: MaxLTVParams, - creditor?: string, - ): Promise { - const offer = new MaxLTVLoanOffer(dharma, params); - - await offer.signAsCreditor(creditor); - - return offer; - } - - private readonly data: MaxLTVData; - - private creditor?: string; - private creditorSignature?: ECDSASignature; - private debtorSignature?: ECDSASignature; - private collateralAmount?: number; - private principalPrice?: SignedPrice; - private collateralPrice?: SignedPrice; - private debtor?: string; - - constructor(private readonly dharma: Dharma, params: MaxLTVParams) { + public static async create(dharma: Dharma, params: MaxLTVParams): Promise { const { + collateralToken, + creditorFeeAmount, + debtorFeeAmount, + expiresInDuration, + expiresInUnit, + interestRate, maxLTV, priceProvider, - collateralToken, principalAmount, principalToken, relayerAddress, relayerFeeAmount, - interestRate, termDuration, termUnit, - expiresInDuration, - expiresInUnit, } = params; - const data = { - principal: new TokenAmount(principalAmount, principalToken), - interestRate: new InterestRate(interestRate), - termLength: new TimeInterval(termDuration, termUnit), + const kernelVersion = (await this.dharma.contracts.loadDebtKernelAsync()).address; + const issuanceVersion = (await this.dharma.contracts.loadRepaymentRouterAsync()).address; + const termsContract = (await this.dharma.contracts.loadCollateralizedSimpleInterestTermsContract()) + .address; + const principalTokenAddress = await this.dharma.contracts.getTokenAddressBySymbolAsync( + this.data.principal.tokenSymbol, + ); + const collateralTokenAddress = await this.dharma.contracts.getTokenAddressBySymbolAsync( + this.data.collateralTokenSymbol, + ); + + const data: MaxLTVData = { + collateralTokenAddress + collateralTokenSymbol: collateralToken, expiresIn: new TimeInterval(expiresInDuration, expiresInUnit), + interestRate: new InterestRate(interestRate), + issuanceVersion, + kernelVersion, maxLTV: new BigNumber(maxLTV), - collateralTokenSymbol: collateralToken, priceProvider, + principal: new TokenAmount(principalAmount, principalToken), + principalTokenAddress, + salt: MaxLTVLoanOffer.generateSalt(), + termLength: new TimeInterval(termDuration, termUnit), + termsContract, }; if (relayerAddress && relayerFeeAmount) { @@ -105,9 +114,48 @@ export class MaxLTVLoanOffer { data.relayerFee = new TokenAmount(relayerFeeAmount, principalToken); } - this.data = data; + if (creditorFeeAmount && creditorFeeAmount > 0) { + const creditorFeeTokenAmount = new TokenAmount(creditorFeeAmount, principalToken); + data.creditorFee = creditorFeeTokenAmount.rawAmount; + } + + if (debtorFeeAmount && debtorFeeAmount > 0) { + const debtorFeeTokenAmount = new TokenAmount(debtorFeeAmount, principalToken); + data.debtorFee = debtorFeeTokenAmount.rawAmount; + } + + return new MaxLTVLoanOffer(dharma, data); + } + + public static async createAndSignAsCreditor( + dharma: Dharma, + params: MaxLTVParams, + creditor?: string, + ): Promise { + const offer = new MaxLTVLoanOffer(dharma, params); + + await offer.signAsCreditor(creditor); + + return offer; + } + + public static generateSalt(): BigNumber { + return BigNumber.random(SALT_DECIMALS).times(new BigNumber(10).pow(SALT_DECIMALS)); } + private readonly data: MaxLTVData; + + private creditor?: string; + private creditorSignature?: ECDSASignature; + private debtorSignature?: ECDSASignature; + private collateralAmount?: number; + private principalPrice?: SignedPrice; + private collateralPrice?: SignedPrice; + private debtor?: string; + private expirationTimestampInSec?: BigNumber; + + constructor(private readonly dharma: Dharma, private readonly data: MaxLTVData) {} + /** * Eventually signs the loan offer as the creditor. * @@ -127,6 +175,11 @@ export class MaxLTVLoanOffer { creditorAddress, ); + // Set the expiration timestamp + const currentBlocktime = new BigNumber(await this.dharma.blockchain.getCurrentBlockTime()); + + this.expirationTimestampInSec = this.data.expiresIn.fromTimestamp(currentBlocktime); + const loanOfferHash = this.getCreditorCommitmentHash(); const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; @@ -263,24 +316,21 @@ export class MaxLTVLoanOffer { this.data.kernelVersion, this.data.issuanceVersion, this.data.termsContract, - this.data.principalAmount, - this.data.principalToken, - this.data.collateralToken, + this.data.principal.rawAmount, + this.data.principalTokenAddress, + this.data.collateralTokenAddress, this.data.maxLTV, this.data.interestRate, - this.data.debtorFee, - this.data.creditorFee, - this.data.relayer, - this.data.relayerFee, - this.data.expirationTimestampInSec, - this.data.salt, + this.data.debtorFee ? this.data.debtorFee : new BigNumber(0), + this.data.creditorFee ? this.data.creditorFee : new BigNumber(0), + this.data.relayer ? this.data.relayer : NULL_ECDSA_SIGNATURE, + this.data.relayerFee ? this.data.relayerFee : new BigNumber(0), + this.expirationTimestampInSec, + this.salt, ); } private getCreditorCommitmentHash(): string { - // TODO: remove mock implementation - return Web3Utils.soliditySHA3("mockCreditorCommitmentHash"); - return Web3Utils.soliditySHA3( MaxLTVLoanOffer.decisionEngineAddress, this.getCreditorCommitmentTermsHash(), From a0b52e7d91f4c06ab2622bf9851d803aae36ef6e Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 20:29:23 -0700 Subject: [PATCH 27/38] test: fix failing tests --- .../max_ltv_loan_offer/runners/constructor.ts | 4 +-- .../runners/sign_as_creditor.ts | 4 +-- .../runners/sign_as_debtor.ts | 4 +-- src/types/loan_offer/max_ltv.ts | 31 ++++++++++--------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/__test__/integration/max_ltv_loan_offer/runners/constructor.ts b/__test__/integration/max_ltv_loan_offer/runners/constructor.ts index 24cd265b..90266ecc 100644 --- a/__test__/integration/max_ltv_loan_offer/runners/constructor.ts +++ b/__test__/integration/max_ltv_loan_offer/runners/constructor.ts @@ -8,8 +8,8 @@ export function testConstructor(dharma: Dharma, params: MaxLTVParams) { describe("passing valid params", () => { let loanOffer: MaxLTVLoanOffer; - beforeEach(() => { - loanOffer = new MaxLTVLoanOffer(dharma, params); + beforeEach(async () => { + loanOffer = await MaxLTVLoanOffer.create(dharma, params); }); test("returns a MaxLTVLoanOffer", () => { diff --git a/__test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts b/__test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts index 2dba4002..acf2e872 100644 --- a/__test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts +++ b/__test__/integration/max_ltv_loan_offer/runners/sign_as_creditor.ts @@ -12,8 +12,8 @@ export async function testSignAsCreditor(dharma: Dharma, params: MaxLTVParams) { let loanOffer: MaxLTVLoanOffer; - beforeEach(() => { - loanOffer = new MaxLTVLoanOffer(dharma, params); + beforeEach(async () => { + loanOffer = await MaxLTVLoanOffer.create(dharma, params); }); test("signs the offer as the creditor", async () => { diff --git a/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts index eebce9cd..09066bf9 100644 --- a/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts +++ b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts @@ -43,8 +43,8 @@ export async function testSignAsDebtor(dharma: Dharma, params: MaxLTVParams) { loanOffer.setCollateralPrice(collateralPrice); } - beforeEach(() => { - loanOffer = new MaxLTVLoanOffer(dharma, params); + beforeEach(async () => { + loanOffer = await MaxLTVLoanOffer.create(dharma, params); }); test("signs the offer as the debtor if all prerequisites are met", async () => { diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 0b7b2070..ceebdb7c 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -8,7 +8,7 @@ import { DebtOrderParams } from "../../loan/debt_order"; import { SignatureUtils } from "../../../utils/signature_utils"; -import { NULL_ECDSA_SIGNATURE, SALT_DECIMALS } from "../../../utils/constants"; +import { NULL_ADDRESS, NULL_ECDSA_SIGNATURE, SALT_DECIMALS } from "../../../utils/constants"; import { Dharma } from "../dharma"; @@ -25,6 +25,9 @@ import { SignedPrice } from "./signed_price"; import { BigNumber } from "../../../utils/bignumber"; +const MAX_INTEREST_RATE_PRECISION = 4; +const FIXED_POINT_SCALING_FACTOR = 10 ** MAX_INTEREST_RATE_PRECISION; + export const MAX_LTV_LOAN_OFFER_ERRORS = { ALREADY_SIGNED_BY_DEBTOR: () => `The debtor has already signed the loan offer.`, COLLATERAL_AMOUNT_NOT_SET: () => `The collateral amount must be set first`, @@ -82,19 +85,19 @@ export class MaxLTVLoanOffer { termUnit, } = params; - const kernelVersion = (await this.dharma.contracts.loadDebtKernelAsync()).address; - const issuanceVersion = (await this.dharma.contracts.loadRepaymentRouterAsync()).address; - const termsContract = (await this.dharma.contracts.loadCollateralizedSimpleInterestTermsContract()) + const kernelVersion = (await dharma.contracts.loadDebtKernelAsync()).address; + const issuanceVersion = (await dharma.contracts.loadRepaymentRouterAsync()).address; + const termsContract = (await dharma.contracts.loadCollateralizedSimpleInterestTermsContract()) .address; - const principalTokenAddress = await this.dharma.contracts.getTokenAddressBySymbolAsync( - this.data.principal.tokenSymbol, + const principalTokenAddress = await dharma.contracts.getTokenAddressBySymbolAsync( + principalToken, ); - const collateralTokenAddress = await this.dharma.contracts.getTokenAddressBySymbolAsync( - this.data.collateralTokenSymbol, + const collateralTokenAddress = await dharma.contracts.getTokenAddressBySymbolAsync( + collateralToken, ); const data: MaxLTVData = { - collateralTokenAddress + collateralTokenAddress, collateralTokenSymbol: collateralToken, expiresIn: new TimeInterval(expiresInDuration, expiresInUnit), interestRate: new InterestRate(interestRate), @@ -132,7 +135,7 @@ export class MaxLTVLoanOffer { params: MaxLTVParams, creditor?: string, ): Promise { - const offer = new MaxLTVLoanOffer(dharma, params); + const offer = await MaxLTVLoanOffer.create(dharma, params); await offer.signAsCreditor(creditor); @@ -320,13 +323,13 @@ export class MaxLTVLoanOffer { this.data.principalTokenAddress, this.data.collateralTokenAddress, this.data.maxLTV, - this.data.interestRate, + this.data.interestRate.raw.mul(FIXED_POINT_SCALING_FACTOR), this.data.debtorFee ? this.data.debtorFee : new BigNumber(0), this.data.creditorFee ? this.data.creditorFee : new BigNumber(0), - this.data.relayer ? this.data.relayer : NULL_ECDSA_SIGNATURE, + this.data.relayer ? this.data.relayer.toString() : NULL_ADDRESS, this.data.relayerFee ? this.data.relayerFee : new BigNumber(0), this.expirationTimestampInSec, - this.salt, + this.data.salt, ); } @@ -363,7 +366,7 @@ export class MaxLTVLoanOffer { this.data.creditorFee, this.data.relayer, this.data.relayerFee, - this.data.expirationTimestampInSec, + this.expirationTimestampInSec, ); } From cf70836e79a65abcecf177425f0801f11cf0fb6c Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 20:44:36 -0700 Subject: [PATCH 28/38] feat: introduce debtorFee to DebtOrder base class --- src/loan/debt_order.ts | 9 +++++++++ src/types/loan_offer/max_ltv.ts | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/loan/debt_order.ts b/src/loan/debt_order.ts index 599eb16a..af26023e 100644 --- a/src/loan/debt_order.ts +++ b/src/loan/debt_order.ts @@ -80,6 +80,7 @@ export interface DebtOrderParams { relayerAddress?: string; relayerFeeAmount?: number; creditorFeeAmount?: number; + debtorFeeAmount?: number; } export interface CollateralizedDebtOrderParams extends DebtOrderParams { @@ -120,6 +121,7 @@ export class DebtOrder { expiresInDuration, expiresInUnit, creditorFeeAmount, + debtorFeeAmount, } = params; const principal = new TokenAmount(principalAmount, principalToken); @@ -175,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; diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index ceebdb7c..93cebc78 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -60,7 +60,6 @@ export interface MaxLTVParams extends DebtOrderParams { maxLTV: number; collateralToken: string; priceProvider: string; - debtorFeeAmount?: number; } export class MaxLTVLoanOffer { From 06feebd20a16779806c396c5618cc7dcfc6aba8c Mon Sep 17 00:00:00 2001 From: Chris Min Date: Mon, 1 Oct 2018 20:45:04 -0700 Subject: [PATCH 29/38] refactor: make data interface more strict --- src/types/loan_offer/max_ltv.ts | 55 +++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 93cebc78..b73ac470 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -40,8 +40,8 @@ export const MAX_LTV_LOAN_OFFER_ERRORS = { export interface MaxLTVData { collateralTokenAddress: string; collateralTokenSymbol: string; - creditorFee?: BigNumber; - debtorFee?: BigNumber; + creditorFee: BigNumber; + debtorFee: BigNumber; expiresIn: TimeInterval; interestRate: InterestRate; issuanceVersion: string; @@ -50,8 +50,8 @@ export interface MaxLTVData { priceProvider: string; principal: TokenAmount; principalTokenAddress: string; - relayer?: EthereumAddress; - relayerFee?: TokenAmount; + relayer: EthereumAddress; + relayerFee: TokenAmount; termLength: TimeInterval; termsContract: string; } @@ -95,9 +95,31 @@ export class MaxLTVLoanOffer { collateralToken, ); + let relayer = NULL_ADDRESS; + let relayerFee = new BigNumber(0); + let creditorFee = new BigNumber(0); + let debtorFee = new BigNumber(0); + + if (relayerAddress && relayerFeeAmount) { + relayer = new EthereumAddress(relayerAddress); + relayerFee = new TokenAmount(relayerFeeAmount, principalToken); + } + + if (creditorFeeAmount && creditorFeeAmount > 0) { + const creditorFeeTokenAmount = new TokenAmount(creditorFeeAmount, principalToken); + creditorFee = creditorFeeTokenAmount.rawAmount; + } + + if (debtorFeeAmount && debtorFeeAmount > 0) { + debtorFeeTokenAmount = new TokenAmount(debtorFeeAmount, principalToken); + debtorFee = debtorFeeTokenAmount.rawAmount; + } + const data: MaxLTVData = { collateralTokenAddress, collateralTokenSymbol: collateralToken, + creditorFee, + debtorFee, expiresIn: new TimeInterval(expiresInDuration, expiresInUnit), interestRate: new InterestRate(interestRate), issuanceVersion, @@ -106,26 +128,13 @@ export class MaxLTVLoanOffer { priceProvider, principal: new TokenAmount(principalAmount, principalToken), principalTokenAddress, + relayer, + relayerFee, salt: MaxLTVLoanOffer.generateSalt(), termLength: new TimeInterval(termDuration, termUnit), termsContract, }; - if (relayerAddress && relayerFeeAmount) { - data.relayer = new EthereumAddress(relayerAddress); - data.relayerFee = new TokenAmount(relayerFeeAmount, principalToken); - } - - if (creditorFeeAmount && creditorFeeAmount > 0) { - const creditorFeeTokenAmount = new TokenAmount(creditorFeeAmount, principalToken); - data.creditorFee = creditorFeeTokenAmount.rawAmount; - } - - if (debtorFeeAmount && debtorFeeAmount > 0) { - const debtorFeeTokenAmount = new TokenAmount(debtorFeeAmount, principalToken); - data.debtorFee = debtorFeeTokenAmount.rawAmount; - } - return new MaxLTVLoanOffer(dharma, data); } @@ -323,10 +332,10 @@ export class MaxLTVLoanOffer { this.data.collateralTokenAddress, this.data.maxLTV, this.data.interestRate.raw.mul(FIXED_POINT_SCALING_FACTOR), - this.data.debtorFee ? this.data.debtorFee : new BigNumber(0), - this.data.creditorFee ? this.data.creditorFee : new BigNumber(0), - this.data.relayer ? this.data.relayer.toString() : NULL_ADDRESS, - this.data.relayerFee ? this.data.relayerFee : new BigNumber(0), + this.data.debtorFee, + this.data.creditorFee, + this.data.relayer, + this.data.relayerFee, this.expirationTimestampInSec, this.data.salt, ); From 1744163ead572db27035a509ff25e33eb2905787 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 09:15:06 -0700 Subject: [PATCH 30/38] feat: unmock hashing of debtor commitment and pack parameters --- src/types/loan_offer/max_ltv.ts | 75 +++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index b73ac470..7ccad251 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -2,6 +2,12 @@ import { RepaymentRouter } from "@dharmaprotocol/contracts"; import * as singleLineString from "single-line-string"; +import { + CollateralizedSimpleInterestLoanAdapter, + CollateralizedTermsContractParameters, +} from "../../../src/adapters/collateralized_simple_interest_loan_adapter"; +import { SimpleInterestTermsContractParameters } from "../../../src/adapters/simple_interest_loan_adapter"; + import { Web3Utils } from "../../../utils/web3_utils"; import { DebtOrderParams } from "../../loan/debt_order"; @@ -52,6 +58,7 @@ export interface MaxLTVData { principalTokenAddress: string; relayer: EthereumAddress; relayerFee: TokenAmount; + salt: BigNumber; termLength: TimeInterval; termsContract: string; } @@ -154,16 +161,15 @@ export class MaxLTVLoanOffer { return BigNumber.random(SALT_DECIMALS).times(new BigNumber(10).pow(SALT_DECIMALS)); } - private readonly data: MaxLTVData; - - private creditor?: string; - private creditorSignature?: ECDSASignature; - private debtorSignature?: ECDSASignature; private collateralAmount?: number; - private principalPrice?: SignedPrice; private collateralPrice?: SignedPrice; + private creditor?: string; + private creditorSignature?: ECDSASignature; private debtor?: string; + private debtorSignature?: ECDSASignature; private expirationTimestampInSec?: BigNumber; + private principalPrice?: SignedPrice; + private termsContractParameters?: string; constructor(private readonly dharma: Dharma, private readonly data: MaxLTVData) {} @@ -281,6 +287,8 @@ export class MaxLTVLoanOffer { ); } + await this.packAndSetTermsContractParameters(); + this.debtor = await EthereumAddress.validAddressOrCurrentUser(this.dharma, debtorAddress); const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; @@ -319,7 +327,7 @@ export class MaxLTVLoanOffer { } public async acceptAsDebtor(): Promise { - // TODO: send transaction to CreditorProxtContract + // TODO: send transaction to CreditorProxyContract } private getCreditorCommitmentTermsHash(): string { @@ -349,27 +357,30 @@ export class MaxLTVLoanOffer { } private getIssuanceCommitmentHash(): string { + // We remove underwriting as a feature, since the creditor has no mechanism to mandate a maximum + // underwriter risk rating. + return Web3Utils.soliditySHA3( this.data.issuanceVersion, this.debtor, - this.data.underwriter, - this.data.underwriterRiskRating, + NULL_ADDRESS, // underwriter + new BigNumber(0), // undwriter risk rating this.data.termsContract, - this.data.termsContractParameters, + this.termsContractParameters, this.data.salt, ); } private getDebtorCommitHash(): string { - // TODO: remove mock implementation - return Web3Utils.soliditySHA3("mockDebtorCommitmentHash"); + // We remove underwriting as a feature, since the creditor has no mechanism to mandate a maximum + // underwriter risk rating. return Web3Utils.soliditySHA3( this.data.kernelVersion, this.getIssuanceCommitmentHash(), - this.data.underwriterFee, + new BigNumber(0), // underwriter fee this.data.principal.rawAmount, - this.data.principalToken, + this.data.principalTokenAddress, this.data.debtorFee, this.data.creditorFee, this.data.relayer, @@ -396,4 +407,40 @@ export class MaxLTVLoanOffer { return principalValue.div(collateralValue).lte(this.data.maxLTV.div(100)); } + + private async packAndSetTermsContractParameters(): Promise { + const adapter = (await this.dharma.adapters.getAdapterByTermsContractAddress( + this.data.termsContract, + )) as CollateralizedSimpleInterestLoanAdapter; + + const principalTokenIndex = await this.dharma.contracts.getTokenIndexBySymbolAsync( + this.data.principal.tokenSymbol, + ); + const collateralTokenIndex = await this.dharma.contracts.getTokenIndexBySymbolAsync( + this.data.collateralTokenSymbol, + ); + + const collateralTokenAmount = new TokenAmount( + this.collateralAmount, + this.data.collateralTokenSymbol, + ); + + const simpleInterestTerms: SimpleInterestTermsContractParameters = { + principalAmount: this.data.principal.rawAmount, + interestRate: this.data.interestRate.raw, + amortizationUnit: this.data.termLength.getAmortizationUnit(), + termLength: new BigNumber(this.data.termLength.amount), + principalTokenIndex, + }; + const collateralizedSimpleInterestTerms: CollateralizedTermsContractParameters = { + collateralTokenIndex, + collateralAmount: collateralTokenAmount.rawAmount, + gracePeriodInDays: new BigNumber(0), + }; + + this.termsContractParameters = await adapter.packParameters( + simpleInterestTerms, + collateralizedSimpleInterestTerms, + ); + } } From afc90ae56739bff68e7b86eafacaf5e4bc35f52a Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 12:09:42 -0700 Subject: [PATCH 31/38] fix: use correct types for relayer and relayer fee --- src/types/loan_offer/max_ltv.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 7ccad251..0e0a5e68 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -102,8 +102,8 @@ export class MaxLTVLoanOffer { collateralToken, ); - let relayer = NULL_ADDRESS; - let relayerFee = new BigNumber(0); + let relayer = new EthereumAddress(NULL_ADDRESS); + let relayerFee = new TokenAmount(0, principalToken); let creditorFee = new BigNumber(0); let debtorFee = new BigNumber(0); @@ -118,7 +118,7 @@ export class MaxLTVLoanOffer { } if (debtorFeeAmount && debtorFeeAmount > 0) { - debtorFeeTokenAmount = new TokenAmount(debtorFeeAmount, principalToken); + const debtorFeeTokenAmount = new TokenAmount(debtorFeeAmount, principalToken); debtorFee = debtorFeeTokenAmount.rawAmount; } @@ -342,8 +342,8 @@ export class MaxLTVLoanOffer { this.data.interestRate.raw.mul(FIXED_POINT_SCALING_FACTOR), this.data.debtorFee, this.data.creditorFee, - this.data.relayer, - this.data.relayerFee, + this.data.relayer.toString(), + this.data.relayerFee.rawAmount, this.expirationTimestampInSec, this.data.salt, ); @@ -383,8 +383,8 @@ export class MaxLTVLoanOffer { this.data.principalTokenAddress, this.data.debtorFee, this.data.creditorFee, - this.data.relayer, - this.data.relayerFee, + this.data.relayer.toString(), + this.data.relayerFee.rawAmount, this.expirationTimestampInSec, ); } From 378932851b34855ba93c69783b93b5aa1a3e8d36 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 12:27:34 -0700 Subject: [PATCH 32/38] refactor: make packing of terms contract params sync --- src/types/loan_offer/max_ltv.ts | 34 ++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 0e0a5e68..edbbe3a3 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -44,7 +44,9 @@ export const MAX_LTV_LOAN_OFFER_ERRORS = { }; export interface MaxLTVData { + adapter: CollateralizedSimpleInterestLoanAdapter; collateralTokenAddress: string; + collateralTokenIndex: BigNumber; collateralTokenSymbol: string; creditorFee: BigNumber; debtorFee: BigNumber; @@ -56,6 +58,7 @@ export interface MaxLTVData { priceProvider: string; principal: TokenAmount; principalTokenAddress: string; + principalTokenIndex: BigNumber; relayer: EthereumAddress; relayerFee: TokenAmount; salt: BigNumber; @@ -102,6 +105,17 @@ export class MaxLTVLoanOffer { collateralToken, ); + const adapter = (await dharma.adapters.getAdapterByTermsContractAddress( + termsContract, + )) as CollateralizedSimpleInterestLoanAdapter; + + const principalTokenIndex = await dharma.contracts.getTokenIndexBySymbolAsync( + principalToken, + ); + const collateralTokenIndex = await dharma.contracts.getTokenIndexBySymbolAsync( + collateralToken, + ); + let relayer = new EthereumAddress(NULL_ADDRESS); let relayerFee = new TokenAmount(0, principalToken); let creditorFee = new BigNumber(0); @@ -123,7 +137,9 @@ export class MaxLTVLoanOffer { } const data: MaxLTVData = { + adapter, collateralTokenAddress, + collateralTokenIndex, collateralTokenSymbol: collateralToken, creditorFee, debtorFee, @@ -135,6 +151,7 @@ export class MaxLTVLoanOffer { priceProvider, principal: new TokenAmount(principalAmount, principalToken), principalTokenAddress, + principalTokenIndex, relayer, relayerFee, salt: MaxLTVLoanOffer.generateSalt(), @@ -287,7 +304,7 @@ export class MaxLTVLoanOffer { ); } - await this.packAndSetTermsContractParameters(); + this.packAndSetTermsContractParameters(); this.debtor = await EthereumAddress.validAddressOrCurrentUser(this.dharma, debtorAddress); @@ -408,17 +425,8 @@ export class MaxLTVLoanOffer { return principalValue.div(collateralValue).lte(this.data.maxLTV.div(100)); } - private async packAndSetTermsContractParameters(): Promise { - const adapter = (await this.dharma.adapters.getAdapterByTermsContractAddress( - this.data.termsContract, - )) as CollateralizedSimpleInterestLoanAdapter; - - const principalTokenIndex = await this.dharma.contracts.getTokenIndexBySymbolAsync( - this.data.principal.tokenSymbol, - ); - const collateralTokenIndex = await this.dharma.contracts.getTokenIndexBySymbolAsync( - this.data.collateralTokenSymbol, - ); + private packAndSetTermsContractParameters(): Promise { + const { adapter, collateralTokenIndex, principalTokenIndex } = this.data; const collateralTokenAmount = new TokenAmount( this.collateralAmount, @@ -438,7 +446,7 @@ export class MaxLTVLoanOffer { gracePeriodInDays: new BigNumber(0), }; - this.termsContractParameters = await adapter.packParameters( + this.termsContractParameters = adapter.packParameters( simpleInterestTerms, collateralizedSimpleInterestTerms, ); From c224602895367a1528166cee9e6bf51ea6675e20 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 12:35:19 -0700 Subject: [PATCH 33/38] refactor: pack parameters when collateral amount is set --- src/types/loan_offer/max_ltv.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index edbbe3a3..ce1a6e38 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -273,6 +273,8 @@ export class MaxLTVLoanOffer { // TODO: assert prices are set // TODO: assert collateralAmount sufficient this.collateralAmount = collateralAmount; + + this.termsContractParameters = this.packTermsContractParameters(); } public getCollateralAmount(): number { @@ -304,8 +306,6 @@ export class MaxLTVLoanOffer { ); } - this.packAndSetTermsContractParameters(); - this.debtor = await EthereumAddress.validAddressOrCurrentUser(this.dharma, debtorAddress); const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; @@ -374,6 +374,10 @@ export class MaxLTVLoanOffer { } private getIssuanceCommitmentHash(): string { + if (!this.collateralAmount) { + throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.COLLATERAL_AMOUNT_NOT_SET()); + } + // We remove underwriting as a feature, since the creditor has no mechanism to mandate a maximum // underwriter risk rating. @@ -425,7 +429,11 @@ export class MaxLTVLoanOffer { return principalValue.div(collateralValue).lte(this.data.maxLTV.div(100)); } - private packAndSetTermsContractParameters(): Promise { + private packTermsContractParameters(): string { + if (!this.collateralAmount) { + throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.COLLATERAL_AMOUNT_NOT_SET()); + } + const { adapter, collateralTokenIndex, principalTokenIndex } = this.data; const collateralTokenAmount = new TokenAmount( @@ -446,9 +454,6 @@ export class MaxLTVLoanOffer { gracePeriodInDays: new BigNumber(0), }; - this.termsContractParameters = adapter.packParameters( - simpleInterestTerms, - collateralizedSimpleInterestTerms, - ); + return adapter.packParameters(simpleInterestTerms, collateralizedSimpleInterestTerms); } } From 51a20c4bc608817cf3e89d41a8239dbdbe3a383f Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 13:12:19 -0700 Subject: [PATCH 34/38] feat: add more error handling --- .../runners/sign_as_debtor.ts | 4 +- src/types/loan_offer/max_ltv.ts | 56 +++++++++++++++---- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts index 09066bf9..6daccdd7 100644 --- a/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts +++ b/__test__/integration/max_ltv_loan_offer/runners/sign_as_debtor.ts @@ -114,12 +114,12 @@ export async function testSignAsDebtor(dharma: Dharma, params: MaxLTVParams) { await loanOffer.signAsCreditor(creditor); - await setPrices(); - const collateralAmount = 10; loanOffer.setCollateralAmount(collateralAmount); + await setPrices(); + await expect(loanOffer.signAsDebtor(debtor)).rejects.toThrow( MAX_LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT( collateralAmount, diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index ce1a6e38..dd45da47 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -35,11 +35,15 @@ const MAX_INTEREST_RATE_PRECISION = 4; const FIXED_POINT_SCALING_FACTOR = 10 ** MAX_INTEREST_RATE_PRECISION; export const MAX_LTV_LOAN_OFFER_ERRORS = { + ALREADY_SIGNED_BY_CREDITOR: () => `The creditor has already signed the loan offer.`, ALREADY_SIGNED_BY_DEBTOR: () => `The debtor has already signed the loan offer.`, COLLATERAL_AMOUNT_NOT_SET: () => `The collateral amount must be set first`, INSUFFICIENT_COLLATERAL_AMOUNT: (collateralAmount: number, collateralTokenSymbol: string) => singleLineString`Collateral of ${collateralAmount} ${collateralTokenSymbol} is insufficient for the maximum loan-to-value.`, + PRICE_OF_INCORRECT_TOKEN: (receivedAddress: string, expectedAddress: string) => + singleLineString`Received price of token at address ${receivedAddress}, + but expected price of token at address ${expectedAddress}.`, PRICES_NOT_SET: () => `The prices of the principal and collateral must be set first.`, }; @@ -202,7 +206,9 @@ export class MaxLTVLoanOffer { * @return {Promise} */ public async signAsCreditor(creditorAddress?: string): Promise { - // TODO: check if already signed by creditor + if (this.isSignedByCreditor()) { + throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.ALREADY_SIGNED_BY_CREDITOR()); + } this.creditor = await EthereumAddress.validAddressOrCurrentUser( this.dharma, @@ -250,8 +256,17 @@ export class MaxLTVLoanOffer { } public setPrincipalPrice(principalPrice: SignedPrice) { - // TODO: assert signed price feed provider address is the address we expect + if (principalPrice.tokenAddress !== this.data.principalTokenAddress) { + throw new Error( + MAX_LTV_LOAN_OFFER_ERRORS.PRICE_OF_INCORRECT_TOKEN( + principalPrice.tokenAddress, + this.data.principalTokenAddress, + ), + ); + } + // TODO: assert signed time is within some delta of current time (?) + this.principalPrice = principalPrice; } @@ -260,8 +275,17 @@ export class MaxLTVLoanOffer { } public setCollateralPrice(collateralPrice: SignedPrice) { - // TODO: assert signed price feed provider address is the address we expect + if (collateralPrice.tokenAddress !== this.data.collateralTokenAddress) { + throw new Error( + MAX_LTV_LOAN_OFFER_ERRORS.PRICE_OF_INCORRECT_TOKEN( + collateralPrice.tokenAddress, + this.data.collateralTokenAddress, + ), + ); + } + // TODO: assert signed time is within some delta of current time (?) + this.collateralPrice = collateralPrice; } @@ -270,8 +294,19 @@ export class MaxLTVLoanOffer { } public setCollateralAmount(collateralAmount: number) { - // TODO: assert prices are set - // TODO: assert collateralAmount sufficient + if ( + this.principalPrice && + this.collateralPrice && + !this.collateralAmountIsSufficient(collateralAmount) + ) { + throw new Error( + MAX_LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT( + collateralAmount, + this.data.collateralTokenSymbol, + ), + ); + } + this.collateralAmount = collateralAmount; this.termsContractParameters = this.packTermsContractParameters(); @@ -294,10 +329,7 @@ export class MaxLTVLoanOffer { throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.COLLATERAL_AMOUNT_NOT_SET()); } - // TODO: assert signed address matches principal token address - // TODO: assert signed address matches collateral token address - - if (!this.collateralAmountIsSufficient()) { + if (!this.collateralAmountIsSufficient(this.collateralAmount)) { throw new Error( MAX_LTV_LOAN_OFFER_ERRORS.INSUFFICIENT_COLLATERAL_AMOUNT( this.collateralAmount, @@ -410,8 +442,8 @@ export class MaxLTVLoanOffer { ); } - private collateralAmountIsSufficient(): boolean { - if (!this.collateralAmount || !this.principalPrice || !this.collateralPrice) { + private collateralAmountIsSufficient(collateralAmount: number): boolean { + if (!this.principalPrice || !this.collateralPrice) { return false; } @@ -422,7 +454,7 @@ export class MaxLTVLoanOffer { this.principalPrice.tokenPrice, ); - const collateralValue = new BigNumber(this.collateralAmount).times( + const collateralValue = new BigNumber(collateralAmount).times( this.collateralPrice.tokenPrice, ); From 7682fb75f4155659e5d25c063f445f7fdfc4b570 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 13:31:21 -0700 Subject: [PATCH 35/38] refactor: rename to getDebtorCommitmentHash --- src/types/loan_offer/max_ltv.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index dd45da47..8fe84cdf 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -342,7 +342,7 @@ export class MaxLTVLoanOffer { const isMetaMask = !!this.dharma.web3.currentProvider.isMetaMask; - const debtorCommitmentHash = this.getDebtorCommitHash(); + const debtorCommitmentHash = this.getDebtorCommitmentHash(); this.debtorSignature = await this.dharma.sign.signPayloadWithAddress( debtorCommitmentHash, @@ -364,7 +364,7 @@ export class MaxLTVLoanOffer { if ( this.debtorSignature && SignatureUtils.isValidSignature( - this.getDebtorCommitHash(), + this.getDebtorCommitmentHash(), this.debtorSignature, this.debtor, ) @@ -424,7 +424,7 @@ export class MaxLTVLoanOffer { ); } - private getDebtorCommitHash(): string { + private getDebtorCommitmentHash(): string { // We remove underwriting as a feature, since the creditor has no mechanism to mandate a maximum // underwriter risk rating. From 745571343d8cf343feba85e94d036a2f9db8dfa2 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 13:50:20 -0700 Subject: [PATCH 36/38] feat: add documentation --- src/types/loan_offer/max_ltv.ts | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 8fe84cdf..64a4a119 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -255,6 +255,15 @@ export class MaxLTVLoanOffer { return false; } + /** + * Sets the principal price. + * + * @throws Throws if the price is for the wrong token + * + * @example + * loanOffer.setPrincipalPrice(signedPrincipalPrice); + * + */ public setPrincipalPrice(principalPrice: SignedPrice) { if (principalPrice.tokenAddress !== this.data.principalTokenAddress) { throw new Error( @@ -270,10 +279,27 @@ export class MaxLTVLoanOffer { this.principalPrice = principalPrice; } + /** + * Gets the principal price. + * + * @example + * loanOffer.getPrincipalPrice(); + * + * @return {SignedPrice} + */ public getPrincipalPrice(): SignedPrice { return this.principalPrice; } + /** + * Sets the collateral price. + * + * @throws Throws if the price is for the wrong token + * + * @example + * loanOffer.setCollateralPrice(signedPrincipalPrice); + * + */ public setCollateralPrice(collateralPrice: SignedPrice) { if (collateralPrice.tokenAddress !== this.data.collateralTokenAddress) { throw new Error( @@ -289,10 +315,27 @@ export class MaxLTVLoanOffer { this.collateralPrice = collateralPrice; } + /** + * Gets the collateral price. + * + * @example + * loanOffer.getCollateralPrice(); + * + * @return {SignedPrice} + */ public getCollateralPrice(): SignedPrice { return this.principalPrice; } + /** + * Sets the collateral amount. + * + * @throws Throws if the collateral amount is insufficient. + * + * @example + * loanOffer.setCollateralAmount(1000); + * + */ public setCollateralAmount(collateralAmount: number) { if ( this.principalPrice && @@ -312,10 +355,32 @@ export class MaxLTVLoanOffer { this.termsContractParameters = this.packTermsContractParameters(); } + /** + * Gets the collateral amount. + * + * @example + * loanOffer.getCollateralAmount(); + * + * @return {SignedPrice} + */ public getCollateralAmount(): number { return this.collateralAmount; } + /** + * Eventually signs the loan offer as the debtor. + * + * @throws Throws if the loan offer is already signed by a debtor. + * @throws Throws if the prices are not set. + * @throws Throws if the collateral amount is not set. + * @throws Throws if the collateral amount is insufficient. + * + * @example + * loanOffer.signAsDebtor(); + * => Promise + * + * @return {Promise} + */ public async signAsDebtor(debtorAddress?: string): Promise { if (this.isSignedByDebtor()) { throw new Error(MAX_LTV_LOAN_OFFER_ERRORS.ALREADY_SIGNED_BY_DEBTOR()); From e6d546580bef43f73468bfea1ebe5118f8243480 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 13:58:56 -0700 Subject: [PATCH 37/38] refactor: import constant from simple interest loan terms --- src/adapters/simple_interest_loan_terms.ts | 2 +- src/types/loan_offer/max_ltv.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/adapters/simple_interest_loan_terms.ts b/src/adapters/simple_interest_loan_terms.ts index b3eddf8a..bdc2747e 100644 --- a/src/adapters/simple_interest_loan_terms.ts +++ b/src/adapters/simple_interest_loan_terms.ts @@ -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 { diff --git a/src/types/loan_offer/max_ltv.ts b/src/types/loan_offer/max_ltv.ts index 64a4a119..ef792fa8 100644 --- a/src/types/loan_offer/max_ltv.ts +++ b/src/types/loan_offer/max_ltv.ts @@ -7,6 +7,7 @@ import { CollateralizedTermsContractParameters, } from "../../../src/adapters/collateralized_simple_interest_loan_adapter"; import { SimpleInterestTermsContractParameters } from "../../../src/adapters/simple_interest_loan_adapter"; +import { FIXED_POINT_SCALING_FACTOR } from "../../../src/adapters/simple_interest_loan_terms"; import { Web3Utils } from "../../../utils/web3_utils"; @@ -31,9 +32,6 @@ import { SignedPrice } from "./signed_price"; import { BigNumber } from "../../../utils/bignumber"; -const MAX_INTEREST_RATE_PRECISION = 4; -const FIXED_POINT_SCALING_FACTOR = 10 ** MAX_INTEREST_RATE_PRECISION; - export const MAX_LTV_LOAN_OFFER_ERRORS = { ALREADY_SIGNED_BY_CREDITOR: () => `The creditor has already signed the loan offer.`, ALREADY_SIGNED_BY_DEBTOR: () => `The debtor has already signed the loan offer.`, From 7d87c2c96646f7dd4ca2c0562b12fe2dd0ccf229 Mon Sep 17 00:00:00 2001 From: Chris Min Date: Tue, 2 Oct 2018 14:09:40 -0700 Subject: [PATCH 38/38] fix: placate hound --- src/loan/loan_request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loan/loan_request.ts b/src/loan/loan_request.ts index b587dae9..48ce721b 100644 --- a/src/loan/loan_request.ts +++ b/src/loan/loan_request.ts @@ -2,7 +2,7 @@ import * as singleLineString from "single-line-string"; import { Dharma } from "../types/dharma"; -import { DEBT_ORDER_ERRORS, DebtOrder, CollateralizedDebtOrderParams } from "./debt_order"; +import { CollateralizedDebtOrderParams, DEBT_ORDER_ERRORS, DebtOrder } from "./debt_order"; import { EthereumAddress } from "../types";