From 0101d7f9380442c655c1a0b38146353f26612b1f Mon Sep 17 00:00:00 2001 From: nhpd Date: Tue, 29 Aug 2023 14:22:52 +0400 Subject: [PATCH 01/12] add test for failed timelock execution result check --- src/helpers/dao.ts | 14 ++++++++++++++ src/testcases/parallel/subdao.test.ts | 3 +++ 2 files changed, 17 insertions(+) diff --git a/src/helpers/dao.ts b/src/helpers/dao.ts index b4969a1a..6a4b9e58 100644 --- a/src/helpers/dao.ts +++ b/src/helpers/dao.ts @@ -454,6 +454,20 @@ export class Dao { ); } + async getTimelockedProposalError( + proposalId: number, + customModule = 'single', + ): Promise { + return this.chain.queryContract( + this.contracts.proposals[customModule].pre_propose.timelock.address, + { + proposal_failed_execution_error: { + proposal_id: proposalId, + }, + }, + ); + } + async getSubDaoList(): Promise { const res = await this.chain.queryContract<{ addr: string }[]>( this.contracts.core.address, diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 5c83a0bb..20dc6543 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -134,6 +134,9 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.id).toEqual(proposalId); expect(timelockedProp.status).toEqual('execution_failed'); expect(timelockedProp.msgs).toHaveLength(1); + + const reason = await subDao.getTimelockedProposalError(proposalId); + expect(reason).toEqual('codespace: bank, code: 999'); }); test('execute timelocked(ExecutionFailed): WrongStatus error', async () => { From 2a9814979173b375816f379be5acca5e7a483eea Mon Sep 17 00:00:00 2001 From: nhpd Date: Tue, 29 Aug 2023 15:52:54 +0400 Subject: [PATCH 02/12] add check for error reason for timelocked contract execution failure --- src/testcases/parallel/subdao.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 20dc6543..51fa6471 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -129,14 +129,13 @@ describe('Neutron / Subdao', () => { await wait(20); // timelocked proposal execution failed due to insufficient funds on timelock contract await subdaoMember1.executeTimelockedProposal(proposalId); - // TODO: check the reason of the failure const timelockedProp = await subDao.getTimelockedProposal(proposalId); expect(timelockedProp.id).toEqual(proposalId); expect(timelockedProp.status).toEqual('execution_failed'); expect(timelockedProp.msgs).toHaveLength(1); const reason = await subDao.getTimelockedProposalError(proposalId); - expect(reason).toEqual('codespace: bank, code: 999'); + expect(reason).toEqual('codespace: sdk, code: 5'); // 'insufficient funds' error }); test('execute timelocked(ExecutionFailed): WrongStatus error', async () => { From bd00bdb45fe9c09eeef685ad10265bfb9f07a0f1 Mon Sep 17 00:00:00 2001 From: nhpd Date: Fri, 1 Sep 2023 18:47:04 +0400 Subject: [PATCH 03/12] more tests for fail reason --- src/testcases/parallel/subdao.test.ts | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 51fa6471..8d52c9b9 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -149,6 +149,71 @@ describe('Neutron / Subdao', () => { overruleTimelockedProposalMock(subdaoMember1, proposalId), ).rejects.toThrow(/Wrong proposal status \(execution_failed\)/); }); + + let proposalId2: number; + test('proposal timelock 2', async () => { + proposalId2 = await subdaoMember1.submitParameterChangeProposal( + 'paramchange', + 'paramchange', + 'icahost', + 'HostEnabled', + '123123123', // expected boolean, provided number + '1000', + ); + + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( + proposalId2, + ); + + expect(timelockedProp.id).toEqual(proposalId2); + expect(timelockedProp.status).toEqual('timelocked'); + expect(timelockedProp.msgs).toHaveLength(1); + }); + + test('execute timelocked: execution failed', async () => { + //wait for timelock durations + await wait(20); + // timelocked proposal execution failed due to invalid param value + await subdaoMember1.executeTimelockedProposal(proposalId2); + const timelockedProp = await subDao.getTimelockedProposal(proposalId2); + expect(timelockedProp.id).toEqual(proposalId2); + expect(timelockedProp.status).toEqual('execution_failed'); + expect(timelockedProp.msgs).toHaveLength(1); + + const reason = await subDao.getTimelockedProposalError(proposalId2); + expect(reason).toEqual('codespace: params, code: kekw'); + }); + + let proposalId3: number; + test('proposal timelock 2', async () => { + proposalId3 = await subdaoMember1.submitCancelSoftwareUpgradeProposal( + 'cancel', + 'cancel', + '1000', + ); + + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( + proposalId3, + ); + + expect(timelockedProp.id).toEqual(proposalId3); + expect(timelockedProp.status).toEqual('timelocked'); + expect(timelockedProp.msgs).toHaveLength(1); + }); + + test('execute timelocked: execution failed', async () => { + //wait for timelock durations + await wait(20); + // timelocked proposal execution failed due to invalid param value + await subdaoMember1.executeTimelockedProposal(proposalId3); + const timelockedProp = await subDao.getTimelockedProposal(proposalId3); + expect(timelockedProp.id).toEqual(proposalId3); + expect(timelockedProp.status).toEqual('execution_failed'); + expect(timelockedProp.msgs).toHaveLength(1); + + const reason = await subDao.getTimelockedProposalError(proposalId3); + expect(reason).toEqual('codespace: upgrade, code: kekw'); + }); }); describe('Timelock: Succeed execution', () => { From d229cb71fa73a7a37b3f67bbcafe27d1808abb81 Mon Sep 17 00:00:00 2001 From: nhpd Date: Tue, 5 Sep 2023 16:29:59 +0400 Subject: [PATCH 04/12] feat: change errors in failed execution resp to be an array with heights --- src/helpers/dao.ts | 7 +++-- src/helpers/types.ts | 7 +++++ src/testcases/parallel/subdao.test.ts | 42 +++++---------------------- 3 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/helpers/dao.ts b/src/helpers/dao.ts index 6a4b9e58..8be385b0 100644 --- a/src/helpers/dao.ts +++ b/src/helpers/dao.ts @@ -11,6 +11,7 @@ import { getWithAttempts } from './wait'; import { MultiChoiceOption, NeutronContract, + ProposalFailedExecutionErrorResponse, SingleChoiceProposal, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, @@ -454,11 +455,11 @@ export class Dao { ); } - async getTimelockedProposalError( + async getTimelockedProposalErrors( proposalId: number, customModule = 'single', - ): Promise { - return this.chain.queryContract( + ): Promise { + return this.chain.queryContract( this.contracts.proposals[customModule].pre_propose.timelock.address, { proposal_failed_execution_error: { diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 5b183587..69181af9 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -277,3 +277,10 @@ export type ContractAdminResponse = { admin: string; }; }; + +export type ProposalFailedExecutionErrorResponse = { + errors: { + height: number; + error: string; + }[]; +}; diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 8d52c9b9..06422f2c 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -134,8 +134,10 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.status).toEqual('execution_failed'); expect(timelockedProp.msgs).toHaveLength(1); - const reason = await subDao.getTimelockedProposalError(proposalId); - expect(reason).toEqual('codespace: sdk, code: 5'); // 'insufficient funds' error + const res = await subDao.getTimelockedProposalErrors(proposalId); + expect(res.errors.length).toEqual(1); + expect(res.errors[0].error).toEqual('codespace: sdk, code: 5'); // 'insufficient funds' error + expect(res.errors[0].height).toBeGreaterThan(0); }); test('execute timelocked(ExecutionFailed): WrongStatus error', async () => { @@ -180,39 +182,9 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.status).toEqual('execution_failed'); expect(timelockedProp.msgs).toHaveLength(1); - const reason = await subDao.getTimelockedProposalError(proposalId2); - expect(reason).toEqual('codespace: params, code: kekw'); - }); - - let proposalId3: number; - test('proposal timelock 2', async () => { - proposalId3 = await subdaoMember1.submitCancelSoftwareUpgradeProposal( - 'cancel', - 'cancel', - '1000', - ); - - const timelockedProp = await subdaoMember1.supportAndExecuteProposal( - proposalId3, - ); - - expect(timelockedProp.id).toEqual(proposalId3); - expect(timelockedProp.status).toEqual('timelocked'); - expect(timelockedProp.msgs).toHaveLength(1); - }); - - test('execute timelocked: execution failed', async () => { - //wait for timelock durations - await wait(20); - // timelocked proposal execution failed due to invalid param value - await subdaoMember1.executeTimelockedProposal(proposalId3); - const timelockedProp = await subDao.getTimelockedProposal(proposalId3); - expect(timelockedProp.id).toEqual(proposalId3); - expect(timelockedProp.status).toEqual('execution_failed'); - expect(timelockedProp.msgs).toHaveLength(1); - - const reason = await subDao.getTimelockedProposalError(proposalId3); - expect(reason).toEqual('codespace: upgrade, code: kekw'); + const res = await subDao.getTimelockedProposalErrors(proposalId2); + expect(res.errors.length).toEqual(0); + expect(res.errors[0].error).toEqual('codespace: undefined, code: 1'); }); }); From 69c953e94506b560af2f994feb9e03db3b8ab787 Mon Sep 17 00:00:00 2001 From: nhpd Date: Tue, 5 Sep 2023 16:59:43 +0400 Subject: [PATCH 05/12] fix tests; add check for two failed executions for one proposal --- src/testcases/parallel/subdao.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 06422f2c..ebf5907f 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -183,8 +183,16 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.msgs).toHaveLength(1); const res = await subDao.getTimelockedProposalErrors(proposalId2); - expect(res.errors.length).toEqual(0); + expect(res.errors.length).toEqual(1); expect(res.errors[0].error).toEqual('codespace: undefined, code: 1'); + + // try execute second time + await subdaoMember1.executeTimelockedProposal(proposalId2); + await neutronChain.blockWaiter.waitBlocks(2); + + // should add new error here + const res2 = await subDao.getTimelockedProposalErrors(proposalId2); + expect(res2.errors.length).toEqual(2); }); }); From f22db198962050404315be720d1ee5b369812a95 Mon Sep 17 00:00:00 2001 From: nhpd Date: Fri, 15 Sep 2023 17:35:40 +0400 Subject: [PATCH 06/12] fix tests --- src/helpers/dao.ts | 45 +++++++- src/testcases/parallel/subdao.test.ts | 153 +++++++++++++++++++++++--- 2 files changed, 182 insertions(+), 16 deletions(-) diff --git a/src/helpers/dao.ts b/src/helpers/dao.ts index 8be385b0..94832df8 100644 --- a/src/helpers/dao.ts +++ b/src/helpers/dao.ts @@ -48,6 +48,20 @@ export type TimelockConfig = { subdao: string; }; +export type SubdaoProposalConfig = { + threshold: any; + max_voting_period: Duration; + min_voting_period: Duration; + allow_revoting: boolean; + dao: string; + close_proposal_on_execution_failure: boolean; +}; + +export type Duration = { + height: number | null; + time: number | null; +}; + export type TimelockProposalListResponse = { proposals: Array; }; @@ -915,6 +929,32 @@ export class DaoMember { ); } + async submitUpdateConfigProposal( + title: string, + description: string, + config: SubdaoProposalConfig, + deposit: string, + ): Promise { + const msg = { + update_config: config, + }; + const message = { + wasm: { + execute: { + contract_addr: this.dao.contracts.proposals['single'].address, + msg: Buffer.from(JSON.stringify(msg)).toString('base64'), + funds: [], + }, + }, + }; + return await this.submitSingleChoiceProposal( + title, + description, + [message], + deposit, + ); + } + async overruleTimelockedProposal( timelockAddress: string, proposalId: number, @@ -1317,6 +1357,7 @@ export const deploySubdao = async ( mainDaoCoreAddress: string, overrulePreProposeAddress: string, securityDaoAddr: string, + closeProposalOnExecutionFailure: boolean, ): Promise => { const coreCodeId = await cm.storeWasm(NeutronContract.SUBDAO_CORE); const cw4VotingCodeId = await cm.storeWasm(NeutronContract.CW4_VOTING); @@ -1366,7 +1407,7 @@ export const deploySubdao = async ( }, }, }, - close_proposal_on_execution_failure: false, + close_proposal_on_execution_failure: closeProposalOnExecutionFailure, }; const proposalModuleInstantiateInfo = { code_id: proposeCodeId, @@ -1460,6 +1501,7 @@ export const setupSubDaoTimelockSet = async ( mainDaoAddress: string, securityDaoAddr: string, mockMainDao: boolean, + closeProposalOnExecutionFailure: boolean, ): Promise => { const daoContracts = await getDaoContracts(cm.chain, mainDaoAddress); const subDao = await deploySubdao( @@ -1467,6 +1509,7 @@ export const setupSubDaoTimelockSet = async ( mockMainDao ? cm.wallet.address.toString() : daoContracts.core.address, daoContracts.proposals.overrule.pre_propose.address, securityDaoAddr, + closeProposalOnExecutionFailure, ); const mainDaoMember = new DaoMember(cm, new Dao(cm.chain, daoContracts)); diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index ebf5907f..18852084 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -13,12 +13,15 @@ import { DaoMember, setupSubDaoTimelockSet, deployNeutronDao, + SubdaoProposalConfig, } from '../../helpers/dao'; import { getHeight, wait } from '../../helpers/wait'; import { TestStateLocalCosmosTestNet } from '../common_localcosmosnet'; import { AccAddress, ValAddress } from '@cosmos-client/core/cjs/types'; import { Wallet } from '../../types'; import { BroadcastTx200ResponseTxResponse } from '@cosmos-client/core/cjs/openapi/api'; +import { paramChangeProposal, sendProposal } from '../../helpers/proposal'; +import Long from 'long'; describe('Neutron / Subdao', () => { let testState: TestStateLocalCosmosTestNet; @@ -64,6 +67,7 @@ describe('Neutron / Subdao', () => { mainDao.contracts.core.address, securityDaoAddr.toString(), true, + true, // close proposal on execution failure ); subdaoMember1 = new DaoMember(neutronAccount1, subDao); @@ -112,7 +116,7 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.msgs).toHaveLength(1); }); - test('execute timelocked: nonexistant ', async () => { + test('execute timelocked: nonexistant', async () => { await expect( subdaoMember1.executeTimelockedProposal(1_000_000), ).rejects.toThrow(/SingleChoiceProposal not found/); @@ -153,16 +157,36 @@ describe('Neutron / Subdao', () => { }); let proposalId2: number; - test('proposal timelock 2', async () => { - proposalId2 = await subdaoMember1.submitParameterChangeProposal( - 'paramchange', - 'paramchange', - 'icahost', - 'HostEnabled', - '123123123', // expected boolean, provided number + test('proposal timelock 2 with two messages, one of them fails', async () => { + // pack two messages in one proposal + const failMessage = paramChangeProposal({ + title: 'paramchange', + description: 'paramchange', + subspace: 'icahost', + key: 'HostEnabled', + value: '123123123', // expected boolean, provided number + }); + const goodMessage = sendProposal({ + to: neutronAccount2.wallet.address.toString(), + denom: NEUTRON_DENOM, + amount: '100', + }); + const fee = { + gas_limit: Long.fromString('4000000'), + amount: [{ denom: NEUTRON_DENOM, amount: '10000' }], + }; + proposalId2 = await subdaoMember1.submitSingleChoiceProposal( + 'proposal2', + 'proposal2', + [goodMessage, failMessage], '1000', + 'single', + fee, ); + // TODO: remove wait? + neutronChain.blockWaiter.waitBlocks(2); + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( proposalId2, ); @@ -172,7 +196,11 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.msgs).toHaveLength(1); }); - test('execute timelocked: execution failed', async () => { + test('execute timelocked 2: execution failed', async () => { + // from here + await neutronAccount1.msgSend(subDao.contracts.core.address, '100000'); // fund the subdao treasury + const balance2 = await neutronAccount2.queryDenomBalance(NEUTRON_DENOM); + //wait for timelock durations await wait(20); // timelocked proposal execution failed due to invalid param value @@ -186,13 +214,104 @@ describe('Neutron / Subdao', () => { expect(res.errors.length).toEqual(1); expect(res.errors[0].error).toEqual('codespace: undefined, code: 1'); - // try execute second time - await subdaoMember1.executeTimelockedProposal(proposalId2); + // check that goodMessage failed as well + const balance2After = await neutronAccount2.queryDenomBalance( + NEUTRON_DENOM, + ); + expect(balance2After).toEqual(balance2); + + // cannot execute failed proposal with closeOnProposalExecutionFailed=true + await expect( + subdaoMember1.executeTimelockedProposal(proposalId2), + ).rejects.toThrow(/Wrong proposal status \(execution_failed\)/); await neutronChain.blockWaiter.waitBlocks(2); + }); - // should add new error here - const res2 = await subDao.getTimelockedProposalErrors(proposalId2); - expect(res2.errors.length).toEqual(2); + test('prepare subdao with closeOnProposalExecutionFailed = false', async () => { + const subdaoConfig = + await neutronChain.queryContract( + subDao.contracts.proposals.single.address, + { + config: {}, + }, + ); + expect(subdaoConfig.close_proposal_on_execution_failure).toEqual(true); + subdaoConfig.close_proposal_on_execution_failure = false; + + const proposalId = await subdaoMember1.submitUpdateConfigProposal( + 'updateconfig', + 'updateconfig', + subdaoConfig, + '1000', + ); + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( + proposalId, + ); // Q: will throw? + expect(timelockedProp.status).toEqual('timelocked'); + //wait for timelock durations + await wait(20); + await subdaoMember1.executeTimelockedProposal(proposalId); // should execute no problem + + neutronChain.blockWaiter.waitBlocks(2); + }); + + let proposalId3: number; + test('proposal timelock 3', async () => { + proposalId3 = await subdaoMember1.submitParameterChangeProposal( + 'paramchange', + 'paramchange', + 'icahost', + 'HostEnabled', + '123123123', // expected boolean, provided number + '1000', + ); + + const timelockedProp = await subdaoMember1.supportAndExecuteProposal( + proposalId3, + ); // Q: will throw? + + expect(timelockedProp.id).toEqual(proposalId3); + expect(timelockedProp.status).toEqual('timelocked'); + expect(timelockedProp.msgs).toHaveLength(1); + }); + + test('execute timelocked 3: execution failed', async () => { + const subdaoConfig = + await neutronChain.queryContract( + subDao.contracts.proposals.single.address, + { + config: {}, + }, + ); + expect(subdaoConfig.close_proposal_on_execution_failure).toEqual(false); + + //wait for timelock durations + await wait(20); + // timelocked proposal execution failed due to invalid param value + await subdaoMember1.executeTimelockedProposal(proposalId3); + const timelockedProp = await subDao.getTimelockedProposal(proposalId3); + expect(timelockedProp.id).toEqual(proposalId3); + expect(timelockedProp.status).toEqual('timelocked'); + expect(timelockedProp.msgs).toHaveLength(1); + + const res = await subDao.getTimelockedProposalErrors(proposalId3); + expect(res.errors.length).toEqual(0); // do not have errors because we do not write error here + + await subdaoMember1.executeTimelockedProposal(proposalId3); // should execute no problem + await neutronChain.blockWaiter.waitBlocks(2); + }); + + test('get back subdao proposal params', async () => { + const subdaoConfig = + await neutronChain.queryContract( + subDao.contracts.proposals.single.address, + { + config: {}, + }, + ); + expect(subdaoConfig.close_proposal_on_execution_failure).toEqual(false); + subdaoConfig.close_proposal_on_execution_failure = true; + // TODO: set them }); }); @@ -218,6 +337,7 @@ describe('Neutron / Subdao', () => { }); test('execute timelocked: success', async () => { + // from here await neutronAccount1.msgSend(subDao.contracts.core.address, '20000'); // fund the subdao treasury const balance2 = await neutronAccount2.queryDenomBalance(NEUTRON_DENOM); await wait(20); @@ -731,6 +851,7 @@ describe('Neutron / Subdao', () => { mainDao.contracts.core.address, demo1Addr.toString(), true, + true, ); subDAOQueryTestScopeMember = new DaoMember( neutronAccount1, @@ -870,7 +991,9 @@ describe('Neutron / Subdao', () => { await subdaoMember1.supportAndExecuteProposal(proposalId); await wait(20); - await subdaoMember1.executeTimelockedProposal(proposalId); + await expect( + subdaoMember1.executeTimelockedProposal(proposalId), + ).rejects.toThrow(/config name cannot be empty/); const timelockedProp = await subDao.getTimelockedProposal(proposalId); expect(timelockedProp.id).toEqual(proposalId); expect(timelockedProp.status).toEqual('execution_failed'); From bf6e10125f879d9f2f3241c2c1cbe2cf191af05e Mon Sep 17 00:00:00 2001 From: nhpd Date: Fri, 15 Sep 2023 18:01:48 +0400 Subject: [PATCH 07/12] more fixes --- src/testcases/parallel/subdao.test.ts | 53 +++++++++++++-------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 18852084..ac69be65 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -184,9 +184,6 @@ describe('Neutron / Subdao', () => { fee, ); - // TODO: remove wait? - neutronChain.blockWaiter.waitBlocks(2); - const timelockedProp = await subdaoMember1.supportAndExecuteProposal( proposalId2, ); @@ -257,18 +254,17 @@ describe('Neutron / Subdao', () => { let proposalId3: number; test('proposal timelock 3', async () => { - proposalId3 = await subdaoMember1.submitParameterChangeProposal( - 'paramchange', - 'paramchange', - 'icahost', - 'HostEnabled', - '123123123', // expected boolean, provided number - '1000', - ); + proposalId3 = await subdaoMember1.submitSendProposal('send', 'send', [ + { + recipient: demo2Addr.toString(), + amount: 200000, + denom: neutronChain.denom, + }, + ]); const timelockedProp = await subdaoMember1.supportAndExecuteProposal( proposalId3, - ); // Q: will throw? + ); expect(timelockedProp.id).toEqual(proposalId3); expect(timelockedProp.status).toEqual('timelocked'); @@ -288,7 +284,9 @@ describe('Neutron / Subdao', () => { //wait for timelock durations await wait(20); // timelocked proposal execution failed due to invalid param value - await subdaoMember1.executeTimelockedProposal(proposalId3); + await expect( + subdaoMember1.executeTimelockedProposal(proposalId3), + ).rejects.toThrow(); const timelockedProp = await subDao.getTimelockedProposal(proposalId3); expect(timelockedProp.id).toEqual(proposalId3); expect(timelockedProp.status).toEqual('timelocked'); @@ -297,21 +295,22 @@ describe('Neutron / Subdao', () => { const res = await subDao.getTimelockedProposalErrors(proposalId3); expect(res.errors.length).toEqual(0); // do not have errors because we do not write error here - await subdaoMember1.executeTimelockedProposal(proposalId3); // should execute no problem + await neutronAccount1.msgSend(subDao.contracts.core.address, '300000'); + + // now that we have funds should execute without problems + + const balanceBefore = await neutronChain.queryDenomBalance( + demo2Addr.toString(), + NEUTRON_DENOM, + ); + await subdaoMember1.executeTimelockedProposal(proposalId3); await neutronChain.blockWaiter.waitBlocks(2); - }); + const balanceAfter = await neutronChain.queryDenomBalance( + demo2Addr.toString(), + NEUTRON_DENOM, + ); - test('get back subdao proposal params', async () => { - const subdaoConfig = - await neutronChain.queryContract( - subDao.contracts.proposals.single.address, - { - config: {}, - }, - ); - expect(subdaoConfig.close_proposal_on_execution_failure).toEqual(false); - subdaoConfig.close_proposal_on_execution_failure = true; - // TODO: set them + expect(balanceAfter - balanceBefore).toEqual(200000); }); }); @@ -996,7 +995,7 @@ describe('Neutron / Subdao', () => { ).rejects.toThrow(/config name cannot be empty/); const timelockedProp = await subDao.getTimelockedProposal(proposalId); expect(timelockedProp.id).toEqual(proposalId); - expect(timelockedProp.status).toEqual('execution_failed'); + expect(timelockedProp.status).toEqual('timelocked'); expect(timelockedProp.msgs).toHaveLength(1); const configAfter = await neutronChain.queryContract( subDao.contracts.core.address, From e9c182996004860d201f7e9d692f561bfc1d04f6 Mon Sep 17 00:00:00 2001 From: nhpd Date: Mon, 18 Sep 2023 12:36:51 +0400 Subject: [PATCH 08/12] cleanup --- src/testcases/parallel/subdao.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index ac69be65..2812d400 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -224,7 +224,7 @@ describe('Neutron / Subdao', () => { await neutronChain.blockWaiter.waitBlocks(2); }); - test('prepare subdao with closeOnProposalExecutionFailed = false', async () => { + test('change subdao proposal config with closeOnProposalExecutionFailed = false', async () => { const subdaoConfig = await neutronChain.queryContract( subDao.contracts.proposals.single.address, @@ -243,7 +243,7 @@ describe('Neutron / Subdao', () => { ); const timelockedProp = await subdaoMember1.supportAndExecuteProposal( proposalId, - ); // Q: will throw? + ); expect(timelockedProp.status).toEqual('timelocked'); //wait for timelock durations await wait(20); @@ -293,7 +293,8 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.msgs).toHaveLength(1); const res = await subDao.getTimelockedProposalErrors(proposalId3); - expect(res.errors.length).toEqual(0); // do not have errors because we do not write error here + // do not have errors because we did not have reply + expect(res.errors.length).toEqual(0); await neutronAccount1.msgSend(subDao.contracts.core.address, '300000'); @@ -336,7 +337,6 @@ describe('Neutron / Subdao', () => { }); test('execute timelocked: success', async () => { - // from here await neutronAccount1.msgSend(subDao.contracts.core.address, '20000'); // fund the subdao treasury const balance2 = await neutronAccount2.queryDenomBalance(NEUTRON_DENOM); await wait(20); From 4a38ffa4609ac61a090c2e7b9c7687fd95504fd4 Mon Sep 17 00:00:00 2001 From: nhpd Date: Wed, 4 Oct 2023 15:04:37 +0300 Subject: [PATCH 09/12] fix tests --- src/helpers/dao.ts | 12 ++++++++++-- src/helpers/types.ts | 7 +------ src/testcases/parallel/overrule.test.ts | 1 + src/testcases/parallel/subdao.test.ts | 18 +++++++----------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/helpers/dao.ts b/src/helpers/dao.ts index 04a4d06e..f7e835d0 100644 --- a/src/helpers/dao.ts +++ b/src/helpers/dao.ts @@ -949,7 +949,15 @@ export class DaoMember { ): Promise { await this.voteYes(proposalId, customModule); await this.executeProposal(proposalId, customModule); - return await this.dao.getTimelockedProposal(proposalId, customModule); + return await getWithAttempts( + this.dao.chain.blockWaiter, + async () => + await this.dao.getTimelockedProposal(proposalId, customModule), + async (response) => { + return response.id && +response.id > 0; + }, + 5, + ); } async executeTimelockedProposal( @@ -1442,7 +1450,7 @@ export const deploySubdao = async ( mainDaoCoreAddress: string, overrulePreProposeAddress: string, securityDaoAddr: string, - closeProposalOnExecutionFailure: boolean, + closeProposalOnExecutionFailure = true, ): Promise => { const coreCodeId = await cm.storeWasm(NeutronContract.SUBDAO_CORE); const cw4VotingCodeId = await cm.storeWasm(NeutronContract.CW4_VOTING); diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 17ded19d..f568c6a4 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -284,9 +284,4 @@ export type ContractAdminResponse = { }; }; -export type ProposalFailedExecutionErrorResponse = { - errors: { - height: number; - error: string; - }[]; -}; +export type ProposalFailedExecutionErrorResponse = string; diff --git a/src/testcases/parallel/overrule.test.ts b/src/testcases/parallel/overrule.test.ts index f8da0e86..b8458e75 100644 --- a/src/testcases/parallel/overrule.test.ts +++ b/src/testcases/parallel/overrule.test.ts @@ -56,6 +56,7 @@ describe('Neutron / Subdao', () => { daoContracts.core.address, daoContracts.proposals.overrule?.pre_propose?.address || '', neutronAccount1.wallet.address.toString(), + false, // do not close proposal on failure since otherwise we wont get an error exception from submsgs ); subdaoMember1 = new DaoMember(neutronAccount1, subDao); diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index 2812d400..a1326d26 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -138,10 +138,8 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.status).toEqual('execution_failed'); expect(timelockedProp.msgs).toHaveLength(1); - const res = await subDao.getTimelockedProposalErrors(proposalId); - expect(res.errors.length).toEqual(1); - expect(res.errors[0].error).toEqual('codespace: sdk, code: 5'); // 'insufficient funds' error - expect(res.errors[0].height).toBeGreaterThan(0); + const error = await subDao.getTimelockedProposalErrors(proposalId); + expect(error).toEqual('codespace: sdk, code: 5'); // 'insufficient funds' error }); test('execute timelocked(ExecutionFailed): WrongStatus error', async () => { @@ -194,7 +192,6 @@ describe('Neutron / Subdao', () => { }); test('execute timelocked 2: execution failed', async () => { - // from here await neutronAccount1.msgSend(subDao.contracts.core.address, '100000'); // fund the subdao treasury const balance2 = await neutronAccount2.queryDenomBalance(NEUTRON_DENOM); @@ -207,9 +204,8 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.status).toEqual('execution_failed'); expect(timelockedProp.msgs).toHaveLength(1); - const res = await subDao.getTimelockedProposalErrors(proposalId2); - expect(res.errors.length).toEqual(1); - expect(res.errors[0].error).toEqual('codespace: undefined, code: 1'); + const error = await subDao.getTimelockedProposalErrors(proposalId2); + expect(error).toEqual('codespace: undefined, code: 1'); // check that goodMessage failed as well const balance2After = await neutronAccount2.queryDenomBalance( @@ -292,9 +288,9 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.status).toEqual('timelocked'); expect(timelockedProp.msgs).toHaveLength(1); - const res = await subDao.getTimelockedProposalErrors(proposalId3); - // do not have errors because we did not have reply - expect(res.errors.length).toEqual(0); + const error = await subDao.getTimelockedProposalErrors(proposalId3); + // do not have an error because we did not have reply + expect(error).toEqual(null); await neutronAccount1.msgSend(subDao.contracts.core.address, '300000'); From 6b7f203a99ceee17263ccb76f0c851d12f82545e Mon Sep 17 00:00:00 2001 From: nhpd Date: Fri, 6 Oct 2023 14:35:22 +0300 Subject: [PATCH 10/12] fix lint --- src/helpers/dao.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/helpers/dao.ts b/src/helpers/dao.ts index f7e835d0..3afe4b57 100644 --- a/src/helpers/dao.ts +++ b/src/helpers/dao.ts @@ -953,9 +953,7 @@ export class DaoMember { this.dao.chain.blockWaiter, async () => await this.dao.getTimelockedProposal(proposalId, customModule), - async (response) => { - return response.id && +response.id > 0; - }, + async (response) => response.id && +response.id > 0, 5, ); } From f54bdf96922c6b277c8a176975c8e81ee3adb6c7 Mon Sep 17 00:00:00 2001 From: nhpd Date: Tue, 10 Oct 2023 18:49:30 +0400 Subject: [PATCH 11/12] rename query to proposal_execution_error --- src/helpers/dao.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/dao.ts b/src/helpers/dao.ts index 3afe4b57..f9072ccc 100644 --- a/src/helpers/dao.ts +++ b/src/helpers/dao.ts @@ -479,7 +479,7 @@ export class Dao { return this.chain.queryContract( this.contracts.proposals[customModule].pre_propose.timelock.address, { - proposal_failed_execution_error: { + proposal_execution_error: { proposal_id: proposalId, }, }, From 931c038563be557242716b6f618ae8e9579fc6a4 Mon Sep 17 00:00:00 2001 From: nhpd Date: Wed, 11 Oct 2023 14:33:32 +0400 Subject: [PATCH 12/12] review fix --- src/testcases/parallel/subdao.test.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/testcases/parallel/subdao.test.ts b/src/testcases/parallel/subdao.test.ts index a1326d26..d7fc2c3f 100644 --- a/src/testcases/parallel/subdao.test.ts +++ b/src/testcases/parallel/subdao.test.ts @@ -245,11 +245,22 @@ describe('Neutron / Subdao', () => { await wait(20); await subdaoMember1.executeTimelockedProposal(proposalId); // should execute no problem - neutronChain.blockWaiter.waitBlocks(2); + await neutronChain.blockWaiter.waitBlocks(2); + + const subdaoConfigAfter = + await neutronChain.queryContract( + subDao.contracts.proposals.single.address, + { + config: {}, + }, + ); + expect(subdaoConfigAfter.close_proposal_on_execution_failure).toEqual( + false, + ); }); let proposalId3: number; - test('proposal timelock 3', async () => { + test('proposal timelock 3 with not enough funds initially to resubmit later', async () => { proposalId3 = await subdaoMember1.submitSendProposal('send', 'send', [ { recipient: demo2Addr.toString(), @@ -267,7 +278,7 @@ describe('Neutron / Subdao', () => { expect(timelockedProp.msgs).toHaveLength(1); }); - test('execute timelocked 3: execution failed', async () => { + test('execute timelocked 3: execution failed at first and then successful after funds sent', async () => { const subdaoConfig = await neutronChain.queryContract( subDao.contracts.proposals.single.address, @@ -279,10 +290,10 @@ describe('Neutron / Subdao', () => { //wait for timelock durations await wait(20); - // timelocked proposal execution failed due to invalid param value + // timelocked proposal execution failed due to insufficient funds await expect( subdaoMember1.executeTimelockedProposal(proposalId3), - ).rejects.toThrow(); + ).rejects.toThrow(/insufficient funds/); const timelockedProp = await subDao.getTimelockedProposal(proposalId3); expect(timelockedProp.id).toEqual(proposalId3); expect(timelockedProp.status).toEqual('timelocked');