Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Payback of DUSD loan with DFI #1024

Merged
merged 17 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/masternodes/masternodes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -920,13 +920,25 @@ CAmount CCollateralLoans::precisionRatio() const
return ratio > maxRatio / precision ? -COIN : CAmount(ratio * precision);
}

ResVal<CAmount> CCustomCSView::GetAmountInCurrency(CAmount amount, CTokenCurrencyPair priceFeedId, bool useNextPrice, bool requireLivePrice)
ResVal<CAmount> CCustomCSView::GetAmountInCurrency(CAmount amount, CTokenCurrencyPair priceFeedId, bool useNextPrice, bool requireLivePrice, bool reverseDirection)
{
auto priceResult = GetValidatedIntervalPrice(priceFeedId, useNextPrice, requireLivePrice);
if (!priceResult)
return std::move(priceResult);

auto price = priceResult.val.get();

if (reverseDirection)
{
if (price == 0)
return Res::Err("Price is zero (%s - %s/%s)", GetDecimaleString(price), priceFeedId.first, priceFeedId.second);
auto amountInCurrency = DivideAmounts(amount, price);
if (MultiplyAmounts(amountInCurrency, price) != amount)
amountInCurrency += 1;
Mixa84 marked this conversation as resolved.
Show resolved Hide resolved

return ResVal<CAmount>(amountInCurrency, Res::Ok());
}

auto amountInCurrency = MultiplyAmounts(price, amount);
if (price > COIN && amountInCurrency < amount)
return Res::Err("Value/price too high (%s/%s)", GetDecimaleString(amount), GetDecimaleString(price));
Expand All @@ -950,7 +962,7 @@ ResVal<CCollateralLoans> CCustomCSView::GetLoanCollaterals(CVaultId const& vault
if (!res)
return std::move(res);

LogPrint(BCLog::LOAN, "\t\t%s(): totalCollaterals - %lld, totalLoans - %lld, ratio - %d\n",
LogPrint(BCLog::LOAN, "\t\t%s(): totalCollaterals - %lld, totalLoans - %lld, ratio - %d\n",
__func__, result.totalCollaterals, result.totalLoans, result.ratio());

return ResVal<CCollateralLoans>(result, Res::Ok());
Expand Down
2 changes: 1 addition & 1 deletion src/masternodes/masternodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ class CCustomCSView

bool CalculateOwnerRewards(CScript const & owner, uint32_t height);

ResVal<CAmount> GetAmountInCurrency(CAmount amount, CTokenCurrencyPair priceFeedId, bool useNextPrice = false, bool requireLivePrice = true);
ResVal<CAmount> GetAmountInCurrency(CAmount amount, CTokenCurrencyPair priceFeedId, bool useNextPrice = false, bool requireLivePrice = true, bool reverseDirection = false);

ResVal<CCollateralLoans> GetLoanCollaterals(CVaultId const & vaultId, CBalances const & collaterals, uint32_t height, int64_t blockTime, bool useNextPrice = false, bool requireLivePrice = true);

Expand Down
65 changes: 46 additions & 19 deletions src/masternodes/mn_checks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2645,6 +2645,14 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor
for (const auto& kv : obj.amounts.balances)
{
DCT_ID tokenId = kv.first;
auto paybackAmount = kv.second;

if (height >= Params().GetConsensus().FortCanningHillHeight && kv.first == DCT_ID{0})
{
tokenId = mnview.GetToken("DUSD")->first;
paybackAmount = mnview.GetAmountInCurrency(paybackAmount, {"DFI","USD"});
Mixa84 marked this conversation as resolved.
Show resolved Hide resolved
}

auto loanToken = mnview.GetLoanTokenByID(tokenId);
if (!loanToken)
return Res::Err("Loan token with id (%s) does not exist!", tokenId.ToString());
Expand All @@ -2663,17 +2671,17 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor

LogPrint(BCLog::LOAN,"CLoanPaybackMessage()->%s->", loanToken->symbol); /* Continued */
auto subInterest = TotalInterest(*rate, height);
auto subLoan = kv.second - subInterest;
auto subLoan = paybackAmount - subInterest;

if (kv.second < subInterest)
if (paybackAmount < subInterest)
{
subInterest = kv.second;
subInterest = paybackAmount;
subLoan = 0;
}
else if (it->second - subLoan < 0)
subLoan = it->second;

res = mnview.SubLoanToken(obj.vaultId, CTokenAmount{kv.first, subLoan});
res = mnview.SubLoanToken(obj.vaultId, CTokenAmount{tokenId, subLoan});
if (!res)
return res;

Expand All @@ -2692,21 +2700,40 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor
return Res::Err("Cannot payback this amount of loan for %s, either payback full amount or less than this amount!", loanToken->symbol);
}

res = mnview.SubMintedTokens(loanToken->creationTx, subLoan);
if (!res)
return res;
if (height < Params().GetConsensus().FortCanningHillHeight || kv.first != DCT_ID{0})
{
res = mnview.SubMintedTokens(loanToken->creationTx, subLoan);
if (!res)
return res;
}

CalculateOwnerRewards(obj.from);
// subtract loan amount first, interest is burning below
res = mnview.SubBalance(obj.from, CTokenAmount{kv.first, subLoan});
if (!res)
return res;

// burn interest Token->USD->DFI->burnAddress
// sub amount from balance and burn interest Token->USD->DFI->burnAddress
if (subInterest)
{
LogPrint(BCLog::LOAN, "CLoanTakeLoanMessage(): Swapping %s interest to DFI - %lld, height - %d\n", loanToken->symbol, subInterest, height);
res = SwapToDFIOverUSD(mnview, kv.first, subInterest, obj.from, consensus.burnAddress, height);
if (height < Params().GetConsensus().FortCanningHillHeight || kv.first != DCT_ID{0})
{
// subtract loan amount first, interest is burning below
res = mnview.SubBalance(obj.from, CTokenAmount{tokenId, subLoan});
if (!res)
return res;

LogPrint(BCLog::LOAN, "CLoanTakeLoanMessage(): Swapping %s interest to DFI - %lld, height - %d\n", loanToken->symbol, subInterest, height);
res = SwapToDFIOverUSD(mnview, tokenId, subInterest, obj.from, consensus.burnAddress, height);
}
else
{
CAmount subLoanInDFI = mnview.GetAmountInCurrency(subLoan, {"DFI","USD"}, false, true, true);
Mixa84 marked this conversation as resolved.
Show resolved Hide resolved
res = TransferTokenBalance(DCT_ID{0}, subLoanInDFI, obj.from, consensus.burnAddress);
Mixa84 marked this conversation as resolved.
Show resolved Hide resolved
if (!res)
return res;

LogPrint(BCLog::LOAN, "CLoanTakeLoanMessage(): Burning interest in DFI directly - %lld, height - %d\n", subInterest, height);
CAmount subInterestInDFI = mnview.GetAmountInCurrency(subInterest, {"DFI","USD"}, false, true, true);
Mixa84 marked this conversation as resolved.
Show resolved Hide resolved
res = TransferTokenBalance(DCT_ID{0}, subInterestInDFI, obj.from, consensus.burnAddress);
}

if (!res)
return res;
}
Expand Down Expand Up @@ -3150,7 +3177,7 @@ void PopulateVaultHistoryData(CHistoryWriters* writers, CAccountsHistoryWriter&
bool IsDisabledTx(uint32_t height, CustomTxType type, const Consensus::Params& consensus) {
if (height < consensus.FortCanningParkHeight)
return false;

// ICXCreateOrder = '1',
// ICXMakeOffer = '2',
// ICXSubmitDFCHTLC = '3',
Expand Down Expand Up @@ -3182,7 +3209,7 @@ Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTr


const auto metadataValidation = height >= consensus.FortCanningHeight;

auto txType = GuessCustomTxType(tx, metadata, metadataValidation);
if (txType == CustomTxType::None) {
return res;
Expand All @@ -3191,7 +3218,7 @@ Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTr
if (IsDisabledTx(height, txType, consensus)) {
return Res::ErrCode(CustomTxErrCodes::Fatal, "Disabled custom transaction");
}

if (metadataValidation && txType == CustomTxType::Reject) {
return Res::ErrCode(CustomTxErrCodes::Fatal, "Invalid custom transaction");
}
Expand Down Expand Up @@ -3507,7 +3534,7 @@ std::vector<std::vector<DCT_ID>> CPoolSwap::CalculatePoolPaths(CCustomCSView& vi

// Note: `testOnly` doesn't update views, and as such can result in a previous price calculations
// for a pool, if used multiple times (or duplicated pool IDs) with the same view.
// testOnly is only meant for one-off tests per well defined view.
// testOnly is only meant for one-off tests per well defined view.
Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector<DCT_ID> poolIDs, bool testOnly) {

CTokenAmount swapAmountResult{{},0};
Expand Down Expand Up @@ -3577,7 +3604,7 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector<DCT_ID> poolIDs, boo

// If we're just testing, don't do any balance transfers.
// Just go over pools and return result. The only way this can
// cause inaccurate result is if we go over the same path twice,
// cause inaccurate result is if we go over the same path twice,
// which shouldn't happen in the first place.
if (testOnly)
return Res::Ok();
Expand Down
180 changes: 180 additions & 0 deletions test/functional/feature_loan_payback_dfi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2019 The Bitcoin Core developers
# Copyright (c) DeFi Blockchain Developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""Test Loan - payback loan."""

from test_framework.test_framework import DefiTestFramework
from test_framework.util import assert_equal, assert_raises_rpc_error

import calendar
import time
from decimal import Decimal


class PaybackLoanTest (DefiTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
self.extra_args = [
['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=1',
'-fortcanningheight=50', '-fortcanninghillheight=50', '-eunosheight=50', '-txindex=1']
]

def run_test(self):
self.nodes[0].generate(150)

account0 = self.nodes[0].get_genesis_keys().ownerAuthAddress

symbolDFI = "DFI"
symbolBTC = "BTC"
symboldUSD = "DUSD"
symbolTSLA = "TSLA"

self.nodes[0].createtoken({
"symbol": symbolBTC,
"name": "BTC token",
"isDAT": True,
"collateralAddress": account0
})

self.nodes[0].generate(1)

idDFI = list(self.nodes[0].gettoken(symbolDFI).keys())[0]
idBTC = list(self.nodes[0].gettoken(symbolBTC).keys())[0]

self.nodes[0].utxostoaccount({account0: "1000@" + symbolDFI})

oracle_address1 = self.nodes[0].getnewaddress("", "legacy")
price_feeds1 = [
{"currency": "USD", "token": "DFI"},
{"currency": "USD", "token": "BTC"},
{"currency": "USD", "token": "TSLA"}
]
oracle_id1 = self.nodes[0].appointoracle(
oracle_address1, price_feeds1, 10)
self.nodes[0].generate(1)

# feed oracle
oracle1_prices = [
{"currency": "USD", "tokenAmount": "10@TSLA"},
{"currency": "USD", "tokenAmount": "10@DFI"},
{"currency": "USD", "tokenAmount": "10@BTC"}
]
timestamp = calendar.timegm(time.gmtime())
self.nodes[0].setoracledata(oracle_id1, timestamp, oracle1_prices)
self.nodes[0].generate(1)

self.nodes[0].setcollateraltoken({
'token': idDFI,
'factor': 1,
'fixedIntervalPriceId': "DFI/USD"
})

self.nodes[0].setcollateraltoken({
'token': idBTC,
'factor': 1,
'fixedIntervalPriceId': "BTC/USD"})

self.nodes[0].generate(1)

self.nodes[0].setloantoken({
'symbol': symbolTSLA,
'name': "Tesla stock token",
'fixedIntervalPriceId': "TSLA/USD",
'mintable': True,
'interest': 1
})

self.nodes[0].setloantoken({
'symbol': symboldUSD,
'name': "DUSD stable token",
'fixedIntervalPriceId': "DUSD/USD",
'mintable': True,
'interest': 1
})
self.nodes[0].generate(1)

self.nodes[0].createloanscheme(150, 5, 'LOAN150')

self.nodes[0].generate(5)

iddUSD = list(self.nodes[0].gettoken(symboldUSD).keys())[0]

vaultId = self.nodes[0].createvault(account0, 'LOAN150')
self.nodes[0].generate(1)

self.nodes[0].deposittovault(vaultId, account0, "400@DFI")
self.nodes[0].generate(1)

self.nodes[0].takeloan({
'vaultId': vaultId,
'amounts': "2000@" + symboldUSD
})
self.nodes[0].generate(1)

poolOwner = self.nodes[0].getnewaddress("", "legacy")
# create pool DUSD-DFI
self.nodes[0].createpoolpair({
"tokenA": iddUSD,
"tokenB": idDFI,
"commission": Decimal('0.002'),
"status": True,
"ownerAddress": poolOwner,
"pairSymbol": "DUSD-DFI",
})
self.nodes[0].generate(1)

self.nodes[0].addpoolliquidity(
{account0: ["30@" + symbolDFI, "300@" + symboldUSD]}, account0)
self.nodes[0].generate(1)

# Should not be able to payback loan with BTC
assert_raises_rpc_error(-32600, "Loan token with id (1) does not exist!", self.nodes[0].paybackloan, {
'vaultId': vaultId,
'from': account0,
'amounts': "1@BTC"
})

vaultBefore = self.nodes[0].getvault(vaultId)
[amountBefore, _] = vaultBefore['loanAmounts'][0].split('@')

# Partial loan payback in DFI
self.nodes[0].paybackloan({
'vaultId': vaultId,
'from': account0,
'amounts': "1@DFI"
})
self.nodes[0].generate(1)

vaultAfter = self.nodes[0].getvault(vaultId)
[amountAfter, _] = vaultAfter['loanAmounts'][0].split('@')
[interestAfter, _] = vaultAfter['interestAmounts'][0].split('@')

assert_equal(float(amountAfter) -
float(interestAfter), float(amountBefore) - 10)

# Payback of loan token other than DUSD
vaultId2 = self.nodes[0].createvault(account0, 'LOAN150')
self.nodes[0].generate(1)

self.nodes[0].deposittovault(vaultId2, account0, "100@DFI")
self.nodes[0].generate(1)

self.nodes[0].takeloan({
'vaultId': vaultId2,
'amounts': "10@" + symbolTSLA
})
self.nodes[0].generate(1)

# Should not be able to payback loan token other than DUSD with DFI
assert_raises_rpc_error(-32600, "There is no loan on token (DUSD) in this vault!", self.nodes[0].paybackloan, {
'vaultId': vaultId2,
'from': account0,
'amounts': "10@DFI"
})


if __name__ == '__main__':
PaybackLoanTest().main()
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
'feature_loan_setcollateraltoken.py',
'feature_loan_setloantoken.py',
'feature_loan_basics.py',
'feature_loan_payback_dfi.py',
'feature_loan_listauctions.py',
'feature_loan_auctions.py',
'feature_any_accounts_to_accounts.py',
Expand Down