As a static analyzer, TSA
can operate in two modes: execution error detection for local smart contracts with report generation in SARIF format or test generation for Blueprint projects.
For operating in this mode, use tsa-cli.jar
or corresponding options in the Docker Container.
In execution error detection mode, TSA
accepts as input a contract file in one of the following formats: Tact (experimental) or FunC source code, or Fift assembler code, or BoC (compiled code). Optionally, it also accepts a TL-B scheme for the recv_internal
method (about TL-B schemes importance check the internal design-document)). For detailed input format information, use the --help
argument.
The output in this mode is a SARIF report containing the following information about methods that may encounter a TVM error during execution:
- Instruction coverage percentage by the analyzer for the method
- Method number and TVM bytecode instruction where the error may occur
- Error code and type
- Call stack (method number - instruction)
- Possible (but not necessarily unique) parameter set causing the error
- Approximate gas usage up to the error
For more information about error types, see the relevant section.
Consider a simple smart contract that may encounter a cell overflow error when the write
method receives a value greater than 4:
(builder) write(int loop_count) method_id {
builder b = begin_cell();
if (loop_count < 0) {
return b;
}
var i = 0;
repeat(loop_count) {
builder value = begin_cell().store_int(i, 32);
b = b.store_ref(value.end_cell());
}
return b;
}
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
;; Do nothing
}
The analyzer's output for this contract will identify the error in the following format:
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"properties": {
"coverage": {
"0": 100.0,
"75819": 100.0
}
},
"results": [
{
"codeFlows": [
{
"threadFlows": [
{
"locations": [
{
"location": {
"logicalLocations": [
{
"decoratedName": "75819",
"properties": {
"stmt": "REPEAT#8"
}
}
]
}
}
]
}
]
}
],
"level": "error",
"locations": [
{
"logicalLocations": [
{
"decoratedName": "75819"
}
]
}
],
"message": {
"text": "TvmFailure(exit=TVM integer out of expected range, exit code: 5, type=UnknownError)"
},
"properties": {
"gasUsage": 220,
"usedParameters": [
"2147483648"
],
"resultStack": [
"0"
]
},
"ruleId": "integer-out-of-range"
}
],
"tool": {
"driver": {
"name": "TSA",
"organization": "Explyt"
}
}
}
]
}
For more examples containing erroneous places, take a look at the manually written contracts. Feel free to run TSA by yourself for these contracts or consider tests for them.
In test generation mode, TSA
takes as input a project in the Blueprint format and
the relative path to the source code of the analyzed contract (as before, use --help
argument for more detailed information about input format).
In this mode, TSA
generates a corresponding wrapper
in Typescript
under the wrappers
directory and a test file for the contract in the tests
directory. The test file contains regression tests for execution branches of methods that terminate with a TVM error (for more information about error types, see the relevant section).
For the wallet-v4 contract, a test file will be generated with tests similar to the following:
import {Blockchain} from '@ton/sandbox'
import {Address, beginCell, Builder, Cell, Dictionary, DictionaryValue, Slice} from '@ton/core'
import '@ton/test-utils'
import {compileFunc} from "@ton-community/func-js"
import * as fs from "node:fs"
import {WalletV4Code} from "../wrappers/WalletV4Code"
async function compileContract(): Promise<Cell> {
let compileResult = await compileFunc({
targets: ['contracts/wallet-v4-code.fc'],
sources: (x) => fs.readFileSync(x).toString("utf8"),
})
if (compileResult.status === "error") {
console.error("Compilation Error!")
console.error(`\n${compileResult.message}`)
process.exit(1)
}
return Cell.fromBoc(Buffer.from(compileResult.codeBoc, "base64"))[0]
}
const sliceValue: DictionaryValue<Slice> = {
serialize: (src: Slice, builder: Builder) => {
builder.storeSlice(src)
},
parse: (src: Slice) => {
return src.clone();
}
}
describe('TvmTest', () => {
let code: Cell
let blockchain: Blockchain
beforeAll(async () => {
code = await compileContract()
})
beforeEach(async () => {
blockchain = await Blockchain.create()
})
it('test-0', async () => {
const data = beginCell().storeUint(BigInt("0b0"), 173).endCell()
const msgBody = beginCell().endCell()
const from = Address.parseRaw("0:0000000000000000000000000000000000000000000000000000000000000000")
const bounce = false
const bounced = false
const contractAddress = Address.parseRaw("0:0000000000000000000000000000000000000000000000000000000000000000")
const contract = blockchain.openContract(new WalletV4Code(contractAddress, { code, data }))
await contract.initializeContract(blockchain, 10000000n)
const sentMessageResult = await contract.internal(
blockchain,
from,
msgBody,
10000000n,
bounce,
bounced
)
expect(sentMessageResult.transactions).toHaveTransaction({
from: from,
to: contractAddress,
exitCode: 9,
})
})
})