From 1c6c5ed71ced18a0ebe58b383f99a3d6dcd1b032 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Mon, 16 Sep 2024 02:19:52 +0100 Subject: [PATCH 01/12] Restart: Check collateral valid before usage (#3047) * Restart: Check collateral valid before usage * Add log for no collaterals --- src/dfi/validation.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/dfi/validation.cpp b/src/dfi/validation.cpp index 897f26712e..8b8a33a8f5 100644 --- a/src/dfi/validation.cpp +++ b/src/dfi/validation.cpp @@ -2887,8 +2887,8 @@ static Res PaybackWithSwappedCollateral(const DCT_ID &collId, std::vector collToLoans; // collect all loanValues (in USD) of vaults which contain this collateral cache.ForEachLoanTokenAmount([&](const CVaultId &vaultId, const CBalances &balances) { - auto colls = cache.GetVaultCollaterals(vaultId); - if (colls->balances.count(collId)) { + const auto colls = cache.GetVaultCollaterals(vaultId); + if (colls && colls->balances.count(collId)) { collToLoans.emplace_back(CollToLoan{vaultId, {}, 0}); collToLoans.back().useableCollateralAmount = colls->balances.at(collId); for (const auto &[tokenId, amount] : balances.balances) { @@ -3304,6 +3304,9 @@ static Res ForceCloseAllLoans(const CBlockIndex *pindex, CCustomCSView &cache, B std::set allUsedCollaterals; cache.ForEachLoanTokenAmount([&](const CVaultId &vaultId, const CBalances &balances) { auto colls = cache.GetVaultCollaterals(vaultId); + if (!colls) { + return true; + } for (const auto &[collId, collAmount] : colls->balances) { allUsedCollaterals.insert(collId); } @@ -3389,9 +3392,10 @@ static Res ForceCloseAllLoans(const CBlockIndex *pindex, CCustomCSView &cache, B if (!gotLoan) { return true; } - auto colls = cache.GetVaultCollaterals(vaultId); - for (const auto &[collId, collAmount] : colls->balances) { - allUsedCollaterals.insert(collId); + if (const auto colls = cache.GetVaultCollaterals(vaultId)) { + for (const auto &[collId, collAmount] : colls->balances) { + allUsedCollaterals.insert(collId); + } } return true; }); @@ -3416,10 +3420,13 @@ static Res ForceCloseAllLoans(const CBlockIndex *pindex, CCustomCSView &cache, B for (const auto &loan : loanAmounts->balances) { LogPrintf(" %s@%d\n", GetDecimalString(loan.second), loan.first.v); } - const auto collAmounts = cache.GetVaultCollaterals(vaultId); - LogPrintf("%d collaterals: \n", collAmounts->balances.size()); - for (const auto &loan : collAmounts->balances) { - LogPrintf(" %s@%d\n", GetDecimalString(loan.second), loan.first.v); + if (const auto collAmounts = cache.GetVaultCollaterals(vaultId)) { + LogPrintf("%d collaterals: \n", collAmounts->balances.size()); + for (const auto &loan : collAmounts->balances) { + LogPrintf(" %s@%d\n", GetDecimalString(loan.second), loan.first.v); + } + } else { + LogPrintf("no collaterals: vault %s\n", vaultId.ToString()); } } return true; From 36123ceb6bc40709e4fe4886846e5caa3f9845ca Mon Sep 17 00:00:00 2001 From: kuegi <29012906+kuegi@users.noreply.github.com> Date: Mon, 16 Sep 2024 08:26:18 +0200 Subject: [PATCH 02/12] add history entries for loan payback during dToken restart (#3042) * added vaulthistory entries for dToken restart * added history entry for payback with funds during token restart --- src/dfi/validation.cpp | 62 +++++++- test/functional/feature_restartdtokens.py | 170 ++++++++++++++++++++-- 2 files changed, 217 insertions(+), 15 deletions(-) diff --git a/src/dfi/validation.cpp b/src/dfi/validation.cpp index 8b8a33a8f5..3b53bb62e8 100644 --- a/src/dfi/validation.cpp +++ b/src/dfi/validation.cpp @@ -2059,14 +2059,14 @@ static Res VaultSplits(CCustomCSView &view, return res; } - // FIXME: make this clear to be a collateral change if (const auto vault = view.GetVault(vaultId)) { - VaultHistoryKey subKey{static_cast(height), vaultId, GetNextAccPosition(), vault->ownerAddress}; + // no address -> vault collateral + VaultHistoryKey subKey{static_cast(height), vaultId, GetNextAccPosition(), {}}; VaultHistoryValue subValue{ uint256{}, static_cast(CustomTxType::TokenSplit), {{oldTokenId, -amount}}}; view.GetHistoryWriters().WriteVaultHistory(subKey, subValue); - VaultHistoryKey addKey{static_cast(height), vaultId, GetNextAccPosition(), vault->ownerAddress}; + VaultHistoryKey addKey{static_cast(height), vaultId, GetNextAccPosition(), {}}; VaultHistoryValue addValue{ uint256{}, static_cast(CustomTxType::TokenSplit), {{newTokenId, newAmount}}}; view.GetHistoryWriters().WriteVaultHistory(addKey, addValue); @@ -2790,10 +2790,33 @@ static Res PaybackLoanWithTokenOrDUSDCollateral( // subtract loan amount first, interest is burning below if (!useDUSDCollateral) { - res = mnview.SubBalance(owner, CTokenAmount{loanTokenId, subLoan}); + CAccountsHistoryWriter subView(mnview, + height, + GetNextAccPosition(), + {}, // TODO: use blockHash? + uint8_t(CustomTxType::PaybackLoan)); + res = subView.SubBalance(owner, CTokenAmount{loanTokenId, subLoan}); + subView.Flush(); + + VaultHistoryKey subKey{static_cast(height), vaultId, GetNextAccPosition(), owner}; + VaultHistoryValue subValue{ + uint256{}, static_cast(CustomTxType::PaybackLoan), {{loanTokenId, -subLoan}}}; + mnview.GetHistoryWriters().WriteVaultHistory(subKey, subValue); } else { // paying back DUSD loan with DUSD collateral -> no need to multiply res = mnview.SubVaultCollateral(vaultId, CTokenAmount{dusdToken->first, subLoan}); + + // remove collateral + VaultHistoryKey subCollKey{static_cast(height), vaultId, GetNextAccPosition(), {}}; + VaultHistoryValue subCollValue{ + uint256{}, static_cast(CustomTxType::PaybackWithCollateral), {{loanTokenId, -subLoan}}}; + mnview.GetHistoryWriters().WriteVaultHistory(subCollKey, subCollValue); + + // reduce loan (address = reduced loan?) + VaultHistoryKey subLoanKey{static_cast(height), vaultId, GetNextAccPosition(), owner}; + VaultHistoryValue subLoanValue{ + uint256{}, static_cast(CustomTxType::PaybackWithCollateral), {{loanTokenId, -subLoan}}}; + mnview.GetHistoryWriters().WriteVaultHistory(subLoanKey, subLoanValue); } if (!res) { return res; @@ -2853,6 +2876,19 @@ static Res PaybackLoanWithTokenOrDUSDCollateral( if (!res) { return res; } + + // history + // remove collateral + VaultHistoryKey subCollKey{static_cast(height), vaultId, GetNextAccPosition(), {}}; + VaultHistoryValue subCollValue{ + uint256{}, static_cast(CustomTxType::PaybackWithCollateral), {{dusdToken->first, -subInDUSD}}}; + mnview.GetHistoryWriters().WriteVaultHistory(subCollKey, subCollValue); + + // reduce loan (address = reduced loan?) + VaultHistoryKey subLoanKey{static_cast(height), vaultId, GetNextAccPosition(), owner}; + VaultHistoryValue subLoanValue{ + uint256{}, static_cast(CustomTxType::PaybackWithCollateral), {{loanTokenId, -subLoan}}}; + mnview.GetHistoryWriters().WriteVaultHistory(subLoanKey, subLoanValue); } mnview.Flush(); @@ -3132,6 +3168,17 @@ static Res PaybackWithSwappedCollateral(const DCT_ID &collId, dUsdToken.first, MultiplyDivideAmounts(availableDUSD.nValue, data.usedCollateralAmount, totalCollToSwap)}; cache.AddVaultCollateral(data.vaultId, dusdResult); cache.SubBalance(contractAddressValue, dusdResult); + + // history entry + VaultHistoryKey subCollKey{blockCtx.GetHeight(), data.vaultId, GetNextAccPosition(), {}}; + VaultHistoryValue subCollValue{ + uint256{}, + static_cast(CustomTxType::TokenLock), + {{collId, -data.usedCollateralAmount}, {dUsdToken.first, dusdResult.nValue}} + }; + cache.GetHistoryWriters().WriteVaultHistory(subCollKey, subCollValue); + // --- + if (data.usedCollateralAmount < data.useableCollateralAmount && dusdResult.nValue < data.totalUSDNeeded) { LogPrintf( "didn't use full collateral, but did not get all needed USD. usedColl: %s, availableColl: %s, " @@ -3798,6 +3845,13 @@ static Res LockTokensOfBalancesCollAndPools(const CBlock &block, if (!res) { return false; } + + // history entry + VaultHistoryKey subCollKey{static_cast(pindex->nHeight), vaultId, GetNextAccPosition(), {}}; + VaultHistoryValue subCollValue{ + uint256{}, static_cast(CustomTxType::TokenLock), {{tokenId, -amountToLock}}}; + cache.GetHistoryWriters().WriteVaultHistory(subCollKey, subCollValue); + // --- } } return true; diff --git a/test/functional/feature_restartdtokens.py b/test/functional/feature_restartdtokens.py index 7bbb5346e3..fbc7fdf23b 100755 --- a/test/functional/feature_restartdtokens.py +++ b/test/functional/feature_restartdtokens.py @@ -27,6 +27,7 @@ def set_test_params(self): self.extra_args = [ DefiTestFramework.fork_params_till(21) + [ + "-vaultindex=1", "-txnotokens=0", "-subsidytest=1", "-metachainheight=105", @@ -81,8 +82,6 @@ def check_full_test(self): self.check_token_lock() - # TODO: check history entries - self.check_upgrade_fail() self.check_td() @@ -1337,6 +1336,154 @@ def check_token_lock(self): ], ) + # Check split history + # vaults + assert_equal( + [ + { + "ad": entry["address"], + "h": entry["blockHeight"], + "t": entry["type"], + "a": entry["amounts"], + } + for entry in self.nodes[0].listvaulthistory( + self.vault_id2_1, + {"maxBlockHeight": self.nodes[0].getblockcount(), "depth": 1}, + ) + ], + [ + { + "ad": "vaultCollateral", + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-9.99825838@DUSD/v1"], + }, + { + "ad": self.address2, + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-9.99825838@DUSD/v1"], + }, + { + "ad": "vaultCollateral", + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-10.00000900@DUSD/v1"], + }, + { + "ad": self.address2, + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-0.10000000@SPY/v1"], + }, + { + "ad": "vaultCollateral", + "h": 1000, + "t": "TokenSplit", + "a": ["-20.00173262@DUSD/v1"], + }, + { + "ad": "vaultCollateral", + "h": 1000, + "t": "TokenSplit", + "a": ["20.00173262@DUSD"], + }, + { + "ad": "vaultCollateral", + "h": 1000, + "t": "TokenLock", + "a": ["-18.00155936@DUSD"], + }, + ], + ) + + assert_equal( + [ + { + "ad": entry["address"], + "h": entry["blockHeight"], + "t": entry["type"], + "a": entry["amounts"], + } + for entry in self.nodes[0].listvaulthistory( + self.vault_id1, + {"maxBlockHeight": self.nodes[0].getblockcount(), "depth": 1}, + ) + ], + [ + # payback DUSD loan with DUSD collateral + { + "ad": "vaultCollateral", + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-1.00000000@DUSD/v1"], + }, + { + "ad": self.address1, + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-1.00000000@DUSD/v1"], + }, + # payback SPY loan with funds from address + { + "ad": self.address1, + "h": 1000, + "t": "PaybackLoan", + "a": ["-0.09999909@SPY/v1"], + }, + # swap collateral to DUSD and use to paybackwithCollateral, first DFI coll for SPY and part of DUSD loan + { + "ad": "vaultCollateral", + "h": 1000, + "t": "TokenLock", + "a": ["-30.00000000@DFI", "147.64445053@DUSD/v1"], + }, + { + "ad": "vaultCollateral", + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-90.00009100@DUSD/v1"], + }, + { + "ad": self.address1, + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-0.90000091@SPY/v1"], + }, + { + "ad": "vaultCollateral", + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-57.64435953@DUSD/v1"], + }, + { + "ad": self.address1, + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-57.64435953@DUSD/v1"], + }, + # now USDT coll for remaining DUSD loan + { + "ad": "vaultCollateral", + "h": 1000, + "t": "TokenLock", + "a": ["-38.88589949@USDT", "41.33765630@DUSD/v1"], + }, + { + "ad": "vaultCollateral", + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-41.33765627@DUSD/v1"], + }, + { + "ad": self.address1, + "h": 1000, + "t": "PaybackWithCollateral", + "a": ["-41.33765627@DUSD/v1"], + }, + ], + ) + # addresses assert_equal( [ {"h": entry["blockHeight"], "t": entry["type"], "a": entry["amounts"]} @@ -1391,18 +1538,20 @@ def check_token_lock(self): ], ) + # no tokenlock, just payback assert_equal( - len( - self.nodes[0].listaccounthistory(self.address1, {"txtypes": ["P", "?"]}) - ), - 0, + [ + {"h": entry["blockHeight"], "t": entry["type"], "a": entry["amounts"]} + for entry in self.nodes[0].listaccounthistory( + self.address1, {"depth": 1} + ) + ], + [{"h": 1000, "t": "PaybackLoan", "a": ["-0.09999909@SPY/v1"]}], ) + # nothing happening on address2 (only in its vault) assert_equal( - len( - self.nodes[0].listaccounthistory(self.address2, {"txtypes": ["P", "?"]}) - ), - 0, + len(self.nodes[0].listaccounthistory(self.address2, {"depth": 1})), 0 ) assert_equal( @@ -2174,7 +2323,6 @@ def setup_test_vaults(self): } ) self.nodes[0].generate(1) - # address3 self.vault_id3 = self.nodes[0].createvault(self.address3, "") From a0ac5bc5d7f1cd3de0c9ab31d704219ebd87bdf8 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Mon, 16 Sep 2024 09:01:06 +0100 Subject: [PATCH 03/12] Miner: Check loan tokens are not locked before adding dToken restart (#3048) --- src/miner.cpp | 169 +++++++++++--------- test/functional/feature_restart_interest.py | 29 ++++ 2 files changed, 124 insertions(+), 74 deletions(-) diff --git a/src/miner.cpp b/src/miner.cpp index 2a12286338..cdd2b9f0ba 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -185,6 +185,100 @@ static void AddSplitEVMTxs(BlockContext &blockCtx, const SplitMap &splitMap) { } } +static void AddTokenRestartTxs(BlockContext &blockCtx, + const int height, + const int txVersion, + CBlock &pblock, + CBlockTemplate &pblocktemplate) { + auto &mnview = blockCtx.GetView(); + const auto attributes = mnview.GetAttributes(); + + CDataStructureV0 lockKey{AttributeTypes::Param, ParamIDs::dTokenRestart, static_cast(height)}; + CDataStructureV0 lockedTokenKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::LockedTokens}; + const auto lockRatio = attributes->GetValue(lockKey, CAmount{}); + const auto lockedTokens = attributes->GetValue(lockedTokenKey, CBalances{}); + + if (!lockRatio || !lockedTokens.balances.empty()) { + return; + } + + // Check all collaterals are currently valid + auto tokenPricesValid{true}; + + auto checkLivePrice = [&](const CTokenCurrencyPair ¤cyPair) { + if (auto fixedIntervalPrice = mnview.GetFixedIntervalPrice(currencyPair)) { + if (!fixedIntervalPrice.val->isLive(mnview.GetPriceDeviation())) { + tokenPricesValid = false; + return false; + } + } + return true; + }; + + std::set loanTokenIds; + + attributes->ForEach( + [&](const CDataStructureV0 &attr, const CAttributeValue &) { + if (attr.type != AttributeTypes::Token) { + return false; + } + if (attr.key == TokenKeys::LoanCollateralEnabled) { + if (auto collateralToken = mnview.GetCollateralTokenFromAttributes({attr.typeId})) { + return checkLivePrice(collateralToken->fixedIntervalPriceId); + } + } else if (attr.key == TokenKeys::LoanMintingEnabled) { + if (auto loanToken = mnview.GetLoanTokenFromAttributes({attr.typeId})) { + loanTokenIds.insert(attr.typeId); + return checkLivePrice(loanToken->fixedIntervalPriceId); + } + } + return true; + }, + CDataStructureV0{AttributeTypes::Token}); + + const auto tokensLocked = mnview.AreTokensLocked(loanTokenIds); + + if (!tokenPricesValid || tokensLocked) { + return; + } + + SplitMap lockSplitMapEVM; + auto createTokenLockSplitTx = [&](const uint32_t id, const bool isToken) { + CDataStream metadata(DfTokenSplitMarker, SER_NETWORK, PROTOCOL_VERSION); + int64_t multiplier = COIN; + metadata << (isToken ? 0 : 1) << id << multiplier; + + CMutableTransaction mTx(txVersion); + mTx.vin.resize(1); + mTx.vin[0].prevout.SetNull(); + mTx.vin[0].scriptSig = CScript() << height << OP_0; + mTx.vout.resize(1); + mTx.vout[0].scriptPubKey = CScript() << OP_RETURN << ToByteVector(metadata); + mTx.vout[0].nValue = 0; + auto tx = MakeTransactionRef(std::move(mTx)); + if (isToken) { + lockSplitMapEVM[id] = std::make_pair(multiplier, tx->GetHash()); + } + pblock.vtx.push_back(tx); + pblocktemplate.vTxFees.push_back(0); + pblocktemplate.vTxSigOpsCost.push_back(WITNESS_SCALE_FACTOR * GetLegacySigOpCount(*pblock.vtx.back())); + LogPrintf("Add creation TX ID: %d isToken: %d Hash: %s\n", id, isToken, tx->GetHash().GetHex()); + }; + + ForEachLockTokenAndPool( + [&](const DCT_ID &id, const CLoanSetLoanTokenImplementation &token) { + createTokenLockSplitTx(id.v, true); + return true; + }, + [&](const DCT_ID &id, const CPoolPair &token) { + createTokenLockSplitTx(id.v, false); + return true; + }, + mnview); + + AddSplitEVMTxs(blockCtx, lockSplitMapEVM); +} + template static void AddSplitDVMTxs(CCustomCSView &mnview, CBlock *pblock, @@ -427,80 +521,7 @@ ResVal> BlockAssembler::CreateNewBlock(const CSc if (nHeight >= chainparams.GetConsensus().DF24Height) { // Add token lock creations TXs: duplicate code from AddSplitDVMTxs. - // TODO: refactor - - CDataStructureV0 lockedTokenKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::LockedTokens}; - CDataStructureV0 lockKey{AttributeTypes::Param, ParamIDs::dTokenRestart, static_cast(nHeight)}; - const auto lockedTokens = attributes->GetValue(lockedTokenKey, CBalances{}); - const auto lockRatio = attributes->GetValue(lockKey, CAmount{}); - - // Check all collaterals are currently valid - auto tokenPricesValid{true}; - - auto checkLivePrice = [&](const CTokenCurrencyPair ¤cyPair) { - if (auto fixedIntervalPrice = mnview.GetFixedIntervalPrice(currencyPair)) { - if (!fixedIntervalPrice.val->isLive(mnview.GetPriceDeviation())) { - tokenPricesValid = false; - return false; - } - } - return true; - }; - - attributes->ForEach( - [&](const CDataStructureV0 &attr, const CAttributeValue &) { - if (attr.type != AttributeTypes::Token) { - return false; - } - if (attr.key == TokenKeys::LoanCollateralEnabled) { - if (auto collateralToken = mnview.GetCollateralTokenFromAttributes({attr.typeId})) { - return checkLivePrice(collateralToken->fixedIntervalPriceId); - } - } else if (attr.key == TokenKeys::LoanMintingEnabled) { - if (auto loanToken = mnview.GetLoanTokenFromAttributes({attr.typeId})) { - return checkLivePrice(loanToken->fixedIntervalPriceId); - } - } - return true; - }, - CDataStructureV0{AttributeTypes::Token}); - - if (lockedTokens.balances.empty() && lockRatio && tokenPricesValid) { - SplitMap lockSplitMapEVM; - auto createTokenLockSplitTx = [&](const uint32_t id, const bool isToken) { - CDataStream metadata(DfTokenSplitMarker, SER_NETWORK, PROTOCOL_VERSION); - int64_t multiplier = COIN; - metadata << (isToken ? 0 : 1) << id << multiplier; - - CMutableTransaction mTx(txVersion); - mTx.vin.resize(1); - mTx.vin[0].prevout.SetNull(); - mTx.vin[0].scriptSig = CScript() << nHeight << OP_0; - mTx.vout.resize(1); - mTx.vout[0].scriptPubKey = CScript() << OP_RETURN << ToByteVector(metadata); - mTx.vout[0].nValue = 0; - auto tx = MakeTransactionRef(std::move(mTx)); - if (isToken) { - lockSplitMapEVM[id] = std::make_pair(multiplier, tx->GetHash()); - } - pblock->vtx.push_back(tx); - pblocktemplate->vTxFees.push_back(0); - pblocktemplate->vTxSigOpsCost.push_back(WITNESS_SCALE_FACTOR * - GetLegacySigOpCount(*pblock->vtx.back())); - LogPrintf("Add creation TX ID: %d isToken: %d Hash: %s\n", id, isToken, tx->GetHash().GetHex()); - }; - ForEachLockTokenAndPool( - [&](const DCT_ID &id, const CLoanSetLoanTokenImplementation &token) { - createTokenLockSplitTx(id.v, true); - return true; - }, - [&](const DCT_ID &id, const CPoolPair &token) { - createTokenLockSplitTx(id.v, false); - return true; - }, - mnview); - AddSplitEVMTxs(blockCtx, lockSplitMapEVM); - } + AddTokenRestartTxs(blockCtx, nHeight, txVersion, *pblock, *pblocktemplate); } XVM xvm{}; diff --git a/test/functional/feature_restart_interest.py b/test/functional/feature_restart_interest.py index a1d4aa2d7b..836d00c428 100755 --- a/test/functional/feature_restart_interest.py +++ b/test/functional/feature_restart_interest.py @@ -35,6 +35,9 @@ def run_test(self): # Set up self.setup() + # Check restart skips on locked token + self.skip_restart_on_lock() + # Check minimal balances after restart self.minimal_balances_after_restart() @@ -266,6 +269,32 @@ def interest_paid_by_collateral(self): assert_equal(result["interestToHeight"], "0.000000000000000000000000") assert_equal(result["interestPerBlock"], "0.000000000000000000000000") + def skip_restart_on_lock(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Set lock + self.nodes[0].setgov({"ATTRIBUTES": {f"v0/locks/token/{self.idDUSD}": "true"}}) + self.nodes[0].generate(1) + + # Check lock + attributes = self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] + assert_equal(attributes[f"v0/locks/token/{self.idDUSD}"], "true") + + # Calculate restart height + restart_height = self.nodes[0].getblockcount() + 2 + + # Execute dtoken restart + self.execute_restart() + + # Check we are at restart height + assert_equal(self.nodes[0].getblockcount(), restart_height) + + # Check restart not executed + attributes = self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] + assert "v0/live/economy/token_lock_ratio" not in attributes + def interest_paid_by_balance(self): # Rollback block From a5a167f55c7d19b09592c5289685967b7a8426bf Mon Sep 17 00:00:00 2001 From: kuegi <29012906+kuegi@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:44:19 +0200 Subject: [PATCH 04/12] RPC: add missing entry in conversiontable (#3051) --- src/rpc/client.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 8b7b450953..cd7d0a1832 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -320,6 +320,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "listburnhistory", 0, "options" }, { "accounthistorycount", 1, "options" }, + { "releaselockedtokens", 0, "releasePart" }, + { "setgov", 0, "variables" }, { "setgov", 1, "inputs" }, From 4c8f7882d3bd611ecd84af0ee0af60fb0578be2c Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Mon, 16 Sep 2024 15:00:47 +0100 Subject: [PATCH 05/12] Restart: Check pools valid (#3050) * Restart: Check pools valid * Check collateral tokens are not locked * Fail if creation TXs not present * Restore assert to correct place * For pool status check both tokens are present --- src/dfi/validation.cpp | 14 ++++++++--- src/miner.cpp | 17 ++++++++++++- test/functional/feature_restart_interest.py | 28 ++++++++++++++++++++- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/dfi/validation.cpp b/src/dfi/validation.cpp index 3b53bb62e8..b67f15c735 100644 --- a/src/dfi/validation.cpp +++ b/src/dfi/validation.cpp @@ -3557,11 +3557,13 @@ static Res ConvertAllLoanTokenForTokenLock(const CBlock &block, CBalances lockedTokens; std::vector> creationTxPerPoolId; std::set poolsForConsolidation; + + auto creationRes = Res::Ok(); ForEachLockTokenAndPool( [&](const DCT_ID &id, const CLoanSetLoanTokenImplementation &token) { if (!creationTxPerId.count(id.v)) { - LogPrintf("missing creationTx for Token %d\n", id.v); - return true; + creationRes = Res::Err("missing creationTx for Token %d\n", id.v); + return false; } splits.emplace(id.v, COIN); creationTxs.emplace(id.v, std::make_pair(creationTxPerId[id.v], emptyPoolPairs)); @@ -3574,8 +3576,8 @@ static Res ConvertAllLoanTokenForTokenLock(const CBlock &block, }, [&](const DCT_ID &id, const CPoolPair &token) { if (!creationTxPerId.count(id.v)) { - LogPrintf("missing creationTx for Pool %d\n", id.v); - return true; + creationRes = Res::Err("missing creationTx for Token %d\n", id.v); + return false; } creationTxPerPoolId.emplace_back(id, creationTxPerId[id.v]); poolsForConsolidation.emplace(id); @@ -3583,6 +3585,10 @@ static Res ConvertAllLoanTokenForTokenLock(const CBlock &block, }, cache); + if (!creationRes) { + return creationRes; + } + CDataStructureV0 lockedTokenKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::LockedTokens}; // TODO: this is mainly used to know what token ids got locked (for use in TD later on). maybe add real balances // for stats? diff --git a/src/miner.cpp b/src/miner.cpp index cdd2b9f0ba..3eaefa9c72 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -216,6 +216,7 @@ static void AddTokenRestartTxs(BlockContext &blockCtx, }; std::set loanTokenIds; + std::set allTokenIds; attributes->ForEach( [&](const CDataStructureV0 &attr, const CAttributeValue &) { @@ -224,6 +225,7 @@ static void AddTokenRestartTxs(BlockContext &blockCtx, } if (attr.key == TokenKeys::LoanCollateralEnabled) { if (auto collateralToken = mnview.GetCollateralTokenFromAttributes({attr.typeId})) { + allTokenIds.insert(attr.typeId); return checkLivePrice(collateralToken->fixedIntervalPriceId); } } else if (attr.key == TokenKeys::LoanMintingEnabled) { @@ -238,7 +240,20 @@ static void AddTokenRestartTxs(BlockContext &blockCtx, const auto tokensLocked = mnview.AreTokensLocked(loanTokenIds); - if (!tokenPricesValid || tokensLocked) { + allTokenIds.insert(loanTokenIds.begin(), loanTokenIds.end()); + + bool poolDisabled{false}; + mnview.ForEachPoolPair([&](DCT_ID const &poolId, const CPoolPair &pool) { + if (allTokenIds.count(pool.idTokenA.v) && allTokenIds.count(pool.idTokenB.v)) { + if (!pool.status) { + poolDisabled = true; + return false; + } + } + return true; + }); + + if (!tokenPricesValid || tokensLocked || poolDisabled) { return; } diff --git a/test/functional/feature_restart_interest.py b/test/functional/feature_restart_interest.py index 836d00c428..e7fd323d80 100755 --- a/test/functional/feature_restart_interest.py +++ b/test/functional/feature_restart_interest.py @@ -36,7 +36,10 @@ def run_test(self): self.setup() # Check restart skips on locked token - self.skip_restart_on_lock() + # self.skip_restart_on_lock() + + # Check restart skips on pool disabled + self.skip_restart_on_pool_disabled() # Check minimal balances after restart self.minimal_balances_after_restart() @@ -147,6 +150,7 @@ def setup_test_pools(self): "commission": 0, "status": True, "ownerAddress": self.address, + "pairSymbol": "DFI-DUSD", } ) self.nodes[0].generate(1) @@ -295,6 +299,28 @@ def skip_restart_on_lock(self): attributes = self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] assert "v0/live/economy/token_lock_ratio" not in attributes + def skip_restart_on_pool_disabled(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Disable pool + self.nodes[0].updatepoolpair({"pool": "DFI-DUSD", "status": False}) + self.nodes[0].generate(1) + + # Calculate restart height + restart_height = self.nodes[0].getblockcount() + 2 + + # Execute dtoken restart + self.execute_restart() + + # Check we are at restart height + assert_equal(self.nodes[0].getblockcount(), restart_height) + + # Check restart not executed + attributes = self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] + assert "v0/live/economy/token_lock_ratio" not in attributes + def interest_paid_by_balance(self): # Rollback block From ec0d759ca616fa2df45d5ab4754692a1c8cb273d Mon Sep 17 00:00:00 2001 From: Prasanna Loganathar Date: Mon, 16 Sep 2024 22:02:10 +0800 Subject: [PATCH 06/12] v4.1.7 (#3022) * v4.1.4 * v4.1.7 --------- Co-authored-by: Peter John Bushnell --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index f997c24b82..bca3e416dd 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N) AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 4) define(_CLIENT_VERSION_MINOR, 1) -define(_CLIENT_VERSION_REVISION, 6) +define(_CLIENT_VERSION_REVISION, 7) define(_CLIENT_VERSION_BUILD, 0) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true) From 9d852ec2ffa31721438bd3ee1bedddcda30da835 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Wed, 18 Sep 2024 01:55:21 +0100 Subject: [PATCH 07/12] Restart: Restore loan and collateral from auctions to vaults (#3053) * Additional tests for restart * fix handling of vault in liquidation * Use view to iterate over auctions and get vaults --------- Co-authored-by: kuegi <29012906+kuegi@users.noreply.github.com> --- src/dfi/validation.cpp | 55 +++++++++++--- test/functional/feature_restart_interest.py | 80 ++++++++++++++++++++- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/src/dfi/validation.cpp b/src/dfi/validation.cpp index b67f15c735..409ee8d4c9 100644 --- a/src/dfi/validation.cpp +++ b/src/dfi/validation.cpp @@ -3229,30 +3229,68 @@ static Res PaybackWithSwappedCollateral(const DCT_ID &collId, static Res ForceCloseAllAuctions(const CBlockIndex *pindex, CCustomCSView &cache) { std::map auctionsToErase; + CAccountsHistoryWriter view(cache, pindex->nHeight, ~0u, pindex->GetBlockHash(), uint8_t(CustomTxType::AuctionBid)); + auto res = Res::Ok(); - cache.ForEachVaultAuction( + view.ForEachVaultAuction( [&](const auto &vaultId, const auto &auctionData) { - auto vault = cache.GetVault(vaultId); + auto vault = view.GetVault(vaultId); if (!vault) { res = Res::Err("Vault not found"); return false; } - vault->isUnderLiquidation = false; - cache.StoreVault(vaultId, *vault); + // return coll and loan from auction to vault + CBalances balances; for (uint32_t i = 0; i < auctionData.batchCount; i++) { - if (auto bid = cache.GetAuctionBid({vaultId, i})) { - cache.CalculateOwnerRewards(bid->first, pindex->nHeight); + auto batch = view.GetAuctionBatch({vaultId, i}); + assert(batch); + + if (auto bid = view.GetAuctionBid({vaultId, i})) { + view.CalculateOwnerRewards(bid->first, pindex->nHeight); // repay bid - res = cache.AddBalance(bid->first, bid->second); + res = view.AddBalance(bid->first, bid->second); if (!res) { return false; } } + // return loan and collateral either way + // we should return loan including interest + view.AddLoanToken(vaultId, batch->loanAmount); + balances.Add({batch->loanAmount.nTokenId, batch->loanInterest}); + + // When tracking loan amounts remove interest. + if (const auto token = view.GetToken("DUSD"); token && token->first == batch->loanAmount.nTokenId) { + TrackDUSDAdd(view, {batch->loanAmount.nTokenId, batch->loanAmount.nValue - batch->loanInterest}); + } + + if (auto token = view.GetLoanTokenByID(batch->loanAmount.nTokenId)) { + view.IncreaseInterest(pindex->nHeight, + vaultId, + vault->schemeId, + batch->loanAmount.nTokenId, + token->interest, + batch->loanAmount.nValue); + } + for (const auto &col : batch->collaterals.balances) { + auto tokenId = col.first; + auto tokenAmount = col.second; + view.AddVaultCollateral(vaultId, {tokenId, tokenAmount}); + } } + + // Only store to attributes if there has been a rounding error. + if (!balances.balances.empty()) { + TrackLiveBalances(view, balances, EconomyKeys::ConsolidatedInterest); + } + auctionsToErase.emplace(vaultId, auctionData.liquidationHeight); + vault->isUnderLiquidation = false; + view.StoreVault(vaultId, *vault); + // Store state in vault DB - cache.GetHistoryWriters().WriteVaultState(cache, *pindex, vaultId); + cache.GetHistoryWriters().WriteVaultState(view, *pindex, vaultId); + return true; }, pindex->nHeight); @@ -3260,6 +3298,7 @@ static Res ForceCloseAllAuctions(const CBlockIndex *pindex, CCustomCSView &cache if (!res) { return res; } + view.Flush(); for (const auto &[vaultId, liquidationHeight] : auctionsToErase) { cache.EraseAuction(vaultId, liquidationHeight); diff --git a/test/functional/feature_restart_interest.py b/test/functional/feature_restart_interest.py index e7fd323d80..52d164d24f 100755 --- a/test/functional/feature_restart_interest.py +++ b/test/functional/feature_restart_interest.py @@ -35,8 +35,11 @@ def run_test(self): # Set up self.setup() + # Check restart on liquidated vault + self.vault_liquidation() + # Check restart skips on locked token - # self.skip_restart_on_lock() + self.skip_restart_on_lock() # Check restart skips on pool disabled self.skip_restart_on_pool_disabled() @@ -117,14 +120,14 @@ def setup_test_oracles(self): ] # Appoint Oracle - oracle = self.nodes[0].appointoracle(oracle_address, price_feed, 10) + self.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}"}, ] - self.nodes[0].setoracledata(oracle, int(time.time()), oracle_prices) + self.nodes[0].setoracledata(self.oracle, int(time.time()), oracle_prices) self.nodes[0].generate(10) # Create loan scheme @@ -321,6 +324,77 @@ def skip_restart_on_pool_disabled(self): attributes = self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] assert "v0/live/economy/token_lock_ratio" not in attributes + def vault_liquidation(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Create vault + vault_address = self.nodes[0].getnewaddress("", "legacy") + vault_id = self.nodes[0].createvault(vault_address, "LOAN001") + self.nodes[0].generate(1) + + # Deposit DFI to vault + self.nodes[0].deposittovault(vault_id, self.address, f"150@{self.symbolDFI}") + self.nodes[0].generate(1) + + # Take DUSD loan + self.nodes[0].takeloan( + {"vaultId": vault_id, "amounts": f"100@{self.symbolDUSD}"} + ) + self.nodes[0].generate(1) + + # Set Oracle prices to trigger liquidation + oracle_prices = [ + {"currency": "USD", "tokenAmount": f"0.9@{self.symbolDFI}"}, + ] + self.nodes[0].setoracledata(self.oracle, int(time.time()), oracle_prices) + self.nodes[0].generate(7) + + # Check vault in liquidation + result = self.nodes[0].getvault(vault_id) + assert_equal(result["state"], "inLiquidation") + + # Place bid on auction + self.nodes[0].placeauctionbid( + vault_id, 0, self.address, f"110@{self.symbolDUSD}" + ) + self.nodes[0].generate(1) + + # check balance before + assert_equal( + self.nodes[0].getaccount(self.address)[1], + f"89890.00000000@{self.symbolDUSD}", + ) + assert_equal( + self.nodes[0].getaccount(vault_address), + [f"100.00000000@{self.symbolDUSD}"], + ) + + # Execute dtoken restart + self.execute_restart() + + # Verify that auction bid has been refunded and DUSD locked afterwards + assert_equal( + self.nodes[0].getaccount(self.address)[1], + f"9000.00000000@{self.symbolDUSD}", + ) + + # DUSD is used to pay back loan + assert_equal(self.nodes[0].getaccount(vault_address), []) + + # Check auctions are not cleared + assert_equal(self.nodes[0].listauctions(), []) + + # Check vault + result = self.nodes[0].getvault(vault_id) + print("After vault", result) + assert_equal(result["loanAmounts"], []) + assert_equal( + result["collateralAmounts"], [f"149.99933408@{self.symbolDFI}"] + ) # after paying back with account, interest remains which is paid back with collateral + assert_equal(result["interestAmounts"], []) + def interest_paid_by_balance(self): # Rollback block From b0409f5c428d4b415b3dbdff11367ebabe9a59b6 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Fri, 20 Sep 2024 10:04:32 +0100 Subject: [PATCH 08/12] Upgrade DST20 contract to allow 1:1 splits (#3059) * added test to upgrade locked token after full release * added test and fix behaviour in V2 contract. should be moved to V3 * Upgrade contract to allow 1:1 splits --------- Co-authored-by: kuegi <29012906+kuegi@users.noreply.github.com> --- lib/ain-contracts/build.rs | 1 + lib/ain-contracts/dst20_v3/DST20V3.sol | 605 ++++++++++++++++++ .../dst20_v3/IDST20Upgradeable.sol | 11 + lib/ain-contracts/src/lib.rs | 22 + lib/ain-cpp-imports/src/bridge.rs | 1 + lib/ain-cpp-imports/src/lib.rs | 8 + lib/ain-evm/src/contract/dst20.rs | 20 +- lib/ain-evm/src/evm.rs | 39 +- src/ffi/ffiexports.cpp | 4 + src/ffi/ffiexports.h | 1 + test/functional/feature_evm_token_split.py | 60 +- test/functional/feature_restart_interest.py | 1 - test/functional/feature_restartdtokens.py | 51 +- 13 files changed, 798 insertions(+), 26 deletions(-) create mode 100644 lib/ain-contracts/dst20_v3/DST20V3.sol create mode 100644 lib/ain-contracts/dst20_v3/IDST20Upgradeable.sol diff --git a/lib/ain-contracts/build.rs b/lib/ain-contracts/build.rs index 7bfcc42cc9..00c0143393 100644 --- a/lib/ain-contracts/build.rs +++ b/lib/ain-contracts/build.rs @@ -22,6 +22,7 @@ fn main() -> Result<()> { ("dst20", "DST20"), ("dst20_v1", "DST20V1"), ("dst20_v2", "DST20V2"), + ("dst20_v3", "DST20V3"), ]; for (sol_project_name, contract_name) in contracts { diff --git a/lib/ain-contracts/dst20_v3/DST20V3.sol b/lib/ain-contracts/dst20_v3/DST20V3.sol new file mode 100644 index 0000000000..0dd500ec49 --- /dev/null +++ b/lib/ain-contracts/dst20_v3/DST20V3.sol @@ -0,0 +1,605 @@ +// SPDX-License-Identifier: MIT +// File: @openzeppelin/contracts@4.9.2/utils/Context.sol + +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File: @openzeppelin/contracts@4.9.2/token/ERC20/IERC20.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance( + address owner, + address spender + ) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +// File: @openzeppelin/contracts@4.9.2/token/ERC20/extensions/IERC20Metadata.sol + +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} + +// File: @openzeppelin/contracts@4.9.2/token/ERC20/ERC20.sol + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) + +import "./IDST20Upgradeable.sol"; +pragma solidity ^0.8.0; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ + +contract ERC20 is Context, IERC20, IERC20Metadata, IDST20Upgradeable { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf( + address account + ) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer( + address to, + uint256 amount + ) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance( + address owner, + address spender + ) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve( + address spender, + uint256 amount + ) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance( + address spender, + uint256 addedValue + ) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance( + address spender, + uint256 subtractedValue + ) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require( + currentAllowance >= subtractedValue, + "ERC20: decreased allowance below zero" + ); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer( + address from, + address to, + uint256 amount + ) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require( + fromBalance >= amount, + "ERC20: transfer amount exceeds balance" + ); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require( + currentAllowance >= amount, + "ERC20: insufficient allowance" + ); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + function upgradeToken( + uint256 amount + ) public virtual override returns (address, uint256) { + address precompileAddress = address(0x0a); + bytes memory inputData = abi.encode(msg.sender, address(this), amount); + bytes memory outputData = new bytes(64); + bool success; + + assembly { + success := call( + gas(), + precompileAddress, + 0, + add(inputData, 32), + mload(inputData), + add(outputData, 32), + mload(outputData) + ) + } + require(success, "Precompile call failed"); + + (address newTokenContractAddress, uint256 newAmount) = abi.decode( + outputData, + (address, uint256) + ); + + emit UpgradeResult(newTokenContractAddress, newAmount); + + // Upgrade available + if (newTokenContractAddress != address(this)) { + _burn(msg.sender, amount); + IERC20(newTokenContractAddress).transfer(msg.sender, newAmount); + } + + return (newTokenContractAddress, newAmount); + } +} + +// File: oz.sol + +pragma solidity ^0.8.0; + +contract DST20V3 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} +} diff --git a/lib/ain-contracts/dst20_v3/IDST20Upgradeable.sol b/lib/ain-contracts/dst20_v3/IDST20Upgradeable.sol new file mode 100644 index 0000000000..b5114572ed --- /dev/null +++ b/lib/ain-contracts/dst20_v3/IDST20Upgradeable.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IDST20Upgradeable { + event UpgradeResult( + address indexed newTokenContractAddress, + uint256 newAmount + ); + + function upgradeToken(uint256 amount) external returns (address, uint256); +} diff --git a/lib/ain-contracts/src/lib.rs b/lib/ain-contracts/src/lib.rs index 703a7b11db..bae351e258 100644 --- a/lib/ain-contracts/src/lib.rs +++ b/lib/ain-contracts/src/lib.rs @@ -213,6 +213,24 @@ lazy_static::lazy_static! { fixed_address: H160(slice_20b!(INTRINSICS_ADDR_PREFIX_BYTE, 0x5)) } }; + + pub static ref DST20_V3_CONTRACT: FixedContract = { + let bytecode = solc_artifact_bytecode_str!( + "dst20_v3", "deployed_bytecode.json" + ); + let input = solc_artifact_bytecode_str!( + "dst20_v3", "bytecode.json" + ); + + FixedContract { + contract: Contract { + codehash: Blake2Hasher::hash(&bytecode), + runtime_bytecode: bytecode, + init_bytecode: input, + }, + fixed_address: H160(slice_20b!(INTRINSICS_ADDR_PREFIX_BYTE, 0x6)) + } + }; } pub fn get_split_tokens_function() -> ethabi::Function { @@ -370,6 +388,10 @@ pub fn get_dst20_v2_contract() -> FixedContract { DST20_V2_CONTRACT.clone() } +pub fn get_dst20_v3_contract() -> FixedContract { + DST20_V3_CONTRACT.clone() +} + #[cfg(test)] mod test { use super::*; diff --git a/lib/ain-cpp-imports/src/bridge.rs b/lib/ain-cpp-imports/src/bridge.rs index ebb13c400c..a7f8201def 100644 --- a/lib/ain-cpp-imports/src/bridge.rs +++ b/lib/ain-cpp-imports/src/bridge.rs @@ -90,6 +90,7 @@ pub mod ffi { fn isEthDebugTraceRPCEnabled() -> bool; fn getEVMSystemTxsFromBlock(block_hash: [u8; 32]) -> Vec; fn getDF23Height() -> u64; + fn getDF24Height() -> u64; fn migrateTokensFromEVM( mnview_ptr: usize, old_amount: TokenAmount, diff --git a/lib/ain-cpp-imports/src/lib.rs b/lib/ain-cpp-imports/src/lib.rs index 9981df329f..55c8942be8 100644 --- a/lib/ain-cpp-imports/src/lib.rs +++ b/lib/ain-cpp-imports/src/lib.rs @@ -156,6 +156,9 @@ mod ffi { pub fn getDF23Height() -> u64 { unimplemented!("{}", UNIMPL_MSG) } + pub fn getDF24Height() -> u64 { + unimplemented!("{}", UNIMPL_MSG) + } pub fn migrateTokensFromEVM( _mnview_ptr: usize, _old_amount: TokenAmount, @@ -367,6 +370,11 @@ pub fn get_df23_height() -> u64 { ffi::getDF23Height() } +/// Gets the DF23 height +pub fn get_df24_height() -> u64 { + ffi::getDF24Height() +} + /// Send tokens to DVM to split pub fn split_tokens_from_evm( mnview_ptr: usize, diff --git a/lib/ain-evm/src/contract/dst20.rs b/lib/ain-evm/src/contract/dst20.rs index 2572049159..bf9f9d8830 100644 --- a/lib/ain-evm/src/contract/dst20.rs +++ b/lib/ain-evm/src/contract/dst20.rs @@ -1,6 +1,7 @@ use ain_contracts::{ get_dfi_reserved_contract, get_dst20_contract, get_dst20_v1_contract, get_dst20_v2_contract, - get_transfer_domain_contract, Contract, FixedContract, IMPLEMENTATION_SLOT, + get_dst20_v3_contract, get_transfer_domain_contract, Contract, FixedContract, + IMPLEMENTATION_SLOT, }; use anyhow::format_err; use ethereum::{ @@ -86,6 +87,19 @@ pub fn dst20_v2_deploy_info() -> DeployContractInfo { } } +pub fn dst20_v3_deploy_info() -> DeployContractInfo { + let FixedContract { + contract, + fixed_address, + } = get_dst20_v3_contract(); + + DeployContractInfo { + address: fixed_address, + bytecode: Bytes::from(contract.runtime_bytecode), + storage: Vec::new(), + } +} + pub fn bridge_dst20_in( backend: &EVMBackend, contract: H160, @@ -245,7 +259,9 @@ pub fn get_dst20_migration_txs(mnview_ptr: usize) -> Result> { } pub fn dst20_name_info(dvm_block: u64, name: &str, symbol: &str) -> Vec<(H256, H256)> { - let contract_address = if dvm_block >= ain_cpp_imports::get_df23_height() { + let contract_address = if dvm_block >= ain_cpp_imports::get_df24_height() { + get_dst20_v3_contract().fixed_address + } else if dvm_block >= ain_cpp_imports::get_df23_height() { get_dst20_v2_contract().fixed_address } else { get_dst20_v1_contract().fixed_address diff --git a/lib/ain-evm/src/evm.rs b/lib/ain-evm/src/evm.rs index 9adf013fd6..263381a369 100644 --- a/lib/ain-evm/src/evm.rs +++ b/lib/ain-evm/src/evm.rs @@ -2,10 +2,10 @@ use std::{path::PathBuf, sync::Arc}; use ain_contracts::{ get_dfi_instrinics_registry_contract, get_dfi_intrinsics_v1_contract, get_dst20_v1_contract, - get_dst20_v2_contract, get_transfer_domain_contract, get_transfer_domain_v1_contract, - IMPLEMENTATION_SLOT, + get_dst20_v2_contract, get_dst20_v3_contract, get_transfer_domain_contract, + get_transfer_domain_v1_contract, IMPLEMENTATION_SLOT, }; -use ain_cpp_imports::{get_df23_height, Attributes}; +use ain_cpp_imports::{get_df23_height, get_df24_height, Attributes}; use anyhow::format_err; use ethereum::{Block, PartialHeader}; use ethereum_types::{Bloom, H160, H256, H64, U256}; @@ -19,8 +19,8 @@ use crate::{ contract::{ deploy_contract_tx, dfi_intrinsics_registry_deploy_info, dfi_intrinsics_v1_deploy_info, dst20::{ - dst20_v1_deploy_info, dst20_v2_deploy_info, get_dst20_migration_txs, - reserve_dst20_namespace, + dst20_v1_deploy_info, dst20_v2_deploy_info, dst20_v3_deploy_info, + get_dst20_migration_txs, reserve_dst20_namespace, }, h160_to_h256, reserve_intrinsics_namespace, transfer_domain_deploy_info, transfer_domain_v1_contract_deploy_info, DeployContractInfo, @@ -331,6 +331,7 @@ impl EVMServices { // reserve DST20 namespace; let is_evm_genesis_block = template.get_block_number() == U256::zero(); let is_df23_fork = template.ctx.dvm_block == get_df23_height(); + let is_df24_fork = template.ctx.dvm_block == get_df24_height(); let mut logs_bloom = template.get_latest_logs_bloom(); let mut executor = AinExecutor::new(&mut template.backend); @@ -504,6 +505,34 @@ impl EVMServices { executor.update_storage(address, storage)?; } + if is_df24_fork { + // Deploy contract with updated upgradeToken function + let DeployContractInfo { + address, + storage, + bytecode, + } = dst20_v3_deploy_info(); + + trace!("deploying {:x?} bytecode {:?}", address, bytecode); + executor.deploy_contract(address, bytecode, storage)?; + + let (tx, receipt) = + deploy_contract_tx(get_dst20_v3_contract().contract.init_bytecode, &base_fee)?; + template.transactions.push(TemplateTxItem::new_system_tx( + Box::new(tx), + (receipt, Some(address)), + logs_bloom, + )); + + // Point proxy to DST20_v3 + let storage = vec![( + IMPLEMENTATION_SLOT, + h160_to_h256(get_dst20_v3_contract().fixed_address), + )]; + + executor.update_storage(address, storage)?; + } + template.backend.increase_tx_count(); Ok(()) } diff --git a/src/ffi/ffiexports.cpp b/src/ffi/ffiexports.cpp index d88033cad6..76c7cfdb69 100644 --- a/src/ffi/ffiexports.cpp +++ b/src/ffi/ffiexports.cpp @@ -522,6 +522,10 @@ uint64_t getDF23Height() { return Params().GetConsensus().DF23Height; } +uint64_t getDF24Height() { + return Params().GetConsensus().DF24Height; +} + bool migrateTokensFromEVM(std::size_t mnview_ptr, TokenAmount old_amount, TokenAmount &new_amount) { return ExecuteTokenMigrationEVM(mnview_ptr, old_amount, new_amount); } diff --git a/src/ffi/ffiexports.h b/src/ffi/ffiexports.h index 7199403051..ad2420b7fa 100644 --- a/src/ffi/ffiexports.h +++ b/src/ffi/ffiexports.h @@ -127,6 +127,7 @@ bool isEthDebugTraceRPCEnabled(); // Gets all EVM system txs and their respective types from DVM block. rust::vec getEVMSystemTxsFromBlock(std::array evmBlockHash); uint64_t getDF23Height(); +uint64_t getDF24Height(); bool migrateTokensFromEVM(std::size_t mnview_ptr, TokenAmount old_amount, TokenAmount &new_amount); #endif // DEFI_FFI_FFIEXPORTS_H diff --git a/test/functional/feature_evm_token_split.py b/test/functional/feature_evm_token_split.py index 3e79397d42..9871e959c1 100755 --- a/test/functional/feature_evm_token_split.py +++ b/test/functional/feature_evm_token_split.py @@ -36,6 +36,7 @@ def set_test_params(self): "-grandcentralheight=1", "-metachainheight=105", "-df23height=150", + "-df24height=150", ], ] @@ -53,6 +54,9 @@ def run_test(self): # Split token multiple times via transfer domain self.transfer_domain_multiple_split() + # Split tokens 1:1 via v3 intrinsics contract + self.intrinsic_token_split(20, 1, True) + # Split tokens via intrinsics contract self.intrinsic_token_split(20, 2) @@ -171,6 +175,12 @@ def setup_variables(self): encoding="utf8", ).read() + self.dst20_v3_abi = open( + get_solc_artifact_path("dst20_v3", "abi.json"), + "r", + encoding="utf8", + ).read() + # Check META variables self.meta_contract = self.nodes[0].w3.eth.contract( address=self.contract_address_metav1, abi=self.dst20_v2_abi @@ -471,7 +481,7 @@ def transfer_domain_multiple_split(self): Decimal(4000.00000000), ) - def intrinsic_token_split(self, amount, split_multiplier): + def intrinsic_token_split(self, amount, split_multiplier, use_v3=False): # Rollback self.rollback_to(self.block_height) @@ -499,6 +509,7 @@ def intrinsic_token_split(self, amount, split_multiplier): self.contract_address_metav2, amount, split_multiplier, + use_v3, ) # Get values from after transfer out @@ -537,8 +548,9 @@ def intrinsic_token_split(self, amount, split_multiplier): + (amount * decimal_multiplier), ) + # Check already updated token cannot be updated again self.execute_split_transaction_at_highest_level( - self.contract_address_metav2, amount + self.contract_address_metav2, amount, use_v3 ) def intrinsic_token_merge(self, amount, split_multiplier): @@ -703,7 +715,12 @@ def split_token( ) def execute_split_transaction( - self, source_contract, destination_contract, amount=20, split_multiplier=2 + self, + source_contract, + destination_contract, + amount=20, + split_multiplier=2, + use_v3=False, ): # Create the amount to approve @@ -722,9 +739,14 @@ def execute_split_transaction( amount_to_receive = 0 # Get old contract - meta_contract = self.nodes[0].w3.eth.contract( - address=source_contract, abi=self.dst20_v2_abi - ) + if use_v3: + meta_contract = self.nodes[0].w3.eth.contract( + address=source_contract, abi=self.dst20_v3_abi + ) + else: + meta_contract = self.nodes[0].w3.eth.contract( + address=source_contract, abi=self.dst20_v2_abi + ) totalSupplyBefore = meta_contract.functions.totalSupply().call() balance_before = meta_contract.functions.balanceOf(self.evm_address).call() @@ -769,9 +791,14 @@ def execute_split_transaction( assert_equal(totalSupplyAfter, totalSupplyBefore - amount_to_send) # Get new contract - meta_contract_new = self.nodes[0].w3.eth.contract( - address=destination_contract, abi=self.dst20_v2_abi - ) + if use_v3: + meta_contract_new = self.nodes[0].w3.eth.contract( + address=destination_contract, abi=self.dst20_v3_abi + ) + else: + meta_contract_new = self.nodes[0].w3.eth.contract( + address=destination_contract, abi=self.dst20_v2_abi + ) # Check transfer from sender to burn address events = meta_contract_new.events.Transfer().process_log( @@ -798,10 +825,17 @@ def execute_split_transaction( return amount_to_receive - def execute_split_transaction_at_highest_level(self, source_contract, amount=20): - meta_contract = self.nodes[0].w3.eth.contract( - address=source_contract, abi=self.dst20_v2_abi - ) + def execute_split_transaction_at_highest_level( + self, source_contract, amount=20, use_v3=False + ): + if use_v3: + meta_contract = self.nodes[0].w3.eth.contract( + address=source_contract, abi=self.dst20_v3_abi + ) + else: + meta_contract = self.nodes[0].w3.eth.contract( + address=source_contract, abi=self.dst20_v2_abi + ) amount_to_send = Web3.to_wei(amount, "ether") diff --git a/test/functional/feature_restart_interest.py b/test/functional/feature_restart_interest.py index 52d164d24f..fe3f9cdaa1 100755 --- a/test/functional/feature_restart_interest.py +++ b/test/functional/feature_restart_interest.py @@ -388,7 +388,6 @@ def vault_liquidation(self): # Check vault result = self.nodes[0].getvault(vault_id) - print("After vault", result) assert_equal(result["loanAmounts"], []) assert_equal( result["collateralAmounts"], [f"149.99933408@{self.symbolDFI}"] diff --git a/test/functional/feature_restartdtokens.py b/test/functional/feature_restartdtokens.py index fbc7fdf23b..75a0716b6d 100755 --- a/test/functional/feature_restartdtokens.py +++ b/test/functional/feature_restartdtokens.py @@ -612,6 +612,47 @@ def release_final_1(self): [], ) + # check that upgrade token now works + assert_equal( + self.dusd_contract.functions.balanceOf(self.evmaddress).call() / (10**18), + Decimal(19.99999999), + ) + assert_equal( + self.usdd_contract.functions.balanceOf(self.evmaddress).call(), + Decimal(0), + ) + + amount = Web3.to_wei(10, "ether") + + upgrade_txn = self.dusd_contract.functions.upgradeToken( + amount + ).build_transaction( + { + "from": self.evmaddress, + "nonce": self.nodes[0].eth_getTransactionCount(self.evmaddress), + "maxFeePerGas": 10_000_000_000, + "maxPriorityFeePerGas": 1_500_000_000, + "gas": 5_000_000, + } + ) + signed_txn = self.nodes[0].w3.eth.account.sign_transaction( + upgrade_txn, self.evm_privkey + ) + + self.nodes[0].w3.eth.send_raw_transaction(signed_txn.rawTransaction) + self.nodes[0].generate(1) + tx_receipt = self.nodes[0].w3.eth.wait_for_transaction_receipt(signed_txn.hash) + assert_equal(tx_receipt["status"], 1) + + assert_equal( + self.dusd_contract.functions.balanceOf(self.evmaddress).call() / (10**18), + Decimal(9.99999999), + ) + assert_equal( + self.usdd_contract.functions.balanceOf(self.evmaddress).call() / (10**18), + Decimal(10), + ) + def check_token_split(self): # updated SPY self.idSPY = list(self.nodes[0].gettoken("SPY").keys())[0] @@ -1045,7 +1086,7 @@ def check_token_lock(self): address=self.nodes[0].w3.to_checksum_address( f"0xff0000000000000000000000000000000000{self.usddId:0{4}x}" ), - abi=self.dst20_v2_abi, + abi=self.dst20_v3_abi, ) assert_equal( [ @@ -1974,22 +2015,22 @@ def setup_variables(self): ) # DST20 ABI - self.dst20_v2_abi = open( - get_solc_artifact_path("dst20_v2", "abi.json"), + self.dst20_v3_abi = open( + get_solc_artifact_path("dst20_v3", "abi.json"), "r", encoding="utf8", ).read() # Check DUSD variables self.dusd_contract = self.nodes[0].w3.eth.contract( - address=self.contract_address_dusdv1, abi=self.dst20_v2_abi + address=self.contract_address_dusdv1, abi=self.dst20_v3_abi ) assert_equal(self.dusd_contract.functions.symbol().call(), "DUSD") assert_equal(self.dusd_contract.functions.name().call(), "dUSD") # Check SPY variables self.spy_contract = self.nodes[0].w3.eth.contract( - address=self.contract_address_spyv1, abi=self.dst20_v2_abi + address=self.contract_address_spyv1, abi=self.dst20_v3_abi ) assert_equal(self.spy_contract.functions.symbol().call(), "SPY") assert_equal(self.spy_contract.functions.name().call(), "SP500") From 6ff00a58abaf4dd16f0f0be1a3c789f461f0ae13 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Fri, 20 Sep 2024 13:06:10 +0100 Subject: [PATCH 09/12] v4.1.8 (#3060) --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index bca3e416dd..9be90ed893 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N) AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 4) define(_CLIENT_VERSION_MINOR, 1) -define(_CLIENT_VERSION_REVISION, 7) +define(_CLIENT_VERSION_REVISION, 8) define(_CLIENT_VERSION_BUILD, 0) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true) From 628ffa5d193968299b9ec1daeab81a3417c3035e Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Tue, 24 Sep 2024 02:04:52 +0100 Subject: [PATCH 10/12] Add unsetgovheight/cleargovheights. Remove Gov from setgov/unsetgov. (#3063) --- src/dfi/consensus/governance.cpp | 93 ++++++- src/dfi/consensus/governance.h | 4 + src/dfi/customtx.cpp | 6 + src/dfi/customtx.h | 4 +- src/dfi/govvariables/attributes.cpp | 13 +- src/dfi/govvariables/attributes.h | 4 +- src/dfi/gv.cpp | 42 +++ src/dfi/gv.h | 49 +++- src/dfi/masternodes.h | 2 +- src/dfi/mn_checks.cpp | 9 +- src/dfi/mn_checks.h | 2 + src/dfi/mn_rpc.cpp | 253 ++++++++++++++++-- src/dfi/rpc_customtx.cpp | 14 + src/dfi/validation.cpp | 24 ++ src/rpc/client.cpp | 6 + .../feature_community_governance.py | 240 ++++++++++++++--- 16 files changed, 678 insertions(+), 87 deletions(-) diff --git a/src/dfi/consensus/governance.cpp b/src/dfi/consensus/governance.cpp index da1b875dda..ab364f725f 100644 --- a/src/dfi/consensus/governance.cpp +++ b/src/dfi/consensus/governance.cpp @@ -9,8 +9,7 @@ Res CGovernanceConsensus::operator()(const CGovernanceMessage &obj) const { // Check foundation auth - auto authCheck = AuthManager(blockCtx, txCtx); - if (auto res = authCheck.HasGovOrFoundationAuth(); !res) { + if (auto res = HasFoundationAuth(); !res) { return res; } @@ -49,10 +48,6 @@ Res CGovernanceConsensus::operator()(const CGovernanceMessage &obj) const { if (newExport.empty()) { return Res::Err("Cannot export empty attribute map"); } - - if (res = authCheck.CanSetGov(*newVar); !res) { - return res; - } } CDataStructureV0 foundationMembers{AttributeTypes::Param, ParamIDs::Foundation, DFIPKeys::Members}; @@ -106,7 +101,39 @@ Res CGovernanceConsensus::operator()(const CGovernanceMessage &obj) const { return Res::Ok(); } -Res CGovernanceConsensus::operator()(const CGovernanceUnsetMessage &obj) const { +Res CGovernanceConsensus::operator()(const CGovernanceClearHeightMessage &obj) const { + // Check auth + auto authCheck = AuthManager(blockCtx, txCtx); + if (auto res = authCheck.HasGovOrFoundationAuth(); !res) { + return res; + } + + auto &mnview = blockCtx.GetView(); + + auto heightToClear = [](const auto &collection) { + std::set heights; + for (const auto &[_, values] : collection) { + for (const auto &[height, _] : values) { + heights.insert(height); + } + } + return heights; + }; + + const auto setVars = mnview.GetAllStoredVariables(); + for (const auto &height : heightToClear(setVars)) { + mnview.EraseStoredVariables(height); + } + + const auto unsetVars = mnview.GetAllUnsetStoredVariables(); + for (const auto &height : heightToClear(unsetVars)) { + mnview.EraseUnsetStoredVariables(height); + } + + return Res::Ok(); +} + +Res CGovernanceConsensus::operator()(const CGovernanceUnsetHeightMessage &obj) const { // Check foundation auth auto authCheck = AuthManager(blockCtx, txCtx); if (auto res = authCheck.HasGovOrFoundationAuth(); !res) { @@ -122,6 +149,17 @@ Res CGovernanceConsensus::operator()(const CGovernanceUnsetMessage &obj) const { return Res::Err("Unset Gov variables not currently enabled in attributes."); } + if (obj.unsetHeight <= height) { + return Res::Err("unsetHeight must be above the current block height"); + } + + CDataStructureV0 minBlockKey{AttributeTypes::Param, ParamIDs::GovernanceParam, DFIPKeys::GovHeightMinBlocks}; + const auto minBlocks = attributes->GetValue(minBlockKey, uint64_t{}); + + if (obj.unsetHeight <= height + minBlocks) { + return Res::Err("Height must be %d blocks above the current height", minBlocks); + } + for (const auto &[name, keys] : obj.govs) { if (name == "ATTRIBUTES") { if (auto res = authCheck.CanSetGov(keys); !res) { @@ -138,6 +176,37 @@ Res CGovernanceConsensus::operator()(const CGovernanceUnsetMessage &obj) const { if (!res) { return Res::Err("%s: %s", name, res.msg); } + } + + // Store pending Gov var changes + return StoreUnsetGovVars(obj, mnview); +} + +Res CGovernanceConsensus::operator()(const CGovernanceUnsetMessage &obj) const { + // Check foundation auth + if (!HasFoundationAuth()) { + return Res::Err("tx not from foundation member"); + } + + const auto height = txCtx.GetHeight(); + auto &mnview = blockCtx.GetView(); + const auto attributes = mnview.GetAttributes(); + + CDataStructureV0 key{AttributeTypes::Param, ParamIDs::Feature, DFIPKeys::GovUnset}; + if (!attributes->GetValue(key, false)) { + return Res::Err("Unset Gov variables not currently enabled in attributes."); + } + + for (const auto &[name, keys] : obj.govs) { + auto var = mnview.GetVariable(name); + if (!var) { + return Res::Err("'%s': variable does not registered", name); + } + + auto res = var->Erase(mnview, height, keys); + if (!res) { + return Res::Err("%s: %s", name, res.msg); + } if (!(res = mnview.SetVariable(*var))) { return Res::Err("%s: %s", name, res.msg); @@ -156,11 +225,19 @@ Res CGovernanceConsensus::operator()(const CGovernanceHeightMessage &obj) const const auto &consensus = txCtx.GetConsensus(); const auto height = txCtx.GetHeight(); auto &mnview = blockCtx.GetView(); + const auto attributes = mnview.GetAttributes(); if (obj.startHeight <= height) { return Res::Err("startHeight must be above the current block height"); } + CDataStructureV0 minBlockKey{AttributeTypes::Param, ParamIDs::GovernanceParam, DFIPKeys::GovHeightMinBlocks}; + const auto minBlocks = attributes->GetValue(minBlockKey, uint64_t{}); + + if (obj.startHeight <= height + minBlocks) { + return Res::Err("Height must be %d blocks above the current height", minBlocks); + } + if (obj.govVar->GetName() == "ORACLE_BLOCK_INTERVAL") { return Res::Err("%s: %s", obj.govVar->GetName(), "Cannot set via setgovheight."); } @@ -194,7 +271,6 @@ Res CGovernanceConsensus::operator()(const CGovernanceHeightMessage &obj) const auto storedGovVars = mnview.GetStoredVariablesRange(height, obj.startHeight); Res res{}; - CCustomCSView govCache(mnview); for (const auto &[varHeight, var] : storedGovVars) { if (var->GetName() == "ATTRIBUTES") { if (res = govVar->Import(var->Export()); !res) { @@ -222,6 +298,7 @@ Res CGovernanceConsensus::operator()(const CGovernanceHeightMessage &obj) const } } + CCustomCSView govCache(mnview); if (!(res = govVar->Import(obj.govVar->Export())) || !(res = govVar->Validate(govCache)) || !(res = govVar->Apply(govCache, obj.startHeight))) { return Res::Err("%s: Cumulative application of Gov vars failed: %s", obj.govVar->GetName(), res.msg); diff --git a/src/dfi/consensus/governance.h b/src/dfi/consensus/governance.h index 42bbfcb440..befde8e6c8 100644 --- a/src/dfi/consensus/governance.h +++ b/src/dfi/consensus/governance.h @@ -10,6 +10,8 @@ struct CGovernanceMessage; struct CGovernanceHeightMessage; struct CGovernanceUnsetMessage; +struct CGovernanceUnsetHeightMessage; +struct CGovernanceClearHeightMessage; class CGovernanceConsensus : public CCustomTxVisitor { public: @@ -17,6 +19,8 @@ class CGovernanceConsensus : public CCustomTxVisitor { Res operator()(const CGovernanceMessage &obj) const; Res operator()(const CGovernanceHeightMessage &obj) const; Res operator()(const CGovernanceUnsetMessage &obj) const; + Res operator()(const CGovernanceUnsetHeightMessage &obj) const; + Res operator()(const CGovernanceClearHeightMessage &obj) const; }; #endif // DEFI_DFI_CONSENSUS_GOVERNANCE_H diff --git a/src/dfi/customtx.cpp b/src/dfi/customtx.cpp index 6bc7a86842..190aa50fe9 100644 --- a/src/dfi/customtx.cpp +++ b/src/dfi/customtx.cpp @@ -71,6 +71,8 @@ CustomTxType CustomTxCodeToType(uint8_t ch) { case CustomTxType::Vote: case CustomTxType::CreateVoc: case CustomTxType::UnsetGovVariable: + case CustomTxType::UnsetGovHeightVariable: + case CustomTxType::ClearGovHeights: case CustomTxType::TransferDomain: case CustomTxType::EvmTx: case CustomTxType::None: @@ -203,6 +205,10 @@ std::string ToString(CustomTxType type) { return "Vote"; case CustomTxType::UnsetGovVariable: return "UnsetGovVariable"; + case CustomTxType::UnsetGovHeightVariable: + return "UnsetGovHeightVariable"; + case CustomTxType::ClearGovHeights: + return "ClearGovHeights"; case CustomTxType::TransferDomain: return "TransferDomain"; case CustomTxType::EvmTx: diff --git a/src/dfi/customtx.h b/src/dfi/customtx.h index eb08ecc211..74e6217813 100644 --- a/src/dfi/customtx.h +++ b/src/dfi/customtx.h @@ -50,6 +50,9 @@ enum class CustomTxType : uint8_t { // set governance variable SetGovVariable = 'G', SetGovVariableHeight = 'j', + UnsetGovVariable = 'Z', + UnsetGovHeightVariable = '-', + ClearGovHeights = '+', // Auto auth TX AutoAuthPrep = 'A', // oracles @@ -93,7 +96,6 @@ enum class CustomTxType : uint8_t { Vote = 'O', // NOTE: Check whether this overlapping with CreateOrder above is fine CreateVoc = 'E', // NOTE: Check whether this overlapping with DestroyOrder above is fine ProposalFeeRedistribution = 'Y', - UnsetGovVariable = 'Z', // EVM TransferDomain = '8', EvmTx = '9', diff --git a/src/dfi/govvariables/attributes.cpp b/src/dfi/govvariables/attributes.cpp index afdc60cf0e..3522e9f4cf 100644 --- a/src/dfi/govvariables/attributes.cpp +++ b/src/dfi/govvariables/attributes.cpp @@ -289,6 +289,7 @@ const std::map> &ATTRIBUTES::allowedKeys {"average_liquidity_percentage", DFIPKeys::AverageLiquidityPercentage}, {"governance", DFIPKeys::CommunityGovernance}, {"ascending_block_time", DFIPKeys::AscendingBlockTime}, + {"govheight_min_blocks", DFIPKeys::GovHeightMinBlocks}, }}, {AttributeTypes::EVMType, { @@ -396,6 +397,7 @@ const std::map> &ATTRIBUTES::displayKeys {DFIPKeys::AverageLiquidityPercentage, "average_liquidity_percentage"}, {DFIPKeys::CommunityGovernance, "governance"}, {DFIPKeys::AscendingBlockTime, "ascending_block_time"}, + {DFIPKeys::GovHeightMinBlocks, "govheight_min_blocks"}, }}, {AttributeTypes::EVMType, { @@ -829,6 +831,7 @@ const std::map( {DFIPKeys::AverageLiquidityPercentage, VerifyPctInt64}, {DFIPKeys::CommunityGovernance, VerifyBool}, {DFIPKeys::AscendingBlockTime, VerifyBool}, + {DFIPKeys::GovHeightMinBlocks, VerifyMoreThenZeroUInt64}, }}, {AttributeTypes::Locks, { @@ -973,6 +976,14 @@ Res StoreGovVars(const CGovernanceHeightMessage &obj, CCustomCSView &view) { return view.SetStoredVariables(storedGovVars, obj.startHeight); } +Res StoreUnsetGovVars(const CGovernanceUnsetHeightMessage &obj, CCustomCSView &view) { + auto storedGovVars = view.GetUnsetStoredVariables(obj.unsetHeight); + for (const auto &[name, keys] : obj.govs) { + storedGovVars.emplace(name, keys); + } + return view.SetUnsetStoredVariables(storedGovVars, obj.unsetHeight); +} + static Res CheckValidAttrV0Key(const uint8_t type, const uint32_t typeId, const uint32_t typeKey) { if (type == AttributeTypes::Param) { if (typeId == ParamIDs::DFIP2201) { @@ -1004,7 +1015,7 @@ static Res CheckValidAttrV0Key(const uint8_t type, const uint32_t typeId, const return DeFiErrors::GovVarVariableUnsupportedFeatureType(typeKey); } } else if (typeId == ParamIDs::Foundation || typeId == ParamIDs::GovernanceParam) { - if (typeKey != DFIPKeys::Members) { + if (typeKey != DFIPKeys::Members && typeKey != DFIPKeys::GovHeightMinBlocks) { return DeFiErrors::GovVarVariableUnsupportedFoundationType(typeKey); } } else if (typeId != ParamIDs::dTokenRestart) { diff --git a/src/dfi/govvariables/attributes.h b/src/dfi/govvariables/attributes.h index 42de85e769..3d8dea5ec4 100644 --- a/src/dfi/govvariables/attributes.h +++ b/src/dfi/govvariables/attributes.h @@ -128,8 +128,9 @@ enum DFIPKeys : uint8_t { TransferDomain = 'w', LiquidityCalcSamplingPeriod = 'x', AverageLiquidityPercentage = 'y', - CommunityGovernance = 'C', AscendingBlockTime = 'A', + GovHeightMinBlocks = 'B', + CommunityGovernance = 'C', }; enum GovernanceKeys : uint8_t { @@ -406,6 +407,7 @@ void TrackDUSDSub(CCustomCSView &mnview, const CTokenAmount &amount); bool IsEVMEnabled(const std::shared_ptr attributes); bool IsEVMEnabled(const CCustomCSView &view); Res StoreGovVars(const CGovernanceHeightMessage &obj, CCustomCSView &view); +Res StoreUnsetGovVars(const CGovernanceUnsetHeightMessage &obj, CCustomCSView &view); Res GovernanceMemberRemoval(ATTRIBUTES &newVar, ATTRIBUTES &prevVar, const CDataStructureV0 &memberKey, diff --git a/src/dfi/gv.cpp b/src/dfi/gv.cpp index 315768f4ac..cc7c71b58b 100644 --- a/src/dfi/gv.cpp +++ b/src/dfi/gv.cpp @@ -116,6 +116,48 @@ void CGovView::EraseStoredVariables(const uint32_t height) { } } +Res CGovView::SetUnsetStoredVariables(const UnsetGovVars &govVars, const uint32_t height) { + for (auto &[name, keys] : govVars) { + if (auto res = WriteBy(GovVarKey{height, name}, keys); !res) { + return DeFiErrors::GovVarFailedWrite(); + } + } + + return Res::Ok(); +} + +CGovView::UnsetGovVars CGovView::GetUnsetStoredVariables(const uint32_t height) { + UnsetGovVars govVars; + auto it = LowerBound(GovVarKey{height, {}}); + for (; it.Valid() && it.Key().height == height; it.Next()) { + govVars.emplace(it.Key().name, it.Value()); + } + return govVars; +} + +std::multimap>> CGovView::GetAllUnsetStoredVariables() { + std::multimap>> govVars; + auto it = LowerBound(GovVarKey{std::numeric_limits::min(), {}}); + for (; it.Valid(); it.Next()) { + std::map> entry{ + {it.Key().height, it.Value()} + }; + govVars.emplace(it.Key().name, entry); + } + + return govVars; +} + +void CGovView::EraseUnsetStoredVariables(const uint32_t height) { + // Retrieve map of vars at specified height + const auto vars = GetUnsetStoredVariables(height); + + // Iterate over names at this height and erase + for (const auto &[name, _] : vars) { + EraseBy(GovVarKey{height, name}); + } +} + std::shared_ptr CGovView::GetAttributes() const { const auto var = GetVariable("ATTRIBUTES"); assert(var); diff --git a/src/dfi/gv.h b/src/dfi/gv.h index 173060804b..1d8fbb5389 100644 --- a/src/dfi/gv.h +++ b/src/dfi/gv.h @@ -95,18 +95,10 @@ struct CGovernanceHeightMessage { } }; -struct CGovernanceUnsetMessage { - std::map> govs; - - ADD_SERIALIZE_METHODS; - template - inline void SerializationOp(Stream &s, Operation ser_action) { - READWRITE(govs); - } -}; - class CGovView : public virtual CStorageView { public: + using UnsetGovVars = std::map>; + Res SetVariable(const GovVariable &var); std::shared_ptr GetVariable(const std::string &govKey) const; @@ -117,6 +109,11 @@ class CGovView : public virtual CStorageView { std::map>> GetAllStoredVariables(); void EraseStoredVariables(const uint32_t height); + Res SetUnsetStoredVariables(const UnsetGovVars &govVars, const uint32_t height); + UnsetGovVars GetUnsetStoredVariables(const uint32_t height); + std::multimap>> GetAllUnsetStoredVariables(); + void EraseUnsetStoredVariables(const uint32_t height); + std::shared_ptr GetAttributes() const; [[nodiscard]] virtual bool AreTokensLocked(const std::set &tokenIds) const = 0; @@ -127,6 +124,38 @@ class CGovView : public virtual CStorageView { struct ByName { static constexpr uint8_t prefix() { return 'g'; } }; + + struct ByUnsetHeightVars { + static constexpr uint8_t prefix() { return 0x7E; } + }; +}; + +struct CGovernanceUnsetMessage { + CGovView::UnsetGovVars govs; + + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream &s, Operation ser_action) { + READWRITE(govs); + } +}; + +struct CGovernanceUnsetHeightMessage { + CGovView::UnsetGovVars govs; + uint32_t unsetHeight; + + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream &s, Operation ser_action) { + READWRITE(govs); + READWRITE(unsetHeight); + } +}; + +struct CGovernanceClearHeightMessage { + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream &s, Operation ser_action) {} }; struct GovVarKey { diff --git a/src/dfi/masternodes.h b/src/dfi/masternodes.h index f510966518..b75ded726a 100644 --- a/src/dfi/masternodes.h +++ b/src/dfi/masternodes.h @@ -508,7 +508,7 @@ class CCustomCSView : public CMasternodesView, ByPoolReward, ByDailyReward, ByCustomReward, ByTotalLiquidity, ByDailyLoanReward, ByPoolLoanReward, ByTokenDexFeePct, ByLoanTokenLiquidityPerBlock, ByLoanTokenLiquidityAverage, ByTotalRewardPerShare, ByTotalLoanRewardPerShare, ByTotalCustomRewardPerShare, ByTotalCommissionPerShare, - CGovView :: ByName, ByHeightVars, + CGovView :: ByName, ByHeightVars, ByUnsetHeightVars, CAnchorConfirmsView :: BtcTx, COracleView :: ByName, FixedIntervalBlockKey, FixedIntervalPriceKey, PriceDeviation, CICXOrderView :: ICXOrderCreationTx, ICXMakeOfferCreationTx, ICXSubmitDFCHTLCCreationTx, diff --git a/src/dfi/mn_checks.cpp b/src/dfi/mn_checks.cpp index 49de8eb424..5beb282180 100644 --- a/src/dfi/mn_checks.cpp +++ b/src/dfi/mn_checks.cpp @@ -156,6 +156,10 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType) { return CCustomTxMessageNone{}; case CustomTxType::UnsetGovVariable: return CGovernanceUnsetMessage{}; + case CustomTxType::UnsetGovHeightVariable: + return CGovernanceUnsetHeightMessage{}; + case CustomTxType::ClearGovHeights: + return CGovernanceClearHeightMessage{}; case CustomTxType::TransferDomain: return CTransferDomainMessage{}; case CustomTxType::EvmTx: @@ -282,7 +286,10 @@ class CCustomMetadataParseVisitor { return IsHardforkEnabled(consensus.DF20GrandCentralHeight); } else if constexpr (IsOneOf()) { return IsHardforkEnabled(consensus.DF22MetachainHeight); - } else if constexpr (IsOneOf()) { + } else if constexpr (IsOneOf()) { return IsHardforkEnabled(consensus.DF24Height); } else if constexpr (IsOneOf()) { return Res::Ok(); diff --git a/src/dfi/mn_checks.h b/src/dfi/mn_checks.h index db1fcb286c..69d5c59b04 100644 --- a/src/dfi/mn_checks.h +++ b/src/dfi/mn_checks.h @@ -122,6 +122,8 @@ using CCustomTxMessage = std::variant> govs; - if (request.params.size() > 0 && request.params[0].isObject()) { - for (const std::string &name : request.params[0].getKeys()) { - auto gv = GovVariable::Create(name); - if (!gv) { - throw JSONRPCError(RPC_INVALID_REQUEST, "Variable " + name + " not registered"); - } - auto &keys = govs[name]; - const auto &value = request.params[0][name]; - if (value.isArray()) { - for (const auto &key : value.getValues()) { - keys.push_back(key.get_str()); - } + CGovView::UnsetGovVars govs; + for (const std::string &name : request.params[0].getKeys()) { + if (auto gv = GovVariable::Create(name); !gv) { + throw JSONRPCError(RPC_INVALID_REQUEST, "Variable " + name + " not registered"); + } + auto &keys = govs[name]; + const auto &value = request.params[0][name]; + if (value.isArray()) { + for (const auto &key : value.getValues()) { + keys.push_back(key.get_str()); } } } @@ -821,21 +818,21 @@ UniValue setgovheight(const JSONRPCRequest &request) { CDataStream varStream(SER_NETWORK, PROTOCOL_VERSION); const auto keys = request.params[0].getKeys(); - if (!keys.empty()) { - const std::string &name = request.params[0].getKeys()[0]; - auto gv = GovVariable::Create(name); - if (!gv) { - throw JSONRPCError(RPC_INVALID_REQUEST, "Variable " + name + " not registered"); - } - const auto res = gv->Import(request.params[0][name]); - if (!res) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, res.msg); - } - varStream << name << *gv; - } else { + if (keys.empty()) { throw JSONRPCError(RPC_INVALID_REQUEST, "No Governance variable provided."); } + const std::string &name = request.params[0].getKeys()[0]; + auto gv = GovVariable::Create(name); + if (!gv) { + throw JSONRPCError(RPC_INVALID_REQUEST, "Variable " + name + " not registered"); + } + const auto res = gv->Import(request.params[0][name]); + if (!res) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, res.msg); + } + varStream << name << *gv; + const uint32_t startHeight = request.params[1].get_int(); CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); @@ -875,6 +872,193 @@ UniValue setgovheight(const JSONRPCRequest &request) { return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); } +UniValue unsetgovheight(const JSONRPCRequest &request) { + auto pwallet = GetWallet(request); + + RPCHelpMan{ + "unsetgovheight", + "\nUnset governance variables at height: ATTRIBUTES, ICX_TAKERFEE_PER_BTC, LP_LOAN_TOKEN_SPLITS, LP_SPLITS, " + "ORACLE_BLOCK_INTERVAL, ORACLE_DEVIATION\n", + { + { + "variables", + RPCArg::Type::OBJ, + RPCArg::Optional::NO, + "Object with variables", + { + {"name", RPCArg::Type::STR, RPCArg::Optional::NO, "Variable's name is the key."}, + }, + }, {"height", RPCArg::Type::NUM, RPCArg::Optional::NO, "Start height for the changes to take effect."}, + { + "inputs", + RPCArg::Type::ARR, + RPCArg::Optional::OMITTED_NAMED_ARG, + "A json array of json objects", + { + { + "", + RPCArg::Type::OBJ, + RPCArg::Optional::OMITTED, + "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, + }, + }, + }, + }, }, + RPCResult{"\"hash\" (string) The hex-encoded hash of broadcasted transaction\n"}, + RPCExamples{HelpExampleCli("unsetgovheight", "'{\"LP_SPLITS\": [\"2\",\"3\"]}'") + + HelpExampleRpc("unsetgovheight", "'{\"ATTRIBUTES\": [\"v0/params/feature/pizza-party\"]}'")}, + } + .Check(request); + + if (pwallet->chain().isInitialBlockDownload()) { + throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, + "Cannot create transactions while still in Initial Block Download"); + } + pwallet->BlockUntilSyncedToCurrentChain(); + + RPCTypeCheck(request.params, {UniValue::VOBJ, UniValue::VNUM, UniValue::VARR}, true); + + CGovView::UnsetGovVars govs; + for (const std::string &name : request.params[0].getKeys()) { + if (auto gv = GovVariable::Create(name); !gv) { + throw JSONRPCError(RPC_INVALID_REQUEST, "Variable " + name + " not registered"); + } + auto &unsetKeys = govs[name]; + const auto &value = request.params[0][name]; + if (value.isArray()) { + for (const auto &key : value.getValues()) { + unsetKeys.push_back(key.get_str()); + } + } + } + + const uint32_t unsetHeight = request.params[1].get_int(); + + CGovernanceUnsetHeightMessage unsetGovMsg{govs, unsetHeight}; + + CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + metadata << static_cast(CustomTxType::UnsetGovHeightVariable) << unsetGovMsg; + + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(metadata); + + auto [view, accountView, vaultView] = GetSnapshots(); + auto targetHeight = view->GetLastHeight() + 1; + + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + rawTx.vout.push_back(CTxOut(0, scriptMeta)); + + const UniValue &txInputs = request.params[1]; + CTransactionRef optAuthTx; + std::set auths; + rawTx.vin = GetAuthInputsSmart( + pwallet, rawTx.nVersion, auths, true, optAuthTx, txInputs, *view, request.metadata.coinSelectOpts, true); + + CCoinControl coinControl; + + // Set change to selected foundation address + if (!auths.empty()) { + CTxDestination dest; + ExtractDestination(*auths.cbegin(), dest); + if (IsValidDestination(dest)) { + coinControl.destChange = dest; + } + } + + fund(rawTx, pwallet, optAuthTx, &coinControl, request.metadata.coinSelectOpts); + + // check execution + execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); + + return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); +} + +UniValue cleargovheights(const JSONRPCRequest &request) { + auto pwallet = GetWallet(request); + + RPCHelpMan{ + "cleargovheights", + "\nClear all pending setgovheight and unsetgovheight changes\n", + {{ + "inputs", + RPCArg::Type::ARR, + RPCArg::Optional::OMITTED_NAMED_ARG, + "A json array of json objects", + { + { + "", + RPCArg::Type::OBJ, + RPCArg::Optional::OMITTED, + "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, + }, + }, + }, + }}, + RPCResult{"\"hash\" (string) The hex-encoded hash of broadcasted transaction\n"}, + RPCExamples{HelpExampleRpc("cleargovheights", "")}, + } + .Check(request); + + RPCTypeCheck(request.params, {UniValue::VARR}, true); + + if (pwallet->chain().isInitialBlockDownload()) { + throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, + "Cannot create transactions while still in Initial Block Download"); + } + pwallet->BlockUntilSyncedToCurrentChain(); + + auto [view, accountView, vaultView] = GetSnapshots(); + + CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + metadata << static_cast(CustomTxType::ClearGovHeights); + + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(metadata); + + int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; + + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + rawTx.vout.emplace_back(0, scriptMeta); + + CTransactionRef optAuthTx; + std::set auths; + rawTx.vin = GetAuthInputsSmart(pwallet, + rawTx.nVersion, + auths, + true, + optAuthTx, + request.params[0], + *view, + request.metadata.coinSelectOpts, + true); + + CCoinControl coinControl; + + // Set change to selected address + if (!auths.empty()) { + CTxDestination dest; + ExtractDestination(*auths.cbegin(), dest); + if (IsValidDestination(dest)) { + coinControl.destChange = dest; + } + } + + fund(rawTx, pwallet, optAuthTx, &coinControl, request.metadata.coinSelectOpts); + + // check execution + execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); + + return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); +} + UniValue getgov(const JSONRPCRequest &request) { RPCHelpMan{ "getgov", @@ -986,6 +1170,7 @@ UniValue listgovs(const JSONRPCRequest &request) { // Get all stored Gov var changes auto pending = view->GetAllStoredVariables(); + auto pendingUnset = view->GetAllUnsetStoredVariables(); const auto height = view->GetLastHeight(); UniValue result(UniValue::VARR); @@ -1022,11 +1207,23 @@ UniValue listgovs(const JSONRPCRequest &request) { } // Get and add any pending changes - for (const auto &items : pending[name]) { + for (const auto &[height, var] : pending[name]) { UniValue ret(UniValue::VOBJ); - ret.pushKV(std::to_string(items.first), items.second->Export()); + ret.pushKV(std::to_string(height), var->Export()); innerResult.push_back(ret); } + const auto range = pendingUnset.equal_range(name); + for (auto it = range.first; it != range.second; ++it) { + for (auto &[height, keys] : it->second) { + UniValue retKeys(UniValue::VARR); + for (const auto &key : keys) { + retKeys.push_back(key); + } + UniValue ret(UniValue::VOBJ); + ret.pushKV(std::to_string(height), retKeys); + innerResult.push_back(ret); + } + } result.push_back(innerResult); } @@ -1191,11 +1388,13 @@ static const CRPCCommand commands[] = { {"blockchain", "setgov", &setgov, {"variables", "inputs"} }, {"blockchain", "unsetgov", &unsetgov, {"variables", "inputs"} }, {"blockchain", "setgovheight", &setgovheight, {"variables", "height", "inputs"}}, + {"blockchain", "unsetgovheight", &unsetgovheight, {"variables", "height", "inputs"}}, {"blockchain", "getgov", &getgov, {"name"} }, {"blockchain", "listgovs", &listgovs, {"prefix"} }, {"blockchain", "isappliedcustomtx", &isappliedcustomtx, {"txid", "blockHeight"} }, {"blockchain", "listsmartcontracts", &listsmartcontracts, {} }, {"blockchain", "clearmempool", &clearmempool, {} }, + {"blockchain", "cleargovheights", &cleargovheights, {"inputs"} }, }; void RegisterMNBlockchainRPCCommands(CRPCTable &tableRPC) { diff --git a/src/dfi/rpc_customtx.cpp b/src/dfi/rpc_customtx.cpp index d0fa3b143c..ffa0aaa31d 100644 --- a/src/dfi/rpc_customtx.cpp +++ b/src/dfi/rpc_customtx.cpp @@ -341,6 +341,20 @@ class CCustomTxRpcVisitor { rpcInfo.pushKV("startHeight", static_cast(obj.startHeight)); } + void operator()(const CGovernanceUnsetHeightMessage &obj) const { + for (const auto &gov : obj.govs) { + UniValue keys(UniValue::VARR); + for (const auto &key : gov.second) { + keys.push_back(key); + } + + rpcInfo.pushKV(gov.first, keys); + } + rpcInfo.pushKV("unsetHeight", static_cast(obj.unsetHeight)); + } + + void operator()(const CGovernanceClearHeightMessage &obj) const {} + void operator()(const CAppointOracleMessage &obj) const { rpcInfo.pushKV("oracleAddress", ScriptToString(obj.oracleAddress)); rpcInfo.pushKV("weightage", obj.weightage); diff --git a/src/dfi/validation.cpp b/src/dfi/validation.cpp index 409ee8d4c9..f1bed7ae43 100644 --- a/src/dfi/validation.cpp +++ b/src/dfi/validation.cpp @@ -1395,6 +1395,30 @@ static void ProcessGovEvents(const CBlockIndex *pindex, } } cache.EraseStoredVariables(static_cast(pindex->nHeight)); + + if (pindex->nHeight < consensus.DF24Height) { + return; + } + + const auto storedUnsetGovVars = cache.GetUnsetStoredVariables(pindex->nHeight); + for (const auto &[name, keys] : storedUnsetGovVars) { + CCustomCSView govCache(cache); + auto var = govCache.GetVariable(name); + if (!var) { + continue; + } + + auto res = var->Erase(govCache, pindex->nHeight, keys); + if (!res) { + continue; + } + + if (govCache.SetVariable(*var)) { + govCache.Flush(); + } + } + + cache.EraseUnsetStoredVariables(pindex->nHeight); } static bool ApplyGovVars(CCustomCSView &cache, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index cd7d0a1832..f5f9025654 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -332,6 +332,12 @@ static const CRPCConvertParam vRPCConvertParams[] = { "setgovheight", 1, "height" }, { "setgovheight", 2, "inputs" }, + { "unsetgovheight", 0, "variables" }, + { "unsetgovheight", 1, "height" }, + { "unsetgovheight", 2, "inputs" }, + + { "cleargovheights", 0, "inputs" }, + { "isappliedcustomtx", 1, "blockHeight" }, { "sendtokenstoaddress", 0, "from" }, { "sendtokenstoaddress", 1, "to" }, diff --git a/test/functional/feature_community_governance.py b/test/functional/feature_community_governance.py index 4654b23088..8b3e1a444b 100755 --- a/test/functional/feature_community_governance.py +++ b/test/functional/feature_community_governance.py @@ -9,6 +9,7 @@ from test_framework.util import assert_equal, assert_raises_rpc_error import time +from decimal import Decimal token_depr_gov_err_msg = "Only token deprecation toggle is allowed by governance" @@ -65,6 +66,12 @@ def run_test(self): # Test updating Governance self.govvar_checks() + # Test unsetgovheight + self.unsetgovheight_checks() + + # Test cleargovheights + self.cleargovheights_checks() + # Test creating Governance DAT self.governanace_dat() @@ -189,6 +196,13 @@ def pre_fork_checks(self): {"ATTRIBUTES": {"v0/params/feature/governance": "true"}}, ) + assert_raises_rpc_error( + -32600, + "Cannot be set before DF24Height", + self.nodes[0].setgov, + {"ATTRIBUTES": {"v0/params/governance/govheight_min_blocks": "1000"}}, + ) + assert_raises_rpc_error( -32600, "Token cannot be deprecated below DF24Height", @@ -382,40 +396,6 @@ def member_update_and_errors(self): def govvar_checks(self): - # Test updating Governanace - self.nodes[1].setgov( - { - "ATTRIBUTES": { - "v0/params/dfip2201/active": "false", - } - } - ) - self.nodes[1].generate(1) - - # Check other key updated - assert_equal( - self.nodes[1].getgov("ATTRIBUTES")["ATTRIBUTES"][ - "v0/params/dfip2203/active" - ], - "true", - ) - - # Try to update Foundation member - assert_raises_rpc_error( - -32600, - "Foundation cannot be modified by governance", - self.nodes[1].setgov, - {"ATTRIBUTES": {"v0/params/foundation/members": [self.governance_member]}}, - ) - - # Try to deactivate foundation - assert_raises_rpc_error( - -32600, - "Foundation cannot be modified by governance", - self.nodes[1].setgov, - {"ATTRIBUTES": {"v0/params/feature/gov-foundation": "false"}}, - ) - # Try to update Foundation member by height activation_height = self.nodes[1].getblockcount() + 5 assert_raises_rpc_error( @@ -426,7 +406,7 @@ def govvar_checks(self): activation_height, ) - # Try to deactivate foundation bu height + # Try to deactivate foundation by height assert_raises_rpc_error( -32600, "Foundation cannot be modified by governance", @@ -439,17 +419,203 @@ def govvar_checks(self): assert_raises_rpc_error( -32600, "Foundation cannot be modified by governance", - self.nodes[1].unsetgov, + self.nodes[1].unsetgovheight, {"ATTRIBUTES": ["v0/params/foundation/members"]}, + activation_height, ) # Try to unset Foundation activation assert_raises_rpc_error( -32600, "Foundation cannot be modified by governance", - self.nodes[1].unsetgov, + self.nodes[1].unsetgovheight, {"ATTRIBUTES": ["v0/params/feature/gov-foundation"]}, + activation_height, + ) + + def cleargovheights_checks(self): + + # Get current block + current_block = self.nodes[0].getblockcount() + + # Create multiple set and unset heights + self.nodes[0].setgovheight({"ORACLE_DEVIATION": "0.5"}, current_block + 100) + self.nodes[0].setgovheight( + {"ATTRIBUTES": {"v0/params/dfip2206f/active": "true"}}, current_block + 200 ) + self.nodes[0].unsetgovheight( + {"ORACLE_DEVIATION": ""}, + current_block + 101, + ) + self.nodes[0].unsetgovheight( + {"ATTRIBUTES": ["v0/params/dfip2206f/active"]}, + current_block + 201, + ) + self.nodes[0].generate(1) + + # Check pending changes shown + result = self.nodes[0].listgovs() + assert_equal(len(result[7]), 3) + assert_equal(len(result[8]), 3) + assert_equal(result[7][1], {f"{current_block + 100}": Decimal("0.50000000")}) + assert_equal(result[7][2], {f"{current_block + 101}": []}) + assert_equal( + result[8][1], + {f"{current_block + 200}": {"v0/params/dfip2206f/active": "true"}}, + ) + assert_equal( + result[8][2], {f"{current_block + 201}": ["v0/params/dfip2206f/active"]} + ) + + # Clear all pending changes + self.nodes[0].cleargovheights() + self.nodes[0].generate(1) + + # Check pending changes cleared + result = self.nodes[0].listgovs() + assert_equal(len(result[7]), 1) + assert_equal(len(result[8]), 1) + + def unsetgovheight_checks(self): + + # Set params to unset + self.nodes[0].setgov( + { + "ORACLE_DEVIATION": "1", + "ATTRIBUTES": { + "v0/params/dfip2206f/active": "false", + }, + } + ) + self.nodes[0].generate(1) + self.sync_blocks() + + # Check vars set + result = self.nodes[0].getgov("ORACLE_DEVIATION") + assert_equal(result["ORACLE_DEVIATION"], Decimal("1.00000000")) + result = self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] + assert_equal(result["v0/params/dfip2206f/active"], "false") + + # Save block for rollback + rollback_height = self.nodes[0].getblockcount() + + # Try to unset and next height + assert_raises_rpc_error( + -32600, + "unsetHeight must be above the current block height", + self.nodes[0].unsetgovheight, + {"ORACLE_DEVIATION": ""}, + rollback_height + 1, + ) + + # Foundation unset + self.nodes[0].unsetgovheight( + {"ORACLE_DEVIATION": "", "ATTRIBUTES": ["v0/params/dfip2206f/active"]}, + rollback_height + 2, + ) + self.nodes[0].generate(2) + + # Check keys no longer set + result = self.nodes[0].getgov("ORACLE_DEVIATION") + assert_equal(result["ORACLE_DEVIATION"], Decimal("0E-8")) + assert ( + "v0/params/dfip2206f/active" + not in self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] + ) + + # Rollback to before unset + self.rollback_to(rollback_height) + + # Governance unset + self.nodes[1].unsetgovheight( + {"ORACLE_DEVIATION": "", "ATTRIBUTES": ["v0/params/dfip2206f/active"]}, + rollback_height + 2, + ) + self.nodes[1].generate(2) + + # Check keys no longer set + result = self.nodes[1].getgov("ORACLE_DEVIATION") + assert_equal(result["ORACLE_DEVIATION"], Decimal("0E-8")) + assert ( + "v0/params/dfip2206f/active" + not in self.nodes[1].getgov("ATTRIBUTES")["ATTRIBUTES"] + ) + + # Rollback to before unset + self.rollback_to(rollback_height) + + # Set minimum height for set and under gov + self.nodes[0].setgov( + { + "ATTRIBUTES": { + "v0/params/governance/govheight_min_blocks": "10", + }, + } + ) + self.nodes[0].generate(1) + self.sync_blocks() + + # Get current block + current_block = self.nodes[0].getblockcount() + + # Test setting with the min block limit + assert_raises_rpc_error( + -32600, + "Height must be 10 blocks above the current height", + self.nodes[0].unsetgovheight, # Foundation + {"ORACLE_DEVIATION": ""}, + current_block + 10, + ) + + assert_raises_rpc_error( + -32600, + "Height must be 10 blocks above the current height", + self.nodes[1].unsetgovheight, # Governance + {"ORACLE_DEVIATION": ""}, + current_block + 10, + ) + + assert_raises_rpc_error( + -32600, + "Height must be 10 blocks above the current height", + self.nodes[0].setgovheight, # Foundation + {"ORACLE_DEVIATION": "0.5"}, + current_block + 10, + ) + + assert_raises_rpc_error( + -32600, + "Height must be 10 blocks above the current height", + self.nodes[1].setgovheight, # Governance + {"ORACLE_DEVIATION": "0.5"}, + current_block + 10, + ) + + # Test setting with the min block limit + self.nodes[0].setgovheight({"ORACLE_DEVIATION": "0.5"}, current_block + 12) + self.nodes[0].generate(12) + + # Check results + result = self.nodes[0].getgov("ORACLE_DEVIATION") + assert_equal(result["ORACLE_DEVIATION"], Decimal("0.50000000")) + + # Get current block + current_block = self.nodes[0].getblockcount() + + # Test unsetting with min block limit + self.nodes[0].unsetgovheight({"ORACLE_DEVIATION": ""}, current_block + 12) + self.nodes[0].generate(1) + + # Check pending changes shown + result = self.nodes[0].listgovs() + assert_equal(result[7][1], {f"{current_block + 12}": []}) + + # Move to unset height + self.nodes[0].generate(11) + + # Check results + result = self.nodes[0].getgov("ORACLE_DEVIATION") + assert_equal(result["ORACLE_DEVIATION"], Decimal("0E-8")) def governanace_dat(self): From e1972d86bafb6758edbc5c486f92decf57e99604 Mon Sep 17 00:00:00 2001 From: canonbrother Date: Tue, 24 Sep 2024 10:05:51 +0800 Subject: [PATCH 11/12] feat: unfreeze mn (#3062) * unfreeze mn * br --------- Co-authored-by: Prasanna Loganathar --- src/dfi/consensus/masternodes.cpp | 7 + src/dfi/errors.h | 4 + src/dfi/govvariables/attributes.cpp | 27 ++- src/dfi/govvariables/attributes.h | 2 + src/dfi/masternodes.cpp | 25 ++- src/dfi/masternodes.h | 8 +- src/dfi/rpc_masternodes.cpp | 2 +- src/dfi/validation.cpp | 22 +++ src/miner.cpp | 2 +- src/pos.cpp | 2 +- src/rpc/mining.cpp | 2 +- .../feature_unfreeze_masternodes.py | 187 ++++++++++++++++++ test/functional/test_runner.py | 1 + 13 files changed, 280 insertions(+), 11 deletions(-) create mode 100755 test/functional/feature_unfreeze_masternodes.py diff --git a/src/dfi/consensus/masternodes.cpp b/src/dfi/consensus/masternodes.cpp index 2c1b16741d..0c08d1db70 100644 --- a/src/dfi/consensus/masternodes.cpp +++ b/src/dfi/consensus/masternodes.cpp @@ -42,6 +42,13 @@ Res CMasternodesConsensus::operator()(const CCreateMasterNodeMessage &obj) const } if (height >= static_cast(consensus.DF10EunosPayaHeight)) { + const auto attributes = mnview.GetAttributes(); + CDataStructureV0 unfreezeKey{AttributeTypes::Param, ParamIDs::Feature, DFIPKeys::UnfreezeMasternodes}; + const auto unfreezeHeight = attributes->GetValue(unfreezeKey, std::numeric_limits::max()); + if (static_cast(unfreezeHeight) < height && obj.timelock != 0) { + return Res::Err("Masternode timelock disabled"); + } + switch (obj.timelock) { case CMasternode::ZEROYEAR: case CMasternode::FIVEYEAR: diff --git a/src/dfi/errors.h b/src/dfi/errors.h index b9465b420d..6e3e33bf1a 100644 --- a/src/dfi/errors.h +++ b/src/dfi/errors.h @@ -323,6 +323,10 @@ class DeFiErrors { static Res GovVarApplyBelowHeight() { return Res::Err("Cannot be set at or below current height"); } + static Res GovVarAfterFreezerActivation() { + return Res::Err("Cannot change masternode unfreeze height after activation"); + } + static Res GovVarApplyAutoNoToken(const uint32_t token) { return Res::Err("Auto lock. No loan token with id (%d)", token); } diff --git a/src/dfi/govvariables/attributes.cpp b/src/dfi/govvariables/attributes.cpp index 3522e9f4cf..88992b7343 100644 --- a/src/dfi/govvariables/attributes.cpp +++ b/src/dfi/govvariables/attributes.cpp @@ -287,6 +287,7 @@ const std::map> &ATTRIBUTES::allowedKeys {"transferdomain", DFIPKeys::TransferDomain}, {"liquidity_calc_sampling_period", DFIPKeys::LiquidityCalcSamplingPeriod}, {"average_liquidity_percentage", DFIPKeys::AverageLiquidityPercentage}, + {"unfreeze_masternodes", DFIPKeys::UnfreezeMasternodes}, {"governance", DFIPKeys::CommunityGovernance}, {"ascending_block_time", DFIPKeys::AscendingBlockTime}, {"govheight_min_blocks", DFIPKeys::GovHeightMinBlocks}, @@ -395,6 +396,7 @@ const std::map> &ATTRIBUTES::displayKeys {DFIPKeys::TransferDomain, "transferdomain"}, {DFIPKeys::LiquidityCalcSamplingPeriod, "liquidity_calc_sampling_period"}, {DFIPKeys::AverageLiquidityPercentage, "average_liquidity_percentage"}, + {DFIPKeys::UnfreezeMasternodes, "unfreeze_masternodes"}, {DFIPKeys::CommunityGovernance, "governance"}, {DFIPKeys::AscendingBlockTime, "ascending_block_time"}, {DFIPKeys::GovHeightMinBlocks, "govheight_min_blocks"}, @@ -829,6 +831,7 @@ const std::map( {DFIPKeys::TransferDomain, VerifyBool}, {DFIPKeys::LiquidityCalcSamplingPeriod, VerifyMoreThenZeroInt64}, {DFIPKeys::AverageLiquidityPercentage, VerifyPctInt64}, + {DFIPKeys::UnfreezeMasternodes, VerifyMoreThenZeroUInt64}, {DFIPKeys::CommunityGovernance, VerifyBool}, {DFIPKeys::AscendingBlockTime, VerifyBool}, {DFIPKeys::GovHeightMinBlocks, VerifyMoreThenZeroUInt64}, @@ -1011,7 +1014,7 @@ static Res CheckValidAttrV0Key(const uint8_t type, const uint32_t typeId, const typeKey != DFIPKeys::CFPPayout && typeKey != DFIPKeys::EmissionUnusedFund && typeKey != DFIPKeys::MintTokens && typeKey != DFIPKeys::EVMEnabled && typeKey != DFIPKeys::ICXEnabled && typeKey != DFIPKeys::TransferDomain && typeKey != DFIPKeys::CommunityGovernance && - typeKey != DFIPKeys::AscendingBlockTime) { + typeKey != DFIPKeys::UnfreezeMasternodes && typeKey != DFIPKeys::AscendingBlockTime) { return DeFiErrors::GovVarVariableUnsupportedFeatureType(typeKey); } } else if (typeId == ParamIDs::Foundation || typeId == ParamIDs::GovernanceParam) { @@ -1527,6 +1530,14 @@ Res ATTRIBUTES::Import(const UniValue &val) { return Res::Ok(); } else if (attrV0->type == AttributeTypes::Token && attrV0->key == TokenKeys::LoanMintingInterest) { interestTokens.insert(attrV0->typeId); + } else if (attrV0->type == AttributeTypes::Param && attrV0->typeId == ParamIDs::Feature && + attrV0->key == DFIPKeys::UnfreezeMasternodes) { + CDataStructureV0 unfreezeKey{ + AttributeTypes::Param, ParamIDs::Feature, DFIPKeys::UnfreezeMasternodes}; + if (CheckKey(unfreezeKey)) { + // Store current unfreeze height for validation later + unfreezeMasternodeHeight = GetValue(unfreezeKey, std::numeric_limits::max()); + } } if (attrV0->type == AttributeTypes::Param) { @@ -2100,6 +2111,20 @@ Res ATTRIBUTES::Validate(const CCustomCSView &view) const { if (view.GetLastHeight() < Params().GetConsensus().DF22MetachainHeight) { return Res::Err("Cannot be set before MetachainHeight"); } + } else if (attrV0->key == DFIPKeys::UnfreezeMasternodes) { + if (view.GetLastHeight() < Params().GetConsensus().DF24Height) { + return DeFiErrors::GovVarValidateDF24Height(); + } + if (unfreezeMasternodeHeight && *unfreezeMasternodeHeight < view.GetLastHeight()) { + return DeFiErrors::GovVarAfterFreezerActivation(); + } + const auto height = std::get_if(&value); + if (!height) { + return DeFiErrors::GovVarUnsupportedValue(); + } + if (*height <= view.GetLastHeight()) { + return DeFiErrors::GovVarApplyBelowHeight(); + } } else if (attrV0->key == DFIPKeys::CommunityGovernance || attrV0->key == DFIPKeys::AscendingBlockTime) { if (view.GetLastHeight() < Params().GetConsensus().DF24Height) { diff --git a/src/dfi/govvariables/attributes.h b/src/dfi/govvariables/attributes.h index 3d8dea5ec4..6892acf55f 100644 --- a/src/dfi/govvariables/attributes.h +++ b/src/dfi/govvariables/attributes.h @@ -128,6 +128,7 @@ enum DFIPKeys : uint8_t { TransferDomain = 'w', LiquidityCalcSamplingPeriod = 'x', AverageLiquidityPercentage = 'y', + UnfreezeMasternodes = 'z', AscendingBlockTime = 'A', GovHeightMinBlocks = 'B', CommunityGovernance = 'C', @@ -543,6 +544,7 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator unfreezeMasternodeHeight = std::nullopt; std::set tokenSplits{}; std::set interestTokens{}; std::set changed; diff --git a/src/dfi/masternodes.cpp b/src/dfi/masternodes.cpp index 9cd0e1870b..97f0c5f30d 100644 --- a/src/dfi/masternodes.cpp +++ b/src/dfi/masternodes.cpp @@ -102,6 +102,19 @@ CAmount GetProposalCreationFee(int, const CCustomCSView &view, const CCreateProp return -1; } +uint8_t GetTimelockLoops(const uint16_t timelock, const int blockHeight, const CCustomCSView &view) { + if (blockHeight < Params().GetConsensus().DF10EunosPayaHeight) { + return 1; + } + const auto attributes = view.GetAttributes(); + CDataStructureV0 unfreezeKey{AttributeTypes::Param, ParamIDs::Feature, DFIPKeys::UnfreezeMasternodes}; + const auto unfreezeHeight = attributes->GetValue(unfreezeKey, std::numeric_limits::max()); + if (static_cast(blockHeight) >= unfreezeHeight) { + return 1; + } + return timelock == CMasternode::TENYEAR ? 4 : timelock == CMasternode::FIVEYEAR ? 3 : 2; +} + CMasternode::CMasternode() : mintedBlocks(0), ownerAuthAddress(), @@ -531,7 +544,7 @@ void CMasternodesView::EraseSubNodesLastBlockTime(const uint256 &nodeId, const u std::optional CMasternodesView::GetTimelock(const uint256 &nodeId, const CMasternode &node, const uint64_t height) const { - if (const auto timelock = ReadBy(nodeId); timelock) { + if (const auto timelock = ReadTimelock(nodeId); timelock) { LOCK(cs_main); // Get last height auto lastHeight = height - 1; @@ -568,6 +581,14 @@ std::optional CMasternodesView::GetTimelock(const uint256 &nodeId, return 0; } +void CMasternodesView::EraseTimelock(const uint256 &nodeId) { + EraseBy(nodeId); +} + +std::optional CMasternodesView::ReadTimelock(const uint256 &nodeId) const { + return ReadBy(nodeId); +} + std::vector CMasternodesView::GetBlockTimes(const CKeyID &keyID, const uint32_t blockHeight, const int32_t creationHeight, @@ -592,7 +613,7 @@ std::vector CMasternodesView::GetBlockTimes(const CKeyID &keyID, } // If no values set for pre-fork MN use the fork time - const auto loops = GetTimelockLoops(timelock); + const auto loops = GetTimelockLoops(timelock, blockHeight, *static_cast(this)); for (uint8_t i{0}; i < loops; ++i) { if (!subNodesBlockTime[i]) { subNodesBlockTime[i] = block->GetBlockTime(); diff --git a/src/dfi/masternodes.h b/src/dfi/masternodes.h index b75ded726a..c529549afe 100644 --- a/src/dfi/masternodes.h +++ b/src/dfi/masternodes.h @@ -72,7 +72,7 @@ class CMasternode { UNKNOWN // unreachable }; - enum TimeLock { ZEROYEAR, FIVEYEAR = 260, TENYEAR = 520 }; + enum TimeLock : uint16_t { ZEROYEAR, FIVEYEAR = 260, TENYEAR = 520 }; enum Version : int32_t { PRE_FORT_CANNING = -1, @@ -143,9 +143,7 @@ class CMasternode { friend bool operator!=(const CMasternode &a, const CMasternode &b); }; -inline uint8_t GetTimelockLoops(uint16_t timelock) { - return timelock == CMasternode::TENYEAR ? 4 : timelock == CMasternode::FIVEYEAR ? 3 : 2; -} +uint8_t GetTimelockLoops(const uint16_t timelock, const int blockHeight, const CCustomCSView &view); struct CCreateMasterNodeMessage { char operatorType; @@ -313,6 +311,8 @@ class CMasternodesView : public virtual CStorageView { std::numeric_limits::max()}); std::optional GetTimelock(const uint256 &nodeId, const CMasternode &node, const uint64_t height) const; + std::optional ReadTimelock(const uint256 &nodeId) const; + void EraseTimelock(const uint256 &nodeId); // tags struct ID { diff --git a/src/dfi/rpc_masternodes.cpp b/src/dfi/rpc_masternodes.cpp index a69b3dd153..37f7198d6c 100644 --- a/src/dfi/rpc_masternodes.cpp +++ b/src/dfi/rpc_masternodes.cpp @@ -58,7 +58,7 @@ UniValue mnToJSON(CCustomCSView &view, view.GetBlockTimes(node.operatorAuthAddress, currentHeight + 1, node.creationHeight, *timelock); if (currentHeight >= Params().GetConsensus().DF10EunosPayaHeight) { - const auto loops = GetTimelockLoops(*timelock); + const auto loops = GetTimelockLoops(*timelock, currentHeight, view); UniValue multipliers(UniValue::VARR); for (uint8_t i{0}; i < loops; ++i) { multipliers.push_back( diff --git a/src/dfi/validation.cpp b/src/dfi/validation.cpp index f1bed7ae43..99a308c76b 100644 --- a/src/dfi/validation.cpp +++ b/src/dfi/validation.cpp @@ -4016,6 +4016,25 @@ static void ProcessTokenLock(const CBlock &block, LogPrintf(" - locking dToken oversupply took: %dms\n", GetTimeMillis() - time); } +static void ProcessUnfreezeMasternodes(const CBlockIndex *pindex, CCustomCSView &cache, BlockContext &blockCtx) { + const auto &consensus = blockCtx.GetConsensus(); + if (pindex->nHeight < consensus.DF24Height) { + return; + } + const auto attributes = cache.GetAttributes(); + CDataStructureV0 unfreezeKey{AttributeTypes::Param, ParamIDs::Feature, DFIPKeys::UnfreezeMasternodes}; + const auto unfreezeHeight = attributes->GetValue(unfreezeKey, std::numeric_limits::max()); + if (pindex->nHeight != unfreezeHeight) { + return; + } + cache.ForEachMasternode([&](const uint256 &id, CMasternode node) { + if (const auto timelock = cache.ReadTimelock(id)) { + cache.EraseTimelock(id); + } + return true; + }); +} + static void ProcessTokenSplits(const CBlockIndex *pindex, CCustomCSView &cache, const CreationTxs &creationTxs, @@ -4641,6 +4660,9 @@ Res ProcessDeFiEventFallible(const CBlock &block, } } + // Process unfreeze masternodes + ProcessUnfreezeMasternodes(pindex, cache, blockCtx); + // Construct undo FlushCacheCreateUndo(pindex, mnview, cache, uint256S(std::string(64, '1'))); diff --git a/src/miner.cpp b/src/miner.cpp index 3eaefa9c72..5c18b9c025 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -1632,7 +1632,7 @@ namespace pos { const auto subNodesBlockTimes = pcustomcsview->GetBlockTimes(operatorId, blockHeight, creationHeight, *timeLock); - auto loops = GetTimelockLoops(*timeLock); + auto loops = GetTimelockLoops(*timeLock, blockHeight, *pcustomcsview); if (blockHeight < Params().GetConsensus().DF10EunosPayaHeight) { loops = 1; } diff --git a/src/pos.cpp b/src/pos.cpp index 6da1981ac6..8f6c7ee0d9 100644 --- a/src/pos.cpp +++ b/src/pos.cpp @@ -92,7 +92,7 @@ bool ContextualCheckProofOfStake(const CBlockHeader& blockHeader, const Consensu } // checking PoS kernel is faster, so check it first - auto loops = GetTimelockLoops(timelock); + auto loops = GetTimelockLoops(timelock, height, *mnView); if (height < static_cast(params.DF10EunosPayaHeight)) { loops = 1; } diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 983d7fe9f5..95bf2802e0 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -336,7 +336,7 @@ static UniValue getmininginfo(const JSONRPCRequest& request) const auto subNodesBlockTime = pcustomcsview->GetBlockTimes(nodePtr->operatorAuthAddress, height, nodePtr->creationHeight, *timelock); if (height >= Params().GetConsensus().DF10EunosPayaHeight) { - const auto loops = GetTimelockLoops(*timelock); + const auto loops = GetTimelockLoops(*timelock, height, *pcustomcsview); UniValue multipliers(UniValue::VARR); for (uint8_t i{0}; i < loops; ++i) { multipliers.push_back(pos::CalcCoinDayWeight(Params().GetConsensus(), GetTime(), subNodesBlockTime[i]).getdouble()); diff --git a/test/functional/feature_unfreeze_masternodes.py b/test/functional/feature_unfreeze_masternodes.py new file mode 100755 index 0000000000..34ef85f3dd --- /dev/null +++ b/test/functional/feature_unfreeze_masternodes.py @@ -0,0 +1,187 @@ +#!/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 unfreezing of masternodes""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class UnfreezeMasternodesTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.df24height = 200 + self.extra_args = [ + [ + "-txnotokens=0", + "-subsidytest=1", + "-amkheight=1", + "-bayfrontheight=1", + "-bayfrontmarinaheight=1", + "-bayfrontgardensheight=1", + "-clarkequayheight=1", + "-dakotaheight=1", + "-dakotacrescentheight=1", + "-eunosheight=1", + "-eunospayaheight=1", + "-fortcanningheight=1", + "-fortcanningmuseumheight=1", + "-fortcanningparkheight=1", + "-fortcanninghillheight=1", + "-fortcanningroadheight=1", + "-fortcanningcrunchheight=1", + "-fortcanningspringheight=1", + "-fortcanninggreatworldheight=1", + "-grandcentralheight=1", + "-grandcentralepilogueheight=1", + "-metachainheight=105", + "-df23height=110", + f"-df24height={self.df24height}", + ], + ] + + def run_test(self): + + # Set up + self.setup() + + # Run pre-fork checks + self.pre_fork_checks() + + # Set up Governance vars + self.set_gov_vars() + + # Check masternodes unfrozen + self.check_masternodes_unfrozen() + + # Check unfreeze height cannot be changed after activation + self.test_change_unfreeze() + + # Check setting of new frozen masternodes + self.test_new_frozen_masternodes() + + def setup(self): + + # Get masternode owner address + self.address = self.nodes[0].get_genesis_keys().ownerAuthAddress + + # Generate chain + self.nodes[0].generate(110) + + # Create time locked masternodes + self.node_5 = self.nodes[0].createmasternode( + self.nodes[0].getnewaddress("", "legacy"), "", [], "FIVEYEARTIMELOCK" + ) + self.node_10 = self.nodes[0].createmasternode( + self.nodes[0].getnewaddress("", "legacy"), "", [], "TENYEARTIMELOCK" + ) + self.nodes[0].generate(21) + + # Check masternodes + result = self.nodes[0].getmasternode(self.node_5)[self.node_5] + assert_equal(result["timelock"], "5 years") + assert_equal(len(result["targetMultipliers"]), 3) + result = self.nodes[0].getmasternode(self.node_10)[self.node_10] + assert_equal(result["timelock"], "10 years") + assert_equal(len(result["targetMultipliers"]), 4) + + def pre_fork_checks(self): + + # Unfreeze masternodes before fork + assert_raises_rpc_error( + -32600, + "Cannot be set before DF24Height", + self.nodes[0].setgov, + {"ATTRIBUTES": {"v0/params/feature/unfreeze_masternodes": "210"}}, + ) + + def set_gov_vars(self): + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Try and set below current height + assert_raises_rpc_error( + -32600, + "Cannot be set at or below current height", + self.nodes[0].setgov, + {"ATTRIBUTES": {"v0/params/feature/unfreeze_masternodes": "200"}}, + ) + + # Set unfreeze height + self.nodes[0].setgov( + {"ATTRIBUTES": {"v0/params/feature/unfreeze_masternodes": "210"}} + ) + self.nodes[0].generate(1) + + # Check unfreeze height + assert_equal( + self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"][ + "v0/params/feature/unfreeze_masternodes" + ], + "210", + ) + + def check_masternodes_unfrozen(self): + + # Move to unfreezing height + self.nodes[0].generate(210 - self.nodes[0].getblockcount()) + + # Check time lock and target multipliers + result = self.nodes[0].getmasternode(self.node_5)[self.node_5] + assert "timelock" not in result + assert_equal(len(result["targetMultipliers"]), 1) + result = self.nodes[0].getmasternode(self.node_10)[self.node_10] + assert "timelock" not in result + assert_equal(len(result["targetMultipliers"]), 1) + + # Test resigning masternodes + self.nodes[0].resignmasternode(self.node_5) + self.nodes[0].resignmasternode(self.node_10) + self.nodes[0].generate(41) + + # Check masternodes resigned + result = self.nodes[0].getmasternode(self.node_5)[self.node_5] + assert_equal(result["state"], "RESIGNED") + result = self.nodes[0].getmasternode(self.node_10)[self.node_10] + assert_equal(result["state"], "RESIGNED") + + def test_change_unfreeze(self): + + # Try and change unfreeze height after activation + assert_raises_rpc_error( + -32600, + "Cannot change masternode unfreeze height after activation", + self.nodes[0].setgov, + {"ATTRIBUTES": {"v0/params/feature/unfreeze_masternodes": "400"}}, + ) + + def test_new_frozen_masternodes(self): + + # Try and create a masternode with a time lock + assert_raises_rpc_error( + -32600, + "Masternode timelock disabled", + self.nodes[0].createmasternode, + self.nodes[0].getnewaddress("", "legacy"), + "", + [], + "FIVEYEARTIMELOCK", + ) + assert_raises_rpc_error( + -32600, + "Masternode timelock disabled", + self.nodes[0].createmasternode, + self.nodes[0].getnewaddress("", "legacy"), + "", + [], + "TENYEARTIMELOCK", + ) + + +if __name__ == "__main__": + UnfreezeMasternodesTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index c7aba03ce8..d017348073 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -352,6 +352,7 @@ "feature_shutdown.py", "feature_oracles.py", "feature_checkpoint.py", + "feature_unfreeze_masternodes.py", "rpc_getmininginfo.py", "feature_burn_address.py", "feature_eunos_balances.py", From 6582e1d48cd0bf31a9c758300e7f3aca4724ed33 Mon Sep 17 00:00:00 2001 From: Prasanna Loganathar Date: Tue, 24 Sep 2024 10:12:10 +0800 Subject: [PATCH 12/12] v4.1.9 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 9be90ed893..57077e8bb2 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N) AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 4) define(_CLIENT_VERSION_MINOR, 1) -define(_CLIENT_VERSION_REVISION, 8) +define(_CLIENT_VERSION_REVISION, 9) define(_CLIENT_VERSION_BUILD, 0) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true)