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 14 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
128 changes: 109 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,71 @@ 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 != nullptr && *active) {
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 penalty
CAmount penalty{99000000}; // Update from Gov var
CDataStructureV0 penaltyKey{AttributeTypes::Token, tokenDUSD->first.v, TokenKeys::PaybackDFIFeePCT};
try {
const auto& value = attrs.at(penaltyKey);
auto valueV0 = boost::get<const CValueV0>(&value);
if (valueV0) {
if (auto storedPenalty = boost::get<const CAmount>(valueV0)) {
penalty = COIN - *storedPenalty;
}
}
} catch (const std::out_of_range&) {}
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 +2758,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 +2781,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
138 changes: 101 additions & 37 deletions test/functional/feature_loan_dusd_as_collateral.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,23 @@
from test_framework.test_framework import DefiTestFramework

from test_framework.util import assert_equal, assert_raises_rpc_error
from decimal import Decimal
import time

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

def run_test(self):
assert_equal(len(self.nodes[0].listtokens()), 1) # only one token == DFI
print("Generating initial chain...")
self.nodes[0].generate(120)

symbolDFI = "DFI"
symbolDUSD = "DUSD"
mn_address = self.nodes[0].get_genesis_keys().ownerAuthAddress

symbol_dfi = "DFI"
symbol_dusd = "DUSD"

self.nodes[0].setloantoken({
'symbol': "DUSD",
Expand All @@ -34,70 +35,133 @@ def run_test(self):
})
self.nodes[0].generate(1)

oracleAddress = self.nodes[0].getnewaddress("", "legacy")
price_feeds1 = [
id_usd = list(self.nodes[0].gettoken(symbol_dusd).keys())[0]

# Mint DUSD
self.nodes[0].minttokens("100000@DUSD")
self.nodes[0].generate(1)

# Create DFI tokens
self.nodes[0].utxostoaccount({mn_address: "100000@" + symbol_dfi})
self.nodes[0].generate(1)

# Create pool pair
self.nodes[0].createpoolpair({
"tokenA": symbol_dfi,
"tokenB": symbol_dusd,
"commission": 0,
"status": True,
"ownerAddress": mn_address
})
self.nodes[0].generate(1)

# Add pool liquidity
self.nodes[0].addpoolliquidity({
mn_address: [
'10000@' + symbol_dfi,
'8000@' + symbol_dusd]
}, mn_address)
self.nodes[0].generate(1)

# Set up Oracles
oracle_address = self.nodes[0].getnewaddress("", "legacy")
price_feed = [
{"currency": "USD", "token": "DFI"}
]
oracle_id1 = self.nodes[0].appointoracle(oracleAddress, price_feeds1, 10)

oracle = self.nodes[0].appointoracle(oracle_address, price_feed, 10)
self.nodes[0].generate(1)

oracle1_prices = [
oracle_prices = [
{"currency": "USD", "tokenAmount": "1@DFI"},
]
mock_time = int(time.time())
self.nodes[0].setmocktime(mock_time)
self.nodes[0].setoracledata(oracle_id1, mock_time, oracle1_prices)

self.nodes[0].setoracledata(oracle, int(time.time()), oracle_prices)
self.nodes[0].generate(1)

# Set collateral tokens
self.nodes[0].setcollateraltoken({
'token': symbolDFI,
'token': symbol_dfi,
'factor': 1,
'fixedIntervalPriceId': "DFI/USD"
})

tokenFactorDUSD = 0.9
activateAfterBlock = self.nodes[0].getblockcount() + 50
token_factor_dusd = 0.99
activate = self.nodes[0].getblockcount() + 50
self.nodes[0].setcollateraltoken({
'token': "DUSD",
'factor': tokenFactorDUSD,
'token': symbol_dusd,
'factor': token_factor_dusd,
'fixedIntervalPriceId': "DUSD/USD",
'activateAfterBlock': activateAfterBlock
'activateAfterBlock': activate
})
self.nodes[0].generate(1)

self.nodes[0].createloanscheme(200, 1, 'LOAN001')
# Create loan scheme
self.nodes[0].createloanscheme(150, 1, 'LOAN001')
self.nodes[0].generate(1)

vaultAddress = self.nodes[0].getnewaddress('', 'legacy')
vaultId = self.nodes[0].createvault(vaultAddress, 'LOAN001')
# Create vault
vault_address = self.nodes[0].getnewaddress('', 'legacy')
vault_id = self.nodes[0].createvault(vault_address, 'LOAN001')
self.nodes[0].generate(1)

amountDFI = 500
amountDUSD = 250

self.nodes[0].utxostoaccount({vaultAddress: str(amountDFI) + "@" + symbolDFI})
# Fund vault address with DUSD and DFI
collateral = 2000
loan_dusd = 1000
self.nodes[0].accounttoaccount(mn_address, {vault_address: str(collateral) + "@" + symbol_dusd})
self.nodes[0].accounttoaccount(mn_address, {vault_address: str(collateral) + "@" + symbol_dfi})
self.nodes[0].generate(1)

self.nodes[0].deposittovault(vaultId, vaultAddress, str(amountDFI) + "@" + symbolDFI)
self.nodes[0].generate(1)
# DUSD is not active as a collateral token yet
assert_raises_rpc_error(-32600, "Collateral token with id (1) does not exist!", self.nodes[0].deposittovault, vault_id, vault_address, str(collateral) + "@" + symbol_dusd)

# Activates DUSD as collateral token
self.nodes[0].generate(activate - self.nodes[0].getblockcount())

self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(amountDUSD) + "@" + symbolDUSD })
# Deposit DUSD and DFI to vault
self.nodes[0].deposittovault(vault_id, vault_address, str(collateral) + "@" + symbol_dusd)
self.nodes[0].deposittovault(vault_id, vault_address, str(collateral) + "@" + symbol_dfi)
self.nodes[0].generate(1)

# DUSD is not active as a collateral token yet
assert_raises_rpc_error(-32600, "Collateral token with id (1) does not exist!", self.nodes[0].deposittovault, vaultId, vaultAddress, str(amountDUSD) + "@" + symbolDUSD)
vault = self.nodes[0].getvault(vault_id)
assert("DUSD" in vault['collateralAmounts'][1])
assert_equal(vault['collateralValue'], collateral * token_factor_dusd + collateral)

self.nodes[0].generate(50) # Activates DUSD as collateral token
# Move to FortCanningHill fork
self.nodes[0].generate(200 - self.nodes[0].getblockcount())

# Should be able to deposit DUSD to vault
self.nodes[0].deposittovault(vaultId, vaultAddress, str(amountDUSD) + "@" + symbolDUSD)
# Enable loan payback
self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + id_usd + '/payback_dfi':'true'}})
self.nodes[0].generate(1)

vault = self.nodes[0].getvault(vaultId)
# Take DUSD loan
self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": str(loan_dusd) + "@" + symbol_dusd })
self.nodes[0].generate(1)

assert("DUSD" in vault['collateralAmounts'][1])
assert_equal(vault['collateralValue'], amountDUSD * tokenFactorDUSD + amountDFI)
# Loan value loan amount + interest
vault = self.nodes[0].getvault(vault_id)
assert_equal(vault['loanValue'], Decimal(loan_dusd) + vault['interestValue'])

# Swap DUSD from loan to DFI
self.nodes[0].poolswap({
"from": vault_address,
"tokenFrom": symbol_dusd,
"amountFrom": loan_dusd,
"to": vault_address,
"tokenTo": symbol_dfi
})
self.nodes[0].generate(1)

# Payback loan with DFI
[dfi_balance, _] = self.nodes[0].getaccount(vault_address)[0].split('@')
self.nodes[0].paybackloan({
'vaultId': vault_id,
'from': vault_address,
'amounts': [dfi_balance + '@' + symbol_dfi]})
self.nodes[0].generate(1)

# Loan should be paid back in full
vault = self.nodes[0].getvault(vault_id)
assert_equal(vault['loanValue'], Decimal('0'))

if __name__ == '__main__':
LoanDUSDCollateralTest().main()
Loading