From 065913bfb180346113fa3fe51a69e3a1f64f1afe Mon Sep 17 00:00:00 2001 From: Veetrag Jain Date: Sat, 14 Feb 2026 07:47:20 +0530 Subject: [PATCH] fix(sdk-coin-hash): add address validations when decoding group policy messages Ticket: WIN-8984 --- .../src/lib/ContractCallBuilder.ts | 4 +-- modules/abstract-cosmos/src/lib/utils.ts | 14 ++++++--- modules/sdk-coin-hash/test/resources/hash.ts | 8 +++++ .../transactionBuilder/transactionBuilder.ts | 16 ++++++++++ modules/sdk-coin-hash/test/unit/utils.ts | 31 ++++++++++--------- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts b/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts index 5524e6a386..42f0cf1be3 100644 --- a/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts +++ b/modules/abstract-cosmos/src/lib/ContractCallBuilder.ts @@ -28,14 +28,14 @@ export class ContractCallBuilder extends CosmosTransactio return message as MessageData; } - if (CosmosUtils.isGroupProposal(executeContractMessage)) { + if (this._utils.isGroupProposal(executeContractMessage)) { return { typeUrl: constants.groupProposalMsgTypeUrl, value: executeContractMessage.msg, } as MessageData; } - if (CosmosUtils.isGroupVote(executeContractMessage)) { + if (this._utils.isGroupVote(executeContractMessage)) { return { typeUrl: constants.groupVoteMsgTypeUrl, value: executeContractMessage.msg, diff --git a/modules/abstract-cosmos/src/lib/utils.ts b/modules/abstract-cosmos/src/lib/utils.ts index 16293a2356..b3e9809826 100644 --- a/modules/abstract-cosmos/src/lib/utils.ts +++ b/modules/abstract-cosmos/src/lib/utils.ts @@ -1007,11 +1007,11 @@ export class CosmosUtils implements BaseUtils { * @param {ExecuteContractMessage} message - The execute contract message to check * @returns {boolean} true if the msg decodes to a group proposal */ - static isGroupProposal(message: ExecuteContractMessage): boolean { + isGroupProposal(message: ExecuteContractMessage): boolean { if (!message.msg || message.msg.length === 0) { return false; } - const result = CosmosUtils.decodeMsg(message.msg); + const result = this.decodeMsg(message.msg); return result.typeUrl === constants.groupProposalMsgTypeUrl; } @@ -1020,11 +1020,11 @@ export class CosmosUtils implements BaseUtils { * @param {ExecuteContractMessage} message - The execute contract message to check * @returns {boolean} true if the msg decodes to a group vote */ - static isGroupVote(message: ExecuteContractMessage): boolean { + isGroupVote(message: ExecuteContractMessage): boolean { if (!message.msg || message.msg.length === 0) { return false; } - const result = CosmosUtils.decodeMsg(message.msg); + const result = this.decodeMsg(message.msg); return result.typeUrl === constants.groupVoteMsgTypeUrl; } @@ -1034,7 +1034,7 @@ export class CosmosUtils implements BaseUtils { * @param data - Message data as base64 string or Uint8Array * @returns Decoded message result with typeUrl if successfully identified */ - static decodeMsg(data: string | Uint8Array): { typeUrl?: string; error?: string } { + decodeMsg(data: string | Uint8Array): { typeUrl?: string; error?: string } { try { const messageBytes = typeof data === 'string' ? Buffer.from(data, 'base64') : data; @@ -1043,6 +1043,9 @@ export class CosmosUtils implements BaseUtils { if ( proposal.groupPolicyAddress && typeof proposal.groupPolicyAddress === 'string' && + (this.isValidAddress(proposal.groupPolicyAddress) || + this.isValidContractAddress(proposal.groupPolicyAddress) || + this.isValidValidatorAddress(proposal.groupPolicyAddress)) && proposal.groupPolicyAddress.length > 0 && Array.isArray(proposal.proposers) && proposal.proposers.length > 0 @@ -1058,6 +1061,7 @@ export class CosmosUtils implements BaseUtils { if ( vote.voter && typeof vote.voter === 'string' && + this.isValidAddress(vote.voter) && vote.voter.length > 0 && vote.proposalId !== undefined && vote.proposalId !== null diff --git a/modules/sdk-coin-hash/test/resources/hash.ts b/modules/sdk-coin-hash/test/resources/hash.ts index 5c295513c0..4b9e41ea1f 100644 --- a/modules/sdk-coin-hash/test/resources/hash.ts +++ b/modules/sdk-coin-hash/test/resources/hash.ts @@ -21,6 +21,11 @@ export const TEST_CONTRACT_CALL = { '0ae9020ae6020a222f636f736d6f732e67726f75702e76312e4d73675375626d697450726f706f73616c12bf020a3d74703174617a6566776b32653337326679326a71303877366c7a7467397972727663343930723267703476743864306663686c726671717961686730751229747031326e796e3833796e6577746d706b773332777136646738337778386e71706174363567636c641a0f65786368616e67652d636f6d6d697422bf010a2d2f70726f76656e616e63652e65786368616e67652e76312e4d7367436f6d6d697446756e647352657175657374128d010a3d74703174617a6566776b32653337326679326a71303877366c7a7467397972727663343930723267703476743864306663686c7266717179616867307510011a140a0975796c64732e6663631207313030303030302a3465786368616e67652d636f6d6d69743a32316561363334302d393961662d346338632d386661302d37356665306431626264343428011299010a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21039f03af548098ff794456d05f2adcc389cfc04abc6e16d92669a6255e33145b2112040a020801181012450a140a056e68617368120b34303030303030303030301090a10f22297470313276646e72376464636b78306d38753632717573727a713572363663656a35726434397a77661a0d70696f2d746573746e65742d3120f2cc0e', }; +export const TEST_SUBMIT_PROPOSAL = { + randomMsgSubmitProposalEncoded: + '0ae9020ae6020a222f636f736d6f732e67726f75702e76312e4d73675375626d697450726f706f73616c12bf020a3d747031776838366b6561746c663879796e6765677675326a657567686e35706e7538306375397a7775397874636e6b6c72686b717277713474363864611229747031666b7963747165326b72636736743232726764726e796c6e7a68326e336c36613338323539731a0f65786368616e67652d636f6d6d697422bf010a2d2f70726f76656e616e63652e65786368616e67652e76312e4d7367436f6d6d697446756e647352657175657374128d010a3d747031776838366b6561746c663879796e6765677675326a657567686e35706e7538306375397a7775397874636e6b6c72686b7172777134743638646110011a140a0975796c64732e6663631207313030303030302a3465786368616e67652d636f6d6d69743a38326535666538392d393162652d346235332d623034322d3737666536383165343237372801126c0a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103eb24ad39ec900dc2370a48e66d196e10df27ed9e4045b208f208d22adc2ca23512040a020801121a0a140a056e68617368120b353737373737373737373810dce40c', +}; + export const TEST_GROUP_VOTE = { // Real encoded MsgVote payload from explorer tx 29CDCDFFB38AE89BA0311D040BB0D83541B4E5B2973C12DE5B00E6C0F9078B12 encodedVote: 'COn8ChIpdHAxZGoybjV5NDdheXEydDg0cGF5OGN5eTY1emg2ZTV1NWowZGpuajcYASICe30oAQ==', @@ -37,6 +42,9 @@ export const TEST_GROUP_VOTE = { messageTypeUrl: '/cosmos.group.v1.MsgVote', expectedSignBytesHex: '0a550a530a182f636f736d6f732e67726f75702e76312e4d7367566f7465123708e9fc0a1229747031646a326e35793437617971327438347061793863797936357a6836653575356a30646a6e6a37180122027b7d28011299010a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21039f03af548098ff794456d05f2adcc389cfc04abc6e16d92669a6255e33145b2112040a020801181112450a140a056e68617368120b34303030303030303030301090a10f22297470313276646e72376464636b78306d38753632717573727a713572363663656a35726434397a77661a0d70696f2d746573746e65742d3120f2cc0e', + //random unsigned MsgVote payload + randomMsgVoteEncoded: + '0a4e0a4c0a182f636f736d6f732e67726f75702e76312e4d7367566f7465123008ad571229747031666b7963747165326b72636736743232726764726e796c6e7a68326e336c366133383235397318011299010a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103eb24ad39ec900dc2370a48e66d196e10df27ed9e4045b208f208d22adc2ca23512040a020801180112450a140a056e68617368120b34303030303030303030301090a10f22297470313276646e72376464636b78306d38753632717573727a713572363663656a35726434397a7766', }; export const TEST_ACCOUNT = { diff --git a/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts b/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts index 3cef8595e4..56b85fadd3 100644 --- a/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts +++ b/modules/sdk-coin-hash/test/unit/transactionBuilder/transactionBuilder.ts @@ -62,6 +62,22 @@ describe('Hash Transaction Builder', async () => { should.equal(rawTx, testTxData.signedTxBase64); }); + it('should be able to build a submit proposal transaction from submit proposal transaction data', async function () { + const txBuilder = factory.from(testData.TEST_SUBMIT_PROPOSAL.randomMsgSubmitProposalEncoded); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.ContractCall); + // Should recreate the same raw tx data when re-build and turned to broadcast format + tx.cosmosLikeTransaction.sendMessages[0].typeUrl.should.equal('/cosmos.group.v1.MsgSubmitProposal'); + }); + + it('should be able to build a group vote transaction from group vote transaction data', async function () { + const txBuilder = factory.from(testData.TEST_GROUP_VOTE.randomMsgVoteEncoded); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.ContractCall); + // Should recreate the same raw tx data when re-build and turned to broadcast format + tx.cosmosLikeTransaction.sendMessages[0].typeUrl.should.equal('/cosmos.group.v1.MsgVote'); + }); + it('should build a signed token tx from signed token tx data', async function () { const txBuilder = factory.from(tokenTestTxData.signedTxBase64); const tx = await txBuilder.build(); diff --git a/modules/sdk-coin-hash/test/unit/utils.ts b/modules/sdk-coin-hash/test/unit/utils.ts index e8ee80ede3..74ccd94a9e 100644 --- a/modules/sdk-coin-hash/test/unit/utils.ts +++ b/modules/sdk-coin-hash/test/unit/utils.ts @@ -1,11 +1,12 @@ import should from 'should'; -import { CosmosUtils } from '@bitgo/abstract-cosmos'; +import { NetworkType } from '@bitgo/statics'; -import utils from '../../src/lib/utils'; +import utils, { HashUtils } from '../../src/lib/utils'; import * as testData from '../resources/hash'; import { blockHash, txIds, TEST_CONTRACT_CALL, TEST_GROUP_VOTE } from '../resources/hash'; describe('utils', () => { + const testnetHashUtils = new HashUtils(NetworkType.TESTNET); it('should validate block hash correctly', () => { should.equal(utils.isValidBlockId(blockHash.hash1), true); should.equal(utils.isValidBlockId(blockHash.hash2), true); @@ -48,7 +49,7 @@ describe('utils', () => { describe('decodeMsg', () => { it('should detect valid base64-encoded group proposal', () => { - const result = CosmosUtils.decodeMsg(TEST_CONTRACT_CALL.encodedProposal); + const result = testnetHashUtils.decodeMsg(TEST_CONTRACT_CALL.encodedProposal); should.exist(result.typeUrl); if (result.typeUrl) { @@ -58,28 +59,28 @@ describe('utils', () => { }); it('should reject invalid base64 string', () => { - const result = CosmosUtils.decodeMsg('not-valid-base64!!!'); + const result = testnetHashUtils.decodeMsg('not-valid-base64!!!'); should.not.exist(result.typeUrl); should.exist(result.error); }); it('should reject valid base64 but invalid protobuf', () => { - const result = CosmosUtils.decodeMsg(Buffer.from('random data').toString('base64')); + const result = testnetHashUtils.decodeMsg(Buffer.from('random data').toString('base64')); should.not.exist(result.typeUrl); should.exist(result.error); }); it('should reject hex-encoded contract call data', () => { - const result = CosmosUtils.decodeMsg('7b22696e6372656d656e74223a7b7d7d'); + const result = testnetHashUtils.decodeMsg('7b22696e6372656d656e74223a7b7d7d'); should.not.exist(result.typeUrl); }); it('should accept Uint8Array input', () => { const bytes = Buffer.from(TEST_CONTRACT_CALL.encodedProposal, 'base64'); - const result = CosmosUtils.decodeMsg(bytes); + const result = testnetHashUtils.decodeMsg(bytes); should.exist(result.typeUrl); if (result.typeUrl) { @@ -90,7 +91,7 @@ describe('utils', () => { describe('decodeMsg - group vote', () => { it('should detect valid base64-encoded group vote', () => { - const result = CosmosUtils.decodeMsg(TEST_GROUP_VOTE.encodedVote); + const result = testnetHashUtils.decodeMsg(TEST_GROUP_VOTE.encodedVote); should.exist(result.typeUrl); if (result.typeUrl) { @@ -101,7 +102,7 @@ describe('utils', () => { it('should accept Uint8Array input for group vote', () => { const bytes = Buffer.from(TEST_GROUP_VOTE.encodedVote, 'base64'); - const result = CosmosUtils.decodeMsg(bytes); + const result = testnetHashUtils.decodeMsg(bytes); should.exist(result.typeUrl); if (result.typeUrl) { @@ -117,7 +118,7 @@ describe('utils', () => { contract: 'tp12nyn83ynewtmpkw32wq6dg83wx8nqpat65gcld', msg: Buffer.from(TEST_GROUP_VOTE.encodedVote, 'base64'), }; - should.equal(CosmosUtils.isGroupVote(message), true); + should.equal(testnetHashUtils.isGroupVote(message), true); }); it('should return false when msg contains a group proposal', () => { @@ -126,7 +127,7 @@ describe('utils', () => { contract: 'tp12nyn83ynewtmpkw32wq6dg83wx8nqpat65gcld', msg: Buffer.from(TEST_CONTRACT_CALL.encodedProposal, 'base64'), }; - should.equal(CosmosUtils.isGroupVote(message), false); + should.equal(testnetHashUtils.isGroupVote(message), false); }); it('should return false when msg is empty', () => { @@ -135,7 +136,7 @@ describe('utils', () => { contract: 'tp12nyn83ynewtmpkw32wq6dg83wx8nqpat65gcld', msg: new Uint8Array(0), }; - should.equal(CosmosUtils.isGroupVote(message), false); + should.equal(testnetHashUtils.isGroupVote(message), false); }); }); @@ -146,7 +147,7 @@ describe('utils', () => { contract: 'tp12nyn83ynewtmpkw32wq6dg83wx8nqpat65gcld', msg: Buffer.from(TEST_CONTRACT_CALL.encodedProposal, 'base64'), }; - should.equal(CosmosUtils.isGroupProposal(message), true); + should.equal(testnetHashUtils.isGroupProposal(message), true); }); it('should return false when msg contains regular contract call data', () => { @@ -155,7 +156,7 @@ describe('utils', () => { contract: 'tp12nyn83ynewtmpkw32wq6dg83wx8nqpat65gcld', msg: Buffer.from(JSON.stringify({ increment: {} })), }; - should.equal(CosmosUtils.isGroupProposal(message), false); + should.equal(testnetHashUtils.isGroupProposal(message), false); }); it('should return false when msg is empty', () => { @@ -164,7 +165,7 @@ describe('utils', () => { contract: 'tp12nyn83ynewtmpkw32wq6dg83wx8nqpat65gcld', msg: new Uint8Array(0), }; - should.equal(CosmosUtils.isGroupProposal(message), false); + should.equal(testnetHashUtils.isGroupProposal(message), false); }); }); });