diff --git a/src/amount.h b/src/amount.h index c69ed5f8f3..d00fd5a381 100644 --- a/src/amount.h +++ b/src/amount.h @@ -75,7 +75,8 @@ struct DCT_ID { } }; -static const CAmount COIN = 100000000; +static constexpr CAmount COIN = 100000000; +static constexpr CAmount CENT = 1000000; //Converts the given value to decimal format string with COIN precision. inline std::string GetDecimaleString(CAmount nValue) diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 1f17f56cd9..e3860bc4e2 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -349,7 +349,7 @@ const std::maptypeId); } break; + case TokenKeys::LoanCollateralFactor: + if (view.GetLastHeight() < Params().GetConsensus().FortCanningEpilogueHeight) { + const auto amount = std::get_if(&value); + if (amount && *amount > COIN) { + return Res::Err("Percentage exceeds 100%%"); + } + } + [[fallthrough]]; case TokenKeys::LoanMintingInterest: if (view.GetLastHeight() < Params().GetConsensus().FortCanningGreatWorldHeight) { const auto amount = std::get_if(&value); @@ -926,7 +934,6 @@ Res ATTRIBUTES::Validate(const CCustomCSView & view) const } [[fallthrough]]; case TokenKeys::LoanCollateralEnabled: - case TokenKeys::LoanCollateralFactor: case TokenKeys::LoanMintingEnabled: { if (view.GetLastHeight() < Params().GetConsensus().FortCanningCrunchHeight) { return Res::Err("Cannot be set before FortCanningCrunch"); @@ -1177,6 +1184,24 @@ Res ATTRIBUTES::Apply(CCustomCSView & mnview, const uint32_t height) mnview.IncreaseInterest(height, vaultId, vault->schemeId, {attrV0->typeId}, *tokenInterest, 0); } } + } else if (attrV0->key == TokenKeys::LoanCollateralFactor) { + if (height >= Params().GetConsensus().FortCanningEpilogueHeight) { + std::set ratio; + mnview.ForEachLoanScheme([&ratio](const std::string &identifier, const CLoanSchemeData &data) { + ratio.insert(data.ratio); + return true; + }); + if (ratio.empty()) { + return Res::Err("Set loan scheme before setting collateral factor."); + } + const auto factor = std::get_if(&attribute.second); + if (!factor) { + return Res::Err("Unexpected type"); + } + if (*factor >= *ratio.begin() * CENT) { + return Res::Err("Factor cannot be more than or equal to the lowest scheme rate of %d\n", GetDecimaleString(*ratio.begin() * CENT)); + } + } } } else if (attrV0->type == AttributeTypes::Param) { if (attrV0->typeId == ParamIDs::DFIP2203) { diff --git a/src/test/setup_common.h b/src/test/setup_common.h index 8513402a2f..4dab9ce4ee 100644 --- a/src/test/setup_common.h +++ b/src/test/setup_common.h @@ -48,8 +48,6 @@ static inline uint64_t InsecureRandBits(int bits) { return g_insecure_rand_ctx.r static inline uint64_t InsecureRandRange(uint64_t range) { return g_insecure_rand_ctx.randrange(range); } static inline bool InsecureRandBool() { return g_insecure_rand_ctx.randbool(); } -static constexpr CAmount CENT{1000000}; - struct TestMasternodeKeys { CKey ownerKey; CKey operatorKey; diff --git a/test/functional/feature_higher_collateral_factor.py b/test/functional/feature_higher_collateral_factor.py new file mode 100644 index 0000000000..f921a5bcb9 --- /dev/null +++ b/test/functional/feature_higher_collateral_factor.py @@ -0,0 +1,198 @@ +#!/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 DUSD collateral factor.""" + +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 DUSDCollateralFactorTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-bayfrontgardensheight=1', '-eunosheight=1', '-txindex=1', '-fortcanningheight=1', '-fortcanningroadheight=1', '-fortcanninghillheight=1', '-fortcanningcrunchheight=1', '-fortcanninggreatworldheight=1', '-fortcanningepilogueheight=200', '-jellyfish_regtest=1']] + + def run_test(self): + # Setup + self.setup() + + # Test setting of DUSD collateral factor + self.set_collateral_factor() + + # Test new factor when taking DUSD loan + self.take_dusd_loan() + + # Test with multiple loan tokens + self.take_multiple_loans() + + def setup(self): + # Generate chain + self.nodes[0].generate(120) + + # Get MN address + self.address = self.nodes[0].get_genesis_keys().ownerAuthAddress + + # Token symbols + self.symbolDFI = "DFI" + self.symbolDUSD = "DUSD" + self.symbolTSLA = "TSLA" + + # Create Oracle address + oracle_address = self.nodes[0].getnewaddress("", "legacy") + + # Define price feeds + price_feed = [ + {"currency": "USD", "token": f"{self.symbolDFI}"}, + {"currency": "USD", "token": f"{self.symbolTSLA}"}, + ] + + # Appoint Oracle + oracle = self.nodes[0].appointoracle(oracle_address, price_feed, 10) + self.nodes[0].generate(1) + + # Set Oracle prices + oracle_prices = [ + {"currency": "USD", "tokenAmount": f"1@{self.symbolDFI}"}, + {"currency": "USD", "tokenAmount": f"1@{self.symbolTSLA}"}, + ] + self.nodes[0].setoracledata(oracle, int(time.time()), oracle_prices) + self.nodes[0].generate(10) + + # Create loan tokens + self.nodes[0].setloantoken({ + 'symbol': self.symbolDUSD, + 'name': self.symbolDUSD, + 'fixedIntervalPriceId': f"{self.symbolDUSD}/USD", + 'mintable': True, + 'interest': -1 + }) + self.nodes[0].generate(1) + + self.nodes[0].setloantoken({ + 'symbol': self.symbolTSLA, + 'name': self.symbolTSLA, + 'fixedIntervalPriceId': f"{self.symbolTSLA}/USD", + 'mintable': True, + 'interest': -1 + }) + self.nodes[0].generate(1) + + # Set collateral tokens + self.nodes[0].setcollateraltoken({ + 'token': self.symbolDFI, + 'factor': 1, + 'fixedIntervalPriceId': f'{self.symbolDFI}/USD' + }) + self.nodes[0].generate(1) + + self.nodes[0].setcollateraltoken({ + 'token': self.symbolDUSD, + 'factor': 1, + 'fixedIntervalPriceId': f'{self.symbolDFI}/USD' + }) + self.nodes[0].generate(1) + + # Store DUSD ID + self.idDUSD = list(self.nodes[0].gettoken(self.symbolDUSD).keys())[0] + + # Mint DUSD + self.nodes[0].minttokens("100000@DUSD") + self.nodes[0].generate(1) + + # Create DFI tokens + self.nodes[0].utxostoaccount({self.address: "100000@" + self.symbolDFI}) + self.nodes[0].generate(1) + + def set_collateral_factor(self): + + # Test setting new token factor before fork + assert_raises_rpc_error(-32600, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_collateral_factor': '1.49'}}) + + # Move to fork + self.nodes[0].generate(200 - self.nodes[0].getblockcount()) + + # Test setting before scheme set + assert_raises_rpc_error(-32600, "Set loan scheme before setting collateral factor", self.nodes[0].setgov, {"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_collateral_factor': '1.50'}}) + + # Create loan scheme + self.nodes[0].createloanscheme(150, 1, 'LOAN001') + self.nodes[0].generate(1) + + # Test setting higher than the lowest scheme rate + assert_raises_rpc_error(-32600, "Factor cannot be more than or equal to the lowest scheme rate of 1.50000000", self.nodes[0].setgov, {"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_collateral_factor': '1.50'}}) + + # Now set new token factor + self.nodes[0].setgov({"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_collateral_factor': '1.49'}}) + self.nodes[0].generate(1) + + # Check results + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/token/1/loan_collateral_factor'], '1.49') + + def take_dusd_loan(self): + + # Create vault + vault_address = self.nodes[0].getnewaddress('', 'legacy') + vault_id = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + # Deposit DUSD and DFI to vault + self.nodes[0].deposittovault(vault_id, self.address, f"1.5@{self.symbolDFI}") + self.nodes[0].generate(1) + self.nodes[0].deposittovault(vault_id, self.address, f"1.5@{self.symbolDUSD}") + self.nodes[0].generate(1) + + # Take DUSD loan greater than collateral amount + self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": f"2.49@{self.symbolDUSD}"}) + self.nodes[0].generate(1) + + # Check that we are on 150% collateral ratio + vault = self.nodes[0].getvault(vault_id) + assert_equal(vault['collateralRatio'], 150) + assert_equal(vault['informativeRatio'], Decimal('150.00000000')) + assert_equal(vault['collateralValue'], Decimal('3.73500000')) + assert_equal(vault['loanValue'], Decimal('2.49000000')) + + def take_multiple_loans(self): + + # Create vault + vault_address = self.nodes[0].getnewaddress('', 'legacy') + vault_id = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + # Deposit DUSD and DFI to vault + self.nodes[0].deposittovault(vault_id, self.address, f"1.5@{self.symbolDFI}") + self.nodes[0].generate(1) + self.nodes[0].deposittovault(vault_id, self.address, f"1.5@{self.symbolDUSD}") + self.nodes[0].generate(1) + + # Take TSLA loan for half the available ratio + self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": f"1@{self.symbolTSLA}"}) + self.nodes[0].generate(1) + + # Check that we are on 300% collateral ratio + vault = self.nodes[0].getvault(vault_id) + assert_equal(vault['collateralRatio'], 374) + assert_equal(vault['informativeRatio'], Decimal('373.50000000')) + assert_equal(vault['collateralValue'], Decimal('3.73500000')) + assert_equal(vault['loanValue'], Decimal('1.00000000')) + + # Take a DUSD loan + self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": f"1@{self.symbolDUSD}"}) + self.nodes[0].generate(1) + + # Check that vault still has capacity to take more loans + vault = self.nodes[0].getvault(vault_id) + assert_equal(vault['loanValue'], Decimal('2.00000000')) + assert_equal(vault['collateralValue'], Decimal('3.73500000')) + assert_equal(vault['informativeRatio'], Decimal('186.75000000')) + assert_equal(vault['collateralRatio'], 187) + +if __name__ == '__main__': + DUSDCollateralFactorTest().main() + diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py index 8f21324461..58aa26b801 100755 --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -670,7 +670,7 @@ def run_test(self): assert_raises_rpc_error(-32600, "No such token (127)", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/127/loan_collateral_enabled':'true'}}) assert_raises_rpc_error(-32600, "Fixed interval price currency pair must be set first", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/loan_collateral_enabled':'true'}}) assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/loan_collateral_factor':'-1'}}) - assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/loan_collateral_factor':'1.00000001'}}) + assert_raises_rpc_error(-32600, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/loan_collateral_factor':'1.00000001'}}) assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/loan_collateral_factor':'not_a_number'}}) assert_raises_rpc_error(-32600, "No such token (127)", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/127/loan_collateral_factor':'1'}}) assert_raises_rpc_error(-32600, "Fixed interval price currency pair must be set first", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/loan_collateral_factor':'1'}}) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index a3a5015b28..177677a641 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -242,6 +242,7 @@ 'rpc_named_arguments.py', 'wallet_listsinceblock.py', 'p2p_leak.py', + 'feature_higher_collateral_factor.py', 'wallet_encryption.py', 'feature_dersig.py', 'feature_cltv.py',