From ded1692e4ce23d67330139750a63bf12c3f2bdc8 Mon Sep 17 00:00:00 2001 From: bra1nsurfer Date: Wed, 27 Mar 2024 11:46:49 +0300 Subject: [PATCH 1/7] WXDAO Funding --- ride/wxdao_funding.ride | 169 ++++++++++++++++++ test/components/wxdao_funding/_hooks.mjs | 96 ++++++++++ .../wxdao_funding/mock/mainTreasury.mock.ride | 11 ++ .../wxdao_funding/mock/pool.mock.ride | 13 ++ .../wxdao_funding/mock/wxdao.mock.ride | 10 ++ .../wxdao_funding/processClaim.spec.mjs | 55 ++++++ 6 files changed, 354 insertions(+) create mode 100644 ride/wxdao_funding.ride create mode 100644 test/components/wxdao_funding/_hooks.mjs create mode 100644 test/components/wxdao_funding/mock/mainTreasury.mock.ride create mode 100644 test/components/wxdao_funding/mock/pool.mock.ride create mode 100644 test/components/wxdao_funding/mock/wxdao.mock.ride create mode 100644 test/components/wxdao_funding/processClaim.spec.mjs diff --git a/ride/wxdao_funding.ride b/ride/wxdao_funding.ride new file mode 100644 index 00000000..77ea50cd --- /dev/null +++ b/ride/wxdao_funding.ride @@ -0,0 +1,169 @@ +{-# STDLIB_VERSION 6 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +let SEP = "__" +let CONTRACT_NAME = "wxdao_funding.ride" +let SCALE8 = 100_000_000 +let WAVES = "WAVES" + +func wrapErr(s: String) = { + CONTRACT_NAME + ": " + s +} + +func throwErr(s: String) = { + throw(wrapErr(s)) +} + +func assetIdToString(assetId: ByteVector|Unit) = { + match assetId { + case b: ByteVector => b.toBase58String() + case _: Unit => WAVES + } +} + +func stringToAssetId(assetIdString: String) = { + if (assetIdString == WAVES) then unit else assetIdString.fromBase58String() +} + +func keyMainTreasuryAddress() = ["%s", "mainTreasuryAddress"].makeString(SEP) +func keyWavesUSDTPoolAddress() = ["%s", "WavesUSDTPoolAddress"].makeString(SEP) +func keyWXDAOAddress() = ["%s", "WXDAOcontractAddress"].makeString(SEP) +func keyWXDAOassetId() = ["%s", "WXDAOassetId"].makeString(SEP) +func keyUSDTassetId() = ["%s", "USDTassetId"].makeString(SEP) +func keyProcessFeeAmount() = ["%s", "processFeeAmount"].makeString(SEP) + +func keyHistory(action: String, txId: String) = ["%s%s", action, txId].makeString(SEP) + +func formatProcessHistory(claimedWavesAmount: Int, sendWXDAOamount: Int, processFeeAmount: Int) = { + [ + "%d%d%d", + claimedWavesAmount.toString(), + sendWXDAOamount.toString(), + processFeeAmount.toString() + ].makeString(SEP) +} + +let mainTreasuryAddressString = this.getStringValue(keyMainTreasuryAddress()) +let mainTreasuryAddressOrFail = mainTreasuryAddressString.addressFromStringValue() +let wavesUSDTpoolAddressString = this.getStringValue(keyWavesUSDTPoolAddress()) +let wavesUSDTpoolAddressOrFail = wavesUSDTpoolAddressString.addressFromStringValue() +let wxdaoAddressString = this.getStringValue(keyWXDAOAddress()) +let wxdaoAddressOrFail = wxdaoAddressString.addressFromStringValue() + +func stringToAsset(assetIdString: String) = { + if (assetIdString == WAVES) then unit else assetIdString.fromBase58String() +} + +func assetToString(assetId: ByteVector|Unit) = { + match (assetId) { + case b:ByteVector => b.toBase58String() + case _ => WAVES + } +} + +func getBalance(address: Address, assetIdString: String) = { + let assetId = stringToAsset(assetIdString) + + match (assetId) { + case b:ByteVector => address.assetBalance(b) + case _ => address.wavesBalance().available + } +} + +let processFeeAmount = this.getInteger(keyProcessFeeAmount()).valueOrElse(500000) +let usdtAssetIdString = this.getStringValue(keyUSDTassetId()) +let wxdaoAssetIdString = this.getStringValue(keyWXDAOassetId()) +let wxdaoAssetId = stringToAsset(wxdaoAssetIdString) + + +func claimWavesFromTreasury() = { + strict oldWavesBalance = this.getBalance(WAVES) + strict getWavesInvoke = mainTreasuryAddressOrFail.invoke("Claim", [], []) + strict newWavesBalance = this.getBalance(WAVES) + + newWavesBalance - oldWavesBalance +} + +func getPoolBalance(poolAddress: Address, assetIdString: String) = { + let invokeResult = poolAddress.invoke("getAccBalanceWrapperREADONLY", [assetIdString], []) + match (invokeResult) { + case balance:Int => balance + case _ => "getAccBalanceWrapperREADONLY unexpected value".throwErr() + } +} + +func getWavesUSDTPrice() = { + let poolWavesBalance = wavesUSDTpoolAddressOrFail.getPoolBalance(WAVES) + let poolUsdtBalance = wavesUSDTpoolAddressOrFail.getPoolBalance(usdtAssetIdString) + + strict ch = [ + poolWavesBalance > 0 || "WAVES/USDT pool Waves balance should be greater that 0".throwErr(), + poolUsdtBalance > 0 || "WAVES/USDT pool USDT balance should be greater that 0".throwErr() + ] + + fraction(poolUsdtBalance, SCALE8, poolWavesBalance) +} + +func getWXDAOUsdtPrice() = { + let priceInvoke = wxdaoAddressOrFail.invoke("call", ["price", []], []) + let invokeResult = match (priceInvoke) { + case r:List[Any] => { + match(r[0]) { + case i:Int => i + case _ => unit + } + } + case _ => unit + } + let wxDAOprice = invokeResult.valueOrErrorMessage("Unexpected WXDAO Price invoke result".wrapErr()) + + strict ch = [ + wxDAOprice > 0 || "WXDAO price should be greater than 0".throwErr() + ] + + wxDAOprice +} + +func getWavesWXDAOPrice() = { + let wavesUsdtPrice = getWavesUSDTPrice() + let wxdaoUsdtPrice = getWXDAOUsdtPrice() + + fraction(wavesUsdtPrice, SCALE8, wxdaoUsdtPrice) +} + +func calcWXDAOamount(wavesAmount: Int) = { + let price = getWavesWXDAOPrice() + let wxDAOamount = fraction(wavesAmount, price, SCALE8) + + strict ch =[ + wavesAmount > 0 || "wavesAmount should be greater than 0".throwErr(), + price > 0 || "price should be greater than 0".throwErr(), + wxDAOamount > 0 || "wxDAO swap amount is 0".throwErr() + ] + + wxDAOamount +} + +@Callable(i) +func process() = { + strict wavesAmount = claimWavesFromTreasury() + strict swapWavesAmount = wavesAmount - processFeeAmount + strict wxDAOsendAmount = calcWXDAOamount(swapWavesAmount) + + strict ch = [ + swapWavesAmount > 0 || "claimed waves amount should be greater than processing fee".throwErr(), + wxDAOsendAmount > 0 || "WXDAO send amount should be greater than 0".throwErr() + ] + + let processingCaller = i.originCaller + + [ + ScriptTransfer(processingCaller, processFeeAmount, stringToAsset(WAVES)), + ScriptTransfer(mainTreasuryAddressOrFail, wxDAOsendAmount, wxdaoAssetId), + StringEntry( + keyHistory("process", i.transactionId.toBase58String()), + formatProcessHistory(wavesAmount, wxDAOsendAmount, processFeeAmount) + ) + ] +} diff --git a/test/components/wxdao_funding/_hooks.mjs b/test/components/wxdao_funding/_hooks.mjs new file mode 100644 index 00000000..f361e4db --- /dev/null +++ b/test/components/wxdao_funding/_hooks.mjs @@ -0,0 +1,96 @@ +import { address, randomSeed, publicKey } from '@waves/ts-lib-crypto'; +import { + data, + massTransfer, + issue, +} from '@waves/waves-transactions'; +import { format } from 'path'; +import { setScriptFromFile } from '../../utils/utils.mjs'; + +import { + broadcastAndWait, chainId, baseSeed, +} from '../../utils/api.mjs'; + +const seedWordsCount = 5; +const ridePath = '../ride'; +const mockPath = 'components/wxdao_funding/mock'; +const wxdaoFundingPath = format({ dir: ridePath, base: 'wxdao_funding.ride' }); +const wxdaoMockPath = format({ dir: mockPath, base: 'wxdao.mock.ride' }); +const poolMockPath = format({ dir: mockPath, base: 'pool.mock.ride' }); +const mainTreasury = format({ dir: mockPath, base: 'mainTreasury.mock.ride' }); + +export const mochaHooks = { + async beforeAll() { + const names = [ + 'wxdaoFunding', + 'wxdao', + 'mainTreasury', + 'wavesUsdtPool', + 'user1', + ]; + this.accounts = Object.fromEntries(names.map((item) => { + const itemSeed = randomSeed(seedWordsCount); + return [ + item, + { seed: itemSeed, addr: address(itemSeed, chainId), publicKey: publicKey(itemSeed) }, + ]; + })); + const amount = 100e8; + const massTransferTx = massTransfer({ + transfers: Object.values(this.accounts).map((item) => ({ recipient: item.addr, amount })), + chainId, + }, baseSeed); + await broadcastAndWait(massTransferTx); + + this.wxdaoAssetId = await broadcastAndWait(issue({ + quantity: 1e6 * 1e8, + decimals: 8, + name: 'WXDAO', + description: 'WXDAO', + chainId, + }, baseSeed)).then((tx) => tx.id); + + await broadcastAndWait(massTransfer({ + transfers: Object.values(this.accounts).map((item) => ({ recipient: item.addr, amount })), + chainId, + assetId: this.wxdaoAssetId, + }, baseSeed)); + + await broadcastAndWait(data({ + additionalFee: 4e5, + data: [ + { + key: '%s__mainTreasuryAddress', + type: 'string', + value: this.accounts.mainTreasury.addr, + }, + { + key: '%s__WavesUSDTPoolAddress', + type: 'string', + value: this.accounts.wavesUsdtPool.addr, + }, + { + key: '%s__WXDAOcontractAddress', + type: 'string', + value: this.accounts.wxdao.addr, + }, + { + key: '%s__WXDAOassetId', + type: 'string', + value: this.wxdaoAssetId, + }, + { + key: '%s__USDTassetId', + type: 'string', + value: 'MOCKED_USDT_ASSET_ID', + }, + ], + chainId, + }, this.accounts.wxdaoFunding.seed)); + + await setScriptFromFile(wxdaoFundingPath, this.accounts.wxdaoFunding.seed); + await setScriptFromFile(wxdaoMockPath, this.accounts.wxdao.seed); + await setScriptFromFile(poolMockPath, this.accounts.wavesUsdtPool.seed); + await setScriptFromFile(mainTreasury, this.accounts.mainTreasury.seed); + }, +}; diff --git a/test/components/wxdao_funding/mock/mainTreasury.mock.ride b/test/components/wxdao_funding/mock/mainTreasury.mock.ride new file mode 100644 index 00000000..6ad57ba0 --- /dev/null +++ b/test/components/wxdao_funding/mock/mainTreasury.mock.ride @@ -0,0 +1,11 @@ +{-# STDLIB_VERSION 6 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +# Send 1.5 WAVES +@Callable(i) +func Claim() = { + [ + ScriptTransfer(i.caller, 1_5000_0000, unit) + ] +} diff --git a/test/components/wxdao_funding/mock/pool.mock.ride b/test/components/wxdao_funding/mock/pool.mock.ride new file mode 100644 index 00000000..be22984e --- /dev/null +++ b/test/components/wxdao_funding/mock/pool.mock.ride @@ -0,0 +1,13 @@ +{-# STDLIB_VERSION 6 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +# Send 1.0 WAVES or 5.5 USDT +@Callable(i) +func getAccBalanceWrapperREADONLY(assetIdString: String) = { + let balance = if(assetIdString == "WAVES") + then 1_0000_0000 + else 5_500_000 + + ([], balance) +} diff --git a/test/components/wxdao_funding/mock/wxdao.mock.ride b/test/components/wxdao_funding/mock/wxdao.mock.ride new file mode 100644 index 00000000..36224829 --- /dev/null +++ b/test/components/wxdao_funding/mock/wxdao.mock.ride @@ -0,0 +1,10 @@ +{-# STDLIB_VERSION 6 #-} +{-# CONTENT_TYPE DAPP #-} +{-# SCRIPT_TYPE ACCOUNT #-} + +# WXDAO Price == 11.0 USDT +@Callable(i) +func call(function: String, args: List[String]) = { + + ([], [11_000_000]) +} diff --git a/test/components/wxdao_funding/processClaim.spec.mjs b/test/components/wxdao_funding/processClaim.spec.mjs new file mode 100644 index 00000000..8c924753 --- /dev/null +++ b/test/components/wxdao_funding/processClaim.spec.mjs @@ -0,0 +1,55 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { invokeScript } from '@waves/waves-transactions'; +import { + chainId, broadcastAndWait, +} from '../../utils/api.mjs'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +describe('wxdao_funding: process', /** @this {MochaSuiteModified} */() => { + it( + 'should successfully claim Waves and send WXDAO according to Price', + async function () { + // Prices set in mocks + const claimAmount = 1.5 * 1e8; + const wavesUsdtPrice = 5.5 * 1e6; + const wxdaoUsdtPrice = 11.0 * 1e6; + const processFee = 0.005 * 1e8; + + const swapAmount = claimAmount - processFee; + const wavesWxdaoPrice = Math.floor((wavesUsdtPrice / wxdaoUsdtPrice) * 1e8); + const expectedWxdaoSendAmount = Math.floor((swapAmount / 1e8) * wavesWxdaoPrice); + + const { id: txId, stateChanges } = await broadcastAndWait(invokeScript({ + dApp: this.accounts.wxdaoFunding.addr, + call: { + function: 'process', + }, + chainId, + }, this.accounts.user1.seed)); + + expect(stateChanges.transfers).to.deep.equal([ + { + asset: null, + amount: processFee, + address: this.accounts.user1.addr, + }, + { + asset: this.wxdaoAssetId, + amount: expectedWxdaoSendAmount, + address: this.accounts.mainTreasury.addr, + }, + ]); + + expect(stateChanges.data).to.deep.equal([ + { + key: `%s%s__process__${txId}`, + type: 'string', + value: `%d%d%d__${claimAmount}__${expectedWxdaoSendAmount}__${processFee}`, + }, + ]); + }, + ); +}); From 6ae298e831363193644c8f4f0d115d6419e0138a Mon Sep 17 00:00:00 2001 From: bra1nsurfer Date: Thu, 28 Mar 2024 17:35:29 +0300 Subject: [PATCH 2/7] Added minClaimAmount --- ride/wxdao_funding.ride | 18 +++++---- .../processClaim_minClaimAmount.spec.mjs | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 test/components/wxdao_funding/processClaim_minClaimAmount.spec.mjs diff --git a/ride/wxdao_funding.ride b/ride/wxdao_funding.ride index 77ea50cd..362b791e 100644 --- a/ride/wxdao_funding.ride +++ b/ride/wxdao_funding.ride @@ -32,6 +32,7 @@ func keyWXDAOAddress() = ["%s", "WXDAOcontractAddress"].makeString(SEP) func keyWXDAOassetId() = ["%s", "WXDAOassetId"].makeString(SEP) func keyUSDTassetId() = ["%s", "USDTassetId"].makeString(SEP) func keyProcessFeeAmount() = ["%s", "processFeeAmount"].makeString(SEP) +func keyMinClaimAmount() = ["%s", "minClaimAmount"].makeString(SEP) func keyHistory(action: String, txId: String) = ["%s%s", action, txId].makeString(SEP) @@ -71,10 +72,11 @@ func getBalance(address: Address, assetIdString: String) = { } } -let processFeeAmount = this.getInteger(keyProcessFeeAmount()).valueOrElse(500000) -let usdtAssetIdString = this.getStringValue(keyUSDTassetId()) -let wxdaoAssetIdString = this.getStringValue(keyWXDAOassetId()) -let wxdaoAssetId = stringToAsset(wxdaoAssetIdString) +let processFeeAmount = this.getInteger(keyProcessFeeAmount()).valueOrElse(500000) +let minClaimAmount = this.getInteger(keyMinClaimAmount()).valueOrElse(0) +let usdtAssetIdString = this.getStringValue(keyUSDTassetId()) +let wxdaoAssetIdString = this.getStringValue(keyWXDAOassetId()) +let wxdaoAssetId = stringToAsset(wxdaoAssetIdString) func claimWavesFromTreasury() = { @@ -147,11 +149,13 @@ func calcWXDAOamount(wavesAmount: Int) = { @Callable(i) func process() = { - strict wavesAmount = claimWavesFromTreasury() - strict swapWavesAmount = wavesAmount - processFeeAmount + strict claimedWavesAmount = claimWavesFromTreasury() + strict swapWavesAmount = claimedWavesAmount - processFeeAmount strict wxDAOsendAmount = calcWXDAOamount(swapWavesAmount) strict ch = [ + claimedWavesAmount >= minClaimAmount || + ["not enough claim amount (", claimedWavesAmount.toString(), " < ", minClaimAmount.toString(), ")"].makeString("").throwErr(), swapWavesAmount > 0 || "claimed waves amount should be greater than processing fee".throwErr(), wxDAOsendAmount > 0 || "WXDAO send amount should be greater than 0".throwErr() ] @@ -163,7 +167,7 @@ func process() = { ScriptTransfer(mainTreasuryAddressOrFail, wxDAOsendAmount, wxdaoAssetId), StringEntry( keyHistory("process", i.transactionId.toBase58String()), - formatProcessHistory(wavesAmount, wxDAOsendAmount, processFeeAmount) + formatProcessHistory(claimedWavesAmount, wxDAOsendAmount, processFeeAmount) ) ] } diff --git a/test/components/wxdao_funding/processClaim_minClaimAmount.spec.mjs b/test/components/wxdao_funding/processClaim_minClaimAmount.spec.mjs new file mode 100644 index 00000000..b93fc976 --- /dev/null +++ b/test/components/wxdao_funding/processClaim_minClaimAmount.spec.mjs @@ -0,0 +1,38 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { invokeScript, data } from '@waves/waves-transactions'; +import { + chainId, broadcastAndWait, +} from '../../utils/api.mjs'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +describe('wxdao_funding: process', /** @this {MochaSuiteModified} */() => { + it( + 'should fail if Waves amount is less than minClaimAmount', + async function () { + await broadcastAndWait(data({ + additionalFee: 4e5, + data: [ + { + key: '%s__minClaimAmount', + type: 'integer', + value: 300_0000_0000, // 300.0 WAVES + }, + ], + chainId, + }, this.accounts.wxdaoFunding.seed)); + + const invoke = invokeScript({ + dApp: this.accounts.wxdaoFunding.addr, + call: { + function: 'process', + }, + chainId, + }, this.accounts.user1.seed); + + return expect(broadcastAndWait(invoke)).to.be.rejectedWith('not enough claim amount'); + }, + ); +}); From 32c2dc037ac5bd3620fbed8f59d39872b0d584a2 Mon Sep 17 00:00:00 2001 From: bra1nsurfer Date: Fri, 29 Mar 2024 21:52:18 +0300 Subject: [PATCH 3/7] Added minClaimAmount Update history key Update test Added migration data tx --- .../01_wxdao_funding_data_tx.json | 48 +++++++ ride/wxdao_funding.ride | 129 +++++++++++++++++- .../wxdao_funding/processClaim.spec.mjs | 2 +- 3 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json diff --git a/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json b/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json new file mode 100644 index 00000000..658f6b87 --- /dev/null +++ b/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json @@ -0,0 +1,48 @@ +{ + "type": 12, + "fee": 900000, + "version": 2, + "senderPublicKey": "5WG53hB4ZK2TWX7UnLeTU4ieDp8Fy4VtaYx6gKFYxGHu", + "data": [ + { + "key": "%s__adminAddressList", + "type": "string", + "value": "3PBUguU83ccjXz7NBbR3vn67fVHawbxwaTH__3PLU2bd4CDhHC35bf5qsQxYLpkEkpgkrcvc__3PF3AgB3cEE8F9ZUxuA9aLyM2PBjKrrszZQ__3PHdaP2BLUXFacnqZm6WoFjidvzqZZMbb8j__3P33RBZUXcdx5JW918DqFfrscEuEZpAPUPz" + }, + { + "key": "%s__mainTreasuryAddress", + "type": "string", + "value": "3PEwRcYNAUtoFvKpBhKoiwajnZfdoDR6h4h" + }, + { + "key": "%s__WavesUSDTPoolAddress", + "type": "string", + "value": "3PKfrupEydU2nZAghVjZAfvCwMBkzuR1F52" + }, + { + "key": "%s__WXDAOcontractAddress", + "type": "string", + "value": "3PEhMFF2mfSWVxWqbW8guXVYcNeoW77ga7T" + }, + { + "key": "%s__WXDAOassetId", + "type": "string", + "value": "BE4VVq1VsrwGyUWpUkNjVFR5j9vzioiRhrUT52p8RW2m" + }, + { + "key": "%s__USDTassetId", + "type": "string", + "value": "G5WWWzzVsWRyzGf32xojbnfp7gXbWrgqJT8RcVWEfLmC" + }, + { + "key": "%s__minClaimAmount", + "type": "integer", + "value": 30000000000 + }, + { + "key": "%s__processFeeAmount", + "type": "integer", + "value": 500000 + } + ] +} \ No newline at end of file diff --git a/ride/wxdao_funding.ride b/ride/wxdao_funding.ride index 362b791e..b6fb47a1 100644 --- a/ride/wxdao_funding.ride +++ b/ride/wxdao_funding.ride @@ -32,9 +32,9 @@ func keyWXDAOAddress() = ["%s", "WXDAOcontractAddress"].makeString(SEP) func keyWXDAOassetId() = ["%s", "WXDAOassetId"].makeString(SEP) func keyUSDTassetId() = ["%s", "USDTassetId"].makeString(SEP) func keyProcessFeeAmount() = ["%s", "processFeeAmount"].makeString(SEP) -func keyMinClaimAmount() = ["%s", "minClaimAmount"].makeString(SEP) +func keyMinClaimAmount() = ["%s", "minClaimAmount"].makeString(SEP) -func keyHistory(action: String, txId: String) = ["%s%s", action, txId].makeString(SEP) +func keyHistory(action: String, txId: String) = ["%s%s%s", "history", action, txId].makeString(SEP) func formatProcessHistory(claimedWavesAmount: Int, sendWXDAOamount: Int, processFeeAmount: Int) = { [ @@ -79,6 +79,95 @@ let wxdaoAssetIdString = this.getStringValue(keyWXDAOassetId()) let wxdaoAssetId = stringToAsset(wxdaoAssetIdString) + +###################### +# MULTISIG FUNCTIONS # +###################### +let ADMIN_LIST_SIZE = 5 +let QUORUM = 3 +let TXID_BYTES_LENGTH = 32 + +func keyAllowedTxIdVotePrefix(txId: String) = makeString(["%s%s%s", "allowTxId", txId], SEP) +# Make Admin vote key +func keyFullAdminVote(prefix: String, adminAddress: String) = makeString([prefix, adminAddress], SEP) +# Admin List key +func keyAdminAddressList() = makeString(["%s", "adminAddressList"], SEP) +# Allowed TXID key +func keyAllowedTxId() = makeString(["%s", "txId"], SEP) + +func getAdminVote(prefix: String, admin: String) = { + let voteKey = keyFullAdminVote(prefix, admin) + getInteger(voteKey).valueOrElse(0) +} + +func getAdminsList() = { + match (this.getString(keyAdminAddressList())) { + case s:String => s.split(SEP) + case _ => [] + } +} + +func isInAdminList(address: String) = { + getAdminsList().containsElement(address) +} + +# Generate List of keys with same prefix for all admins +func genVotesKeysHelper(a: (List[String], String), adminAddress: String) = { + let (result, prefix) = a + (result :+ keyFullAdminVote(prefix, adminAddress), prefix) +} +func genVotesKeys(keyPrefix: String) = { + let adminList = keyAdminAddressList() + let (result, prefix) = FOLD<5>(getAdminsList(), ([], keyPrefix), genVotesKeysHelper) + result +} + +# Count all votes for Prefix +func countVotesHelper(result: Int, voteKey: String) = { + result + getInteger(voteKey).valueOrElse(0) +} +func countVotes(prefix: String) = { + let votes = genVotesKeys(prefix) + FOLD<5>(votes, 0, countVotesHelper) +} + +# Generate DeleteEntry for all votes with Prefix +func clearVotesHelper(result: List[DeleteEntry], key: String) = { + result :+ DeleteEntry(key) +} +func getClearVoteEntries(prefix: String) = { + let votes = genVotesKeys(prefix) + FOLD<5>(votes, [], clearVotesHelper) +} + +func voteINTERNAL( + callerAddressString: String, + keyPrefix: String, + minVotes: Int, + voteResult: List[StringEntry|IntegerEntry|DeleteEntry] +) = { + let voteKey = keyFullAdminVote(keyPrefix, callerAddressString) + let adminCurrentVote = getAdminVote(keyPrefix, callerAddressString) + + strict err = if (!isInAdminList(callerAddressString)) then { + throwErr("Address: " + callerAddressString + " not in Admin list") + } else if (adminCurrentVote == 1) then { + throwErr(voteKey + " you already voted") + } else { unit } + + let votes = countVotes(keyPrefix) + if (votes + 1 >= minVotes) then { + let clearVoteEntries = getClearVoteEntries(keyPrefix) + clearVoteEntries ++ voteResult + } else { + [ IntegerEntry(voteKey, 1) ] + } +} +########################## +# MULTISIG FUNCTIONS END # +########################## + + func claimWavesFromTreasury() = { strict oldWavesBalance = this.getBalance(WAVES) strict getWavesInvoke = mainTreasuryAddressOrFail.invoke("Claim", [], []) @@ -171,3 +260,39 @@ func process() = { ) ] } + + +###################### +# MULTISIG FUNCTIONS # +###################### +# Vote for txId that is allowed in Verifier +@Callable(i) +func voteForTxId(txId: String) = { + let callerAddressString = toBase58String(i.caller.bytes) + let keyPrefix = keyAllowedTxIdVotePrefix(txId) + let result = [ StringEntry(keyAllowedTxId(), txId) ] + let allowedTxIdOption = this.getString(keyAllowedTxId()) + + strict err = [ + txId.fromBase58String().size() == TXID_BYTES_LENGTH || throwErr(txId + " is not valid txId"), + allowedTxIdOption == unit || allowedTxIdOption.value() != txId || throwErr(txId + " is already allowed") + ] + + voteINTERNAL(callerAddressString, keyPrefix, QUORUM, result) +} +########################## +# MULTISIG FUNCTIONS END # +########################## + + +@Verifier(tx) +func verify() = { + let byAdmins = (tx.id == this.getString(keyAllowedTxId()).valueOrElse("").fromBase58String()) + let byOwner = (if (getAdminsList().size() >= QUORUM) then { + false + } else { + sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey) + }) + + byAdmins || byOwner +} \ No newline at end of file diff --git a/test/components/wxdao_funding/processClaim.spec.mjs b/test/components/wxdao_funding/processClaim.spec.mjs index 8c924753..21af3d19 100644 --- a/test/components/wxdao_funding/processClaim.spec.mjs +++ b/test/components/wxdao_funding/processClaim.spec.mjs @@ -45,7 +45,7 @@ describe('wxdao_funding: process', /** @this {MochaSuiteModified} */() => { expect(stateChanges.data).to.deep.equal([ { - key: `%s%s__process__${txId}`, + key: `%s%s%s__history__process__${txId}`, type: 'string', value: `%d%d%d__${claimAmount}__${expectedWxdaoSendAmount}__${processFee}`, }, From 7f71b811959200c472db12cea67018bc54fb3e37 Mon Sep 17 00:00:00 2001 From: bra1nsurfer Date: Mon, 1 Apr 2024 09:08:03 +0300 Subject: [PATCH 4/7] Removed unused code --- ride/wxdao_funding.ride | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/ride/wxdao_funding.ride b/ride/wxdao_funding.ride index b6fb47a1..5d443c30 100644 --- a/ride/wxdao_funding.ride +++ b/ride/wxdao_funding.ride @@ -15,17 +15,6 @@ func throwErr(s: String) = { throw(wrapErr(s)) } -func assetIdToString(assetId: ByteVector|Unit) = { - match assetId { - case b: ByteVector => b.toBase58String() - case _: Unit => WAVES - } -} - -func stringToAssetId(assetIdString: String) = { - if (assetIdString == WAVES) then unit else assetIdString.fromBase58String() -} - func keyMainTreasuryAddress() = ["%s", "mainTreasuryAddress"].makeString(SEP) func keyWavesUSDTPoolAddress() = ["%s", "WavesUSDTPoolAddress"].makeString(SEP) func keyWXDAOAddress() = ["%s", "WXDAOcontractAddress"].makeString(SEP) @@ -190,7 +179,7 @@ func getWavesUSDTPrice() = { strict ch = [ poolWavesBalance > 0 || "WAVES/USDT pool Waves balance should be greater that 0".throwErr(), - poolUsdtBalance > 0 || "WAVES/USDT pool USDT balance should be greater that 0".throwErr() + poolUsdtBalance > 0 || "WAVES/USDT pool USDT balance should be greater that 0".throwErr() ] fraction(poolUsdtBalance, SCALE8, poolWavesBalance) @@ -229,7 +218,7 @@ func calcWXDAOamount(wavesAmount: Int) = { strict ch =[ wavesAmount > 0 || "wavesAmount should be greater than 0".throwErr(), - price > 0 || "price should be greater than 0".throwErr(), + price > 0 || "price should be greater than 0".throwErr(), wxDAOamount > 0 || "wxDAO swap amount is 0".throwErr() ] From 3589397c59f079d63310bcc203629253f71922b6 Mon Sep 17 00:00:00 2001 From: bra1nsurfer Date: Mon, 1 Apr 2024 09:21:39 +0300 Subject: [PATCH 5/7] Added specs --- docs/wxdao_funding/wxdao_funding.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/wxdao_funding/wxdao_funding.md diff --git a/docs/wxdao_funding/wxdao_funding.md b/docs/wxdao_funding/wxdao_funding.md new file mode 100644 index 00000000..94b00fdb --- /dev/null +++ b/docs/wxdao_funding/wxdao_funding.md @@ -0,0 +1,25 @@ +# WavesDAO -> WXDAO Funding + +## Keys +| key | type | value | +| :---------------------------------- | --------: | :----------------------------------------------------------- | +| `%s__mainTreasuryAddress` | `String` | `'3PEwRcYNAUtoFvKpBhKoiwajnZfdoDR6h4h'` | +| `%s__WavesUSDTPoolAddress` | `String` | `'3PKfrupEydU2nZAghVjZAfvCwMBkzuR1F52'` | +| `%s__WXDAOcontractAddress` | `String` | `'3PEhMFF2mfSWVxWqbW8guXVYcNeoW77ga7T'` | +| `%s__WXDAOassetId` | `String` | `'BE4VVq1VsrwGyUWpUkNjVFR5j9vzioiRhrUT52p8RW2m'` | +| `%s__USDTassetId` | `String` | `'G5WWWzzVsWRyzGf32xojbnfp7gXbWrgqJT8RcVWEfLmC'` | +| `%s__minClaimAmount` | `Integer` | `30000000000` | +| `%s__processFeeAmount` | `Integer` | `500000` | +| `%s%s%s__history____` | `String` | `'%d%d%d______'` | + + +# Functions + + +## Process +- Waves amount claimed from WavesDAO should be exceed `minClaimAmount` (*default: 300.0*) +- `processFee` (*default: 0.005*) Waves amount is sent back to caller +``` +@Callable(i) +func process() +``` From 5691274cc3a0754b3c3efe89274fb1b78fd38249 Mon Sep 17 00:00:00 2001 From: bra1nsurfer Date: Tue, 2 Apr 2024 12:51:34 +0300 Subject: [PATCH 6/7] Added WXDAO price coefficient --- docs/wxdao_funding/wxdao_funding.md | 1 + .../2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json | 5 +++++ ride/wxdao_funding.ride | 4 +++- test/components/wxdao_funding/_hooks.mjs | 5 +++++ test/components/wxdao_funding/processClaim.spec.mjs | 2 +- 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/wxdao_funding/wxdao_funding.md b/docs/wxdao_funding/wxdao_funding.md index 94b00fdb..f80af0cd 100644 --- a/docs/wxdao_funding/wxdao_funding.md +++ b/docs/wxdao_funding/wxdao_funding.md @@ -10,6 +10,7 @@ | `%s__USDTassetId` | `String` | `'G5WWWzzVsWRyzGf32xojbnfp7gXbWrgqJT8RcVWEfLmC'` | | `%s__minClaimAmount` | `Integer` | `30000000000` | | `%s__processFeeAmount` | `Integer` | `500000` | +| `%s__WXDAOpriceCoeff` | `Integer` | `110000000` (SCALE8) | | `%s%s%s__history____` | `String` | `'%d%d%d______'` | diff --git a/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json b/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json index 658f6b87..cfd23b8e 100644 --- a/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json +++ b/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json @@ -43,6 +43,11 @@ "key": "%s__processFeeAmount", "type": "integer", "value": 500000 + }, + { + "key": "%s__WXDAOpriceCoeff", + "type": "integer", + "value": 110000000 } ] } \ No newline at end of file diff --git a/ride/wxdao_funding.ride b/ride/wxdao_funding.ride index 5d443c30..8659e4bf 100644 --- a/ride/wxdao_funding.ride +++ b/ride/wxdao_funding.ride @@ -22,6 +22,7 @@ func keyWXDAOassetId() = ["%s", "WXDAOassetId"].makeString(SEP) func keyUSDTassetId() = ["%s", "USDTassetId"].makeString(SEP) func keyProcessFeeAmount() = ["%s", "processFeeAmount"].makeString(SEP) func keyMinClaimAmount() = ["%s", "minClaimAmount"].makeString(SEP) +func keyWXDAOpriceCoeff() = ["%s", "WXDAOpriceCoeff"].makeString(SEP) func keyHistory(action: String, txId: String) = ["%s%s%s", "history", action, txId].makeString(SEP) @@ -63,6 +64,7 @@ func getBalance(address: Address, assetIdString: String) = { let processFeeAmount = this.getInteger(keyProcessFeeAmount()).valueOrElse(500000) let minClaimAmount = this.getInteger(keyMinClaimAmount()).valueOrElse(0) +let wxdaoPriceCoeff = this.getInteger(keyWXDAOpriceCoeff()).valueOrElse(SCALE8) let usdtAssetIdString = this.getStringValue(keyUSDTassetId()) let wxdaoAssetIdString = this.getStringValue(keyWXDAOassetId()) let wxdaoAssetId = stringToAsset(wxdaoAssetIdString) @@ -190,7 +192,7 @@ func getWXDAOUsdtPrice() = { let invokeResult = match (priceInvoke) { case r:List[Any] => { match(r[0]) { - case i:Int => i + case i:Int => fraction(i, wxdaoPriceCoeff, SCALE8) case _ => unit } } diff --git a/test/components/wxdao_funding/_hooks.mjs b/test/components/wxdao_funding/_hooks.mjs index f361e4db..39fbcadc 100644 --- a/test/components/wxdao_funding/_hooks.mjs +++ b/test/components/wxdao_funding/_hooks.mjs @@ -84,6 +84,11 @@ export const mochaHooks = { type: 'string', value: 'MOCKED_USDT_ASSET_ID', }, + { + key: '%s__WXDAOpriceCoeff', + type: 'integer', + value: 1_1000_0000, // 110% + }, ], chainId, }, this.accounts.wxdaoFunding.seed)); diff --git a/test/components/wxdao_funding/processClaim.spec.mjs b/test/components/wxdao_funding/processClaim.spec.mjs index 21af3d19..6adabbb9 100644 --- a/test/components/wxdao_funding/processClaim.spec.mjs +++ b/test/components/wxdao_funding/processClaim.spec.mjs @@ -15,7 +15,7 @@ describe('wxdao_funding: process', /** @this {MochaSuiteModified} */() => { // Prices set in mocks const claimAmount = 1.5 * 1e8; const wavesUsdtPrice = 5.5 * 1e6; - const wxdaoUsdtPrice = 11.0 * 1e6; + const wxdaoUsdtPrice = 11.0 * 1e6 * 1.1; const processFee = 0.005 * 1e8; const swapAmount = claimAmount - processFee; From d713001d08275a658adf5f90aca76771c69aa4be Mon Sep 17 00:00:00 2001 From: bra1nsurfer Date: Tue, 2 Apr 2024 14:16:38 +0300 Subject: [PATCH 7/7] Removed admin verifier Added Proposal verifier --- .../01_wxdao_funding_data_tx.json | 5 - ride/wxdao_funding.ride | 140 +++--------------- 2 files changed, 23 insertions(+), 122 deletions(-) diff --git a/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json b/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json index cfd23b8e..8e673940 100644 --- a/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json +++ b/migrations/2024_03_29_wxdao_funding/01_wxdao_funding_data_tx.json @@ -4,11 +4,6 @@ "version": 2, "senderPublicKey": "5WG53hB4ZK2TWX7UnLeTU4ieDp8Fy4VtaYx6gKFYxGHu", "data": [ - { - "key": "%s__adminAddressList", - "type": "string", - "value": "3PBUguU83ccjXz7NBbR3vn67fVHawbxwaTH__3PLU2bd4CDhHC35bf5qsQxYLpkEkpgkrcvc__3PF3AgB3cEE8F9ZUxuA9aLyM2PBjKrrszZQ__3PHdaP2BLUXFacnqZm6WoFjidvzqZZMbb8j__3P33RBZUXcdx5JW918DqFfrscEuEZpAPUPz" - }, { "key": "%s__mainTreasuryAddress", "type": "string", diff --git a/ride/wxdao_funding.ride b/ride/wxdao_funding.ride index 8659e4bf..32b487d0 100644 --- a/ride/wxdao_funding.ride +++ b/ride/wxdao_funding.ride @@ -70,94 +70,20 @@ let wxdaoAssetIdString = this.getStringValue(keyWXDAOassetId()) let wxdaoAssetId = stringToAsset(wxdaoAssetIdString) +##### PROPOSAL VERIFIER ##### +func keyVotingResultAddress() = "contract_voting_result" +func keyProposalAllowBroadcast(address: Address, txId: ByteVector) = + ((("proposal_allow_broadcast_" + toString(address)) + "_") + toBase58String(txId)) -###################### -# MULTISIG FUNCTIONS # -###################### -let ADMIN_LIST_SIZE = 5 -let QUORUM = 3 -let TXID_BYTES_LENGTH = 32 - -func keyAllowedTxIdVotePrefix(txId: String) = makeString(["%s%s%s", "allowTxId", txId], SEP) -# Make Admin vote key -func keyFullAdminVote(prefix: String, adminAddress: String) = makeString([prefix, adminAddress], SEP) -# Admin List key -func keyAdminAddressList() = makeString(["%s", "adminAddressList"], SEP) -# Allowed TXID key -func keyAllowedTxId() = makeString(["%s", "txId"], SEP) - -func getAdminVote(prefix: String, admin: String) = { - let voteKey = keyFullAdminVote(prefix, admin) - getInteger(voteKey).valueOrElse(0) +let votingResultAddress = match getString(this, keyVotingResultAddress()) { + case s: String => + addressFromString(s) + case _: Unit => + unit + case _ => + throw("Match error") } - -func getAdminsList() = { - match (this.getString(keyAdminAddressList())) { - case s:String => s.split(SEP) - case _ => [] - } -} - -func isInAdminList(address: String) = { - getAdminsList().containsElement(address) -} - -# Generate List of keys with same prefix for all admins -func genVotesKeysHelper(a: (List[String], String), adminAddress: String) = { - let (result, prefix) = a - (result :+ keyFullAdminVote(prefix, adminAddress), prefix) -} -func genVotesKeys(keyPrefix: String) = { - let adminList = keyAdminAddressList() - let (result, prefix) = FOLD<5>(getAdminsList(), ([], keyPrefix), genVotesKeysHelper) - result -} - -# Count all votes for Prefix -func countVotesHelper(result: Int, voteKey: String) = { - result + getInteger(voteKey).valueOrElse(0) -} -func countVotes(prefix: String) = { - let votes = genVotesKeys(prefix) - FOLD<5>(votes, 0, countVotesHelper) -} - -# Generate DeleteEntry for all votes with Prefix -func clearVotesHelper(result: List[DeleteEntry], key: String) = { - result :+ DeleteEntry(key) -} -func getClearVoteEntries(prefix: String) = { - let votes = genVotesKeys(prefix) - FOLD<5>(votes, [], clearVotesHelper) -} - -func voteINTERNAL( - callerAddressString: String, - keyPrefix: String, - minVotes: Int, - voteResult: List[StringEntry|IntegerEntry|DeleteEntry] -) = { - let voteKey = keyFullAdminVote(keyPrefix, callerAddressString) - let adminCurrentVote = getAdminVote(keyPrefix, callerAddressString) - - strict err = if (!isInAdminList(callerAddressString)) then { - throwErr("Address: " + callerAddressString + " not in Admin list") - } else if (adminCurrentVote == 1) then { - throwErr(voteKey + " you already voted") - } else { unit } - - let votes = countVotes(keyPrefix) - if (votes + 1 >= minVotes) then { - let clearVoteEntries = getClearVoteEntries(keyPrefix) - clearVoteEntries ++ voteResult - } else { - [ IntegerEntry(voteKey, 1) ] - } -} -########################## -# MULTISIG FUNCTIONS END # -########################## - +##### PROPOSAL VERIFIER ##### func claimWavesFromTreasury() = { strict oldWavesBalance = this.getBalance(WAVES) @@ -253,37 +179,17 @@ func process() = { } -###################### -# MULTISIG FUNCTIONS # -###################### -# Vote for txId that is allowed in Verifier -@Callable(i) -func voteForTxId(txId: String) = { - let callerAddressString = toBase58String(i.caller.bytes) - let keyPrefix = keyAllowedTxIdVotePrefix(txId) - let result = [ StringEntry(keyAllowedTxId(), txId) ] - let allowedTxIdOption = this.getString(keyAllowedTxId()) - - strict err = [ - txId.fromBase58String().size() == TXID_BYTES_LENGTH || throwErr(txId + " is not valid txId"), - allowedTxIdOption == unit || allowedTxIdOption.value() != txId || throwErr(txId + " is already allowed") - ] - - voteINTERNAL(callerAddressString, keyPrefix, QUORUM, result) -} -########################## -# MULTISIG FUNCTIONS END # -########################## - - @Verifier(tx) -func verify() = { - let byAdmins = (tx.id == this.getString(keyAllowedTxId()).valueOrElse("").fromBase58String()) - let byOwner = (if (getAdminsList().size() >= QUORUM) then { +func verify () = { + let byProposal = match votingResultAddress { + case proposalAddress: Address => + valueOrElse(getBoolean(proposalAddress, keyProposalAllowBroadcast(this, tx.id)), false) + case _ => false - } else { - sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey) - }) + } - byAdmins || byOwner -} \ No newline at end of file + let byOwner = sigVerify(tx.bodyBytes, tx.proofs[0], tx.senderPublicKey) + if (byProposal) + then true + else byOwner +}