Skip to content

Commit

Permalink
Payback of DUSD loan with DFI (#1024)
Browse files Browse the repository at this point in the history
* Payback of DUSD loan with DFI (#1024)

Co-authored-by: jouzo <jdesclercs@gmail.com>
Co-authored-by: Peter Bushnell <bushsolo@gmail.com>
Co-authored-by: Prasanna Loganathar <pvl@prasannavl.com>
  • Loading branch information
4 people authored Jan 24, 2022
1 parent 4fefd57 commit f0f620e
Show file tree
Hide file tree
Showing 5 changed files with 476 additions and 53 deletions.
121 changes: 105 additions & 16 deletions src/masternodes/mn_checks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2825,9 +2825,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 @@ -2846,17 +2908,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 @@ -2875,24 +2939,49 @@ 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;

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

0 comments on commit f0f620e

Please sign in to comment.