From 63452763fef6d4dc7adf9df8e371def8ecd80caf Mon Sep 17 00:00:00 2001 From: Jouzo Date: Fri, 18 Mar 2022 05:43:24 +0100 Subject: [PATCH] DUSD contributes to the 50% minimum required collateral (#1128) * Add fortcanningroad height * DUSD contributes to the 50% minimum required collateral * Set regtest FortCanningRoadHeight * Adds test for DUSD collateral factor * Fix lint * Simpler test for dUSD collateral factor Co-authored-by: Prasanna Loganathar --- src/chainparams.cpp | 5 + src/consensus/params.h | 1 + src/init.cpp | 1 + src/masternodes/mn_checks.cpp | 36 ++++-- src/rpc/blockchain.cpp | 1 + .../feature_loan_dusd_as_collateral.py | 113 ++++++++++++++---- test/functional/rpc_blockchain.py | 1 + 7 files changed, 124 insertions(+), 34 deletions(-) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index a1df7978682..eecba05df5f 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -129,6 +129,7 @@ class CMainParams : public CChainParams { consensus.FortCanningMuseumHeight = 1430640; consensus.FortCanningParkHeight = 1503143; consensus.FortCanningHillHeight = 1604999; // Feb 7, 2022. + consensus.FortCanningRoadHeight = std::numeric_limits::max(); consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -355,6 +356,7 @@ class CTestNetParams : public CChainParams { consensus.FortCanningMuseumHeight = 724000; consensus.FortCanningParkHeight = 828800; consensus.FortCanningHillHeight = 828900; + consensus.FortCanningRoadHeight = std::numeric_limits::max(); consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -542,6 +544,7 @@ class CDevNetParams : public CChainParams { consensus.FortCanningMuseumHeight = std::numeric_limits::max(); consensus.FortCanningParkHeight = std::numeric_limits::max(); consensus.FortCanningHillHeight = std::numeric_limits::max(); + consensus.FortCanningRoadHeight = std::numeric_limits::max(); consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); consensus.pos.nTargetTimespan = 5 * 60; // 5 min == 10 blocks @@ -721,6 +724,7 @@ class CRegTestParams : public CChainParams { consensus.FortCanningMuseumHeight = 10000000; consensus.FortCanningParkHeight = 10000000; consensus.FortCanningHillHeight = 10000000; + consensus.FortCanningRoadHeight = 10000000; consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -939,6 +943,7 @@ void CRegTestParams::UpdateActivationParametersFromArgs() UpdateHeightValidation("Fort Canning Museum", "-fortcanningmuseumheight", consensus.FortCanningMuseumHeight); UpdateHeightValidation("Fort Canning Park", "-fortcanningparkheight", consensus.FortCanningParkHeight); UpdateHeightValidation("Fort Canning Hill", "-fortcanninghillheight", consensus.FortCanningHillHeight); + UpdateHeightValidation("Fort Canning Road", "-fortcanningroadheight", consensus.FortCanningRoadHeight); if (gArgs.GetBoolArg("-simulatemainnet", false)) { consensus.pos.nTargetTimespan = 5 * 60; // 5 min == 10 blocks diff --git a/src/consensus/params.h b/src/consensus/params.h index be296123fcc..2d047d9bcd3 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -94,6 +94,7 @@ struct Params { int FortCanningMuseumHeight; int FortCanningParkHeight; int FortCanningHillHeight; + int FortCanningRoadHeight; /** Foundation share after AMK, normalized to COIN = 100% */ CAmount foundationShareDFIP1; diff --git a/src/init.cpp b/src/init.cpp index b99fc9f63c4..2f775dc02d4 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -475,6 +475,7 @@ void SetupServerArgs() gArgs.AddArg("-fortcanningmuseumheight", "Fort Canning Museum fork activation height (regtest only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); gArgs.AddArg("-fortcanningparkheight", "Fort Canning Park fork activation height (regtest only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); gArgs.AddArg("-fortcanninghillheight", "Fort Canning Hill fork activation height (regtest only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-fortcanningroadheight", "Fort Canning Road fork activation height (regtest only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); gArgs.AddArg("-jellyfish_regtest", "Configure the regtest network for jellyfish testing", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS); gArgs.AddArg("-simulatemainnet", "Configure the regtest network to mainnet target timespan and spacing ", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS); #ifdef USE_UPNP diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 6550caac5da..6dc9ec5f5af 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -2634,6 +2634,10 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { if (auto collaterals = mnview.GetVaultCollaterals(obj.vaultId)) { + boost::optional>> tokenDUSD; + if (static_cast(height) >= consensus.FortCanningRoadHeight) { + tokenDUSD = mnview.GetToken("DUSD"); + } const auto scheme = mnview.GetLoanScheme(vault->schemeId); for (int i = 0; i < 2; i++) { // check collaterals for active and next price @@ -2643,17 +2647,19 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!collateralsLoans) return std::move(collateralsLoans); - uint64_t totalDFI = 0; + uint64_t totalCollaterals = 0; for (auto& col : collateralsLoans.val->collaterals) - if (col.nTokenId == DCT_ID{0}) - totalDFI += col.nValue; + if (col.nTokenId == DCT_ID{0} + || (tokenDUSD && col.nTokenId == tokenDUSD->first)) + totalCollaterals += col.nValue; if (static_cast(height) < consensus.FortCanningHillHeight) { - if (totalDFI < collateralsLoans.val->totalCollaterals / 2) + if (totalCollaterals < collateralsLoans.val->totalCollaterals / 2) return Res::Err("At least 50%% of the collateral must be in DFI"); } else { - if (arith_uint256(totalDFI) * 100 < arith_uint256(collateralsLoans.val->totalLoans) * scheme->ratio / 2) - return Res::Err("At least 50%% of the minimum required collateral must be in DFI"); + if (arith_uint256(totalCollaterals) * 100 < arith_uint256(collateralsLoans.val->totalLoans) * scheme->ratio / 2) + return static_cast(height) < consensus.FortCanningRoadHeight ? Res::Err("At least 50%% of the minimum required collateral must be in DFI") + : Res::Err("At least 50%% of the minimum required collateral must be in DFI or DUSD"); } if (collateralsLoans.val->ratio() < scheme->ratio) @@ -2745,6 +2751,10 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return res; } + boost::optional>> tokenDUSD; + if (static_cast(height) >= consensus.FortCanningRoadHeight) { + tokenDUSD = mnview.GetToken("DUSD"); + } auto scheme = mnview.GetLoanScheme(vault->schemeId); for (int i = 0; i < 2; i++) { // check ratio against current and active price @@ -2754,17 +2764,19 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!collateralsLoans) return std::move(collateralsLoans); - uint64_t totalDFI = 0; + uint64_t totalCollaterals = 0; for (auto& col : collateralsLoans.val->collaterals) - if (col.nTokenId == DCT_ID{0}) - totalDFI += col.nValue; + if (col.nTokenId == DCT_ID{0} + || (tokenDUSD && col.nTokenId == tokenDUSD->first)) + totalCollaterals += col.nValue; if (static_cast(height) < consensus.FortCanningHillHeight) { - if (totalDFI < collateralsLoans.val->totalCollaterals / 2) + if (totalCollaterals < collateralsLoans.val->totalCollaterals / 2) return Res::Err("At least 50%% of the collateral must be in DFI when taking a loan."); } else { - if (arith_uint256(totalDFI) * 100 < arith_uint256(collateralsLoans.val->totalLoans) * scheme->ratio / 2) - return Res::Err("At least 50%% of the minimum required collateral must be in DFI when taking a loan."); + if (arith_uint256(totalCollaterals) * 100 < arith_uint256(collateralsLoans.val->totalLoans) * scheme->ratio / 2) + return static_cast(height) < consensus.FortCanningRoadHeight ? Res::Err("At least 50%% of the minimum required collateral must be in DFI when taking a loan.") + : Res::Err("At least 50%% of the minimum required collateral must be in DFI or DUSD when taking a loan."); } if (collateralsLoans.val->ratio() < scheme->ratio) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index e533e1a23ec..1b63fb36816 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1335,6 +1335,7 @@ UniValue getblockchaininfo(const JSONRPCRequest& request) BuriedForkDescPushBack(softforks, "fortcanningmuseum", consensusParams.FortCanningMuseumHeight); BuriedForkDescPushBack(softforks, "fortcanningpark", consensusParams.FortCanningParkHeight); BuriedForkDescPushBack(softforks, "fortcanninghill", consensusParams.FortCanningHillHeight); + BuriedForkDescPushBack(softforks, "fortcanningroad", consensusParams.FortCanningRoadHeight); BIP9SoftForkDescPushBack(softforks, "testdummy", consensusParams, Consensus::DEPLOYMENT_TESTDUMMY); obj.pushKV("softforks", softforks); diff --git a/test/functional/feature_loan_dusd_as_collateral.py b/test/functional/feature_loan_dusd_as_collateral.py index 18d0775b295..08732d76070 100755 --- a/test/functional/feature_loan_dusd_as_collateral.py +++ b/test/functional/feature_loan_dusd_as_collateral.py @@ -6,6 +6,7 @@ """Test Loan - DUSD as collateral.""" from test_framework.test_framework import DefiTestFramework +from test_framework.authproxy import JSONRPCException from test_framework.util import assert_equal, assert_raises_rpc_error from decimal import Decimal @@ -16,15 +17,16 @@ 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', '-fortcanningheight=1', '-fortcanninghillheight=200', '-jellyfish_regtest=1']] + ['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-eunosheight=1', '-fortcanningheight=1', '-fortcanninghillheight=200', '-fortcanningroadheight=215', '-jellyfish_regtest=1']] def run_test(self): self.nodes[0].generate(120) mn_address = self.nodes[0].get_genesis_keys().ownerAuthAddress - symbol_dfi = "DFI" - symbol_dusd = "DUSD" + symbolDFI = "DFI" + symbolBTC = "BTC" + symbolDUSD = "DUSD" self.nodes[0].setloantoken({ 'symbol': "DUSD", @@ -35,20 +37,36 @@ def run_test(self): }) self.nodes[0].generate(1) - id_usd = list(self.nodes[0].gettoken(symbol_dusd).keys())[0] + self.nodes[0].createtoken({ + "symbol": symbolBTC, + "name": "BTC token", + "isDAT": True, + "collateralAddress": mn_address + }) + self.nodes[0].generate(1) + + idDUSD = list(self.nodes[0].gettoken(symbolDUSD).keys())[0] # Mint DUSD self.nodes[0].minttokens("100000@DUSD") + self.nodes[0].minttokens("100000@BTC") self.nodes[0].generate(1) # Create DFI tokens - self.nodes[0].utxostoaccount({mn_address: "100000@" + symbol_dfi}) + self.nodes[0].utxostoaccount({mn_address: "100000@" + symbolDFI}) self.nodes[0].generate(1) # Create pool pair self.nodes[0].createpoolpair({ - "tokenA": symbol_dfi, - "tokenB": symbol_dusd, + "tokenA": symbolDFI, + "tokenB": symbolDUSD, + "commission": 0, + "status": True, + "ownerAddress": mn_address + }) + self.nodes[0].createpoolpair({ + "tokenA": symbolDFI, + "tokenB": symbolBTC, "commission": 0, "status": True, "ownerAddress": mn_address @@ -58,15 +76,21 @@ def run_test(self): # Add pool liquidity self.nodes[0].addpoolliquidity({ mn_address: [ - '10000@' + symbol_dfi, - '8000@' + symbol_dusd] + '10000@' + symbolDFI, + '8000@' + symbolDUSD] + }, mn_address) + self.nodes[0].addpoolliquidity({ + mn_address: [ + '10000@' + symbolDFI, + '8000@' + symbolBTC] }, mn_address) self.nodes[0].generate(1) # Set up Oracles oracle_address = self.nodes[0].getnewaddress("", "legacy") price_feed = [ - {"currency": "USD", "token": "DFI"} + {"currency": "USD", "token": "DFI"}, + {"currency": "USD", "token": "BTC"} ] oracle = self.nodes[0].appointoracle(oracle_address, price_feed, 10) @@ -74,21 +98,28 @@ def run_test(self): oracle_prices = [ {"currency": "USD", "tokenAmount": "1@DFI"}, + {"currency": "USD", "tokenAmount": "1@BTC"}, ] self.nodes[0].setoracledata(oracle, int(time.time()), oracle_prices) self.nodes[0].generate(1) # Set collateral tokens self.nodes[0].setcollateraltoken({ - 'token': symbol_dfi, + 'token': symbolDFI, 'factor': 1, 'fixedIntervalPriceId': "DFI/USD" }) + self.nodes[0].setcollateraltoken({ + 'token': symbolBTC, + 'factor': 1, + 'fixedIntervalPriceId': "BTC/USD" + }) + token_factor_dusd = 0.99 activate = self.nodes[0].getblockcount() + 50 self.nodes[0].setcollateraltoken({ - 'token': symbol_dusd, + 'token': symbolDUSD, 'factor': token_factor_dusd, 'fixedIntervalPriceId': "DUSD/USD", 'activateAfterBlock': activate @@ -107,19 +138,20 @@ def run_test(self): # 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].accounttoaccount(mn_address, {vault_address: str(collateral) + "@" + symbolDUSD}) + self.nodes[0].accounttoaccount(mn_address, {vault_address: str(collateral) + "@" + symbolDFI}) + self.nodes[0].accounttoaccount(mn_address, {vault_address: str(collateral) + "@" + symbolBTC}) 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) + assert_raises_rpc_error(-32600, "Collateral token with id (1) does not exist!", self.nodes[0].deposittovault, vault_id, vault_address, str(collateral) + "@" + symbolDUSD) # Activates DUSD as collateral token self.nodes[0].generate(activate - self.nodes[0].getblockcount()) # 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].deposittovault(vault_id, vault_address, str(collateral) + "@" + symbolDUSD) + self.nodes[0].deposittovault(vault_id, vault_address, str(collateral) + "@" + symbolDFI) self.nodes[0].generate(1) vault = self.nodes[0].getvault(vault_id) @@ -130,11 +162,11 @@ def run_test(self): self.nodes[0].generate(200 - self.nodes[0].getblockcount()) # Enable loan payback - self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + id_usd + '/payback_dfi':'true'}}) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + idDUSD + '/payback_dfi':'true'}}) self.nodes[0].generate(1) # Take DUSD loan - self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": str(loan_dusd) + "@" + symbol_dusd }) + self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": str(loan_dusd) + "@" + symbolDUSD }) self.nodes[0].generate(1) # Loan value loan amount + interest @@ -144,10 +176,10 @@ def run_test(self): # Swap DUSD from loan to DFI self.nodes[0].poolswap({ "from": vault_address, - "tokenFrom": symbol_dusd, + "tokenFrom": symbolDUSD, "amountFrom": loan_dusd, "to": vault_address, - "tokenTo": symbol_dfi + "tokenTo": symbolDFI }) self.nodes[0].generate(1) @@ -156,12 +188,49 @@ def run_test(self): self.nodes[0].paybackloan({ 'vaultId': vault_id, 'from': vault_address, - 'amounts': [dfi_balance + '@' + symbol_dfi]}) + 'amounts': [dfi_balance + '@' + symbolDFI]}) 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')) + assert_equal(vault['collateralAmounts'], ['2000.00000000@DFI', '2000.00000000@DUSD']) + + # Withdraw DFI and use DUSD as sole collateral + self.nodes[0].withdrawfromvault(vault_id, vault_address, '2000.00000000@DFI') + self.nodes[0].generate(1) + + # Try to take DUSD loan with DUSD as sole collateral + try: + self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": str(loan_dusd) + "@" + symbolDUSD }) + except JSONRPCException as e: + errorString = e.error['message'] + assert("At least 50% of the minimum required collateral must be in DFI when taking a loan." in errorString) + + self.nodes[0].generate(215 - self.nodes[0].getblockcount()) # move to fortcanningroad height + + # Take DUSD loan with DUSD as sole collateral + self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": str(loan_dusd) + "@" + symbolDUSD }) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vault_id) + assert_equal(vault['collateralAmounts'], ['2000.00000000@DUSD']) + + # Try to take DUSD loan with DUSD less than 50% of total collateralized loan value + # This tests for collateral factor + assert_raises_rpc_error(-32600, "Vault does not have enough collateralization ratio defined by loan scheme - 149 < 150", self.nodes[0].takeloan, { "vaultId": vault_id, "amounts": "333@" + symbolDUSD }) + + # Set DUSD collateral factor back to 1 + self.nodes[0].setcollateraltoken({ + 'token': symbolDUSD, + 'factor': 1, + 'fixedIntervalPriceId': "DUSD/USD" + }) + self.nodes[0].generate(10) + + self.nodes[0].takeloan({ "vaultId": vault_id, "amounts": "333@" + symbolDUSD }) + self.nodes[0].generate(1) + if __name__ == '__main__': LoanDUSDCollateralTest().main() diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 440f3bcddcb..cb037f3d3fa 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -135,6 +135,7 @@ def _test_getblockchaininfo(self): 'fortcanningmuseum': {'type': 'buried', 'active': False, 'height': 10000000}, 'fortcanningpark': {'type': 'buried', 'active': False, 'height': 10000000}, 'fortcanninghill': {'type': 'buried', 'active': False, 'height': 10000000}, + 'fortcanningroad': {'type': 'buried', 'active': False, 'height': 10000000}, 'testdummy': { 'type': 'bip9', 'bip9': {