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 9 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
2 changes: 1 addition & 1 deletion src/masternodes/masternodes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,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
3 changes: 2 additions & 1 deletion src/masternodes/masternodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,6 @@ class CCustomCSView
private:
Res PopulateLoansData(CCollateralLoans& result, CVaultId const& vaultId, uint32_t height, int64_t blockTime, bool useNextPrice, bool requireLivePrice);
Res PopulateCollateralData(CCollateralLoans& result, CVaultId const& vaultId, CBalances const& collaterals, uint32_t height, int64_t blockTime, bool useNextPrice, bool requireLivePrice);
ResVal<CAmount> GetValidatedIntervalPrice(CTokenCurrencyPair priceFeedId, bool useNextPrice, bool requireLivePrice);

public:
// Increase version when underlaying tables are changed
Expand Down Expand Up @@ -431,6 +430,8 @@ class CCustomCSView

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

ResVal<CAmount> GetValidatedIntervalPrice(CTokenCurrencyPair priceFeedId, bool useNextPrice, bool requireLivePrice);

void SetDbVersion(int version);

int GetDbVersion() const;
Expand Down
118 changes: 99 additions & 19 deletions src/masternodes/mn_checks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <masternodes/accountshistory.h>
#include <masternodes/anchors.h>
#include <masternodes/balances.h>
#include <masternodes/govvariables/attributes.h>
#include <masternodes/mn_checks.h>
#include <masternodes/oracles.h>
#include <masternodes/res.h>
Expand Down Expand Up @@ -2674,9 +2675,61 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor
if (!IsVaultPriceValid(mnview, obj.vaultId, height))
return Res::Err("Cannot payback loan while any of the asset's price is invalid");

auto allowDFIPayback = false;
std::map<CAttributeType, CAttributeValue> attrs;
auto tokenDUSD = mnview.GetToken("DUSD");
if (tokenDUSD) {
const auto pAttributes = mnview.GetAttributes();
if (pAttributes) {
attrs = pAttributes->attributes;
CDataStructureV0 activeKey{AttributeTypes::Token, tokenDUSD->first.v, TokenKeys::PaybackDFI};
try {
const auto& value = attrs.at(activeKey);
auto valueV0 = boost::get<const CValueV0>(&value);
if (valueV0) {
const auto active = boost::get<const bool>(valueV0);
if (active || *active) {
Bushstar marked this conversation as resolved.
Show resolved Hide resolved
allowDFIPayback = true;
}
}
} catch (const std::out_of_range&) {}
}
}

for (const auto& kv : obj.amounts.balances)
{
DCT_ID tokenId = kv.first;
auto paybackAmount = kv.second;
CAmount dfiUSDPrice{0};

if (height >= Params().GetConsensus().FortCanningHillHeight && kv.first == DCT_ID{0})
{
if (!allowDFIPayback || !tokenDUSD) {
return Res::Err("Payback of DUSD loans with DFI not currently active");
}

// Get DFI price in USD
const std::pair<std::string, std::string> dfiUsd{"DFI","USD"};
bool useNextPrice{false}, requireLivePrice{true};
const auto resVal = mnview.GetValidatedIntervalPrice(dfiUsd, useNextPrice, requireLivePrice);
if (!resVal) {
return std::move(resVal);
}

// Apply 1% penalty
CAmount penalty{99000000}; // Update from Gov var
dfiUSDPrice = MultiplyAmounts(*resVal.val, penalty);

// Set tokenId to DUSD
tokenId = tokenDUSD->first;

// Calculate the DFI amount in DUSD
paybackAmount = MultiplyAmounts(dfiUSDPrice, kv.second);
if (dfiUSDPrice > COIN && paybackAmount < kv.second) {
return Res::Err("Value/price too high (%s/%s)", GetDecimaleString(kv.second), GetDecimaleString(dfiUSDPrice));
}
}

auto loanToken = mnview.GetLoanTokenByID(tokenId);
if (!loanToken)
return Res::Err("Loan token with id (%s) does not exist!", tokenId.ToString());
Expand All @@ -2695,17 +2748,19 @@ 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 @@ -2716,32 +2771,57 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor

if (static_cast<int>(height) >= consensus.FortCanningMuseumHeight && subLoan < it->second)
{
auto newRate = mnview.GetInterestRate(obj.vaultId, tokenId);
if (!newRate)
rate = mnview.GetInterestRate(obj.vaultId, tokenId);
if (!rate)
return Res::Err("Cannot get interest rate for this token (%s)!", loanToken->symbol);

if (newRate->interestPerBlock == 0)
if (rate->interestPerBlock == 0)
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;

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

// subtract loan amount first, interest is burning below
LogPrint(BCLog::LOAN, "CLoanTakeLoanMessage(): Sub loan from balance - %lld, height - %d\n", subLoan, height);
res = mnview.SubBalance(obj.from, CTokenAmount{tokenId, subLoan});
if (!res)
return res;

// 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, tokenId, subInterest, obj.from, consensus.burnAddress, height);
}
}
else
{
CAmount subInDFI;
// if DFI payback overpay loan and interest amount
if (paybackAmount > subLoan + subInterest)
{
subInDFI = DivideAmounts(subLoan + subInterest, dfiUSDPrice);
if (MultiplyAmounts(subInDFI, dfiUSDPrice) != subLoan + subInterest) {
subInDFI += 1;
}
}
else
{
subInDFI = kv.second;
}

LogPrint(BCLog::LOAN, "CLoanTakeLoanMessage(): Burning interest and loan in DFI directly - %lld (%lld DFI), height - %d\n", subLoan + subInterest, subInDFI, height);
res = TransferTokenBalance(DCT_ID{0}, subInDFI, obj.from, consensus.burnAddress);
}

if (!res)
return res;
}

return Res::Ok();
Expand Down
191 changes: 191 additions & 0 deletions test/functional/feature_loan_payback_dfi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#!/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"
})

# Should not be able to payback loan before DFI payback enabled
assert_raises_rpc_error(-32600, "Payback of DUSD loans with DFI not currently active", self.nodes[0].paybackloan, {
'vaultId': vaultId,
'from': account0,
'amounts': "1@DFI"
})

# Enable loan payback
self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + iddUSD + '/payback_dfi':'true'}})
self.nodes[0].generate(1)

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)
print(vaultAfter)
[amountAfter, _] = vaultAfter['loanAmounts'][0].split('@')
[interestAfter, _] = vaultAfter['interestAmounts'][0].split('@')

assert_equal(Decimal(amountAfter) - Decimal(interestAfter), (Decimal(amountBefore) - (10 * Decimal('0.99'))))

# 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 @@ -162,6 +162,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
Loading