Skip to content

Commit

Permalink
Adds estimatecollateral RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
Jouzo committed Nov 23, 2021
1 parent c7c15ea commit 5bd2031
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 0 deletions.
100 changes: 100 additions & 0 deletions src/masternodes/rpc_vault.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,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& [tokenId, tokenAmount] : loanAmounts.balances) {
auto loanToken = pcustomcsview->GetLoanTokenByID(tokenId);
if (!loanToken) {
throw JSONRPCError(RPC_DATABASE_ERROR, strprintf("(%s) is not a loan token!", tokenId));
}

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", tokenId.v));
}
totalLoanValue += MultiplyAmounts(tokenAmount, price);
}

uint32_t height = ::ChainActive().Height();
CBalances collateralBalances;
CAmount totalSplit{0};
for (const auto& [tokenId, splitValue] : collateralSplits) {
CAmount split = AmountFromValue(splitValue);

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

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

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", tokenId));
}

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 = %d vs expected %d", totalSplit, COIN));
}

return AmountsToJSON(collateralBalances.balances);
}

static const CRPCCommand commands[] =
{
// category name actor (function) params
Expand All @@ -1160,6 +1259,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 @@ -260,6 +260,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" 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 = 80000000 vs expected 100000000" 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 @@ -256,6 +256,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

0 comments on commit 5bd2031

Please sign in to comment.