From df817488771e333fa47c6765ff229e475d97b2ba Mon Sep 17 00:00:00 2001 From: chanakasameera <70064803+chanakasameera@users.noreply.github.com> Date: Wed, 30 Mar 2022 09:06:59 +0530 Subject: [PATCH] feat(jellyfish-api-core): DFI Payback of all dTokens (#1190) * Jellyfish minor updates for DFI Payback of all dTokens #1111 * Added setGov test cases with loan_payback and loan_payback_fee_pct keys * Added paybackloan api extention and a testcase * Added paybackloan a new testcase * Updated fortcanningroadheight * Updated fortcanningroadheight * Updated fortcanningroadheight * Added dusd loan payback using dusd test * Fixed failing gov tests. * Updated account.md with new paybackfees and paybacktokens * Updated PaybackLoanMetadata properly and test updates * Updates for review comments - part 1 (Now burn info is checked) * Updates for review comments - part 2 (burn info check, new test cases and paybackLoan method signature update) * Updates for review comments - part 3 (payback multiple loans at once, new test cases) * Minor fixes for CI test fails and interest calculation * Updates for review comments - part 4 (Payback checking before hardfork and minor updates) * Updates for review comments - part 5 (Now Tsla tokens are not minted) * Added dftx interface for Payback Loan V2 * Updated loan.md with payback loan v2 * Ain image from https://github.com/DeFiCh/ain/actions/runs/2020075855 * Update for dftx loan payback v2 * Added tests for dftx loan payback v2 * Comments update about PaybackLoanV2 Co-authored-by: surangap * Comments update about TokenPayback.dToken Co-authored-by: surangap Co-authored-by: surangap --- apps/rich-list-api/docker-compose.yml | 3 +- docs/node/CATEGORIES/08-account.md | 8 + docs/node/CATEGORIES/16-loan.md | 13 +- .../category/account/getBurnInfo.test.ts | 2 + .../governance/governance_container.ts | 1 + .../category/loan/paybackLoan.test.ts | 800 +++++++++++++++++- .../__tests__/category/loan/takeLoan.test.ts | 2 +- .../category/masternode/setGov.test.ts | 69 ++ .../src/category/account.ts | 8 + .../jellyfish-api-core/src/category/loan.ts | 18 +- .../txn/txn_builder_loan_payback_loan.test.ts | 189 ++++- .../txn/txn_builder_loan_take_loan.test.ts | 2 +- ...n_builder_loan_withdraw_from_vault.test.ts | 2 +- .../src/txn/txn_builder_loans.ts | 16 + .../src/script/dftx/dftx.ts | 4 + .../src/script/dftx/dftx_loans.ts | 44 + .../src/script/mapping.ts | 10 + .../src/containers/DeFiDContainer.ts | 2 +- .../src/containers/RegTestContainer/index.ts | 3 +- 19 files changed, 1176 insertions(+), 20 deletions(-) diff --git a/apps/rich-list-api/docker-compose.yml b/apps/rich-list-api/docker-compose.yml index 51d93fa12e..1003ebc612 100644 --- a/apps/rich-list-api/docker-compose.yml +++ b/apps/rich-list-api/docker-compose.yml @@ -41,4 +41,5 @@ services: -eunospayaheight=7 -fortcanningheight=8 -fortcanningmuseumheight=9 - -fortcanninghillheight=10 \ No newline at end of file + -fortcanninghillheight=10 + -fortcanningroadheight=11 diff --git a/docs/node/CATEGORIES/08-account.md b/docs/node/CATEGORIES/08-account.md index 889c69ff23..87d6bfcca4 100644 --- a/docs/node/CATEGORIES/08-account.md +++ b/docs/node/CATEGORIES/08-account.md @@ -456,5 +456,13 @@ interface BurnInfo { * Amount of tokens that are paid back; formatted as AMOUNT@SYMBOL */ dfipaybacktokens: string[] + /** + * Amount of paybacks + */ + paybackfees: string[] + /** + * Amount of tokens that are paid back + */ + paybacktokens: string[] } ``` diff --git a/docs/node/CATEGORIES/16-loan.md b/docs/node/CATEGORIES/16-loan.md index 6632fe3907..632cb40bfe 100644 --- a/docs/node/CATEGORIES/16-loan.md +++ b/docs/node/CATEGORIES/16-loan.md @@ -612,7 +612,7 @@ Return loan in a desired amount. ```ts title="client.loan.paybackLoan()" interface loan { - paybackLoan (metadata: PaybackLoanMetadata, utxos: UTXO[] = []): Promise + paybackLoan (metadata: PaybackLoanMetadata | PaybackLoanMetadataV2, utxos: UTXO[] = []): Promise } interface PaybackLoanMetadata { @@ -621,6 +621,17 @@ interface PaybackLoanMetadata { from: string } +interface TokenPaybackAmount { + dToken: string + amounts: string | string[] // amount@symbol +} + +interface PaybackLoanMetadataV2 { + vaultId: string + from: string + loans: TokenPaybackAmount[] +} + interface UTXO { txid: string vout: number diff --git a/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts b/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts index 54c0ce7c6b..98d032f16c 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts @@ -52,6 +52,8 @@ it('should getBurnInfo', async () => { auctionburn: new BigNumber(0), emissionburn: new BigNumber('6274'), paybackburn: new BigNumber(0), + paybackfees: [], + paybacktokens: [], dexfeetokens: [], dfipaybackfee: new BigNumber(0), dfipaybacktokens: [] diff --git a/packages/jellyfish-api-core/__tests__/category/governance/governance_container.ts b/packages/jellyfish-api-core/__tests__/category/governance/governance_container.ts index 0f531b9467..1573b1cb8b 100644 --- a/packages/jellyfish-api-core/__tests__/category/governance/governance_container.ts +++ b/packages/jellyfish-api-core/__tests__/category/governance/governance_container.ts @@ -14,6 +14,7 @@ export class GovernanceMasterNodeRegTestContainer extends MasterNodeRegTestConta .filter(cmd => cmd !== '-fortcanningheight=8') .filter(cmd => cmd !== '-fortcanningmuseumheight=9') .filter(cmd => cmd !== '-fortcanninghillheight=10') + .filter(cmd => cmd !== '-fortcanningroadheight=11') return [ ...cmd, diff --git a/packages/jellyfish-api-core/__tests__/category/loan/paybackLoan.test.ts b/packages/jellyfish-api-core/__tests__/category/loan/paybackLoan.test.ts index 1e8063abe5..955100e11f 100644 --- a/packages/jellyfish-api-core/__tests__/category/loan/paybackLoan.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/loan/paybackLoan.test.ts @@ -3,7 +3,7 @@ import { GenesisKeys, StartFlags, MasterNodeRegTestContainer } from '@defichain/ import BigNumber from 'bignumber.js' import { Testing, TestingGroup } from '@defichain/jellyfish-testing' import { RpcApiError } from '@defichain/jellyfish-api-core' -import { VaultActive } from '../../../src/category/loan' +import { PaybackLoanMetadataV2, VaultActive } from '../../../src/category/loan' const tGroup = TestingGroup.create(2, i => new LoanMasterNodeRegTestContainer(GenesisKeys[i])) const alice = tGroup.get(0) @@ -1118,6 +1118,51 @@ describe('paybackLoan before FortCanningHeight', () => { await expect(successfulPaybackPromise).resolves.not.toThrow() }) + it('should fail partial payback when interest becomes zero for pre FCH, and should success the same post FCH - use PaybackLoanMetadataV2', async () => { + const fchBlockHeight = 150 + await setupFCHContainer(fchBlockHeight) + await setupVault() + + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const loanMetadata: PaybackLoanMetadataV2 = { + vaultId: vaultId, + from: colAddr, + loans: [{ + dToken: tslaId, + amounts: ['2@TSLA'] + }] + } + + // payback loan + await testing.rpc.loan.paybackLoan(loanMetadata) + await testing.generate(1) + await tGroupFCH.waitForSync() + + await testing.rpc.loan.paybackLoan(loanMetadata) + await testing.generate(1) + await tGroupFCH.waitForSync() + + await testing.rpc.loan.paybackLoan(loanMetadata) + await testing.generate(1) + await tGroupFCH.waitForSync() + + await testing.rpc.loan.paybackLoan(loanMetadata) + await testing.generate(1) + await tGroupFCH.waitForSync() + + const promise = testing.rpc.loan.paybackLoan(loanMetadata) + await expect(promise).rejects.toThrow('RpcApiError: \'Test PaybackLoanV2Tx execution failed:\nCannot payback this amount of loan for TSLA, either payback full amount or less than this amount!\', code: -32600, method: paybackloan') + + await testing.container.waitForBlockHeight(fchBlockHeight) + const blockInfo = await testing.rpc.blockchain.getBlockchainInfo() + expect(blockInfo.softforks.fortcanninghill.active).toBeTruthy() + + // payback loan + const successfulPaybackPromise = testing.rpc.loan.paybackLoan(loanMetadata) + await expect(successfulPaybackPromise).resolves.not.toThrow() + }) + it('should be able to payback loan post Fort Canning Hill hardfork', async () => { await setupFCHContainer(10) await setupVault() @@ -1165,7 +1210,7 @@ describe('paybackLoan before FortCanningHeight', () => { }) }) -describe('paybackloan for dusd using dfi', () => { +describe('paybackloan for any token', () => { const container = new MasterNodeRegTestContainer() const testing = Testing.create(container) @@ -1191,7 +1236,8 @@ describe('paybackloan for dusd using dfi', () => { const priceFeeds = [ { token: 'DFI', currency: 'USD' }, { token: 'TSLA', currency: 'USD' }, - { token: 'DUSD', currency: 'USD' } + { token: 'DUSD', currency: 'USD' }, + { token: 'BTC', currency: 'USD' } ] const oracleId = await testing.rpc.oracle.appointOracle(oracleAddress, priceFeeds, { weightage: 1 }) @@ -1200,6 +1246,7 @@ describe('paybackloan for dusd using dfi', () => { await testing.rpc.oracle.setOracleData(oracleId, timestamp, { prices: [{ tokenAmount: '1@DFI', currency: 'USD' }] }) await testing.rpc.oracle.setOracleData(oracleId, timestamp, { prices: [{ tokenAmount: '1000@TSLA', currency: 'USD' }] }) await testing.rpc.oracle.setOracleData(oracleId, timestamp, { prices: [{ tokenAmount: '1@DUSD', currency: 'USD' }] }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { prices: [{ tokenAmount: '1000@BTC', currency: 'USD' }] }) await testing.generate(1) // setup collateral token @@ -1271,14 +1318,51 @@ describe('paybackloan for dusd using dfi', () => { }) await testing.generate(1) + tslaTakeLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() await testing.rpc.loan.takeLoan({ vaultId: tslaVaultId, amounts: `${tslaLoanAmount}@TSLA` }) - tslaTakeLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() await testing.generate(1) } + // this will borrow tesla tokens and will give to you + async function takeTslaTokensToPayback (): Promise { + const tokenProviderSchemeId = 'LoanTsla' + await testing.rpc.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(0.01), + id: tokenProviderSchemeId + }) + await testing.container.generate(1) + + const tokenProviderVaultAddress = await testing.generateAddress() + await testing.token.dfi({ address: tokenProviderVaultAddress, amount: 1000000 }) + + const tokenProviderVaultId = await testing.rpc.loan.createVault({ + ownerAddress: tokenProviderVaultAddress, + loanSchemeId: tokenProviderSchemeId + }) + await testing.container.generate(1) + + await testing.rpc.loan.depositToVault({ + vaultId: tokenProviderVaultId, + from: tokenProviderVaultAddress, + amount: '1000000@DFI' + }) + await testing.generate(1) + + await testing.rpc.loan.takeLoan({ + vaultId: tokenProviderVaultId, + amounts: '100@TSLA', + to: tokenProviderVaultAddress + }) + await testing.container.generate(1) + + await testing.rpc.account.accountToAccount(tokenProviderVaultAddress, { [vaultOwnerAddress]: '100@TSLA' }) + await testing.container.generate(1) + } + beforeEach(async () => { await testing.container.start() await testing.container.waitForWalletCoinbaseMaturity() @@ -1507,6 +1591,654 @@ describe('paybackloan for dusd using dfi', () => { expect(vaultAfter.interestAmounts).toStrictEqual([]) }) + it('should be able to payback DUSD loan using DFI - use PaybackLoanMetadataV2', async () => { + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const paybackKey = `v0/token/${dusdId}/payback_dfi` + const penaltyRateKey = `v0/token/${dusdId}/payback_dfi_fee_pct` + const penaltyRate = 0.015 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [paybackKey]: 'true', [penaltyRateKey]: penaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoBefore.paybackfees).toStrictEqual([]) + expect(burnInfoBefore.paybacktokens).toStrictEqual([]) + + const dfiPaybackAmount = 100 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: dusdId, + amounts: `${dfiPaybackAmount}@DFI` + }] + }) + + // Price of dfi to dusd depends on the oracle, in this case 1 DFI = 1 DUSD + const effectiveDusdPerDfi = new BigNumber(1).multipliedBy(1 - penaltyRate) // (DUSD per DFI * (1 - penalty rate)) + const dusdPayback = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveDusdPerDfi) + const dusdLoanPayback = dusdPayback.minus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const dusdInterestPerBlockAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + const totalDfiPenalty = new BigNumber(dfiPaybackAmount).multipliedBy(penaltyRate) + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(dfiPaybackAmount).toFixed(8)}@DFI`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(totalDfiPenalty) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`]) + }) + + it('should be able to payback DUSD loan using TSLA - use PaybackLoanMetadataV2', async () => { + await takeTslaTokensToPayback() + + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + + const paybackKey = `v0/token/${dusdId}/loan_payback/${tslaId}` + const penaltyRateKey = `v0/token/${dusdId}/loan_payback_fee_pct/${tslaId}` + const penaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [paybackKey]: 'true', [penaltyRateKey]: penaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoBefore.paybackfees).toStrictEqual([]) + expect(burnInfoBefore.paybacktokens).toStrictEqual([]) + + const tslaPaybackAmount = 1 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: dusdId, + amounts: `${tslaPaybackAmount}@TSLA` + }] + }) + + // Price of tsla to dusd depends on the oracle, in this case 1 TSLA = 1000 DUSD + const effectiveDusdPerTsla = new BigNumber(1000).multipliedBy(1 - penaltyRate) // (DUSD per TSLA * (1 - penalty rate)) + const dusdPayback = new BigNumber(tslaPaybackAmount).multipliedBy(effectiveDusdPerTsla) + const dusdLoanPayback = dusdPayback.minus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const dusdInterestPerBlockAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + const totalTslaPenalty = new BigNumber(tslaPaybackAmount).multipliedBy(penaltyRate) + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(tslaPaybackAmount).toFixed(8)}@TSLA`]) + expect(burnInfoAfter.paybackfees).toStrictEqual([`${totalTslaPenalty.toFixed(8)}@TSLA`]) + expect(burnInfoAfter.paybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`]) + }) + + it('should be able to payback DUSD loan using both TSLA and DFI - use PaybackLoanMetadataV2', async () => { + await takeTslaTokensToPayback() + + const dfiInfo = await testing.rpc.token.getToken('DFI') + const dfiId: string = Object.keys(dfiInfo)[0] + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + + const dfiPaybackKey = `v0/token/${dusdId}/loan_payback/${dfiId}` + const dfiPenaltyRateKey = `v0/token/${dusdId}/loan_payback_fee_pct/${dfiId}` + const dfiPenaltyRate = 0.025 + await testing.rpc.masternode.setGov({ [attributeKey]: { [dfiPaybackKey]: 'true', [dfiPenaltyRateKey]: dfiPenaltyRate.toString() } }) + await testing.generate(1) + + const tslaPaybackKey = `v0/token/${dusdId}/loan_payback/${tslaId}` + const tslaPenaltyRateKey = `v0/token/${dusdId}/loan_payback_fee_pct/${tslaId}` + const tslaPenaltyRate = 0.02 + await testing.rpc.masternode.setGov({ [attributeKey]: { [tslaPaybackKey]: 'true', [tslaPenaltyRateKey]: tslaPenaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoBefore.paybackfees).toStrictEqual([]) + expect(burnInfoBefore.paybacktokens).toStrictEqual([]) + + const tslaPaybackAmount = 1 + const dfiPaybackAmount = 100 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: dusdId, + amounts: [`${dfiPaybackAmount}@DFI`, `${tslaPaybackAmount}@TSLA`] + }] + }) + + // Price of dfi to dusd depends on the oracle, in this case 1 DFI = 1 DUSD + const effectiveDusdPerDfi = new BigNumber(1).multipliedBy(1 - dfiPenaltyRate) // (DUSD per DFI * (1 - penalty rate)) + const dusdPaybackByDfi = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveDusdPerDfi) + + // Price of tsla to dusd depends on the oracle, in this case 1 TSLA = 1000 DUSD + const effectiveDusdPerTsla = new BigNumber(1000).multipliedBy(1 - tslaPenaltyRate) // (DUSD per TSLA * (1 - penalty rate)) + const dusdPaybackByTsla = new BigNumber(tslaPaybackAmount).multipliedBy(effectiveDusdPerTsla) + + const dusdPayback = dusdPaybackByDfi.plus(dusdPaybackByTsla) + + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdPayback).plus(dusdInterestAmountBefore) + const dusdInterestPerBlockAfter = dusdLoanRemainingAfter.multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + + // Let some time, generate blocks + await testing.generate(2) + + const totalDfiPenalty = new BigNumber(dfiPaybackAmount).multipliedBy(dfiPenaltyRate) + const totalTslaPenalty = new BigNumber(tslaPaybackAmount).multipliedBy(tslaPenaltyRate) + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(dfiPaybackAmount).toFixed(8)}@DFI`, `${new BigNumber(tslaPaybackAmount).toFixed(8)}@TSLA`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(totalDfiPenalty) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([`${dusdPaybackByDfi.toFixed(8)}@DUSD`]) + expect(burnInfoAfter.paybackfees).toStrictEqual([`${totalTslaPenalty.toFixed(8)}@TSLA`]) + expect(burnInfoAfter.paybacktokens).toStrictEqual([`${dusdPaybackByTsla.toFixed(8)}@DUSD`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`]) + }) + + it('should be able to payback DUSD loan using both TSLA and BTC - use PaybackLoanMetadataV2', async () => { + await takeTslaTokensToPayback() + + const metadataBtc = { + symbol: 'BTC', + name: 'BTC', + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: vaultOwnerAddress + } + await testing.token.create(metadataBtc) + await testing.container.generate(1) + + await testing.token.mint({ amount: 10, symbol: 'BTC' }) + await testing.container.generate(1) + + await testing.rpc.loan.setCollateralToken({ + token: 'BTC', + factor: new BigNumber(1), + fixedIntervalPriceId: 'BTC/USD' + }) + await testing.generate(1) + + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const btcInfo = await testing.rpc.token.getToken('BTC') + const btcId: string = Object.keys(btcInfo)[0] + + const tslaPaybackKey = `v0/token/${dusdId}/loan_payback/${tslaId}` + const tslaPenaltyRateKey = `v0/token/${dusdId}/loan_payback_fee_pct/${tslaId}` + const tslaPenaltyRate = 0.02 + await testing.rpc.masternode.setGov({ [attributeKey]: { [tslaPaybackKey]: 'true', [tslaPenaltyRateKey]: tslaPenaltyRate.toString() } }) + await testing.generate(1) + + const btcPaybackKey = `v0/token/${dusdId}/loan_payback/${btcId}` + const btcPenaltyRateKey = `v0/token/${dusdId}/loan_payback_fee_pct/${btcId}` + const btcPenaltyRate = 0.01 + await testing.rpc.masternode.setGov({ [attributeKey]: { [btcPaybackKey]: 'true', [btcPenaltyRateKey]: btcPenaltyRate.toString() } }) + await testing.generate(5) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoBefore.paybackfees).toStrictEqual([]) + expect(burnInfoBefore.paybacktokens).toStrictEqual([]) + + const tslaPaybackAmount = 1 + const btcPaybackAmount = 1 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: dusdId, + amounts: [`${tslaPaybackAmount}@TSLA`, `${btcPaybackAmount}@BTC`] + }] + }) + + // Price of tsla to dusd depends on the oracle, in this case 1 TSLA = 1000 DUSD + const effectiveDusdPerTsla = new BigNumber(1000).multipliedBy(1 - tslaPenaltyRate) // (DUSD per TSLA * (1 - penalty rate)) + const dusdPaybackByTsla = new BigNumber(tslaPaybackAmount).multipliedBy(effectiveDusdPerTsla) + + // Price of btc to dusd depends on the oracle, in this case 1 BTC = 1000 DUSD + const effectiveDusdPerBtc = new BigNumber(1000).multipliedBy(1 - btcPenaltyRate) // (DUSD per BTC * (1 - penalty rate)) + const dusdPaybackByBtc = new BigNumber(btcPaybackAmount).multipliedBy(effectiveDusdPerBtc) + + const dusdPayback = dusdPaybackByBtc.plus(dusdPaybackByTsla) + + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdPayback).plus(dusdInterestAmountBefore) + const dusdInterestPerBlockAfter = dusdLoanRemainingAfter.multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + + // Let some time, generate blocks + await testing.generate(2) + + const totalTslaPenalty = new BigNumber(tslaPaybackAmount).multipliedBy(tslaPenaltyRate) + const totalBtcPenalty = new BigNumber(btcPaybackAmount).multipliedBy(btcPenaltyRate) + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(tslaPaybackAmount).toFixed(8)}@TSLA`, `${new BigNumber(btcPaybackAmount).toFixed(8)}@BTC`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoAfter.paybackfees).toStrictEqual([`${totalTslaPenalty.toFixed(8)}@TSLA`, `${totalBtcPenalty.toFixed(8)}@BTC`]) + expect(burnInfoAfter.paybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`]) + }) + + it('should be able to payback DUSD loan using DUSD - use PaybackLoanMetadataV2', async () => { + // create DUSD-DFI + await testing.poolpair.create({ + tokenA: 'DUSD', + tokenB: 'DFI' + }) + await testing.generate(1) + + // add DUSD-DFI + await testing.poolpair.add({ + a: { symbol: 'DUSD', amount: 1000 }, + b: { symbol: 'DFI', amount: 1000 } + }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoBefore.paybackfees).toStrictEqual([]) + expect(burnInfoBefore.paybacktokens).toStrictEqual([]) + + const dusdPaybackAmount = 100 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: dusdId, + amounts: `${dusdPaybackAmount}@DUSD` + }] + }) + + const dusdPayback = new BigNumber(dusdPaybackAmount) + const dusdLoanPayback = dusdPayback.minus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const dusdInterestPerBlockAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const dusdLoanRemainingAfterFirstPayback = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + // const dusdPenalty = new BigNumber(dusdPaybackAmount).multipliedBy(penaltyRate) + // const burnInfoAfter = await testing.rpc.account.getBurnInfo() + // expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(dusdPaybackAmount).toFixed(8)}@DUSD`]) + // expect(burnInfoAfter.paybackfees).toStrictEqual([`${dusdPenalty.toFixed(8)}@DUSD`]) + // expect(burnInfoAfter.paybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfterFirstPayback.plus(dusdInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`]) + }) + + it('should be able to payback TSLA loan using DFI - use PaybackLoanMetadataV2', async () => { + await setupForTslaLoan() + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const paybackKey = `v0/token/${tslaId}/payback_dfi` + const penaltyRateKey = `v0/token/${tslaId}/payback_dfi_fee_pct` + const penaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [paybackKey]: 'true', [penaltyRateKey]: penaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const tslaInterestPerBlockBefore = new BigNumber(netInterest * tslaLoanAmount / (365 * blocksPerDay)) + const tslaInterestAmountBefore = tslaInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - tslaTakeLoanBlockHeight)) + const tslaLoanAmountBefore = new BigNumber(tslaLoanAmount).plus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(tslaVaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${tslaLoanAmountBefore.toFixed(8)}@TSLA`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + + const dfiPaybackAmount = 100 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: tslaVaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: tslaId, + amounts: `${dfiPaybackAmount}@DFI` + }] + }) + + // Price of dfi to tsla depends on the oracle, in this case 1 DFI = 0.001 TSLA + const effectiveTslaPerDfi = new BigNumber(0.001).multipliedBy(1 - penaltyRate) // (TSLA per DFI * (1 - penalty rate)) + const tslaPayback = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveTslaPerDfi) + const tslaLoanPayback = tslaPayback.minus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const tslaInterestPerBlockAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const tslaLoanRemainingAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + const totalDfiPenalty = new BigNumber(dfiPaybackAmount).multipliedBy(penaltyRate) + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(dfiPaybackAmount).toFixed(8)}@DFI`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(totalDfiPenalty) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([`${tslaPayback.toFixed(8)}@TSLA`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const tslaInterestAmountAfter = tslaInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const tslaLoanAmountAfter = tslaLoanRemainingAfter.plus(tslaInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(tslaVaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${tslaLoanAmountAfter.toFixed(8)}@TSLA`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${tslaInterestAmountAfter.toFixed(8)}@TSLA`]) + }) + + it('should be able to payback DUSD and TSLA loans using DFI - use PaybackLoanMetadataV2', async () => { + const tslaTakeLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.takeLoan({ + vaultId: vaultId, + amounts: `${tslaLoanAmount}@TSLA` + }) + await testing.generate(1) + + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const dusdPaybackKey = `v0/token/${dusdId}/payback_dfi` + const dusdPenaltyRateKey = `v0/token/${dusdId}/payback_dfi_fee_pct` + const dusdPenaltyRate = 0.015 + + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const tslaPaybackKey = `v0/token/${tslaId}/payback_dfi` + const tslaPenaltyRateKey = `v0/token/${tslaId}/payback_dfi_fee_pct` + const tslaPenaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [dusdPaybackKey]: 'true', [dusdPenaltyRateKey]: dusdPenaltyRate.toString(), [tslaPaybackKey]: 'true', [tslaPenaltyRateKey]: tslaPenaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const tslaInterestPerBlockBefore = new BigNumber(netInterest * tslaLoanAmount / (365 * blocksPerDay)) + const tslaInterestAmountBefore = tslaInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - tslaTakeLoanBlockHeight)) + const tslaLoanAmountBefore = new BigNumber(tslaLoanAmount).plus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`, `${tslaLoanAmountBefore.toFixed(8)}@TSLA`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + + const dfiPaybackAmount = 100 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: dusdId, + amounts: `${dfiPaybackAmount}@DFI` + }, { + dToken: tslaId, + amounts: `${dfiPaybackAmount}@DFI` + }] + }) + + // Price of dfi to dusd depends on the oracle, in this case 1 DFI = 1 DUSD + const effectiveDusdPerDfi = new BigNumber(1).multipliedBy(1 - dusdPenaltyRate) // (DUSD per DFI * (1 - penalty rate)) + const dusdPayback = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveDusdPerDfi) + const dusdLoanPayback = dusdPayback.minus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const dusdInterestPerBlockAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback) + + // Price of dfi to tsla depends on the oracle, in this case 1 DFI = 0.001 TSLA + const effectiveTslaPerDfi = new BigNumber(0.001).multipliedBy(1 - tslaPenaltyRate) // (TSLA per DFI * (1 - penalty rate)) + const tslaPayback = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveTslaPerDfi) + const tslaLoanPayback = tslaPayback.minus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const tslaInterestPerBlockAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const tslaLoanRemainingAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + const dfiPenaltyForDusdLoan = new BigNumber(dfiPaybackAmount).multipliedBy(dusdPenaltyRate) + const dfiPenaltyForTslaLoan = new BigNumber(dfiPaybackAmount).multipliedBy(tslaPenaltyRate) + + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(dfiPaybackAmount * 2).toFixed(8)}@DFI`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(dfiPenaltyForDusdLoan.plus(dfiPenaltyForTslaLoan)) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`, `${tslaPayback.toFixed(8)}@TSLA`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + const tslaInterestAmountAfter = tslaInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const tslaLoanAmountAfter = tslaLoanRemainingAfter.plus(tslaInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`, `${tslaLoanAmountAfter.toFixed(8)}@TSLA`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`, `${tslaInterestAmountAfter.toFixed(8)}@TSLA`]) + }) + + it('should be able to payback DUSD and TSLA loans using BTC - use PaybackLoanMetadataV2', async () => { + const metadata = { + symbol: 'BTC', + name: 'BTC', + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: vaultOwnerAddress + } + await testing.token.create(metadata) + await testing.container.generate(1) + + await testing.token.mint({ amount: 10, symbol: 'BTC' }) + await testing.container.generate(1) + + await testing.rpc.loan.setCollateralToken({ + token: 'BTC', + factor: new BigNumber(1), + fixedIntervalPriceId: 'BTC/USD' + }) + await testing.generate(1) + + const tslaTakeLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.takeLoan({ + vaultId: vaultId, + amounts: `${tslaLoanAmount}@TSLA` + }) + await testing.generate(1) + + const btcInfo = await testing.rpc.token.getToken('BTC') + const btcId: string = Object.keys(btcInfo)[0] + + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const dusdPaybackKey = `v0/token/${dusdId}/loan_payback/${btcId}` + const dusdPenaltyRateKey = `v0/token/${dusdId}/loan_payback_fee_pct/${btcId}` + const dusdPenaltyRate = 0.015 + + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const tslaPaybackKey = `v0/token/${tslaId}/loan_payback/${btcId}` + const tslaPenaltyRateKey = `v0/token/${tslaId}/loan_payback_fee_pct/${btcId}` + const tslaPenaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [dusdPaybackKey]: 'true', [dusdPenaltyRateKey]: dusdPenaltyRate.toString(), [tslaPaybackKey]: 'true', [tslaPenaltyRateKey]: tslaPenaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const tslaInterestPerBlockBefore = new BigNumber(netInterest * tslaLoanAmount / (365 * blocksPerDay)) + const tslaInterestAmountBefore = tslaInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - tslaTakeLoanBlockHeight)) + const tslaLoanAmountBefore = new BigNumber(tslaLoanAmount).plus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`, `${tslaLoanAmountBefore.toFixed(8)}@TSLA`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoBefore.paybackfees).toStrictEqual([]) + expect(burnInfoBefore.paybacktokens).toStrictEqual([]) + + const btcPaybackAmount = 1 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + loans: [{ + dToken: dusdId, + amounts: `${btcPaybackAmount}@BTC` + }, { + dToken: tslaId, + amounts: `${btcPaybackAmount}@BTC` + }] + }) + + // Price of btc to dusd depends on the oracle, in this case 1 BTC = 1000 DUSD + const effectiveDusdPerBtc = new BigNumber(1000).multipliedBy(1 - dusdPenaltyRate) // (DUSD per BTC * (1 - penalty rate)) + const dusdPayback = new BigNumber(btcPaybackAmount).multipliedBy(effectiveDusdPerBtc) + const dusdLoanPayback = dusdPayback.minus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const dusdInterestPerBlockAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback) + + // Price of btc to tsla depends on the oracle, in this case 1 BTC = 1 TSLA + const effectiveTslaPerBtc = new BigNumber(1).multipliedBy(1 - tslaPenaltyRate) // (TSLA per BTC * (1 - penalty rate)) + const tslaPayback = new BigNumber(btcPaybackAmount).multipliedBy(effectiveTslaPerBtc) + const tslaLoanPayback = tslaPayback.minus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const tslaInterestPerBlockAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const tslaLoanRemainingAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + const btcPenaltyForDusdLoan = new BigNumber(btcPaybackAmount).multipliedBy(dusdPenaltyRate) + const btcPenaltyForTslaLoan = new BigNumber(btcPaybackAmount).multipliedBy(tslaPenaltyRate) + + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(btcPaybackAmount * 2).toFixed(8)}@BTC`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoAfter.paybackfees).toStrictEqual([`${btcPenaltyForDusdLoan.plus(btcPenaltyForTslaLoan).toFixed(8)}@BTC`]) + expect(burnInfoAfter.paybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`, `${tslaPayback.toFixed(8)}@TSLA`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + const tslaInterestAmountAfter = tslaInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const tslaLoanAmountAfter = tslaLoanRemainingAfter.plus(tslaInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`, `${tslaLoanAmountAfter.toFixed(8)}@TSLA`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`, `${tslaInterestAmountAfter.toFixed(8)}@TSLA`]) + }) + it('should not payback DUSD loan using DFI when attribute is not enabled in setGov', async () => { let attribute = await testing.rpc.masternode.getGov(attributeKey) // eslint-disable-next-line no-prototype-builtins @@ -1520,7 +2252,7 @@ describe('paybackloan for dusd using dfi', () => { from: vaultOwnerAddress }) - await expect(promise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nPayback of DUSD loans with DFI not currently active\', code: -32600, method: paybackloan') + await expect(promise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nPayback of loan via DFI token is not currently active\', code: -32600, method: paybackloan') await testing.rpc.masternode.setGov({ [attributeKey]: { [key]: 'false' } }) await testing.container.generate(1) @@ -1535,10 +2267,10 @@ describe('paybackloan for dusd using dfi', () => { from: vaultOwnerAddress }) - await expect(promise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nPayback of DUSD loans with DFI not currently active\', code: -32600, method: paybackloan') + await expect(promise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nPayback of loan via DFI token is not currently active\', code: -32600, method: paybackloan') }) - it('should not be able to payback TSLA loan using DFI', async () => { + it('should not be able to payback TSLA loan using DFI when the governance is set to payback DUSD using DFI', async () => { await setupForTslaLoan() await testing.rpc.masternode.setGov({ [attributeKey]: { [key]: 'true' } }) await testing.generate(1) @@ -1559,7 +2291,7 @@ describe('paybackloan for dusd using dfi', () => { await expect(payBackPromise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nThere is no loan on token (DUSD) in this vault!\', code: -32600, method: paybackloan') }) - it('should not be able to payback DUSD loan using other tokens', async () => { + it('should not be able to payback DUSD loan using other tokens when the governance is set to payback DUSD using DFI', async () => { await setupForTslaLoan() await testing.rpc.masternode.setGov({ [attributeKey]: { [key]: 'true' } }) await testing.generate(1) @@ -1573,4 +2305,56 @@ describe('paybackloan for dusd using dfi', () => { await expect(paybackWithTslaPromise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nThere is no loan on token (TSLA) in this vault!\', code: -32600, method: paybackloan') }) + + it('should not be able to payback TSLA loan using DFI - without PaybackLoanMetadataV2', async () => { + await setupForTslaLoan() + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const paybackKey = `v0/token/${tslaId}/payback_dfi` + const penaltyRateKey = `v0/token/${tslaId}/payback_dfi_fee_pct` + const penaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [paybackKey]: 'true', [penaltyRateKey]: penaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const tslaInterestPerBlockBefore = new BigNumber(netInterest * tslaLoanAmount / (365 * blocksPerDay)) + const tslaInterestAmountBefore = tslaInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - tslaTakeLoanBlockHeight)) + const tslaLoanAmountBefore = new BigNumber(tslaLoanAmount).plus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(tslaVaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${tslaLoanAmountBefore.toFixed(8)}@TSLA`]) + + const payBackPromise = testing.rpc.loan.paybackLoan({ + vaultId: tslaVaultId, + amounts: '10000@DFI', + from: vaultOwnerAddress + }) + + await expect(payBackPromise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nThere is no loan on token (DUSD) in this vault!\', code: -32600, method: paybackloan') + }) + + it('should not be able to payback DUSD loan using TSLA - without PaybackLoanMetadataV2', async () => { + await takeTslaTokensToPayback() + + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + + const paybackKey = `v0/token/${dusdId}/loan_payback/${tslaId}` + const penaltyRateKey = `v0/token/${dusdId}/loan_payback_fee_pct/${tslaId}` + const penaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [paybackKey]: 'true', [penaltyRateKey]: penaltyRate.toString() } }) + await testing.generate(1) + + const tslaPaybackAmount = 1 + const payBackPromise = testing.rpc.loan.paybackLoan({ + vaultId: vaultId, + from: vaultOwnerAddress, + amounts: `${tslaPaybackAmount}@TSLA` + }) + await expect(payBackPromise).rejects.toThrow('RpcApiError: \'Test PaybackLoanTx execution failed:\nThere is no loan on token (TSLA) in this vault!\', code: -32600, method: paybackloan') + }) }) diff --git a/packages/jellyfish-api-core/__tests__/category/loan/takeLoan.test.ts b/packages/jellyfish-api-core/__tests__/category/loan/takeLoan.test.ts index 431993c47e..5f95ac214b 100644 --- a/packages/jellyfish-api-core/__tests__/category/loan/takeLoan.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/loan/takeLoan.test.ts @@ -780,7 +780,7 @@ describe('takeloan failed', () => { amounts: '1000@TSLA' }) await expect(promise).rejects.toThrow(RpcApiError) - await expect(promise).rejects.toThrow('At least 50% of the minimum required collateral must be in DFI when taking a loan.') + await expect(promise).rejects.toThrow('At least 50% of the minimum required collateral must be in DFI or DUSD when taking a loan.') { // revert DFI value changes diff --git a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts index e35b1215b9..28b3c2a1ed 100644 --- a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts @@ -2,6 +2,7 @@ import { RpcApiError } from '@defichain/jellyfish-api-core' import { GenesisKeys, MasterNodeRegTestContainer } from '@defichain/testcontainers' import { createPoolPair, createToken } from '@defichain/testing' import { ContainerAdapterClient } from '../../container_adapter_client' +import { Testing } from '@defichain/jellyfish-testing' describe('Masternode', () => { const container = new MasterNodeRegTestContainer() @@ -69,3 +70,71 @@ describe('Masternode', () => { await expect(promise).rejects.toThrow('LP_DAILY_DFI_REWARD: Cannot be set manually after Eunos hard fork') }) }) + +describe('Masternode setGov ATTRIBUTES', () => { + const container = new MasterNodeRegTestContainer() + const testing = Testing.create(container) + const attributeKey = 'ATTRIBUTES' + let key: string + + beforeAll(async () => { + await testing.container.start() + await testing.container.waitForWalletCoinbaseMaturity() + + // setup loan token + await testing.rpc.loan.setLoanToken({ + symbol: 'DUSD', + fixedIntervalPriceId: 'DUSD/USD' + }) + await testing.generate(1) + + const address = await container.call('getnewaddress') + const metadata = { + symbol: 'BTC', + name: 'BTC Token', + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: address + } + await testing.rpc.token.createToken(metadata) + await testing.generate(1) + + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId = Object.keys(dusdInfo)[0] + key = `v0/token/${dusdId}` + }) + + afterAll(async () => { + await testing.container.stop() + }) + + it('should setGov with loan_payback and loan_payback_fee_pct', async () => { + const key0 = `${key}/loan_payback/1` + const key1 = `${key}/loan_payback/2` + const key2 = `${key}/loan_payback_fee_pct/1` + await testing.rpc.masternode.setGov({ [attributeKey]: { [key0]: 'true', [key1]: 'true', [key2]: '0.25' } }) + await testing.container.generate(1) + + const govAfter = await testing.rpc.masternode.getGov(attributeKey) + expect(govAfter.ATTRIBUTES[key0].toString()).toStrictEqual('true') + expect(govAfter.ATTRIBUTES[key1].toString()).toStrictEqual('true') + expect(govAfter.ATTRIBUTES[key2].toString()).toStrictEqual('0.25') + }) + + it('should setGov dfi keys with loan_payback and loan_payback_fee_pct', async () => { + const key0 = `${key}/loan_payback/0` + const key1 = `${key}/loan_payback_fee_pct/0` + await testing.rpc.masternode.setGov({ [attributeKey]: { [key0]: 'false', [key1]: '0.35' } }) + await testing.container.generate(1) + + const govAfter = await testing.rpc.masternode.getGov(attributeKey) + expect(govAfter.ATTRIBUTES[key0]).toBeUndefined() + expect(govAfter.ATTRIBUTES[key1]).toBeUndefined() + + const key2 = `${key}/payback_dfi` + const key3 = `${key}/payback_dfi_fee_pct` + expect(govAfter.ATTRIBUTES[key2].toString()).toStrictEqual('false') + expect(govAfter.ATTRIBUTES[key3].toString()).toStrictEqual('0.35') + }) +}) diff --git a/packages/jellyfish-api-core/src/category/account.ts b/packages/jellyfish-api-core/src/category/account.ts index e6385c9400..039a3fe5b9 100644 --- a/packages/jellyfish-api-core/src/category/account.ts +++ b/packages/jellyfish-api-core/src/category/account.ts @@ -543,4 +543,12 @@ export interface BurnInfo { * Amount of tokens that are paid back; formatted as AMOUNT@SYMBOL */ dfipaybacktokens: string[] + /** + * Amount of paybacks + */ + paybackfees: string[] + /** + * Amount of tokens that are paid back + */ + paybacktokens: string[] } diff --git a/packages/jellyfish-api-core/src/category/loan.ts b/packages/jellyfish-api-core/src/category/loan.ts index 9bc9840b4d..69ee80679a 100644 --- a/packages/jellyfish-api-core/src/category/loan.ts +++ b/packages/jellyfish-api-core/src/category/loan.ts @@ -357,16 +357,19 @@ export class Loan { /** * Return loan in a desired amount. * - * @param {PaybackLoanMetadata} metadata + * @param {PaybackLoanMetadata | PaybackLoanMetadataV2} metadata * @param {string} metadata.vaultId Vault id * @param {string| string[]} metadata.amounts In "amount@symbol" format * @param {string} metadata.from Address from transfer tokens + * @param {TokenPaybackAmount[]} metadata.loans + * @param {string | string[]} metadata.loans[0].amounts In "amount@symbol" format to be spent + * @param {string} metadata.loans[0].dToken Token to be paid * @param {UTXO[]} [utxos = []] Specific UTXOs to spend * @param {string} utxos.txid Transaction Id * @param {number} utxos.vout Output number * @return {Promise} txid */ - async paybackLoan (metadata: PaybackLoanMetadata, utxos: UTXO[] = []): Promise { + async paybackLoan (metadata: PaybackLoanMetadata | PaybackLoanMetadataV2, utxos: UTXO[] = []): Promise { return await this.client.call('paybackloan', [metadata, utxos], 'number') } @@ -605,6 +608,17 @@ export interface PaybackLoanMetadata { from: string } +export interface TokenPaybackAmount { + dToken: string + amounts: string | string[] // amount@symbol +} + +export interface PaybackLoanMetadataV2 { + vaultId: string + from: string + loans: TokenPaybackAmount[] +} + export interface VaultPagination { start?: string including_start?: boolean diff --git a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_payback_loan.test.ts b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_payback_loan.test.ts index 25eb82c7ad..de92d16ba5 100644 --- a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_payback_loan.test.ts +++ b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_payback_loan.test.ts @@ -868,7 +868,7 @@ describe('paybackLoan failed #2', () => { }) }) -describe('paybackLoan for dusd using dfi', () => { +describe('paybackLoan for any token', () => { const container = new MasterNodeRegTestContainer() const testing = Testing.create(container) @@ -1239,6 +1239,158 @@ describe('paybackLoan for dusd using dfi', () => { expect(vaultAfter.interestAmounts).toStrictEqual([]) }) + it('should be able to payback DUSD loan using DFI - use PaybackLoanMetadataV2', async () => { + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const paybackKey = `v0/token/${dusdId}/payback_dfi` + const penaltyRateKey = `v0/token/${dusdId}/payback_dfi_fee_pct` + const penaltyRate = 0.015 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [paybackKey]: 'true', [penaltyRateKey]: penaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + expect(burnInfoBefore.paybackfees).toStrictEqual([]) + expect(burnInfoBefore.paybacktokens).toStrictEqual([]) + + const dfiPaybackAmount = 100 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + + const colScript = P2WPKH.fromAddress(RegTest, vaultOwnerAddress, P2WPKH).getScript() + const script = await testingProvider.elliptic.script() + const txn = await testingBuilder.loans.paybackLoanV2({ + vaultId: vaultId, + from: colScript, + loans: [{ dToken: parseInt(dusdId), amounts: [{ token: 0, amount: new BigNumber(dfiPaybackAmount) }] }] + }, script) + await sendTransaction(testing.container, txn) + + // Price of dfi to dusd depends on the oracle, in this case 1 DFI = 1 DUSD + const effectiveDusdPerDfi = new BigNumber(1).multipliedBy(1 - penaltyRate) // (DUSD per DFI * (1 - penalty rate)) + const dusdPayback = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveDusdPerDfi) + const dusdLoanPayback = dusdPayback.minus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const dusdInterestPerBlockAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + const totalDfiPenalty = new BigNumber(dfiPaybackAmount).multipliedBy(penaltyRate) + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(dfiPaybackAmount).toFixed(8)}@DFI`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(totalDfiPenalty) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`]) + }) + + it('should be able to payback DUSD and TSLA loans using DFI - use PaybackLoanMetadataV2', async () => { + const tslaTakeLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + await testing.rpc.loan.takeLoan({ + vaultId: vaultId, + amounts: `${tslaLoanAmount}@TSLA` + }) + await testing.generate(1) + + const dusdInfo = await testing.rpc.token.getToken('DUSD') + const dusdId: string = Object.keys(dusdInfo)[0] + const dusdPaybackKey = `v0/token/${dusdId}/payback_dfi` + const dusdPenaltyRateKey = `v0/token/${dusdId}/payback_dfi_fee_pct` + const dusdPenaltyRate = 0.015 + + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const tslaPaybackKey = `v0/token/${tslaId}/payback_dfi` + const tslaPenaltyRateKey = `v0/token/${tslaId}/payback_dfi_fee_pct` + const tslaPenaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [dusdPaybackKey]: 'true', [dusdPenaltyRateKey]: dusdPenaltyRate.toString(), [tslaPaybackKey]: 'true', [tslaPenaltyRateKey]: tslaPenaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + + const dusdInterestPerBlockBefore = new BigNumber(netInterest * dusdLoanAmount / (365 * blocksPerDay)) + const dusdInterestAmountBefore = dusdInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - dusdTakeLoanBlockHeight + 1)) + const dusdLoanAmountBefore = new BigNumber(dusdLoanAmount).plus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const tslaInterestPerBlockBefore = new BigNumber(netInterest * tslaLoanAmount / (365 * blocksPerDay)) + const tslaInterestAmountBefore = tslaInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - tslaTakeLoanBlockHeight)) + const tslaLoanAmountBefore = new BigNumber(tslaLoanAmount).plus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${dusdLoanAmountBefore.toFixed(8)}@DUSD`, `${tslaLoanAmountBefore.toFixed(8)}@TSLA`]) + + const burnInfoBefore = await testing.rpc.account.getBurnInfo() + expect(burnInfoBefore.tokens).toStrictEqual([]) + expect(burnInfoBefore.dfipaybackfee).toStrictEqual(new BigNumber(0)) + expect(burnInfoBefore.dfipaybacktokens).toStrictEqual([]) + + const dfiPaybackAmount = 100 + const paybackLoanBlockHeight = await testing.rpc.blockchain.getBlockCount() + + const colScript = P2WPKH.fromAddress(RegTest, vaultOwnerAddress, P2WPKH).getScript() + const script = await testingProvider.elliptic.script() + const txn = await testingBuilder.loans.paybackLoanV2({ + vaultId: vaultId, + from: colScript, + loans: [{ dToken: parseInt(dusdId), amounts: [{ token: 0, amount: new BigNumber(dfiPaybackAmount) }] }, + { dToken: parseInt(tslaId), amounts: [{ token: 0, amount: new BigNumber(dfiPaybackAmount) }] }] + }, script) + await sendTransaction(testing.container, txn) + + // Price of dfi to dusd depends on the oracle, in this case 1 DFI = 1 DUSD + const effectiveDusdPerDfi = new BigNumber(1).multipliedBy(1 - dusdPenaltyRate) // (DUSD per DFI * (1 - penalty rate)) + const dusdPayback = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveDusdPerDfi) + const dusdLoanPayback = dusdPayback.minus(dusdInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const dusdInterestPerBlockAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const dusdLoanRemainingAfter = new BigNumber(dusdLoanAmount).minus(dusdLoanPayback) + + // Price of dfi to tsla depends on the oracle, in this case 1 DFI = 0.001 TSLA + const effectiveTslaPerDfi = new BigNumber(0.001).multipliedBy(1 - tslaPenaltyRate) // (TSLA per DFI * (1 - penalty rate)) + const tslaPayback = new BigNumber(dfiPaybackAmount).multipliedBy(effectiveTslaPerDfi) + const tslaLoanPayback = tslaPayback.minus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + const tslaInterestPerBlockAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback).multipliedBy(netInterest).dividedBy(365 * blocksPerDay) + const tslaLoanRemainingAfter = new BigNumber(tslaLoanAmount).minus(tslaLoanPayback) + + // Let some time, generate blocks + await testing.generate(2) + + const dfiPenaltyForDusdLoan = new BigNumber(dfiPaybackAmount).multipliedBy(dusdPenaltyRate) + const dfiPenaltyForTslaLoan = new BigNumber(dfiPaybackAmount).multipliedBy(tslaPenaltyRate) + + const burnInfoAfter = await testing.rpc.account.getBurnInfo() + expect(burnInfoAfter.tokens).toStrictEqual([`${new BigNumber(dfiPaybackAmount * 2).toFixed(8)}@DFI`]) + expect(burnInfoAfter.dfipaybackfee).toStrictEqual(dfiPenaltyForDusdLoan.plus(dfiPenaltyForTslaLoan)) + expect(burnInfoAfter.dfipaybacktokens).toStrictEqual([`${dusdPayback.toFixed(8)}@DUSD`, `${tslaPayback.toFixed(8)}@TSLA`]) + + const blockHeightAfter = await testing.rpc.blockchain.getBlockCount() + const dusdInterestAmountAfter = dusdInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const dusdLoanAmountAfter = dusdLoanRemainingAfter.plus(dusdInterestAmountAfter) + const tslaInterestAmountAfter = tslaInterestPerBlockAfter.multipliedBy(blockHeightAfter - paybackLoanBlockHeight).decimalPlaces(8, BigNumber.ROUND_CEIL) + const tslaLoanAmountAfter = tslaLoanRemainingAfter.plus(tslaInterestAmountAfter) + + const vaultAfter = await testing.rpc.loan.getVault(vaultId) as VaultActive + expect(vaultAfter.loanAmounts).toStrictEqual([`${dusdLoanAmountAfter.toFixed(8)}@DUSD`, `${tslaLoanAmountAfter.toFixed(8)}@TSLA`]) + expect(vaultAfter.interestAmounts).toStrictEqual([`${dusdInterestAmountAfter.toFixed(8)}@DUSD`, `${tslaInterestAmountAfter.toFixed(8)}@TSLA`]) + }) + it('should not payback DUSD loan using DFI when attribute is not enabled in setGov', async () => { let attribute = await testing.rpc.masternode.getGov(attributeKey) // eslint-disable-next-line no-prototype-builtins @@ -1255,7 +1407,7 @@ describe('paybackLoan for dusd using dfi', () => { }, script) let promise = sendTransaction(testing.container, txn) - await expect(promise).rejects.toThrow('DeFiDRpcError: \'PaybackLoanTx: Payback of DUSD loans with DFI not currently active (code 16)\', code: -26') + await expect(promise).rejects.toThrow('DeFiDRpcError: \'PaybackLoanTx: Payback of loan via DFI token is not currently active (code 16)\', code: -26') await testing.rpc.masternode.setGov({ [attributeKey]: { [key]: 'false' } }) await testing.container.generate(1) @@ -1274,7 +1426,7 @@ describe('paybackLoan for dusd using dfi', () => { }, script) promise = sendTransaction(testing.container, txn) - await expect(promise).rejects.toThrow('DeFiDRpcError: \'PaybackLoanTx: Payback of DUSD loans with DFI not currently active (code 16)\', code: -26') + await expect(promise).rejects.toThrow('DeFiDRpcError: \'PaybackLoanTx: Payback of loan via DFI token is not currently active (code 16)\', code: -26') }) it('should not be able to payback TSLA loan using DFI', async () => { @@ -1320,4 +1472,35 @@ describe('paybackLoan for dusd using dfi', () => { const promise = sendTransaction(testing.container, txn) await expect(promise).rejects.toThrow('DeFiDRpcError: \'PaybackLoanTx: There is no loan on token (TSLA) in this vault! (code 16)\', code: -26') }) + + it('should not be able to payback TSLA loan using DFI - without PaybackLoanMetadataV2', async () => { + await setupForTslaLoan() + const tslaInfo = await testing.rpc.token.getToken('TSLA') + const tslaId: string = Object.keys(tslaInfo)[0] + const paybackKey = `v0/token/${tslaId}/payback_dfi` + const penaltyRateKey = `v0/token/${tslaId}/payback_dfi_fee_pct` + const penaltyRate = 0.02 + + await testing.rpc.masternode.setGov({ [attributeKey]: { [paybackKey]: 'true', [penaltyRateKey]: penaltyRate.toString() } }) + await testing.generate(1) + + const blockHeightBefore = await testing.rpc.blockchain.getBlockCount() + const tslaInterestPerBlockBefore = new BigNumber(netInterest * tslaLoanAmount / (365 * blocksPerDay)) + const tslaInterestAmountBefore = tslaInterestPerBlockBefore.multipliedBy(new BigNumber(blockHeightBefore - tslaTakeLoanBlockHeight)) + const tslaLoanAmountBefore = new BigNumber(tslaLoanAmount).plus(tslaInterestAmountBefore.decimalPlaces(8, BigNumber.ROUND_CEIL)) + + const vaultBefore = await testing.rpc.loan.getVault(tslaVaultId) as VaultActive + expect(vaultBefore.loanAmounts).toStrictEqual([`${tslaLoanAmountBefore.toFixed(8)}@TSLA`]) + + const colScript = P2WPKH.fromAddress(RegTest, vaultOwnerAddress, P2WPKH).getScript() + const script = await testingProvider.elliptic.script() + const txn = await testingBuilder.loans.paybackLoan({ + vaultId: tslaVaultId, + from: colScript, + tokenAmounts: [{ token: 0, amount: new BigNumber(10000) }] + }, script) + const payBackPromise = sendTransaction(testing.container, txn) + + await expect(payBackPromise).rejects.toThrow('DeFiDRpcError: \'PaybackLoanTx: There is no loan on token (DUSD) in this vault! (code 16)\', code: -26') + }) }) diff --git a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_take_loan.test.ts b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_take_loan.test.ts index ccede90826..a7618a17f9 100644 --- a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_take_loan.test.ts +++ b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_take_loan.test.ts @@ -618,7 +618,7 @@ describe('loans.takeLoan failed', () => { const promise = sendTransaction(bob.container, txn) await expect(promise).rejects.toThrow(DeFiDRpcError) - await expect(promise).rejects.toThrow('At least 50% of the minimum required collateral must be in DFI when taking a loan.') + await expect(promise).rejects.toThrow('At least 50% of the minimum required collateral must be in DFI or DUSD when taking a loan.') { // revert DFI value changes diff --git a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_withdraw_from_vault.test.ts b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_withdraw_from_vault.test.ts index ad4a8270ac..9706012670 100644 --- a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_withdraw_from_vault.test.ts +++ b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_withdraw_from_vault.test.ts @@ -332,7 +332,7 @@ describe('loans.withdrawFromVault', () => { const promise = sendTransaction(tGroup.get(0).container, txn) await expect(promise).rejects.toThrow(DeFiDRpcError) - await expect(promise).rejects.toThrow('WithdrawFromVaultTx: At least 50% of the minimum required collateral must be in DFI (code 16)') + await expect(promise).rejects.toThrow('WithdrawFromVaultTx: At least 50% of the minimum required collateral must be in DFI or DUSD (code 16)') }) it('should not withdraw from liquidated vault', async () => { diff --git a/packages/jellyfish-transaction-builder/src/txn/txn_builder_loans.ts b/packages/jellyfish-transaction-builder/src/txn/txn_builder_loans.ts index 190ad5f1f3..96584adba9 100644 --- a/packages/jellyfish-transaction-builder/src/txn/txn_builder_loans.ts +++ b/packages/jellyfish-transaction-builder/src/txn/txn_builder_loans.ts @@ -13,6 +13,7 @@ import { CloseVault, TakeLoan, PaybackLoan, + PaybackLoanV2, PlaceAuctionBid } from '@defichain/jellyfish-transaction' import { P2WPKHTxnBuilder } from './txn_builder' @@ -210,6 +211,21 @@ export class TxnBuilderLoans extends P2WPKHTxnBuilder { ) } + /** + * PaybackLoanV2 transaction. + * + * @param {PaybackLoanV2} paybackLoanV2 txn to create + * @param {Script} changeScript to send unspent to after deducting the (converted + fees) + * @returns {Promise} + */ + + async paybackLoanV2 (paybackLoanV2: PaybackLoanV2, changeScript: Script): Promise { + return await super.createDeFiTx( + OP_CODES.OP_DEFI_TX_PAYBACK_LOAN_V2(paybackLoanV2), + changeScript + ) + } + /** * placeAuctionBid transaction. * diff --git a/packages/jellyfish-transaction/src/script/dftx/dftx.ts b/packages/jellyfish-transaction/src/script/dftx/dftx.ts index 41e9d8365b..1f5259ed21 100644 --- a/packages/jellyfish-transaction/src/script/dftx/dftx.ts +++ b/packages/jellyfish-transaction/src/script/dftx/dftx.ts @@ -101,6 +101,8 @@ import { TakeLoan, CPaybackLoan, PaybackLoan, + CPaybackLoanV2, + PaybackLoanV2, PlaceAuctionBid, CPlaceAuctionBid } from './dftx_loans' @@ -269,6 +271,8 @@ export class CDfTx extends ComposableBuffer> { return compose(CTakeLoan.OP_NAME, d => new CTakeLoan(d)) case CPaybackLoan.OP_CODE: return compose(CPaybackLoan.OP_NAME, d => new CPaybackLoan(d)) + case CPaybackLoanV2.OP_CODE: + return compose(CPaybackLoanV2.OP_NAME, d => new CPaybackLoanV2(d)) case CPlaceAuctionBid.OP_CODE: return compose(CPlaceAuctionBid.OP_NAME, d => new CPlaceAuctionBid(d)) default: diff --git a/packages/jellyfish-transaction/src/script/dftx/dftx_loans.ts b/packages/jellyfish-transaction/src/script/dftx/dftx_loans.ts index 8488e8eeff..9a5af16fda 100644 --- a/packages/jellyfish-transaction/src/script/dftx/dftx_loans.ts +++ b/packages/jellyfish-transaction/src/script/dftx/dftx_loans.ts @@ -323,6 +323,50 @@ export class CPaybackLoan extends ComposableBuffer { } } +export interface TokenPayback { + dToken: number // ---------------------| VarUInt{1-9 bytes} + amounts: TokenBalance[] // -------| c = VarUInt{1-9 bytes} + c x TokenBalance(4 bytes for token Id + 8 bytes for amount), Amount to pay loan +} + +/** + * Composable TokenPayback, C stands for Composable. + * Immutable by design, bi-directional fromBuffer, toBuffer deep composer. + */ +export class CTokenPayback extends ComposableBuffer { + composers (tp: TokenPayback): BufferComposer[] { + return [ + ComposableBuffer.varUInt(() => tp.dToken, v => tp.dToken = v), + ComposableBuffer.varUIntArray(() => tp.amounts, v => tp.amounts = v, v => new CTokenBalance(v)) + ] + } +} + +/** + * PaybackLoanV2 DeFi Transaction + */ +export interface PaybackLoanV2 { + vaultId: string // --------------------| 32 bytes, Vault Id + from: Script // -----------------------| n = VarUInt{1-9 bytes}, + n bytes, Address containing collateral + loans: TokenPayback[] // -------| c = VarUInt{1-9 bytes} + c x TokenBalance(4 bytes for token Id + 8 bytes for amount), Amount to pay loan +} + +/** + * Composable PaybackLoanV2, C stands for Composable. + * Immutable by design, bi-directional fromBuffer, toBuffer deep composer. + */ +export class CPaybackLoanV2 extends ComposableBuffer { + static OP_CODE = 0x6B // 'k' + static OP_NAME = 'OP_DEFI_TX_PAYBACK_LOAN_V2' + + composers (pl: PaybackLoanV2): BufferComposer[] { + return [ + ComposableBuffer.hexBEBufferLE(32, () => pl.vaultId, v => pl.vaultId = v), + ComposableBuffer.single