Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: save failed result #ntrn-80 #198

Closed
wants to merge 14 commits into from
Closed
68 changes: 66 additions & 2 deletions src/helpers/dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getWithAttempts } from './wait';
import {
MultiChoiceOption,
NeutronContract,
ProposalFailedExecutionErrorResponse,
SingleChoiceProposal,
TotalPowerAtHeightResponse,
VotingPowerAtHeightResponse,
Expand Down Expand Up @@ -50,6 +51,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<TimeLockSingleChoiceProposal>;
};
Expand Down Expand Up @@ -457,6 +472,20 @@ export class Dao {
);
}

async getTimelockedProposalErrors(
proposalId: number,
customModule = 'single',
): Promise<ProposalFailedExecutionErrorResponse> {
return this.chain.queryContract<ProposalFailedExecutionErrorResponse>(
this.contracts.proposals[customModule].pre_propose.timelock.address,
{
proposal_execution_error: {
proposal_id: proposalId,
},
},
);
}

async getSubDaoList(): Promise<string[]> {
const res = await this.chain.queryContract<{ addr: string }[]>(
this.contracts.core.address,
Expand Down Expand Up @@ -920,7 +949,13 @@ export class DaoMember {
): Promise<TimeLockSingleChoiceProposal> {
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) => response.id && +response.id > 0,
5,
);
}

async executeTimelockedProposal(
Expand All @@ -937,6 +972,32 @@ export class DaoMember {
);
}

async submitUpdateConfigProposal(
title: string,
description: string,
config: SubdaoProposalConfig,
deposit: string,
): Promise<number> {
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,
Expand Down Expand Up @@ -1387,6 +1448,7 @@ export const deploySubdao = async (
mainDaoCoreAddress: string,
overrulePreProposeAddress: string,
securityDaoAddr: string,
closeProposalOnExecutionFailure = true,
): Promise<Dao> => {
const coreCodeId = await cm.storeWasm(NeutronContract.SUBDAO_CORE);
const cw4VotingCodeId = await cm.storeWasm(NeutronContract.CW4_VOTING);
Expand Down Expand Up @@ -1436,7 +1498,7 @@ export const deploySubdao = async (
},
},
},
close_proposal_on_execution_failure: false,
close_proposal_on_execution_failure: closeProposalOnExecutionFailure,
};
const proposalModuleInstantiateInfo = {
code_id: proposeCodeId,
Expand Down Expand Up @@ -1530,13 +1592,15 @@ export const setupSubDaoTimelockSet = async (
mainDaoAddress: string,
securityDaoAddr: string,
mockMainDao: boolean,
closeProposalOnExecutionFailure: boolean,
): Promise<Dao> => {
const daoContracts = await getDaoContracts(cm.chain, mainDaoAddress);
const subDao = await deploySubdao(
cm,
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));
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,5 @@ export type ContractAdminResponse = {
admin: string;
};
};

export type ProposalFailedExecutionErrorResponse = string;
1 change: 1 addition & 0 deletions src/testcases/parallel/overrule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
184 changes: 180 additions & 4 deletions src/testcases/parallel/subdao.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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/);
Expand All @@ -129,11 +133,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 error = await subDao.getTimelockedProposalErrors(proposalId);
expect(error).toEqual('codespace: sdk, code: 5'); // 'insufficient funds' error
});

test('execute timelocked(ExecutionFailed): WrongStatus error', async () => {
Expand All @@ -147,6 +153,173 @@ describe('Neutron / Subdao', () => {
overruleTimelockedProposalMock(subdaoMember1, proposalId),
).rejects.toThrow(/Wrong proposal status \(execution_failed\)/);
});

let proposalId2: 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,
);

const timelockedProp = await subdaoMember1.supportAndExecuteProposal(
proposalId2,
);

expect(timelockedProp.id).toEqual(proposalId2);
expect(timelockedProp.status).toEqual('timelocked');
expect(timelockedProp.msgs).toHaveLength(1);
});

test('execute timelocked 2: execution failed', async () => {
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
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 error = await subDao.getTimelockedProposalErrors(proposalId2);
expect(error).toEqual('codespace: undefined, code: 1');

// 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);
});

test('change subdao proposal config with closeOnProposalExecutionFailed = false', async () => {
const subdaoConfig =
await neutronChain.queryContract<SubdaoProposalConfig>(
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,
);
expect(timelockedProp.status).toEqual('timelocked');
//wait for timelock durations
await wait(20);
await subdaoMember1.executeTimelockedProposal(proposalId); // should execute no problem
sotnikov-s marked this conversation as resolved.
Show resolved Hide resolved

await neutronChain.blockWaiter.waitBlocks(2);

const subdaoConfigAfter =
await neutronChain.queryContract<SubdaoProposalConfig>(
subDao.contracts.proposals.single.address,
{
config: {},
},
);
expect(subdaoConfigAfter.close_proposal_on_execution_failure).toEqual(
false,
);
});

let proposalId3: number;
test('proposal timelock 3 with not enough funds initially to resubmit later', async () => {
proposalId3 = await subdaoMember1.submitSendProposal('send', 'send', [
{
recipient: demo2Addr.toString(),
amount: 200000,
denom: neutronChain.denom,
},
]);

const timelockedProp = await subdaoMember1.supportAndExecuteProposal(
proposalId3,
);

expect(timelockedProp.id).toEqual(proposalId3);
expect(timelockedProp.status).toEqual('timelocked');
expect(timelockedProp.msgs).toHaveLength(1);
});

test('execute timelocked 3: execution failed at first and then successful after funds sent', async () => {
const subdaoConfig =
await neutronChain.queryContract<SubdaoProposalConfig>(
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 insufficient funds
await expect(
subdaoMember1.executeTimelockedProposal(proposalId3),
).rejects.toThrow(/insufficient funds/);
const timelockedProp = await subDao.getTimelockedProposal(proposalId3);
expect(timelockedProp.id).toEqual(proposalId3);
expect(timelockedProp.status).toEqual('timelocked');
expect(timelockedProp.msgs).toHaveLength(1);

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');

// 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,
);

expect(balanceAfter - balanceBefore).toEqual(200000);
});
});

describe('Timelock: Succeed execution', () => {
Expand Down Expand Up @@ -684,6 +857,7 @@ describe('Neutron / Subdao', () => {
mainDao.contracts.core.address,
demo1Addr.toString(),
true,
true,
);
subDAOQueryTestScopeMember = new DaoMember(
neutronAccount1,
Expand Down Expand Up @@ -823,10 +997,12 @@ 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');
expect(timelockedProp.status).toEqual('timelocked');
expect(timelockedProp.msgs).toHaveLength(1);
const configAfter = await neutronChain.queryContract<SubDaoConfig>(
subDao.contracts.core.address,
Expand Down