From cb9db9792171640d469b23789caff62efb5aa32b Mon Sep 17 00:00:00 2001 From: Novus Nota <68142933+novusnota@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:00:19 +0200 Subject: [PATCH] test: Tact-reserved and general compute phase exit codes With the correction of error list generated for the `.md` compilation report --- cspell.json | 6 + src/generator/createABI.ts | 21 +- .../exit-codes/compute-phase-errors.spec.ts | 248 ++++++++++++++++++ .../contracts/compute-phase-errors.fc | 8 + .../contracts/compute-phase-errors.tact | 166 ++++++++++++ .../exit-codes/contracts/repeat-range.tact | 2 +- .../tact-reserved-contract-errors.tact | 109 ++++++++ src/test/exit-codes/repeat-range.spec.ts | 6 +- .../tact-reserved-contract-errors.spec.ts | 143 ++++++++++ tact.config.json | 13 + 10 files changed, 717 insertions(+), 5 deletions(-) create mode 100644 src/test/exit-codes/compute-phase-errors.spec.ts create mode 100644 src/test/exit-codes/contracts/compute-phase-errors.fc create mode 100644 src/test/exit-codes/contracts/compute-phase-errors.tact create mode 100644 src/test/exit-codes/contracts/tact-reserved-contract-errors.tact create mode 100644 src/test/exit-codes/tact-reserved-contract-errors.spec.ts diff --git a/cspell.json b/cspell.json index e5b433ec2..d6c9c6416 100644 --- a/cspell.json +++ b/cspell.json @@ -75,6 +75,7 @@ "lvalues", "masterchain", "maxint", + "Merkle", "minmax", "mintable", "mktemp", @@ -90,6 +91,7 @@ "Offchain", "Parens", "pinst", + "PLDDICT", "PLDIX", "PLDREF", "PLDSLICEX", @@ -97,6 +99,7 @@ "POSIX", "postpack", "prando", + "PUSHINT", "PUSHREF", "PUSHSLICE", "RANDU", @@ -118,7 +121,9 @@ "SENDRAWMSG", "SENDMSG", "seqno", + "SETCONTARGS", "SETINDEXVARQ", + "SETNUMARGS", "shiki", "SREFS", "SREMPTY", @@ -175,6 +180,7 @@ "src/test/e2e-emulated/contracts/strings.tact", "src/test/compilation-fail/fail-const-eval.spec.ts", "src/test/e2e-emulated/getter-names-conflict.spec.ts", + "src/test/exit-codes/contracts/compute-phase-errors.tact", "stdlib/stdlib.fc" ] } diff --git a/src/generator/createABI.ts b/src/generator/createABI.ts index 406606370..45f6ae6e1 100644 --- a/src/generator/createABI.ts +++ b/src/generator/createABI.ts @@ -146,11 +146,30 @@ export function createABI(ctx: CompilerContext, name: string): ContractABI { errors["8"] = { message: "Cell overflow" }; errors["9"] = { message: "Cell underflow" }; errors["10"] = { message: "Dictionary error" }; + errors["11"] = { message: "'Unknown' error" }; + errors["12"] = { message: "Fatal error" }; errors["13"] = { message: "Out of gas error" }; - errors["32"] = { message: "Method ID not found" }; + errors["14"] = { message: "Virtualization error" }; + errors["32"] = { message: "Action list is invalid" }; + errors["33"] = { message: "Action list is too long" }; errors["34"] = { message: "Action is invalid or not supported" }; + errors["35"] = { message: "Invalid source address in outbound message" }; + errors["36"] = { + message: "Invalid destination address in outbound message", + }; errors["37"] = { message: "Not enough TON" }; errors["38"] = { message: "Not enough extra-currencies" }; + errors["39"] = { + message: "Outbound message does not fit into a cell after rewriting", + }; + errors["40"] = { message: "Cannot process a message" }; + errors["41"] = { message: "Library reference is null" }; + errors["42"] = { message: "Library change action error" }; + errors["43"] = { + message: + "Exceeded maximum number of cells in the library or the maximum depth of the Merkle tree", + }; + errors["50"] = { message: "Account state size exceeded limits" }; for (const e of Object.values(contractErrors)) { errors[e.id] = { message: e.message }; } diff --git a/src/test/exit-codes/compute-phase-errors.spec.ts b/src/test/exit-codes/compute-phase-errors.spec.ts new file mode 100644 index 000000000..cf953609b --- /dev/null +++ b/src/test/exit-codes/compute-phase-errors.spec.ts @@ -0,0 +1,248 @@ +import { toNano } from "@ton/core"; +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { ComputePhaseErrorsTester as TestContract } from "./contracts/output/compute-phase-errors_ComputePhaseErrorsTester"; +import "@ton/test-utils"; + +describe("compute phase errors", () => { + let blockchain: Blockchain; + let treasure: SandboxContract; + let contract: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.verbosity.print = false; + treasure = await blockchain.treasury("treasure", { + resetBalanceIfZero: true, + }); + + contract = blockchain.openContract(await TestContract.fromInit()); + + const deployResult = await contract.send( + treasure.getSender(), + { value: toNano("100000") }, + null, + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: true, + deploy: true, + }); + }); + + // 0: success + it("should test exit code 0", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "0", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: true, + exitCode: 0, + }); + }); + + // 1: alt. success code + it("should test exit code 1", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "1", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: true, + exitCode: 1, + }); + }); + + // 2: stack underflow + it("should test exit code 2", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "2", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 2, + }); + }); + + // 3: Stack overflow + it("should test exit code 3", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "3", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 3, + }); + }); + + // 4: Integer overflow + it("should test exit code 4", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "4", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 4, + }); + }); + + // 5: Integer out of range + it("should test exit code 5", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "5", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 5, + }); + }); + + // 6: Invalid opcode + it("should test exit code 6", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "6", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 6, + }); + }); + + // 7: Type check error + it("should test exit code 7", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "7", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 7, + }); + }); + + // 8: Cell overflow + it("should test exit code 8", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "8", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 8, + }); + }); + + // 9: Cell underflow + it("should test exit code 9", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "9", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 9, + }); + }); + + // 10: Dictionary error + it("should test exit code 10", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "10", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 10, + }); + }); + + // 11: "Unknown" error + // NOTE: Thrown in various unrelated cases + it("should test exit code 11", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "11", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 11, + }); + }); + + // 12: Fatal error + // NOTE: thrown by TVM in situations deemed impossible) + + // 13 (actually, -14): Out of gas + it("should test exit code 13", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "13", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: -14, + }); + }); + + // 14: Virtualization error + // NOTE: Reserved, but never thrown +}); diff --git a/src/test/exit-codes/contracts/compute-phase-errors.fc b/src/test/exit-codes/contracts/compute-phase-errors.fc new file mode 100644 index 000000000..d3ef07352 --- /dev/null +++ b/src/test/exit-codes/contracts/compute-phase-errors.fc @@ -0,0 +1,8 @@ +() stack_overflow() impure asm """ + <{ + }>CONT // c + 0 SETNUMARGS // c' + 2 PUSHINT // c' 2 + SWAP // 2 c' + 1 -1 SETCONTARGS +"""; diff --git a/src/test/exit-codes/contracts/compute-phase-errors.tact b/src/test/exit-codes/contracts/compute-phase-errors.tact new file mode 100644 index 000000000..0b548d032 --- /dev/null +++ b/src/test/exit-codes/contracts/compute-phase-errors.tact @@ -0,0 +1,166 @@ +import "./compute-phase-errors.fc"; + +contract ComputePhaseErrorsTester { + // Used for storing temporary values + tmpI: Int? as uint8 = 0; + tmpC: Cell? = cell("te6cckEBAQEAAgAAAEysuc0="); // empty cell + + /// To handle deployment + receive() {} + + /// Exit code 0 + receive("0") { + throw(0); // Yes, that still counts as a success + } + + /// Exit code 1 + receive("1") { + throw(1); // Yes, that still counts as a success + } + + /// Exit code 2 + receive("2") { + // Removes 100 elements from the stack, causing an underflow + repeat (100) { + drop(); + } + } + + /// Exit code 3 + receive("3") { + stackOverflow(); + } + + /// Exit code 4 + receive("4") { + // TODO: integer overflow + + // Addition + + // Subtraction + + // Negation + + // Multiplication + + // Division + + // Division by zero + + // Modulo by zero + + // FIXME: + throw(9); + } + + /// Exit code 5 + receive("5") { + // Setup + let targetCode = 5; + let failed = true; + + // Storing too much + try { + // uint8 can only store numbers up to 255, + // so 512 is too much + self.tmpI = 512; + failed = false; + } catch (exitCode) { + nativeThrowIf(exitCode, exitCode != targetCode); + } + require(failed == true, "Storing too much didn't cause an error"); + + // Specifying an out-of-bounds value + failed = true; + try { + // Builder.storeUint() function can only use up to 256 bits, + // so 512 is too much + let s: Slice = beginCell().storeUint(0, 512).asSlice(); + failed = false; + } catch (exitCode) { + nativeThrowIf(exitCode, exitCode != targetCode); + } + require(failed == true, "Specifying an out-of-bounds value didn't cause an error"); + + // As we got here, everything above caused the target exit code, + // so let's throw it explicitly now for the tests on Blueprint's side + throw(targetCode); + } + + // Exit code 6 + receive("6") { + invalidOpcode(); + } + + /// Exit code 7 + receive("7") { + // Map with two entries — 0: 0 and 1: 1 + let m: map = toMapIntInt(cellWithDictIntInt.beginParse()); + + // And accessing the third (2nd index) would cause the error + self.tmpI = m.get(2); + } + + /// Exit code 8 + receive("8") { + // TODO: + // - [ ] cell overflow (data) + // - [ ] cell overflow (refs) + + // FIXME: + throw(8); + } + + /// Exit code 9 + receive("9") { + // TODO: + // - [ ] cell underflow (data) + // - [ ] cell underflow (refs) + + // FIXME: + throw(9); + } + + /// Exit code 10 + receive("10") { + // The Int to Int dictionary is being misinterpreted as a map + let m: map = toMapIntCell(cellWithDictIntInt.beginParse()); + + // And the error happens only when we touch it + self.tmpC = m.get(0); + } + + /// Exit code 11 + receive("11") { + // Unlike nativeSendMessage which uses SENDRAWMSG, this one uses SENDMSG, + // and therefore fails in Compute time when the message is ill-formed + nativeSendMessageReturnForwardFee(emptyCell(), 0); + } + + /// Exit code 13 (-14, to be precise) + receive("13") { + let counter = 0; + repeat (pow(2, 31) - 1) { + counter += 1; + } + } +} + +/// Pre-computed Int to Int dictionary with two entries — 0: 0 and 1: 1 +const cellWithDictIntInt: Cell = cell("te6cckEBBAEAUAABAcABAgPQCAIDAEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMLMbT1U="); + +/// Tries to preload a dictionary from a Slice as a map +asm fun toMapIntInt(x: Slice): map { PLDDICT } + +/// Tries to preload a dictionary from a Slice as a map +asm fun toMapIntCell(x: Slice): map { PLDDICT } + +/// Non-existent opcode +asm fun invalidOpcode() { x{D7FF} @addop } + +/// DROP +asm fun drop() { DROP } + +/// Stack overflow +@name(stack_overflow) +native stackOverflow(); diff --git a/src/test/exit-codes/contracts/repeat-range.tact b/src/test/exit-codes/contracts/repeat-range.tact index 8c4bab42b..1b6136a68 100644 --- a/src/test/exit-codes/contracts/repeat-range.tact +++ b/src/test/exit-codes/contracts/repeat-range.tact @@ -1,4 +1,4 @@ -contract RepeatRange { +contract RepeatRangeTester { /// To handle deployment receive() {} diff --git a/src/test/exit-codes/contracts/tact-reserved-contract-errors.tact b/src/test/exit-codes/contracts/tact-reserved-contract-errors.tact new file mode 100644 index 000000000..2609f9b95 --- /dev/null +++ b/src/test/exit-codes/contracts/tact-reserved-contract-errors.tact @@ -0,0 +1,109 @@ +import "@stdlib/ownable"; +import "@stdlib/dns"; + +message(1478) SpanishInquisition {} + +contract ReservedContractErrorsTester with Ownable { + /// To make Ownable work + owner: Address; + + /// Setups address of this contract to be its owner + init() { self.owner = myAddress() } + + /// To handle deployment + receive() {} + + /// Exit code 128 + receive("128") { + let gotcha: String? = null; + dump(gotcha!!); + } + + /// Exit code 130 + receive("130") { + send(SendParameters{ + to: myAddress(), // Send a message back to this contract + value: 0, + mode: SendRemainingValue | SendIgnoreErrors, + body: SpanishInquisition{}.toCell(), // Nobody expects it! + }); + } + + /// Exit code 132 + receive("132") { + self.requireOwner(); + } + + /// Exit code 134 + receive("134") { + // Setup + let targetCode = 134; + let failed = true; + + // Case 1 + try { + // 0 is code of NUL in ASCII and it is not valid Base64 + dump(beginCell() + .storeUint(0, 8) + .asSlice() + .fromBase64()); + failed = false; + } catch (exitCode) { + nativeThrowIf(exitCode, exitCode != targetCode); + } + require(failed == true, "Slice.fromBase64() didn't error on invalid Base64"); + + // Case 2 + failed = true; + try { + // 0 is code of NUL in ASCII and it is not valid Base64 + dump("\x00".fromBase64()); + failed = false; + } catch (exitCode) { + nativeThrowIf(exitCode, exitCode != targetCode); + } + require(failed == true, "String.fromBase64() didn't error on invalid Base64"); + + // Case 3 + failed = true; + try { + dump((42).toFloatString(-1)); + failed = false; + } catch (exitCode) { + nativeThrowIf(exitCode, exitCode != targetCode); + } + require(failed == true, "Int.toFloatString() didn't error on digits -1"); + + // Case 4 + failed = true; + try { + dump((42).toFloatString(78)); + failed = false; + } catch (exitCode) { + nativeThrowIf(exitCode, exitCode != targetCode); + } + require(failed == true, "Int.toFloatString() didn't error on digits 78"); + + // As we got here, everything above caused the target exit code, + // so let's throw it explicitly now for the tests on Blueprint's side + throw(targetCode); + } + + /// Exit code 136 + receive("136") { + let unsupportedChainId = 1; + dump( + // Zero address in unsupported workchain + newAddress(unsupportedChainId, 0) + ); + } + + /// Exit code 137 + receive("137") { + let masterchainId = -1; + dump( + // Zero address in masterchain without the config option set + newAddress(masterchainId, 0) + ); + } +} diff --git a/src/test/exit-codes/repeat-range.spec.ts b/src/test/exit-codes/repeat-range.spec.ts index 7db8d09a3..e3451f316 100644 --- a/src/test/exit-codes/repeat-range.spec.ts +++ b/src/test/exit-codes/repeat-range.spec.ts @@ -1,12 +1,12 @@ import { toNano } from "@ton/core"; import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; -import { RepeatRange } from "./contracts/output/repeat-range_RepeatRange"; +import { RepeatRangeTester as TestContract } from "./contracts/output/repeat-range_RepeatRangeTester"; import "@ton/test-utils"; describe("repeat range", () => { let blockchain: Blockchain; let treasure: SandboxContract; - let contract: SandboxContract; + let contract: SandboxContract; beforeEach(async () => { blockchain = await Blockchain.create(); @@ -15,7 +15,7 @@ describe("repeat range", () => { resetBalanceIfZero: true, }); - contract = blockchain.openContract(await RepeatRange.fromInit()); + contract = blockchain.openContract(await TestContract.fromInit()); const deployResult = await contract.send( treasure.getSender(), diff --git a/src/test/exit-codes/tact-reserved-contract-errors.spec.ts b/src/test/exit-codes/tact-reserved-contract-errors.spec.ts new file mode 100644 index 000000000..3c951372a --- /dev/null +++ b/src/test/exit-codes/tact-reserved-contract-errors.spec.ts @@ -0,0 +1,143 @@ +import { toNano } from "@ton/core"; +import { Blockchain, SandboxContract, TreasuryContract } from "@ton/sandbox"; +import { ReservedContractErrorsTester as TestContract } from "./contracts/output/tact-reserved-contract-errors_ReservedContractErrorsTester"; +import "@ton/test-utils"; + +describe("Tact-reserved contract errors", () => { + let blockchain: Blockchain; + let treasure: SandboxContract; + let contract: SandboxContract; + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.verbosity.print = false; + treasure = await blockchain.treasury("treasure", { + resetBalanceIfZero: true, + }); + + contract = blockchain.openContract(await TestContract.fromInit()); + + const deployResult = await contract.send( + treasure.getSender(), + { value: toNano("100000") }, + null, + ); + + expect(deployResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: true, + deploy: true, + }); + }); + + // 128: Null reference exception + it("should test exit code 128", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "128", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 128, + }); + }); + + // 129: Invalid serialization prefix + // NOTE: Reserved, but due to a number of prior checks it cannot be thrown unless one hijacks + // the contract code before deployment and changes the opcodes of the Messages expected + // to be received in the contract + + // 130: Invalid incoming message + it("should test exit code 130", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "130", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: contract.address, // From contract back to contract + to: contract.address, + success: false, + exitCode: 130, + }); + }); + + // 131: Constraints error + // NOTE: Reserved, but never thrown anywhere, can't repro + + // 132: Access denied + it("should test exit code 132", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "132", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 132, + }); + }); + + // 133: Contract stopped + // NOTE: Reserved, but never thrown anywhere, can't repro + + // 134: Invalid argument + it("should test exit code 134", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "134", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 134, + }); + }); + + // 135: Code of a contract was not found + // NOTE: Reserved, but one has to replace the contract code to trigger it + + // 136: Invalid address + it("should test exit code 136", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "136", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 136, + }); + }); + + // 137: Masterchain support is not enabled for this contract + it("should test exit code 137", async () => { + const sendResult = await contract.send( + treasure.getSender(), + { value: toNano("10") }, + "137", + ); + + expect(sendResult.transactions).toHaveTransaction({ + from: treasure.address, + to: contract.address, + success: false, + exitCode: 137, + }); + }); +}); diff --git a/tact.config.json b/tact.config.json index d210f252f..4d4cac345 100644 --- a/tact.config.json +++ b/tact.config.json @@ -405,6 +405,19 @@ "name": "repeat-range", "path": "./src/test/exit-codes/contracts/repeat-range.tact", "output": "./src/test/exit-codes/contracts/output" + }, + { + "name": "tact-reserved-contract-errors", + "path": "./src/test/exit-codes/contracts/tact-reserved-contract-errors.tact", + "output": "./src/test/exit-codes/contracts/output", + "options": { + "debug": true + } + }, + { + "name": "compute-phase-errors", + "path": "./src/test/exit-codes/contracts/compute-phase-errors.tact", + "output": "./src/test/exit-codes/contracts/output" } ] }