diff --git a/tests/HighloadWalletV3-audit.spec.ts b/tests/HighloadWalletV3-audit.spec.ts new file mode 100644 index 0000000..20c9699 --- /dev/null +++ b/tests/HighloadWalletV3-audit.spec.ts @@ -0,0 +1,1201 @@ +import { Blockchain, EmulationError, SandboxContract, internal } from '@ton/sandbox'; +import { beginCell, Cell, SendMode, toNano, Address, internal as internal_relaxed, OutActionSendMsg, storeMessageRelaxed } from '@ton/core'; +import { HighloadWalletV3, TIMEOUT_SIZE, TIMESTAMP_SIZE } from '../wrappers/HighloadWalletV3'; +import '@ton/test-utils'; +import { getSecureRandomBytes, KeyPair, keyPairFromSeed, sign } from "ton-crypto"; +import { SUBWALLET_ID, Errors, DEFAULT_TIMEOUT, maxShift } from "./imports/const"; +import { compile } from '@ton/blueprint'; +import { getRandomInt } from '../utils'; +import { randomAddress } from '@ton/test-utils'; +import { HighloadQueryId } from "../wrappers/HighloadQueryId"; + +describe('HighloadWalletV3', () => { + let keyPair: KeyPair; + let code: Cell; + + let blockchain: Blockchain; + let highloadWalletV3: SandboxContract; + + let shouldRejectWith: (p: Promise, code: number) => Promise; + let getContractData: (address: Address) => Promise; + let getContractCode: (address: Address) => Promise; + + beforeAll(async () => { + keyPair = keyPairFromSeed(await getSecureRandomBytes(32)); + code = await compile('HighloadWalletV3'); + + shouldRejectWith = async (p, code) => { + try { + await p; + throw new Error(`Should throw ${code}`); + } + catch(e: unknown) { + if(e instanceof EmulationError) { + expect(e.exitCode !== undefined && e.exitCode == code).toBe(true); + } + else { + throw e; + } + } + } + getContractData = async (address: Address) => { + const smc = await blockchain.getContract(address); + if(!smc.account.account) + throw("Account not found") + if(smc.account.account.storage.state.type != "active" ) + throw("Atempting to get data on inactive account"); + if(!smc.account.account.storage.state.state.data) + throw("Data is not present"); + return smc.account.account.storage.state.state.data + } + getContractCode = async (address: Address) => { + const smc = await blockchain.getContract(address); + if(!smc.account.account) + throw("Account not found") + if(smc.account.account.storage.state.type != "active" ) + throw("Atempting to get code on inactive account"); + if(!smc.account.account.storage.state.state.code) + throw("Code is not present"); + return smc.account.account.storage.state.state.code; + } + + }); + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.now = 1000; + + highloadWalletV3 = blockchain.openContract( + HighloadWalletV3.createFromConfig( + { + publicKey: keyPair.publicKey, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }, + code + ) + ); + + const deployer = await blockchain.treasury('deployer'); + + const deployResult = await highloadWalletV3.sendDeploy(deployer.getSender(), toNano('999999')); + + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: highloadWalletV3.address, + deploy: true + }); + }); + + // ========================= + // AUDIT TESTS + // ========================= + + describe('Query ID Boundary Tests', () => { + it('should work with bit number 0', async () => { + const shift = getRandomInt(0, maxShift); + const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(shift), 0n); + + const result = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + query_id: queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + expect(result.transactions).toHaveTransaction({ + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + + it('should work with bit number 1', async () => { + const shift = getRandomInt(0, maxShift); + const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(shift), 1n); + + const result = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000, + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + expect(result.transactions).toHaveTransaction({ + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + + it('should work with bit number 1021 (near max)', async () => { + const shift = getRandomInt(0, maxShift); + const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(shift), 1021n); + + const result = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000, + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + expect(result.transactions).toHaveTransaction({ + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + + it('should work with shift 0', async () => { + const bitNum = getRandomInt(0, 1022); + const queryId = HighloadQueryId.fromShiftAndBitNumber(0n, BigInt(bitNum)); + + const result = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + query_id: queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + expect(result.transactions).toHaveTransaction({ + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + + it('should work with shift 1', async () => { + const bitNum = getRandomInt(0, 1022); + const queryId = HighloadQueryId.fromShiftAndBitNumber(1n, BigInt(bitNum)); + + const result = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + query_id: queryId, + createdAt: 1000, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + expect(result.transactions).toHaveTransaction({ + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + + it('should handle negative shift values', async () => { + // Test what happens when we manually construct an external message with negative shift + // Since FunC uses load_uint(KEY_SIZE), negative values will underflow to large positive values + const message = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }); + const messageCell = beginCell().store(storeMessageRelaxed(message)).endCell(); + + // Manually construct external message with shift that would be negative if signed + // but will be interpreted as large positive value (2^13 - 1 = 8191) due to load_uint + const messageInner = beginCell() + .storeUint(SUBWALLET_ID, 32) + .storeRef(messageCell) + .storeUint(128, 8) // send_mode + .storeUint(0x1FFF, 13) // shift: max uint13 value (8191) + .storeUint(500, 10) // bit_number + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const signature = sign(messageInner.hash(), keyPair.secretKey); + const extMessage = beginCell() + .storeBuffer(signature) + .storeRef(messageInner) + .endCell(); + + const externalMsg = { + info: { + type: 'external-in' as const, + src: undefined, + dest: highloadWalletV3.address, + importFee: 0n + }, + body: extMessage + }; + + // This should succeed because 0xFFFF & 0x1FFF = 8191 (maxShift) + const result = await blockchain.sendMessage(externalMsg); + expect(result.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true + }); + }); + + it('should handle negative bit_number values', async () => { + const message = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }); + const messageCell = beginCell().store(storeMessageRelaxed(message)).endCell(); + + // Test bit_number that would be negative if signed but becomes large positive + const messageInner = beginCell() + .storeUint(SUBWALLET_ID, 32) + .storeRef(messageCell) + .storeUint(128, 8) // send_mode + .storeUint(100, 13) // shift + .storeUint(0x3FF, 10) // bit_number: -1 as uint10 becomes 1023 (invalid) + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const signature = sign(messageInner.hash(), keyPair.secretKey); + const extMessage = beginCell() + .storeBuffer(signature) + .storeRef(messageInner) + .endCell(); + + const externalMsg = { + info: { + type: 'external-in' as const, + src: undefined, + dest: highloadWalletV3.address, + importFee: 0n + }, + body: extMessage + }; + + // This should fail because bit_number 1023 is invalid (>= 1023) + // The FunC contract will try to access bit 1023 which doesn't exist in a 1023-bit cell + await expect(blockchain.sendMessage(externalMsg)).rejects.toThrow(); + }); + + it('should handle maximum uint13 shift value', async () => { + const message = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }); + const messageCell = beginCell().store(storeMessageRelaxed(message)).endCell(); + + // Test maximum possible shift value for uint13 (8191) + const maxUint13Shift = (1 << 13) - 1; // 8191 + const messageInner = beginCell() + .storeUint(SUBWALLET_ID, 32) + .storeRef(messageCell) + .storeUint(128, 8) // send_mode + .storeUint(maxUint13Shift, 13) // shift = 8191 + .storeUint(500, 10) // bit_number + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const signature = sign(messageInner.hash(), keyPair.secretKey); + const extMessage = beginCell() + .storeBuffer(signature) + .storeRef(messageInner) + .endCell(); + + const externalMsg = { + info: { + type: 'external-in' as const, + src: undefined, + dest: highloadWalletV3.address, + importFee: 0n + }, + body: extMessage + }; + + // This should succeed - FunC will accept any uint13 value + const result = await blockchain.sendMessage(externalMsg); + expect(result.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true + }); + }); + + it('should handle maximum uint10 bit_number value', async () => { + const message = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }); + const messageCell = beginCell().store(storeMessageRelaxed(message)).endCell(); + + // Test maximum possible bit_number value for uint10 (1023) - should fail + const maxUint10BitNumber = (1 << 10) - 1; // 1023 + const messageInner = beginCell() + .storeUint(SUBWALLET_ID, 32) + .storeRef(messageCell) + .storeUint(128, 8) // send_mode + .storeUint(100, 13) // shift + .storeUint(maxUint10BitNumber, 10) // bit_number = 1023 (invalid) + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const signature = sign(messageInner.hash(), keyPair.secretKey); + const extMessage = beginCell() + .storeBuffer(signature) + .storeRef(messageInner) + .endCell(); + + const externalMsg = { + info: { + type: 'external-in' as const, + src: undefined, + dest: highloadWalletV3.address, + importFee: 0n + }, + body: extMessage + }; + + // This should fail because bit_number 1023 exceeds cell bit capacity + await expect(blockchain.sendMessage(externalMsg)).rejects.toThrow(); + }); + + it('should handle query ID bit patterns that could cause dictionary collisions', async () => { + // Test various bit patterns that might cause issues in dictionary implementation + const testPatterns = [ + { shift: 0n, bitNum: 1023n - 1n }, // All 1s in bit_number field + { shift: (1n << 13n) - 1n, bitNum: 0n }, // All 1s in shift field + { shift: 0x1555n, bitNum: 0x155n }, // Alternating bits + { shift: 0x0AAAn, bitNum: 0x2AAn }, // Alternating bits (inverse) + ]; + + for (const pattern of testPatterns) { + if (pattern.shift <= BigInt(maxShift) && pattern.bitNum <= 1022n) { + const queryId = HighloadQueryId.fromShiftAndBitNumber(pattern.shift, pattern.bitNum); + + const result = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000, + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.01'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + expect(result.transactions).toHaveTransaction({ + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + } + } + }); + }); + + describe('Timeout Edge Cases', () => { + it('should reject message exactly at timeout boundary', async () => { + const queryId = new HighloadQueryId(); + const curTimeout = await highloadWalletV3.getTimeout(); + + await shouldRejectWith(highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000 - curTimeout, + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + } + ), Errors.invalid_creation_time); + }); + + it('should accept message just inside timeout boundary', async () => { + const queryId = new HighloadQueryId(); + const curTimeout = await highloadWalletV3.getTimeout(); + + const result = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000 - curTimeout + 1, + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + expect(result.transactions).toHaveTransaction({ + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + + it('should reject future timestamps', async () => { + const queryId = new HighloadQueryId(); + + await shouldRejectWith(highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000 + 100, // Future timestamp + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + } + ), Errors.invalid_creation_time); + }); + + it('should handle timeout boundary with time progression', async () => { + const queryId1 = new HighloadQueryId(); + const queryId2 = queryId1.getNext(); + const curTimeout = await highloadWalletV3.getTimeout(); + + // Send first message at valid time + await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000, + query_id: queryId1, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Progress time and try to send message with old creation time + blockchain.now = 1000 + curTimeout + 10; + + await shouldRejectWith(highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000, // Now too old + query_id: queryId2, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + } + ), Errors.invalid_creation_time); + }); + }); + + describe('Message Processing Edge Cases', () => { + it('should handle exactly 255 actions', async () => { + const queryId = new HighloadQueryId(); + let outMsgs: OutActionSendMsg[] = new Array(255); + + for(let i = 0; i < 255; i++) { + outMsgs[i] = { + type: 'sendMsg', + mode: SendMode.NONE, + outMsg: internal_relaxed({ + to: randomAddress(), + value: toNano('0.01'), + body: beginCell().storeUint(i, 32).endCell() + }), + } + } + + const res = await highloadWalletV3.sendBatch(keyPair.secretKey, outMsgs, SUBWALLET_ID, queryId, DEFAULT_TIMEOUT, 1000); + + // Should split into 254 + 1 + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + outMessagesCount: 254 + }); + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + outMessagesCount: 1 + }); + }); + + it('should handle mixed action types (send + setCode)', async () => { + const queryId = new HighloadQueryId(); + const testAddr = randomAddress(); + const mockCode = beginCell().storeUint(getRandomInt(0, 1000000), 32).endCell(); + + const message = highloadWalletV3.createInternalTransfer({ + actions: [ + { + type: 'sendMsg', + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: testAddr, + value: toNano('0.1'), + }) + }, + { + type: 'setCode', + newCode: mockCode + }, + { + type: 'sendMsg', + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: testAddr, + value: toNano('0.1'), + }) + } + ], + queryId: new HighloadQueryId(), + value: 0n + }); + + const res = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message, + mode: 128, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Code should not change (setCode ignored) + expect(await getContractCode(highloadWalletV3.address)).toEqualCell(code); + // But send messages should work + expect(res.transactions).toHaveTransaction({ + from: highloadWalletV3.address, + to: testAddr, + value: toNano('0.1') + }); + }); + + it('should handle empty action list', async () => { + const queryId = new HighloadQueryId(); + + const message = highloadWalletV3.createInternalTransfer({ + actions: [], + queryId: new HighloadQueryId(), + value: 0n + }); + + const res = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message, + mode: 128, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true, + outMessagesCount: 0 + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + }); + + describe('Cryptographic Edge Cases', () => { + it('should reject signature with correct structure but wrong message', async () => { + const queryId = new HighloadQueryId(); + const message1 = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }); + const message2 = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.2'), + }); + + // Convert messages to cells + const message1Cell = beginCell().store(storeMessageRelaxed(message1)).endCell(); + const message2Cell = beginCell().store(storeMessageRelaxed(message2)).endCell(); + + // Create signature for message1 but send message2 + const signCell = beginCell() + .storeUint(SUBWALLET_ID, 32) + .storeRef(message1Cell) + .storeUint(128, 8) + .storeUint(queryId.getQueryId(), 23) + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const signature = sign(signCell.hash(), keyPair.secretKey); + + // Manually construct external message with wrong message + const messageInner = beginCell() + .storeUint(SUBWALLET_ID, 32) + .storeRef(message2Cell) // Wrong message! + .storeUint(128, 8) + .storeUint(queryId.getQueryId(), 23) + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const extMessage = beginCell() + .storeBuffer(signature) + .storeRef(messageInner) + .endCell(); + + const externalMsg = { + info: { + type: 'external-in' as const, + src: undefined, + dest: highloadWalletV3.address, + importFee: 0n + }, + body: extMessage + }; + + await expect(blockchain.sendMessage(externalMsg)).rejects.toThrow(EmulationError); + }); + + it('should reject tampered query parameters with valid signature', async () => { + const queryId = new HighloadQueryId(); + const message = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }); + + // Convert message to cell + const messageCell = beginCell().store(storeMessageRelaxed(message)).endCell(); + + // Create signature with correct parameters + const signCell = beginCell() + .storeUint(SUBWALLET_ID, 32) + .storeRef(messageCell) + .storeUint(128, 8) + .storeUint(queryId.getQueryId(), 23) + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const signature = sign(signCell.hash(), keyPair.secretKey); + + // Manually construct external message with tampered subwallet + const messageInner = beginCell() + .storeUint(SUBWALLET_ID + 1, 32) // Tampered! + .storeRef(messageCell) + .storeUint(128, 8) + .storeUint(queryId.getQueryId(), 23) + .storeUint(1000, TIMESTAMP_SIZE) + .storeUint(DEFAULT_TIMEOUT, TIMEOUT_SIZE) + .endCell(); + + const extMessage = beginCell() + .storeBuffer(signature) + .storeRef(messageInner) + .endCell(); + + const externalMsg = { + info: { + type: 'external-in' as const, + src: undefined, + dest: highloadWalletV3.address, + importFee: 0n + }, + body: extMessage + }; + + await expect(blockchain.sendMessage(externalMsg)).rejects.toThrow(EmulationError); + }); + }); + + describe('Time-based Race Conditions', () => { + it('should handle rapid successive transactions near timeout', async () => { + const curTimeout = await highloadWalletV3.getTimeout(); + const baseTime = 1000; + + // Send transaction at the edge of timeout + const queryId1 = new HighloadQueryId(); + const res1 = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: baseTime - curTimeout + 1, + query_id: queryId1, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + expect(res1.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true + }); + + // Immediately send another at same creation time + const queryId2 = queryId1.getNext(); + const res2 = await highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: baseTime - curTimeout + 1, + query_id: queryId2, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + expect(res2.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true + }); + + expect(await highloadWalletV3.getProcessed(queryId1)).toBe(true); + expect(await highloadWalletV3.getProcessed(queryId2)).toBe(true); + }); + + it('should handle time progression during batch processing', async () => { + const queryId = new HighloadQueryId(); + let outMsgs: OutActionSendMsg[] = new Array(300); + + for(let i = 0; i < 300; i++) { + outMsgs[i] = { + type: 'sendMsg', + mode: SendMode.PAY_GAS_SEPARATELY, + outMsg: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.01'), + body: beginCell().storeUint(i, 32).endCell() + }), + } + } + + // This will create chained internal transfers + const res = await highloadWalletV3.sendBatch(keyPair.secretKey, outMsgs, SUBWALLET_ID, queryId, DEFAULT_TIMEOUT, 1000); + + // Even with time progression during chained execution, all should succeed + for(let i = 0; i < 300; i++) { + expect(res.transactions).toHaveTransaction({ + from: highloadWalletV3.address, + body: outMsgs[i].outMsg.body + }); + } + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + }); + + describe('Gas Limit Edge Cases', () => { + it('should handle large signature verification payloads', async () => { + const queryId = new HighloadQueryId(); + + // Create large message body within TON's cell reference limits (max 4 refs per cell) + const createLargeCell = (depth: number): Cell => { + const cell = beginCell(); + if (depth > 0) { + // Store data in this cell + cell.storeUint(depth * 1000, 32); + // Add up to 3 references (leaving room for proper structure) + for(let i = 0; i < 3; i++) { + cell.storeRef(createLargeCell(depth - 1)); + } + } else { + // Leaf cell with just data + cell.storeBuffer(Buffer.alloc(100, depth)); // Fill with data + } + return cell.endCell(); + }; + + const largeMessage = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + body: createLargeCell(2) // Create 2-level deep structure + }); + + const res = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message: largeMessage, + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true + }); + expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); + }); + }); + + describe('Message Format Edge Cases', () => { + it('should handle malformed query ID in internal transfer', async () => { + // Create internal transfer body with malformed query ID + const malformedBody = beginCell() + .storeUint(0xae42e5a4, 32) // InternalTransfer op + .storeUint(0xFFFFFFFF, 64) // Invalid query ID (too large) + .storeUint(0, 8) // Empty actions + .endCell(); + + const res = await blockchain.sendMessage(internal({ + from: highloadWalletV3.address, + to: highloadWalletV3.address, + value: toNano('1'), + body: malformedBody + })); + + // Should ignore malformed internal message + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true, + outMessagesCount: 0 + }); + }); + + it('should handle oversized message bodies', async () => { + const queryId = new HighloadQueryId(); + + // Create message with maximum possible size + const maxBody = beginCell(); + for(let i = 0; i < 4; i++) { // 4 levels of refs + const level = beginCell(); + for(let j = 0; j < 4; j++) { + level.storeRef(beginCell().storeBuffer(Buffer.alloc(127, i * 4 + j)).endCell()); + } + maxBody.storeRef(level.endCell()); + } + + const oversizedMessage = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.1'), + body: maxBody.endCell() + }); + + // Should handle large message gracefully + const res = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message: oversizedMessage, + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true + }); + }); + + it('should handle empty external message body', async () => { + const emptyMsg = { + info: { + type: 'external-in' as const, + src: undefined, + dest: highloadWalletV3.address, + importFee: 0n + }, + body: beginCell().endCell() // Empty body + }; + + // Should reject empty external message + await expect(blockchain.sendMessage(emptyMsg)).rejects.toThrow(EmulationError); + }); + }); + + // ========================= + // MUTANT COVERAGE TESTS + // ========================= + + describe('Mutation Coverage Tests', () => { + describe('Cleanup Timing Logic (mutant)', () => { + it('should test exact cleanup boundary conditions (mutant line 75)', async () => { + const timeout = await highloadWalletV3.getTimeout(); + const rndShift = getRandomInt(0, maxShift); + const rndBitNum = getRandomInt(0, 1022); + const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); + + // Send initial message + await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.01'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Test exact boundary: now() - timeout (should trigger cleanup) + // Need: last_clean_time < (now() - timeout), so 1000 < (now() - timeout) + blockchain.now = 1000 + timeout + 1; + + // This should trigger cleanup because last_clean_time < (now() - timeout) + // The mutant changes < to <=, which would change cleanup behavior + const newQueryId = queryId.getNext(); + await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: blockchain.now!, + query_id: newQueryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.01'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Verify cleanup occurred by checking clean time updated + const cleanTimeAfter = await highloadWalletV3.getLastCleaned(); + expect(cleanTimeAfter).toBe(blockchain.now); + expect(await highloadWalletV3.getProcessed(newQueryId)).toBe(true); + }); + + it('should test double timeout cleanup arithmetic (mutant line 77)', async () => { + const timeout = await highloadWalletV3.getTimeout(); + const rndShift = getRandomInt(0, maxShift); + const rndBitNum = getRandomInt(0, 1022); + const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); + + // Send initial message + await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.01'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Test double timeout boundary: now() - (timeout * 2) + // Mutants change -, *, + operators which would break cleanup logic + blockchain.now = 1000 + (timeout * 2) + 1; + + const newQueryId = queryId.getNext(); + await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: blockchain.now!, + query_id: newQueryId, + message: internal_relaxed({ + to: randomAddress(0), + value: toNano('0.01'), + }), + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Both old_queries and queries should be null after double timeout + expect(await highloadWalletV3.getProcessed(queryId)).toBe(false); + expect(await highloadWalletV3.getProcessed(newQueryId)).toBe(true); + }); + }); + + describe('Message Validation Bypasses (mutant)', () => { + it('should reject internal messages with wrong body structure (mutant line 31)', async () => { + // Test various malformed internal message structures that mutants would allow + const malformedBodies = [ + // Wrong reference count (mutant changes == to <= or >=) + beginCell().storeUint(0xae42e5a4, 32).storeUint(123n, 64).endCell(), // 0 refs instead of 1 + // Wrong bit count (total should be 32+64=96 bits) + beginCell().storeUint(0xae42e5a4, 32).storeUint(123n, 32).storeRef(beginCell().endCell()).endCell(), // 64 bits instead of 96 + ]; + + for (const body of malformedBodies) { + const res = await blockchain.sendMessage(internal({ + from: highloadWalletV3.address, + to: highloadWalletV3.address, + value: toNano('1'), + body: body + })); + + // Should ignore malformed messages (just accept TONs) + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true, + outMessagesCount: 0 + }); + } + }); + + it('should reject operations with wrong op codes (mutant line 50)', async () => { + // Test boundary conditions around op::internal_transfer + // Mutants change == to <= or >= which would accept wrong ops + const wrongOps = [ + 0xae42e5a3, // op - 1 + 0xae42e5a5, // op + 1 + 0, // Completely wrong op + ]; + + for (const wrongOp of wrongOps) { + const malformedBody = beginCell() + .storeUint(wrongOp, 32) + .storeUint(123n, 64) + .storeRef(beginCell().endCell()) + .endCell(); + + const res = await blockchain.sendMessage(internal({ + from: highloadWalletV3.address, + to: highloadWalletV3.address, + value: toNano('1'), + body: malformedBody + })); + + // Should ignore wrong operation codes + expect(res.transactions).toHaveTransaction({ + on: highloadWalletV3.address, + success: true, + outMessagesCount: 0 + }); + } + }); + + it('should validate subwallet and timeout exactly (mutant lines 95-96)', async () => { + const rndShift = getRandomInt(0, maxShift); + const rndBitNum = getRandomInt(0, 1022); + const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); + const message = internal_relaxed({ + to: randomAddress(0), + value: toNano('0.01'), + }); + + // Test subwallet validation boundaries (mutant changes == to <= or >=) + const wrongSubwallets = [SUBWALLET_ID - 1, SUBWALLET_ID + 1]; + for (const wrongSubwallet of wrongSubwallets) { + await shouldRejectWith(highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000, + query_id: queryId.getNext(), + message, + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: wrongSubwallet, + timeout: DEFAULT_TIMEOUT + } + ), Errors.invalid_subwallet); + } + + // Test timeout validation boundaries + const wrongTimeouts = [DEFAULT_TIMEOUT - 1, DEFAULT_TIMEOUT + 1]; + for (const wrongTimeout of wrongTimeouts) { + await shouldRejectWith(highloadWalletV3.sendExternalMessage( + keyPair.secretKey, + { + createdAt: 1000, + query_id: queryId.getNext(), + message, + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: wrongTimeout + } + ), 38); // error::invalid_timeout + } + }); + }); + + + describe('Message Format Validation (mutant)', () => { + it('should validate message source address is none (mutant line 161)', async () => { + const queryId = new HighloadQueryId(); + + // Test that messages with non-null source address are rejected + // Manually construct message with source address + const messageWithSource = beginCell() + .storeUint(0, 1) // int_msg_info$0 + .storeUint(0, 3) // ihr_disabled:Bool bounce:Bool bounced:Bool + .storeAddress(randomAddress(0)) // src (should be null!) + .storeAddress(randomAddress(0)) // dest + .storeCoins(toNano('0.1')) // value.coins + .storeUint(0, 1) // value.other dict empty + .storeCoins(0) // ihr_fee + .storeCoins(0) // fwd_fee + .storeUint(0, 64) // created_lt + .storeUint(0, 32) // created_at + .storeUint(0, 1) // no init + .storeUint(0, 1) // body in this cell + .endCell(); + + // The transaction should succeed but throw exit code 37 during message validation + const result = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message: messageWithSource, + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Transaction succeeds but VM exits with code 37 (invalid_message_to_send) + expect(result.transactions).toHaveTransaction({ + exitCode: 37, + success: true + }); + }); + + it('should reject state init in messages (mutant line 169)', async () => { + const queryId = new HighloadQueryId(); + + // Create message with state init manually + // This should be rejected because state init is not supported + const messageWithStateInit = beginCell() + .storeUint(0, 1) // int_msg_info$0 + .storeUint(0, 3) // ihr_disabled:Bool bounce:Bool bounced:Bool + .storeUint(0, 2) // src addr_none + .storeAddress(randomAddress(0)) // dest + .storeCoins(toNano('0.1')) // value.coins + .storeUint(0, 1) // value.other dict empty + .storeCoins(0) // ihr_fee + .storeCoins(0) // fwd_fee + .storeUint(0, 64) // created_lt + .storeUint(0, 32) // created_at + .storeUint(1, 1) // HAS init (should be rejected!) + .storeRef(beginCell().endCell()) // dummy init + .storeUint(0, 1) // body in this cell + .endCell(); + + // The transaction should succeed but throw exit code 37 during message validation + const result = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { + createdAt: 1000, + query_id: queryId, + message: messageWithStateInit, + mode: SendMode.PAY_GAS_SEPARATELY, + subwalletId: SUBWALLET_ID, + timeout: DEFAULT_TIMEOUT + }); + + // Transaction succeeds but VM exits with code 37 (invalid_message_to_send) + expect(result.transactions).toHaveTransaction({ + exitCode: 37, + success: true + }); + }); + }); + }); +});