diff --git a/contracts_thirdparty/cw20_merkle_airdrop_orig.wasm b/contracts_thirdparty/cw20_merkle_airdrop_orig.wasm new file mode 100644 index 00000000..0476848e Binary files /dev/null and b/contracts_thirdparty/cw20_merkle_airdrop_orig.wasm differ diff --git a/setup/Makefile b/setup/Makefile index ea649a28..c55256df 100644 --- a/setup/Makefile +++ b/setup/Makefile @@ -8,7 +8,7 @@ build-neutron: cd $(APP_DIR)/neutron && $(MAKE) build-docker-image build-hermes: - @docker build -f dockerbuilds/Dockerfile.hermes -t hermes:1.3.0-a285c01 . + @docker build -f dockerbuilds/Dockerfile.hermes -t hermes:1.6.0-1c1cf029 . build-relayer: cd $(APP_DIR)/neutron-query-relayer/ && make build-docker diff --git a/setup/docker-compose.yml b/setup/docker-compose.yml index fabdfd22..4df9c980 100644 --- a/setup/docker-compose.yml +++ b/setup/docker-compose.yml @@ -38,7 +38,7 @@ services: - neutron-testing hermes: - image: hermes:1.3.0-a285c01 + image: hermes:1.6.0-1c1cf029 depends_on: - "neutron-node" - "gaia-node" diff --git a/setup/dockerbuilds/Dockerfile.hermes b/setup/dockerbuilds/Dockerfile.hermes index 30d9a04c..150683c1 100644 --- a/setup/dockerbuilds/Dockerfile.hermes +++ b/setup/dockerbuilds/Dockerfile.hermes @@ -3,7 +3,7 @@ COPY ./hermes/ /app/network/hermes/ WORKDIR /app RUN apt-get update && apt-get install -y wget && \ PLATFORM=`uname -a | awk '{print $(NF-1)}'` && \ - VERSION=v1.4.0 && \ + VERSION=v1.6.0 && \ TARNAME="hermes-${VERSION}-${PLATFORM}-unknown-linux-gnu.tar.gz" && \ wget "https://github.com/informalsystems/hermes/releases/download/${VERSION}/${TARNAME}" && \ tar -xf "$TARNAME" && \ diff --git a/setup/hermes/config.toml b/setup/hermes/config.toml index 6627a498..e8edf0b9 100644 --- a/setup/hermes/config.toml +++ b/setup/hermes/config.toml @@ -93,7 +93,8 @@ port = 3001 id = 'test-1' rpc_addr = 'http://neutron-node:26657' grpc_addr = 'http://neutron-node:9090' -websocket_addr = 'ws://neutron-node:26657/websocket' +event_source = { mode = 'push', url = 'ws://neutron-node:26657/websocket', batch_delay = '200ms' } +ccv_consumer_chain = true rpc_timeout = '10s' account_prefix = 'neutron' key_name = 'testkey_1' @@ -101,13 +102,16 @@ store_prefix = 'ibc' default_gas = 100000 max_gas = 3000000 gas_price = { price = 0.0025, denom = 'untrn' } +<<<<<<< HEAD gas_multiplier = 2.0 +======= +gas_multiplier = 1.5 +>>>>>>> main max_msg_num = 30 max_tx_size = 2097152 clock_drift = '5s' max_block_time = '10s' trusting_period = '14days' -unbonding_period = '20days' trust_threshold = { numerator = '1', denominator = '3' } address_type = { derivation = 'cosmos' } @@ -115,7 +119,7 @@ address_type = { derivation = 'cosmos' } id = 'test-2' rpc_addr = 'http://gaia-node:26657' grpc_addr = 'http://gaia-node:9090' -websocket_addr = 'ws://gaia-node:26657/websocket' +event_source = { mode = 'push', url = 'ws://gaia-node:26657/websocket', batch_delay = '200ms' } rpc_timeout = '10s' account_prefix = 'cosmos' key_name = 'testkey_2' @@ -123,12 +127,15 @@ store_prefix = 'ibc' default_gas = 100000 max_gas = 3000000 gas_price = { price = 0.0025, denom = 'uatom' } +<<<<<<< HEAD gas_multiplier = 2.0 +======= +gas_multiplier = 1.5 +>>>>>>> main max_msg_num = 30 max_tx_size = 2097152 clock_drift = '5s' max_block_time = '10s' trusting_period = '14days' -unbonding_period = '21days' trust_threshold = { numerator = '1', denominator = '3' } address_type = { derivation = 'cosmos' } diff --git a/src/helpers/cosmos.ts b/src/helpers/cosmos.ts index f56ed166..1b5ed36b 100644 --- a/src/helpers/cosmos.ts +++ b/src/helpers/cosmos.ts @@ -603,6 +603,34 @@ export class WalletWrapper { ]); } + async migrateContract( + contract: string, + codeId: number, + msg: string | Record, + ): Promise { + const sender = this.wallet.address.toString(); + const msgMigrate = + new cosmwasmclient.proto.cosmwasm.wasm.v1.MsgMigrateContract({ + sender, + contract, + code_id: codeId + '', + msg: Buffer.from(typeof msg === 'string' ? msg : JSON.stringify(msg)), + }); + const res = await this.execTx( + { + gas_limit: Long.fromString('5000000'), + amount: [{ denom: this.chain.denom, amount: '20000' }], + }, + [msgMigrate], + ); + if (res.tx_response.code !== 0) { + throw new Error( + `${res.tx_response.raw_log}\nFailed tx hash: ${res.tx_response.txhash}`, + ); + } + return res?.tx_response; + } + async executeContract( contract: string, msg: string, diff --git a/src/testcases/parallel/stargate_queries.test.ts b/src/testcases/parallel/stargate_queries.test.ts index 0303083f..f8c72b53 100644 --- a/src/testcases/parallel/stargate_queries.test.ts +++ b/src/testcases/parallel/stargate_queries.test.ts @@ -244,5 +244,13 @@ describe('Neutron / Simple', () => { const res = JSON.parse(await querySmart({ feeburner_params: {} })); expect(res.params.neutron_denom).toBe('untrn'); }); + + test('non whitelisted query should NOT work', async () => { + await expect( + querySmart({ feeburner_total_burned_neutrons_amount: {} }), + ).rejects.toThrow( + /Unsupported query type: '\/neutron.feeburner.Query\/TotalBurnedNeutronsAmount'/, + ); + }); }); }); diff --git a/src/testcases/run_in_band/interchaintx.test.ts b/src/testcases/run_in_band/interchaintx.test.ts index 26cb218b..43adebe9 100644 --- a/src/testcases/run_in_band/interchaintx.test.ts +++ b/src/testcases/run_in_band/interchaintx.test.ts @@ -178,7 +178,7 @@ describe('Neutron / Interchain TXs', () => { validator: ( testState.wallets.cosmos.val1.address as cosmosclient.ValAddress ).toString(), - amount: '2000', + amount: '1000', denom: gaiaChain.denom, }, }), @@ -208,6 +208,79 @@ describe('Neutron / Interchain TXs', () => { async (delegations) => delegations.data.delegation_responses?.length == 1, ); + expect(res1.data.delegation_responses).toEqual([ + { + balance: { amount: '1000', denom: gaiaChain.denom }, + delegation: { + delegator_address: icaAddress1, + shares: '1000.000000000000000000', + validator_address: + 'cosmosvaloper18hl5c9xn5dze2g50uaw0l2mr02ew57zk0auktn', + }, + }, + ]); + const res2 = await cosmosclient.rest.staking.delegatorDelegations( + gaiaChain.sdk as CosmosSDK, + icaAddress2 as unknown as AccAddress, + ); + expect(res2.data.delegation_responses).toEqual([]); + }); + test('check contract balance', async () => { + const res = await neutronChain.queryBalances(contractAddress); + const balance = res.balances.find( + (b) => b.denom === neutronChain.denom, + )?.amount; + expect(balance).toEqual('98000'); + }); + }); + + describe('DOUBLE ACK - Send Interchain TX', () => { + test('delegate from first ICA', async () => { + // it will delegate two times of passed amount - first from contract call, and second from successful sudo IBC response + const res = await neutronAccount.executeContract( + contractAddress, + JSON.stringify({ + delegate_double_ack: { + interchain_account_id: icaId1, + validator: ( + testState.wallets.cosmos.val1.address as cosmosclient.ValAddress + ).toString(), + amount: '500', + denom: gaiaChain.denom, + }, + }), + ); + expect(res.code).toEqual(0); + const sequenceId = getSequenceId(res.raw_log); + + await waitForAck(neutronChain, contractAddress, icaId1, sequenceId); + const qres = await getAck( + neutronChain, + contractAddress, + icaId1, + sequenceId, + ); + expect(qres).toMatchObject({ + success: ['/cosmos.staking.v1beta1.MsgDelegate'], + }); + + const ackSequenceId = sequenceId + 1; + await waitForAck(neutronChain, contractAddress, icaId1, ackSequenceId); + expect(qres).toMatchObject({ + success: ['/cosmos.staking.v1beta1.MsgDelegate'], + }); + }); + test('check validator state', async () => { + const res1 = await getWithAttempts( + gaiaChain.blockWaiter, + () => + cosmosclient.rest.staking.delegatorDelegations( + gaiaChain.sdk as CosmosSDK, + icaAddress1 as unknown as AccAddress, + ), + async (delegations) => + delegations.data.delegation_responses?.length === 1, + ); expect(res1.data.delegation_responses).toEqual([ { balance: { amount: '2000', denom: gaiaChain.denom }, @@ -230,9 +303,11 @@ describe('Neutron / Interchain TXs', () => { const balance = res.balances.find( (b) => b.denom === neutronChain.denom, )?.amount; - expect(balance).toEqual('98000'); + // two interchain txs inside (2000 * 2 = 4000) + expect(balance).toEqual('94000'); }); }); + describe('Error cases', () => { test('delegate for unknown validator from second ICA', async () => { const res = await neutronAccount.executeContract( diff --git a/src/testcases/run_in_band/tge.airdrop.test.ts b/src/testcases/run_in_band/tge.airdrop.test.ts index e01a8207..7d15a9d8 100644 --- a/src/testcases/run_in_band/tge.airdrop.test.ts +++ b/src/testcases/run_in_band/tge.airdrop.test.ts @@ -21,6 +21,7 @@ describe('Neutron / TGE / Airdrop', () => { let neutronChain: CosmosWrapper; let neutronAccount1: WalletWrapper; let neutronAccount2: WalletWrapper; + let neutronAccount3: WalletWrapper; const codeIds: Record = {}; const contractAddresses: Record = {}; let airdrop: InstanceType; @@ -45,6 +46,10 @@ describe('Neutron / TGE / Airdrop', () => { neutronChain, testState.wallets.qaNeutronThree.genQaWal1, ); + neutronAccount3 = new WalletWrapper( + neutronChain, + testState.wallets.qaNeutronFour.genQaWal1, + ); const accounts = [ { address: testState.wallets.neutron.demo1.address.toString(), @@ -75,6 +80,15 @@ describe('Neutron / TGE / Airdrop', () => { expect(codeId).toBeGreaterThan(0); codeIds[contract] = codeId; } + // wasmcode to test migration airdrop contract from the mainnet codeId 22 to a new wasm code + // $ sha256sum contracts_thirdparty/cw20_merkle_airdrop_orig.wasm + // b6595694b8cf752a085b34584ae37bf59e2236d916a1fd01dd014af8967204aa contracts_thirdparty/cw20_merkle_airdrop_orig.wasm + // https://neutron.celat.one/neutron-1/codes/22 + const codeId = await neutronAccount1.storeWasm( + '../contracts_thirdparty/cw20_merkle_airdrop_orig.wasm', + ); + expect(codeId).toBeGreaterThan(0); + codeIds['TGE_AIRDROP_ORIG'] = codeId; }); it('should instantiate credits contract', async () => { const res = await neutronAccount1.instantiateContract( @@ -102,7 +116,7 @@ describe('Neutron / TGE / Airdrop', () => { hrp: 'neutron', }; const res = await neutronAccount1.instantiateContract( - codeIds['TGE_AIRDROP'], + codeIds['TGE_AIRDROP_ORIG'], JSON.stringify(initParams), 'airdrop', ); @@ -135,6 +149,30 @@ describe('Neutron / TGE / Airdrop', () => { ); expect(res.code).toEqual(0); }); + + it('should not update reserve address by owner', async () => { + await expect( + neutronAccount1.executeContract( + contractAddresses['TGE_AIRDROP'], + JSON.stringify({ + update_reserve: { + address: neutronAccount3.wallet.address.toString(), + }, + }), + ), + ).rejects.toThrow( + /unknown variant `update_reserve`, expected one of `claim`, `withdraw_all`, `pause`, `resume`/, + ); + expect( + await neutronChain.queryContract(contractAddresses.TGE_AIRDROP, { + config: {}, + }), + ).toMatchObject({ + owner: neutronAccount1.wallet.address.toString(), + credits_address: contractAddresses.TGE_CREDITS, + reserve_address: reserveAddress, + }); + }); }); describe('Airdrop', () => { @@ -269,6 +307,102 @@ describe('Neutron / TGE / Airdrop', () => { ); expect(res.code).toEqual(0); }); + + it('should migrate in the middle of TGE', async () => { + const res = await neutronAccount1.migrateContract( + contractAddresses['TGE_AIRDROP'], + codeIds['TGE_AIRDROP'], + {}, + ); + expect(res.code).toEqual(0); + }); + + it('should not update reserve address by random account', async () => { + await expect( + neutronAccount3.executeContract( + contractAddresses['TGE_AIRDROP'], + JSON.stringify({ + update_reserve: { + address: neutronAccount3.wallet.address.toString(), + }, + }), + ), + ).rejects.toThrow(/Unauthorized/); + expect( + await neutronChain.queryContract(contractAddresses.TGE_AIRDROP, { + config: {}, + }), + ).toMatchObject({ + owner: neutronAccount1.wallet.address.toString(), + credits_address: contractAddresses.TGE_CREDITS, + reserve_address: reserveAddress, + }); + }); + + it('should update reserve address by owner account', async () => { + const res = await neutronAccount1.executeContract( + contractAddresses['TGE_AIRDROP'], + JSON.stringify({ + update_reserve: { + address: neutronAccount3.wallet.address.toString(), + }, + }), + ); + expect(res.code).toEqual(0); + expect( + await neutronChain.queryContract(contractAddresses.TGE_AIRDROP, { + config: {}, + }), + ).toMatchObject({ + owner: neutronAccount1.wallet.address.toString(), + credits_address: contractAddresses.TGE_CREDITS, + reserve_address: neutronAccount3.wallet.address.toString(), + }); + }); + + it('should not update reserve address by old reserve', async () => { + await expect( + neutronAccount2.executeContract( + contractAddresses['TGE_AIRDROP'], + JSON.stringify({ + update_reserve: { + address: neutronAccount2.wallet.address.toString(), + }, + }), + ), + ).rejects.toThrow(/Unauthorized/); + expect( + await neutronChain.queryContract(contractAddresses.TGE_AIRDROP, { + config: {}, + }), + ).toMatchObject({ + owner: neutronAccount1.wallet.address.toString(), + credits_address: contractAddresses.TGE_CREDITS, + reserve_address: neutronAccount3.wallet.address.toString(), + }); + }); + + it('should update reserve address by new reserve', async () => { + const res = await neutronAccount3.executeContract( + contractAddresses['TGE_AIRDROP'], + JSON.stringify({ + update_reserve: { + address: neutronAccount2.wallet.address.toString(), + }, + }), + ); + expect(res.code).toEqual(0); + expect( + await neutronChain.queryContract(contractAddresses.TGE_AIRDROP, { + config: {}, + }), + ).toMatchObject({ + owner: neutronAccount1.wallet.address.toString(), + credits_address: contractAddresses.TGE_CREDITS, + reserve_address: reserveAddress, + }); + }); + it('should return is claimed true', async () => { const res = await neutronChain.queryContract<{ is_claimed: boolean }>( contractAddresses['TGE_AIRDROP'], @@ -392,7 +526,7 @@ describe('Neutron / TGE / Airdrop', () => { }); it('should not be able to withdraw all before end', async () => { await expect( - neutronAccount1.executeContract( + neutronAccount2.executeContract( contractAddresses['TGE_AIRDROP'], JSON.stringify({ withdraw_all: {}, @@ -403,8 +537,20 @@ describe('Neutron / TGE / Airdrop', () => { /withdraw_all is unavailable, it will become available at/, ); }); - it('should be able to withdraw all', async () => { + it('should not be able to withdraw all by non reserve address', async () => { await waitTill(times.airdropVestingStart + times.vestingDuration + 5); + await expect( + neutronAccount1.executeContract( + contractAddresses['TGE_AIRDROP'], + JSON.stringify({ + withdraw_all: {}, + }), + [], + ), + ).rejects.toThrow(/Unauthorized/); + }); + + it('should be able to withdraw all by reserve address', async () => { const availableBalanceCNTRN = await neutronChain.queryContract<{ balance: string; }>(contractAddresses['TGE_CREDITS'], { @@ -415,7 +561,7 @@ describe('Neutron / TGE / Airdrop', () => { const reserveBalanceNTRN = ( await neutronChain.queryBalances(reserveAddress) ).balances.find((b) => b.denom === NEUTRON_DENOM)?.amount; - const res = await neutronAccount1.executeContract( + const res = await neutronAccount2.executeContract( contractAddresses['TGE_AIRDROP'], JSON.stringify({ withdraw_all: {}, @@ -437,7 +583,8 @@ describe('Neutron / TGE / Airdrop', () => { expect(availableBalanceCNTRNAfter.balance).toEqual('0'); expect( parseInt(reserveBalanceNTRNAfter || '0') - - parseInt(reserveBalanceNTRN || '0'), + parseInt(reserveBalanceNTRN || '0') + + 10000, // fee compensation for execution withdraw all ).toEqual(parseInt(availableBalanceCNTRN.balance)); }); });