Skip to content

Commit bf4b65e

Browse files
authored
[KLC-2113] Validate Encoding/Decode (#12)
* feat: add usize, isize, boolean, and hex type support to ABI encoder/decoder * feat: add String/bytes/Address type aliases to ABI encoder/decoder * fix(deps): patch underscore DoS vulnerability via pnpm override * fix(contracts): resolve CodeRabbit review findings on ABI encoder/decoder * fix(contracts): consumed field now has number fallback
1 parent 3cae6cb commit bf4b65e

File tree

6 files changed

+558
-27
lines changed

6 files changed

+558
-27
lines changed

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@
6262
},
6363
"pnpm": {
6464
"overrides": {
65-
"glob": ">=13.0.1",
66-
"@isaacs/brace-expansion": ">=5.0.1",
67-
"rollup": ">=4.59.0",
65+
"glob": "13.0.1",
66+
"@isaacs/brace-expansion": "5.0.1",
67+
"rollup": "4.59.0",
6868
"glob>minimatch": "10.2.3",
69-
"markdown-it": ">=14.1.1"
69+
"markdown-it": "14.1.1",
70+
"underscore": "1.13.8"
7071
}
7172
}
7273
}

packages/connect-contracts/src/__tests__/decoder.test.ts

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,284 @@ describe('Result Decoder', () => {
823823
})
824824
})
825825

826+
describe('Type aliases and hex decoding', () => {
827+
it('should decode usize as u32', () => {
828+
const mockABI: ContractABI = {
829+
buildInfo: {
830+
rustc: { version: '1.0', commitHash: '', commitDate: '', channel: 'Stable', short: '' },
831+
contractCrate: { name: 'test', version: '1.0' },
832+
framework: { name: 'klever-sc', version: '1.0' },
833+
},
834+
name: 'Test',
835+
constructor: { name: 'init', inputs: [], outputs: [] },
836+
upgradeConstructor: { name: 'upgrade', inputs: [], outputs: [] },
837+
endpoints: [
838+
{
839+
name: 'getSize',
840+
mutability: 'readonly',
841+
inputs: [],
842+
outputs: [{ name: 'value', type: 'usize' }],
843+
},
844+
],
845+
kdaAttributes: [],
846+
types: {},
847+
}
848+
849+
const decoder = new ABIDecoder(mockABI)
850+
851+
const data = [encodeBase64(new Uint8Array([0x00, 0x00, 0x00, 0x64]))] // 100
852+
const result = decoder.decodeFunctionResultsWithMetadata('getSize', data)
853+
854+
expect(result.values[0]?.value).toBe(100)
855+
expect(result.values[0]?.type).toBe('usize')
856+
})
857+
858+
it('should decode isize as i32', () => {
859+
const mockABI: ContractABI = {
860+
buildInfo: {
861+
rustc: { version: '1.0', commitHash: '', commitDate: '', channel: 'Stable', short: '' },
862+
contractCrate: { name: 'test', version: '1.0' },
863+
framework: { name: 'klever-sc', version: '1.0' },
864+
},
865+
name: 'Test',
866+
constructor: { name: 'init', inputs: [], outputs: [] },
867+
upgradeConstructor: { name: 'upgrade', inputs: [], outputs: [] },
868+
endpoints: [
869+
{
870+
name: 'getIndex',
871+
mutability: 'readonly',
872+
inputs: [],
873+
outputs: [{ name: 'value', type: 'isize' }],
874+
},
875+
],
876+
kdaAttributes: [],
877+
types: {},
878+
}
879+
880+
const decoder = new ABIDecoder(mockABI)
881+
882+
// -100 in two's complement = 0xffffff9c
883+
const data = [encodeBase64(new Uint8Array([0xff, 0xff, 0xff, 0x9c]))]
884+
const result = decoder.decodeFunctionResultsWithMetadata('getIndex', data)
885+
886+
expect(result.values[0]?.value).toBe(-100)
887+
expect(result.values[0]?.type).toBe('isize')
888+
})
889+
890+
it('should decode boolean as bool', () => {
891+
const mockABI: ContractABI = {
892+
buildInfo: {
893+
rustc: { version: '1.0', commitHash: '', commitDate: '', channel: 'Stable', short: '' },
894+
contractCrate: { name: 'test', version: '1.0' },
895+
framework: { name: 'klever-sc', version: '1.0' },
896+
},
897+
name: 'Test',
898+
constructor: { name: 'init', inputs: [], outputs: [] },
899+
upgradeConstructor: { name: 'upgrade', inputs: [], outputs: [] },
900+
endpoints: [
901+
{
902+
name: 'getFlag',
903+
mutability: 'readonly',
904+
inputs: [],
905+
outputs: [{ name: 'value', type: 'boolean' }],
906+
},
907+
],
908+
kdaAttributes: [],
909+
types: {},
910+
}
911+
912+
const decoder = new ABIDecoder(mockABI)
913+
914+
const trueData = [encodeBase64(new Uint8Array([0x01]))]
915+
const trueResult = decoder.decodeFunctionResultsWithMetadata('getFlag', trueData)
916+
expect(trueResult.values[0]?.value).toBe(true)
917+
expect(trueResult.values[0]?.type).toBe('boolean')
918+
919+
const falseData = [encodeBase64(new Uint8Array([0x00]))]
920+
const falseResult = decoder.decodeFunctionResultsWithMetadata('getFlag', falseData)
921+
expect(falseResult.values[0]?.value).toBe(false)
922+
})
923+
924+
it('should decode hex type as hex string passthrough', () => {
925+
const mockABI: ContractABI = {
926+
buildInfo: {
927+
rustc: { version: '1.0', commitHash: '', commitDate: '', channel: 'Stable', short: '' },
928+
contractCrate: { name: 'test', version: '1.0' },
929+
framework: { name: 'klever-sc', version: '1.0' },
930+
},
931+
name: 'Test',
932+
constructor: { name: 'init', inputs: [], outputs: [] },
933+
upgradeConstructor: { name: 'upgrade', inputs: [], outputs: [] },
934+
endpoints: [
935+
{
936+
name: 'getRaw',
937+
mutability: 'readonly',
938+
inputs: [],
939+
outputs: [{ name: 'value', type: 'hex' }],
940+
},
941+
],
942+
kdaAttributes: [],
943+
types: {},
944+
}
945+
946+
const decoder = new ABIDecoder(mockABI)
947+
948+
const data = [encodeBase64(new Uint8Array([0xca, 0xfe, 0xba, 0xbe]))]
949+
const result = decoder.decodeFunctionResultsWithMetadata('getRaw', data)
950+
951+
expect(result.values[0]?.value).toBe('cafebabe')
952+
expect(result.values[0]?.type).toBe('hex')
953+
})
954+
955+
it('should decode empty hex', () => {
956+
const mockABI: ContractABI = {
957+
buildInfo: {
958+
rustc: { version: '1.0', commitHash: '', commitDate: '', channel: 'Stable', short: '' },
959+
contractCrate: { name: 'test', version: '1.0' },
960+
framework: { name: 'klever-sc', version: '1.0' },
961+
},
962+
name: 'Test',
963+
constructor: { name: 'init', inputs: [], outputs: [] },
964+
upgradeConstructor: { name: 'upgrade', inputs: [], outputs: [] },
965+
endpoints: [
966+
{
967+
name: 'getRaw',
968+
mutability: 'readonly',
969+
inputs: [],
970+
outputs: [{ name: 'value', type: 'hex' }],
971+
},
972+
],
973+
kdaAttributes: [],
974+
types: {},
975+
}
976+
977+
const decoder = new ABIDecoder(mockABI)
978+
979+
const data = [encodeBase64(new Uint8Array([]))]
980+
const result = decoder.decodeFunctionResultsWithMetadata('getRaw', data)
981+
982+
expect(result.values[0]?.value).toBe('')
983+
})
984+
985+
it('should round-trip usize encode/decode', async () => {
986+
const { ABIEncoder } = await import('../encoder/abi-encoder')
987+
988+
const mockABI: ContractABI = {
989+
name: 'Test',
990+
constructor: { name: 'init', inputs: [], outputs: [] },
991+
endpoints: [],
992+
types: {},
993+
}
994+
995+
const encoder = new ABIEncoder(mockABI)
996+
const decoder = new ABIDecoder(mockABI)
997+
998+
const encoded = encoder.encodeValue(42, 'usize')
999+
const decoded = decoder.decodeValue(encoded, 'usize')
1000+
expect(decoded).toBe(42)
1001+
})
1002+
1003+
it('should round-trip isize encode/decode', async () => {
1004+
const { ABIEncoder } = await import('../encoder/abi-encoder')
1005+
1006+
const mockABI: ContractABI = {
1007+
name: 'Test',
1008+
constructor: { name: 'init', inputs: [], outputs: [] },
1009+
endpoints: [],
1010+
types: {},
1011+
}
1012+
1013+
const encoder = new ABIEncoder(mockABI)
1014+
const decoder = new ABIDecoder(mockABI)
1015+
1016+
const encoded = encoder.encodeValue(-50, 'isize')
1017+
const decoded = decoder.decodeValue(encoded, 'isize')
1018+
expect(decoded).toBe(-50)
1019+
})
1020+
1021+
it('should round-trip hex encode/decode', async () => {
1022+
const { ABIEncoder } = await import('../encoder/abi-encoder')
1023+
1024+
const mockABI: ContractABI = {
1025+
name: 'Test',
1026+
constructor: { name: 'init', inputs: [], outputs: [] },
1027+
endpoints: [],
1028+
types: {},
1029+
}
1030+
1031+
const encoder = new ABIEncoder(mockABI)
1032+
const decoder = new ABIDecoder(mockABI)
1033+
1034+
const encoded = encoder.encodeValue('deadbeef', 'hex')
1035+
const decoded = decoder.decodeValue(encoded, 'hex')
1036+
expect(decoded).toBe('deadbeef')
1037+
})
1038+
})
1039+
1040+
describe('String/Bytes aliases decoding', () => {
1041+
it('should decode String alias as string', async () => {
1042+
const { ABIEncoder } = await import('../encoder/abi-encoder')
1043+
1044+
const mockABI: ContractABI = {
1045+
name: 'Test',
1046+
constructor: { name: 'init', inputs: [], outputs: [] },
1047+
endpoints: [],
1048+
types: {},
1049+
}
1050+
1051+
const encoder = new ABIEncoder(mockABI)
1052+
const decoder = new ABIDecoder(mockABI)
1053+
1054+
for (const type of ['String', '&str', 'ManagedBuffer']) {
1055+
const encoded = encoder.encodeValue('hello', type)
1056+
const decoded = decoder.decodeValue(encoded, type)
1057+
expect(decoded).toBe('hello')
1058+
}
1059+
})
1060+
1061+
it('should decode bytes aliases as bytes', async () => {
1062+
const { ABIEncoder } = await import('../encoder/abi-encoder')
1063+
1064+
const mockABI: ContractABI = {
1065+
name: 'Test',
1066+
constructor: { name: 'init', inputs: [], outputs: [] },
1067+
endpoints: [],
1068+
types: {},
1069+
}
1070+
1071+
const encoder = new ABIEncoder(mockABI)
1072+
const decoder = new ABIDecoder(mockABI)
1073+
const data = new Uint8Array([0x01, 0x02, 0x03])
1074+
1075+
for (const type of ['BoxedBytes', 'Vec<u8>', '&[u8]']) {
1076+
const encoded = encoder.encodeValue(data, type)
1077+
const decoded = decoder.decodeValue(encoded, type) as Uint8Array
1078+
expect(Array.from(decoded)).toEqual([1, 2, 3])
1079+
}
1080+
})
1081+
1082+
it('should decode Address aliases', () => {
1083+
const mockABI: ContractABI = {
1084+
name: 'Test',
1085+
constructor: { name: 'init', inputs: [], outputs: [] },
1086+
endpoints: [],
1087+
types: {},
1088+
}
1089+
1090+
const decoder = new ABIDecoder(mockABI)
1091+
1092+
// 32-byte zero address
1093+
const bytes = new Uint8Array(32).fill(0)
1094+
1095+
const decodedAddress = decoder.decodeValue(bytes, 'Address')
1096+
const decodedA = decoder.decodeValue(bytes, 'a')
1097+
const decodedBigA = decoder.decodeValue(bytes, 'A')
1098+
1099+
expect(decodedA).toBe(decodedAddress)
1100+
expect(decodedBigA).toBe(decodedAddress)
1101+
})
1102+
})
1103+
8261104
describe('multi-output endpoints (no variadic)', () => {
8271105
it('should decode multiple separate outputs', () => {
8281106
const mockABI: ContractABI = {

0 commit comments

Comments
 (0)