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

Adds estimatecollateral RPC #927

Merged
merged 9 commits into from
Dec 2, 2021
100 changes: 100 additions & 0 deletions src/masternodes/rpc_vault.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,105 @@ UniValue listauctionhistory(const JSONRPCRequest& request) {
return ret;
}

UniValue estimatecollateral(const JSONRPCRequest& request) {
auto pwallet = GetWallet(request);

RPCHelpMan{"estimatecollateral",
"Returns amount of collateral tokens needed to take an amount of loan tokens for a target collateral ratio.\n",
{
{"loanAmounts", RPCArg::Type::STR, RPCArg::Optional::NO,
"Amount as json string, or array. Example: '[ \"amount@token\" ]'"
},
{"targetRatio", RPCArg::Type::NUM, RPCArg::Optional::NO, "Target collateral ratio."},
{"tokens", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "Object with collateral token as key and their percent split as value. (defaults to { DFI: 1 }",
{
{"split", RPCArg::Type::NUM, RPCArg::Optional::NO, "The percent split"},
},
},
},
RPCResult{
"\"json\" (Array) Array of <amount@token> strings\n"
},
RPCExamples{
HelpExampleCli("estimatecollateral", R"(23.55311144@MSFT 150 '{"DFI": 0.8, "BTC":0.2}')") +
HelpExampleRpc("estimatecollateral", R"("23.55311144@MSFT" 150 {"DFI": 0.8, "BTC":0.2})")
},
}.Check(request);

RPCTypeCheck(request.params, {UniValueType(), UniValue::VNUM, UniValue::VOBJ}, false);

const CBalances loanAmounts = DecodeAmounts(pwallet->chain(), request.params[0], "");
auto ratio = request.params[1].get_int();

std::map<std::string, UniValue> collateralSplits;
if (request.params.size() > 2) {
request.params[2].getObjMap(collateralSplits);
} else {
collateralSplits["DFI"] = 1;
}

LOCK(cs_main);

CAmount totalLoanValue{0};
for (const auto& balance : loanAmounts.balances) {
auto loanToken = pcustomcsview->GetLoanTokenByID(balance.first);
if (!loanToken) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%d) is not a loan token!", balance.first.v));
}

auto priceFeed = pcustomcsview->GetFixedIntervalPrice(loanToken->fixedIntervalPriceId);
if (!priceFeed.ok) {
throw JSONRPCError(RPC_DATABASE_ERROR, priceFeed.msg);
}

auto price = priceFeed.val->priceRecord[0];
if (!priceFeed.val->isLive(pcustomcsview->GetPriceDeviation())) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", loanToken->symbol));
}
totalLoanValue += MultiplyAmounts(balance.second, price);
}

uint32_t height = ::ChainActive().Height();
CBalances collateralBalances;
CAmount totalSplit{0};
for (const auto& collateralSplit : collateralSplits) {
CAmount split = AmountFromValue(collateralSplit.second);

auto token = pcustomcsview->GetToken(collateralSplit.first);
if (!token) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("Token %s does not exist!", collateralSplit.first));
}

auto collateralToken = pcustomcsview->HasLoanCollateralToken({token->first, height});
if (!collateralToken || !collateralToken->factor) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a valid collateral!", collateralSplit.first));
}

auto priceFeed = pcustomcsview->GetFixedIntervalPrice(collateralToken->fixedIntervalPriceId);
if (!priceFeed.ok) {
throw JSONRPCError(RPC_DATABASE_ERROR, priceFeed.msg);
}

auto price = priceFeed.val->priceRecord[0];
if (!priceFeed.val->isLive(pcustomcsview->GetPriceDeviation())) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No live fixed price for %s", collateralSplit.first));
}

auto requiredValue = MultiplyAmounts(totalLoanValue, split);
auto collateralValue = DivideAmounts(requiredValue, price);
auto amountRatio = DivideAmounts(MultiplyAmounts(collateralValue, ratio), 100);
auto totalAmount = DivideAmounts(amountRatio, collateralToken->factor);

collateralBalances.Add({token->first, totalAmount});
totalSplit += split;
}
if (totalSplit != COIN) {
throw JSONRPCError(RPC_MISC_ERROR, strprintf("total split between collateral tokens = %s vs expected %s", GetDecimaleString(totalSplit), GetDecimaleString(COIN)));
}

return AmountsToJSON(collateralBalances.balances);
}

UniValue vaultToJSON(const uint256& vaultID, const std::string& address, const uint64_t blockHeight, const std::string& type,
const uint64_t txn, const std::string& txid, const TAmounts& amounts) {
UniValue obj(UniValue::VOBJ);
Expand Down Expand Up @@ -1506,6 +1605,7 @@ static const CRPCCommand commands[] =
{"vault", "placeauctionbid", &placeauctionbid, {"id", "index", "from", "amount", "inputs"}},
{"vault", "listauctions", &listauctions, {"pagination"}},
{"vault", "listauctionhistory", &listauctionhistory, {"owner", "pagination"}},
{"vault", "estimatecollateral", &estimatecollateral, {"loanAmounts", "targetRatio", "tokens"}},
};

void RegisterVaultRPCCommands(CRPCTable& tableRPC) {
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "listvaults", 1, "pagination" },
{ "listauctions", 0, "pagination" },
{ "listauctionhistory", 1, "pagination" },
{ "estimatecollateral", 1, "targetRatio" },
{ "estimatecollateral", 2, "tokens" },

{ "spv_sendrawtx", 0, "rawtx" },
{ "spv_createanchor", 0, "inputs" },
Expand Down
204 changes: 204 additions & 0 deletions test/functional/feature_loan_estimatecollateral.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/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 - estimatecollateral."""

from test_framework.test_framework import DefiTestFramework

from test_framework.authproxy import JSONRPCException
from test_framework.util import assert_equal
import time

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

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

self.nodes[0].createtoken({
"symbol": "BTC",
"name": "BTC token",
"isDAT": True,
"collateralAddress": self.nodes[0].get_genesis_keys().ownerAuthAddress
})
self.nodes[0].generate(1)

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

self.nodes[0].minttokens("100@" + symbolBTC)
self.nodes[0].generate(1)

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

self.nodes[0].utxostoaccount({account: "500@" + symbolDFI})
self.nodes[0].generate(1)

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

oracle1_prices = [
{"currency": "USD", "tokenAmount": "1@DFI"},
{"currency": "USD", "tokenAmount": "100@BTC"},
{"currency": "USD", "tokenAmount": "5@TSLA"},
{"currency": "USD", "tokenAmount": "10@TWTR"},
]
mock_time = int(time.time())
self.nodes[0].setmocktime(mock_time)
self.nodes[0].setoracledata(oracle_id1, mock_time, 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': 0.8,
'fixedIntervalPriceId': "BTC/USD"})

self.nodes[0].generate(7)

loanSchemeRatio = 200
self.nodes[0].createloanscheme(loanSchemeRatio, 1, 'LOAN0001')
self.nodes[0].generate(1)

ownerAddress1 = self.nodes[0].getnewaddress('', 'legacy')
vaultId1 = self.nodes[0].createvault(ownerAddress1) # default loan scheme
self.nodes[0].generate(1)

self.nodes[0].setloantoken({
'symbol': "TSLA",
'name': "Tesla Token",
'fixedIntervalPriceId': "TSLA/USD",
'mintable': True,
'interest': 0.01})
self.nodes[0].setloantoken({
'symbol': "TWTR",
'name': "Twitter Token",
'fixedIntervalPriceId': "TWTR/USD",
'mintable': True,
'interest': 0.01})
self.nodes[0].generate(1)

# Token that does not exists
try:
self.nodes[0].estimatecollateral("10@TSLAA", 200)
except JSONRPCException as e:
errorString = e.error['message']
assert("Invalid Defi token: TSLAA" in errorString)
# Token not set as loan token
try:
self.nodes[0].estimatecollateral("10@DFI", 200)
except JSONRPCException as e:
errorString = e.error['message']
assert("not a loan token!" in errorString)
# Token without live price
try:
self.nodes[0].estimatecollateral("10@TSLA", 200)
except JSONRPCException as e:
errorString = e.error['message']
assert("No live fixed price for TSLA" in errorString)

oracle1_prices = [
{"currency": "USD", "tokenAmount": "1@DFI"},
{"currency": "USD", "tokenAmount": "100@BTC"},
{"currency": "USD", "tokenAmount": "5@TSLA"},
{"currency": "USD", "tokenAmount": "10@TWTR"},
]
mock_time = int(time.time())
self.nodes[0].setmocktime(mock_time)
self.nodes[0].setoracledata(oracle_id1, mock_time, oracle1_prices)

self.nodes[0].generate(8) # activate prices

# Negative split value
try:
self.nodes[0].estimatecollateral("10@TSLA", 200, {"DFI": -1})
except JSONRPCException as e:
errorString = e.error['message']
assert("Amount out of range" in errorString)
# Token not set as collateral
try:
self.nodes[0].estimatecollateral("10@TSLA", 200, {"TSLA": 1})
except JSONRPCException as e:
errorString = e.error['message']
assert("(TSLA) is not a valid collateral!" in errorString)
# Total split should be equal to 1
try:
self.nodes[0].estimatecollateral("10@TSLA", 200, {"DFI": 0.8})
except JSONRPCException as e:
errorString = e.error['message']
assert("total split between collateral tokens = 0.80000000 vs expected 1.00000000" in errorString)

estimatecollateral = self.nodes[0].estimatecollateral("10@TSLA", 200)

self.nodes[0].deposittovault(vaultId1, account, estimatecollateral[0])
self.nodes[0].generate(1)
# Cannot take more loan than estimated
try:
self.nodes[0].takeloan({ "vaultId": vaultId1, "amounts": "10.1@TSLA" })
except JSONRPCException as e:
errorString = e.error['message']
assert("Vault does not have enough collateralization ratio" in errorString)

self.nodes[0].takeloan({ "vaultId": vaultId1, "amounts": "10@TSLA" }) # should be able to take loan amount from estimatecollateral
self.nodes[0].generate(1)

vault1 = self.nodes[0].getvault(vaultId1)
assert_equal(vault1["collateralRatio"], 200) # vault collateral ratio should be equal to estimatecollateral targetRatio

vaultId2 = self.nodes[0].createvault(ownerAddress1)
estimatecollateral = self.nodes[0].estimatecollateral("10@TSLA", 200, {"BTC":0.5, "DFI": 0.5})

amountDFI = next(x for x in estimatecollateral if "DFI" in x)
amountBTC = next(x for x in estimatecollateral if "BTC" in x)
self.nodes[0].generate(1)
self.nodes[0].deposittovault(vaultId2, account, amountDFI)
self.nodes[0].generate(1)
self.nodes[0].deposittovault(vaultId2, account, amountBTC)
self.nodes[0].generate(1)

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

vault2 = self.nodes[0].getvault(vaultId2)
assert_equal(vault2["collateralRatio"], 200)

vaultId3 = self.nodes[0].createvault(ownerAddress1)
estimatecollateral = self.nodes[0].estimatecollateral(["10@TSLA", "10@TWTR"], 200, {"BTC":0.5, "DFI": 0.5})

amountDFI = next(x for x in estimatecollateral if "DFI" in x)
amountBTC = next(x for x in estimatecollateral if "BTC" in x)
self.nodes[0].generate(1)
self.nodes[0].deposittovault(vaultId3, account, amountDFI)
self.nodes[0].generate(1)
self.nodes[0].deposittovault(vaultId3, account, amountBTC)
self.nodes[0].generate(1)

self.nodes[0].takeloan({ "vaultId": vaultId3, "amounts": ["10@TSLA", "10@TWTR"] })
self.nodes[0].generate(1)

vault3 = self.nodes[0].getvault(vaultId3)
assert_equal(vault3["collateralRatio"], 200)

if __name__ == '__main__':
EstimateCollateralTest().main()
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@
'feature_loan_priceupdate.py',
'feature_loan_vaultstate.py',
'feature_loan.py',
'feature_loan_estimatecollateral.py',
'p2p_node_network_limited.py',
'p2p_permissions.py',
'feature_blocksdir.py',
Expand Down