diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 8feb5effcb1..0200c746f2f 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -65,6 +65,7 @@ std::string ToString(CustomTxType type) { case CustomTxType::Vault: return "Vault"; case CustomTxType::UpdateVault: return "UpdateVault"; case CustomTxType::DepositToVault: return "DepositToVault"; + case CustomTxType::AuctionBid: return "AuctionBid"; case CustomTxType::None: return "None"; } return "None"; @@ -149,6 +150,7 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType) { case CustomTxType::Vault: return CVaultMessage{}; case CustomTxType::UpdateVault: return CUpdateVaultMessage{}; case CustomTxType::DepositToVault: return CDepositToVaultMessage{}; + case CustomTxType::AuctionBid: return CAuctionBidMessage{}; case CustomTxType::None: return CCustomTxMessageNone{}; } return CCustomTxMessageNone{}; @@ -455,6 +457,11 @@ class CCustomMetadataParseVisitor : public boost::static_visitor return !res ? res : serialize(obj); } + Res operator()(CAuctionBidMessage& obj) const { + auto res = isPostFortCanningFork(); + return !res ? res : serialize(obj); + } + Res operator()(CCustomTxMessageNone&) const { return Res::Ok(); } @@ -2150,6 +2157,47 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Ok(); } + Res operator()(const CAuctionBidMessage& obj) const { + // owner auth + if (!HasAuth(obj.from)) { + return Res::Err("tx must have at least one input from token owner"); + } + // vault exists + auto vault = mnview.GetVault(obj.vaultId); + if (!vault) + return Res::Err("Cannot find existing vault with id %s", obj.vaultId.GetHex()); + + // vault under liquidation + if (!vault.val->isUnderLiquidation) + return Res::Err("Cannot bid to vault which is not under liquidation"); + + auto data = mnview.GetAuction(obj.vaultId, height); + if (!data) + return Res::Err("No auction data to vault/index %s/%d", obj.vaultId.GetHex(), obj.index); + + auto batch = mnview.GetAuctionBatch(obj.vaultId, obj.index); + if (!batch) + return Res::Err("No batch to vault/index %s/%d", obj.vaultId.GetHex(), obj.index); + + auto bid = mnview.GetAuctionBid(obj.vaultId, obj.index); + if (!bid) { + auto amount = MultiplyAmounts(batch->loanAmount.nValue, COIN + data->liquidationPenalty); + if (amount > obj.amount.nValue) + return Res::Err("First bid should include liquidation penalty of %d%%", data->liquidationPenalty * 100 / COIN); + } else { + auto amount = MultiplyAmounts(bid->second.nValue, COIN + (COIN / 100)); + if (amount > obj.amount.nValue) + return Res::Err("Bid override should be at least 1%% higher than current one"); + // immediate refund previous bid + CalculateOwnerRewards(bid->first); + mnview.AddBalance(bid->first, bid->second); + } + //check balance + CalculateOwnerRewards(obj.from); + auto res = mnview.SubBalance(obj.from, obj.amount); + return !res ? res : mnview.StoreAuctionBid(obj.vaultId, obj.index, {obj.from, obj.amount}); + } + Res operator()(const CCustomTxMessageNone&) const { return Res::Ok(); } diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index 6c8ebaeda67..ffabc413802 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -83,7 +83,8 @@ enum class CustomTxType : uint8_t DestroyLoanScheme = 'D', Vault = 'V', UpdateVault = 'v', - DepositToVault = 'S' + DepositToVault = 'S', + AuctionBid = 'I' }; inline CustomTxType CustomTxCodeToType(uint8_t ch) { @@ -126,6 +127,7 @@ inline CustomTxType CustomTxCodeToType(uint8_t ch) { case CustomTxType::Vault: case CustomTxType::UpdateVault: case CustomTxType::DepositToVault: + case CustomTxType::AuctionBid: case CustomTxType::None: return type; } @@ -282,7 +284,8 @@ typedef boost::variant< CDestroyLoanSchemeMessage, CVaultMessage, CUpdateVaultMessage, - CDepositToVaultMessage + CDepositToVaultMessage, + CAuctionBidMessage > CCustomTxMessage; CCustomTxMessage customTypeToMessage(CustomTxType txType); diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index cc0d53d6a9d..5731b0f293c 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -352,6 +352,14 @@ class CCustomTxRpcVisitor : public boost::static_visitor rpcInfo.pushKV("from", ScriptToString(obj.from)); rpcInfo.pushKV("amount", obj.amount.ToString()); } + + void operator()(const CAuctionBidMessage& obj) const { + rpcInfo.pushKV("vaultid", obj.vaultId.GetHex()); + rpcInfo.pushKV("index", int64_t(obj.index)); + rpcInfo.pushKV("from", ScriptToString(obj.from)); + rpcInfo.pushKV("amount", obj.amount.ToString()); + } + void operator()(const CCustomTxMessageNone&) const { } }; diff --git a/src/masternodes/rpc_vault.cpp b/src/masternodes/rpc_vault.cpp index 7069a017d0b..4a542b70e51 100644 --- a/src/masternodes/rpc_vault.cpp +++ b/src/masternodes/rpc_vault.cpp @@ -456,6 +456,96 @@ UniValue deposittovault(const JSONRPCRequest& request) { return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); } +UniValue auctionbid(const JSONRPCRequest& request) { + CWallet *const pwallet = GetWallet(request); + + RPCHelpMan{"auctionbid", + "Bid to vault in auction\n" + + HelpRequiringPassphrase(pwallet) + "\n", + { + {"vaultid", RPCArg::Type::STR, RPCArg::Optional::NO, "Vault id"}, + {"index", RPCArg::Type::NUM, RPCArg::Optional::NO, "Auction index"}, + {"from", RPCArg::Type::STR, RPCArg::Optional::NO, "Address to get tokens"}, + {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "Amount of amount@symbol format"}, + {"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{ + "\"txid\" (string) The transaction id.\n" + }, + RPCExamples{ + HelpExampleCli("auctionbid", + "84b22eee1964768304e624c416f29a91d78a01dc5e8e12db26bdac0670c67bb2i 0 mwSDMvn1Hoc8DsoB7AkLv7nxdrf5Ja4jsF 100@TSLA") + + HelpExampleRpc("deposittovault", + "84b22eee1964768304e624c416f29a91d78a01dc5e8e12db26bdac0670c67bb2i 0 mwSDMvn1Hoc8DsoB7AkLv7nxdrf5Ja4jsF 1@DTSLA") + }, + }.Check(request); + + RPCTypeCheck(request.params, + {UniValue::VSTR, UniValue::VNUM, UniValue::VSTR, UniValue::VSTR, UniValue::VARR}, + false); + + if (pwallet->chain().isInitialBlockDownload()) + throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot make auction bid while still in Initial Block Download"); + + pwallet->BlockUntilSyncedToCurrentChain(); + LockedCoinsScopedGuard lcGuard(pwallet); + + // decode vaultid + CVaultId vaultId = ParseHashV(request.params[0], "vaultid"); + uint32_t index = request.params[1].get_int(); + auto from = DecodeScript(request.params[2].get_str()); + CTokenAmount amount = DecodeAmount(pwallet->chain(), request.params[3].get_str(), "amount"); + + CAuctionBidMessage msg{vaultId, index, from, amount}; + CDataStream markedMetadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + markedMetadata << static_cast(CustomTxType::DepositToVault) + << msg; + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(markedMetadata); + + int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; + + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + + rawTx.vout.push_back(CTxOut(0, scriptMeta)); + + CTransactionRef optAuthTx; + std::set auths{from}; + rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false /*needFoundersAuth*/, optAuthTx, request.params[4]); + + CCoinControl coinControl; + + // Set change to from address + CTxDestination dest; + ExtractDestination(from, dest); + if (IsValidDestination(dest)) { + coinControl.destChange = dest; + } + + fund(rawTx, pwallet, optAuthTx, &coinControl); + + // check execution + { + LOCK(cs_main); + CCoinsViewCache coins(&::ChainstateActive().CoinsTip()); + if (optAuthTx) + AddCoins(coins, *optAuthTx, targetHeight); + auto metadata = ToByteVector(CDataStream{SER_NETWORK, PROTOCOL_VERSION, msg}); + execTestTx(CTransaction(rawTx), targetHeight, metadata, CAuctionBidMessage{}, coins); + } + return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); +} + static const CRPCCommand commands[] = { // category name actor (function) params @@ -465,6 +555,7 @@ static const CRPCCommand commands[] = {"vault", "getvault", &getvault, {"id"}}, {"vault", "updatevault", &updatevault, {"id", "parameters", "inputs"}}, {"vault", "deposittovault", &deposittovault, {"id", "from", "amount", "inputs"}}, + {"vault", "auctionbid", &auctionbid, {"id", "index", "from", "amount", "inputs"}}, }; void RegisterVaultRPCCommands(CRPCTable& tableRPC) { diff --git a/src/masternodes/vault.cpp b/src/masternodes/vault.cpp index 50a7bf3a1c6..fa33be565d6 100644 --- a/src/masternodes/vault.cpp +++ b/src/masternodes/vault.cpp @@ -110,6 +110,17 @@ Res CVaultView::EraseAuction(const CVaultId& vaultId, uint32_t height) return Res::Err("Auction for vault <%s> not found", vaultId.GetHex()); } +boost::optional CVaultView::GetAuction(const CVaultId& vaultId, uint32_t height) +{ + auto it = LowerBound(std::make_pair(height, vaultId)); + for (; it.Valid(); it.Next()) { + if (it.Key().second == vaultId) { + return it.Value().as(); + } + } + return {}; +} + Res CVaultView::StoreAuctionBatch(const CVaultId& vaultId, uint32_t id, const CAuctionBatch& batch) { WriteBy(std::make_pair(vaultId, id), batch); @@ -127,13 +138,10 @@ boost::optional CVaultView::GetAuctionBatch(const CVaultId& vault return ReadBy(std::make_pair(vaultId, id)); } -void CVaultView::ForEachVaultAuction(std::function callback, uint32_t height) +void CVaultView::ForEachVaultAuction(std::function callback, uint32_t height) { ForEach, CAuctionData>([&](const std::pair& pair, const CAuctionData& data) { - if (pair.first != height) { - return false; - } - return callback(pair.second, data); + return callback(pair.second, pair.first, data); }, std::make_pair(height, CVaultId{})); } diff --git a/src/masternodes/vault.h b/src/masternodes/vault.h index ade2b9bf1ec..34c832bf8de 100644 --- a/src/masternodes/vault.h +++ b/src/masternodes/vault.h @@ -60,6 +60,24 @@ struct CDepositToVaultMessage { } }; +struct CAuctionBidMessage { + CVaultId vaultId; + uint32_t index; + CScript from; + CTokenAmount amount; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(vaultId); + READWRITE(index); + READWRITE(from); + READWRITE(amount); + } +}; + struct CAuctionData { uint32_t batchCount; CAmount liquidationPenalty; @@ -103,10 +121,11 @@ class CVaultView : public virtual CStorageView Res StoreAuction(const CVaultId& vaultId, uint32_t height, const CAuctionData& data); Res EraseAuction(const CVaultId& vaultId, uint32_t height); + boost::optional GetAuction(const CVaultId& vaultId, uint32_t height); Res StoreAuctionBatch(const CVaultId& vaultId, uint32_t id, const CAuctionBatch& batch); Res EraseAuctionBatch(const CVaultId& vaultId, uint32_t id); boost::optional GetAuctionBatch(const CVaultId& vaultId, uint32_t id); - void ForEachVaultAuction(std::function callback, uint32_t height); + void ForEachVaultAuction(std::function callback, uint32_t height = 0); using COwnerTokenAmount = std::pair; Res StoreAuctionBid(const CVaultId& vaultId, uint32_t id, COwnerTokenAmount amount); diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index e5121c4997f..b306e4790e6 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -246,6 +246,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "updatevault", 1, "parameters" }, { "updatevault", 2, "inputs" }, { "deposittovault", 3, "inputs" }, + { "auctionbid", 4, "inputs" }, { "spv_sendrawtx", 0, "rawtx" }, { "spv_createanchor", 0, "inputs" }, diff --git a/src/validation.cpp b/src/validation.cpp index 439f156f230..544fbe320ec 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2982,7 +2982,10 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl return true; }); } - cache.ForEachVaultAuction([&](const CVaultId& vaultId, const CAuctionData& data) { + cache.ForEachVaultAuction([&](const CVaultId& vaultId, uint32_t height, const CAuctionData& data) { + if (int(height) != pindex->nHeight) { + return false; + } std::set tokensLooseInterest; for (uint32_t i = 0; i < data.batchCount; i++) { auto batch = cache.GetAuctionBatch(vaultId, i);