Repository: tact-lang/tact

Issue:
Implement SBT standard
Add benchmarks for https://github.com/ton-blockchain/TEPs/blob/master/text/0085-sbt-standard.md



Discussion:

Solution patch:
diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md
index 9b437785..bec549d0 100644
--- a/dev-docs/CHANGELOG.md
+++ b/dev-docs/CHANGELOG.md
@@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [fix] Compiler now correctly processes nested structs with default values in the interpreter: PR [#2687](https://github.com/tact-lang/tact/pull/2687)
 - [fix] Compiler now correctly compiles contracts with optional struct fields with default values: PR [#2683](https://github.com/tact-lang/tact/pull/2683)
 - [fix] Compiler now shows a more informative error message for unsupported assembly functions inside traits and contracts: PR [#2689](https://github.com/tact-lang/tact/pull/2689)
+- [fix] Compiler now shows a more informative error message for `Slice? as remaining` fields: PR [#2694](https://github.com/tact-lang/tact/pull/2694)
+- [fix] Compiler now shows a more informative error message for fields with unsupported trait types: PR [#2695](https://github.com/tact-lang/tact/pull/2695)
+- [fix] Generated TypeScript wrappers now export all functions for serialization/deserialization: PR [#2706](https://github.com/tact-lang/tact/pull/2706)
+- [fix] Compiler now correctly generates code for functions with several wildcard parameters: PR [#2703](https://github.com/tact-lang/tact/pull/2703)
 
 ### Standard Library
 
diff --git a/spell/cspell-list.txt b/spell/cspell-list.txt
index ea198817..3ffd0c56 100644
--- a/spell/cspell-list.txt
+++ b/spell/cspell-list.txt
@@ -29,6 +29,7 @@ compilables
 Compilables
 comptime
 Comptime
+CSBT
 Daniil
 Danil
 decompilation
diff --git a/src/benchmarks/bench.build.ts b/src/benchmarks/bench.build.ts
index 168658c8..6b9f3b4b 100644
--- a/src/benchmarks/bench.build.ts
+++ b/src/benchmarks/bench.build.ts
@@ -15,7 +15,7 @@ const main = async () => {
         // Directory does not exist, no need to remove
     }
 
-    await allInFolder(__dirname, ["contracts/*.tact"], {
+    await allInFolder(__dirname, ["contracts/**/*.tact"], {
         debug: false,
         experimental: { inline: true },
         safety: { nullChecks: false },
diff --git a/src/benchmarks/contracts/func/sbt/op-codes.fc b/src/benchmarks/contracts/func/sbt/op-codes.fc
new file mode 100644
index 00000000..e1fb7d8c
--- /dev/null
+++ b/src/benchmarks/contracts/func/sbt/op-codes.fc
@@ -0,0 +1,27 @@
+int op::transfer() asm "0x5fcc3d14 PUSHINT";
+int op::ownership_assigned() asm "0x05138d91 PUSHINT";
+int op::excesses() asm "0xd53276db PUSHINT";
+int op::get_static_data() asm "0x2fcb26a2 PUSHINT";
+int op::report_static_data() asm "0x8b771735 PUSHINT";
+int op::get_royalty_params() asm "0x693d3950 PUSHINT";
+int op::report_royalty_params() asm "0xa8cb00ad PUSHINT";
+
+;; NFTEditable
+int op::edit_content() asm "0x1a0b9d51 PUSHINT";
+int op::transfer_editorship() asm "0x1c04412a PUSHINT";
+int op::editorship_assigned() asm "0x511a4463 PUSHINT";
+
+;; SBT
+int op::request_owner() asm "0xd0c3bfea PUSHINT";
+int op::owner_info() asm "0x0dd607e3 PUSHINT";
+
+int op::prove_ownership() asm "0x04ded148 PUSHINT";
+int op::ownership_proof() asm "0x0524c7ae PUSHINT";
+int op::ownership_proof_bounced() asm "0xc18e86d2 PUSHINT";
+
+int op::destroy() asm "0x1f04537a PUSHINT";
+int op::revoke() asm "0x6f89f5e3 PUSHINT";
+int op::take_excess() asm "0xd136d3b3 PUSHINT";
+
+int jetton::transfer_notification() asm "0x7362d09c PUSHINT";
+int jetton::transfer() asm "0xf8a7ea5 PUSHINT";
diff --git a/src/benchmarks/contracts/func/sbt/params.fc b/src/benchmarks/contracts/func/sbt/params.fc
new file mode 100644
index 00000000..d98ce72b
--- /dev/null
+++ b/src/benchmarks/contracts/func/sbt/params.fc
@@ -0,0 +1,10 @@
+int workchain() asm "0 PUSHINT";
+
+() force_chain(slice addr) impure {
+  (int wc, _) = parse_std_addr(addr);
+  throw_unless(333, wc == workchain());
+}
+
+slice null_addr() asm "b{00} PUSHSLICE";
+int flag::regular() asm "0x10 PUSHINT";
+int flag::bounce() asm "0x8 PUSHINT";
diff --git a/src/benchmarks/contracts/func/sbt/sbt-item.fc b/src/benchmarks/contracts/func/sbt/sbt-item.fc
new file mode 100644
index 00000000..b31d9ed5
--- /dev/null
+++ b/src/benchmarks/contracts/func/sbt/sbt-item.fc
@@ -0,0 +1,214 @@
+#include "op-codes.fc";
+#include "params.fc";
+
+;;
+;;  TON SBT Item Smart Contract
+;;
+
+int min_tons_for_storage() asm "50000000 PUSHINT"; ;; 0.05 TON
+
+;;
+;;  Storage
+;;
+;;  uint64 index
+;;  MsgAddressInt collection_address
+;;  MsgAddressInt owner_address
+;;  cell content
+;;  MsgAddressInt authority_address
+;;  uint64 revoked_at
+;;
+
+global int storage::index;
+global int init?;
+global slice storage::collection_address;
+global slice storage::owner_address;
+global slice storage::authority_address;
+global cell storage::content;
+global int storage::revoked_at;
+
+() load_data() impure {
+    slice ds = get_data().begin_parse();
+
+    storage::index              = ds~load_uint(64);
+    storage::collection_address = ds~load_msg_addr();
+    init?                       = false;
+
+    if (ds.slice_bits() > 0) {
+        init?                      = true;
+        storage::owner_address     = ds~load_msg_addr();
+        storage::content           = ds~load_ref();
+        storage::authority_address = ds~load_msg_addr();
+        storage::revoked_at        = ds~load_uint(64);
+    }
+}
+
+() store_data() impure {
+    set_data(
+        begin_cell()
+            .store_uint(storage::index, 64)
+            .store_slice(storage::collection_address)
+            .store_slice(storage::owner_address)
+            .store_ref(storage::content)
+            .store_slice(storage::authority_address)
+            .store_uint(storage::revoked_at, 64)
+            .end_cell()
+    );
+}
+
+() send_msg(int flag, slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
+    var body = begin_cell().store_uint(op, 32).store_uint(query_id, 64);
+    if (~ builder_null?(payload)) {
+        body = body.store_builder(payload);
+    }
+
+    var msg = begin_cell()
+        .store_uint(flag, 6)
+        .store_slice(to_address)
+        .store_coins(amount)
+        .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+        .store_ref(body.end_cell());
+
+    send_raw_message(msg.end_cell(), send_mode);
+}
+
+() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+        return ();
+    }
+
+    slice cs = in_msg_full.begin_parse();
+    int flags = cs~load_uint(4);
+
+    slice sender_address = cs~load_msg_addr();
+
+    load_data();
+
+    if (~ init?) {
+        throw_unless(405, equal_slices_bits(storage::collection_address, sender_address));
+
+        storage::owner_address     = in_msg_body~load_msg_addr();
+        storage::content           = in_msg_body~load_ref();
+        storage::authority_address = in_msg_body~load_msg_addr();
+        storage::revoked_at        = 0;
+
+        store_data();
+        return ();
+    }
+
+    int op = in_msg_body~load_uint(32);
+
+    if (flags & 1) { ;; route all prove_ownership bounced messages to owner
+        ;; first op was 0xffffffff, because of bounced, now we need to read real one
+        op = in_msg_body~load_uint(32);
+
+        if (op == op::ownership_proof()) {
+            int query_id = in_msg_body~load_uint(64);
+            ;; mode 64 = carry all the remaining value of the inbound message
+            send_msg(flag::regular(), storage::owner_address, 0, op::ownership_proof_bounced(), query_id, null(), 64);
+        }
+        return ();
+    }
+
+    int query_id = in_msg_body~load_uint(64);
+
+    if (op == op::request_owner()) {
+        slice dest = in_msg_body~load_msg_addr();
+        cell body = in_msg_body~load_ref();
+        int with_content = in_msg_body~load_uint(1);
+
+        var msg = begin_cell()
+            .store_uint(storage::index, 256)
+            .store_slice(sender_address)
+            .store_slice(storage::owner_address)
+            .store_ref(body)
+            .store_uint(storage::revoked_at, 64)
+            .store_uint(with_content, 1);
+
+        if (with_content != 0) {
+            msg = msg.store_ref(storage::content);
+        }
+
+        ;; mode 64 = carry all the remaining value of the inbound message
+        send_msg(flag::regular() | flag::bounce(), dest, 0, op::owner_info(), query_id, msg, 64);
+        return ();
+    }
+    if (op == op::prove_ownership()) {
+        throw_unless(401, equal_slices_bits(storage::owner_address, sender_address));
+
+        slice dest = in_msg_body~load_msg_addr();
+        cell body = in_msg_body~load_ref();
+        int with_content = in_msg_body~load_uint(1);
+
+        var msg = begin_cell()
+            .store_uint(storage::index, 256)
+            .store_slice(storage::owner_address)
+            .store_ref(body)
+            .store_uint(storage::revoked_at, 64)
+            .store_uint(with_content, 1);
+
+        if (with_content != 0) {
+            msg = msg.store_ref(storage::content);
+        }
+
+        ;; mode 64 = carry all the remaining value of the inbound message
+        send_msg(flag::regular() | flag::bounce(), dest, 0, op::ownership_proof(), query_id, msg, 64);
+        return ();
+    }
+    if (op == op::get_static_data()) {
+        var msg = begin_cell().store_uint(storage::index, 256).store_slice(storage::collection_address);
+
+        ;; mode 64 = carry all the remaining value of the inbound message
+        send_msg(flag::regular(), sender_address, 0, op::report_static_data(), query_id, msg, 64);
+        return ();
+    }
+    if (op == op::destroy()) {
+        throw_unless(401, equal_slices_bits(storage::owner_address, sender_address));
+
+        send_msg(flag::regular(), sender_address, 0, op::excesses(), query_id, null(), 128);
+
+        storage::owner_address = null_addr();
+        storage::authority_address = null_addr();
+        store_data();
+        return ();
+    }
+    if (op == op::revoke()) {
+        throw_unless(401, equal_slices_bits(storage::authority_address, sender_address));
+        throw_unless(403, storage::revoked_at == 0);
+
+        storage::revoked_at = now();
+        store_data();
+        return ();
+    }
+    if (op == op::take_excess()) {
+        throw_unless(401, equal_slices_bits(storage::owner_address, sender_address));
+
+        ;; reserve amount for storage
+        raw_reserve(min_tons_for_storage(), 0);
+
+        send_msg(flag::regular(), sender_address, 0, op::excesses(), query_id, null(), 128);
+        return ();
+    }
+    if (op == op::transfer()) {
+        throw(413);
+    }
+    throw(0xffff);
+}
+
+;;
+;;  GET Methods
+;;
+
+(int, int, slice, slice, cell) get_nft_data() method_id {
+    load_data();
+    return (init?, storage::index, storage::collection_address, storage::owner_address, storage::content);
+}
+
+slice get_authority_address() method_id {
+    load_data();
+    return storage::authority_address;
+}
+
+int get_revoked_time() method_id {
+    load_data();
+    return storage::revoked_at;
+}
diff --git a/src/benchmarks/contracts/jetton-minter-discoverable.tact b/src/benchmarks/contracts/jetton-minter-discoverable.tact
index b18c8406..00a60a9f 100644
--- a/src/benchmarks/contracts/jetton-minter-discoverable.tact
+++ b/src/benchmarks/contracts/jetton-minter-discoverable.tact
@@ -48,7 +48,7 @@ contract JettonMinter(
 
         let targetJettonWallet: BasechainAddress =
             (ownerWorkchain == Workchain) ?
-            contractBasechainAddress(initOf JettonWallet(0, msg.ownerAddress, myAddress()))
+            contractBasechainAddress(initOf JettonWallet(msg.ownerAddress, myAddress(), 0))
             : emptyBasechainAddress();
 
         message(MessageParameters {
@@ -112,7 +112,7 @@ inline fun makeTakeWalletAddressMsg(targetJettonWallet: BasechainAddress, msg: P
 }
 
 inline fun getJettonWalletInit(address: Address): StateInit {
-    return initOf JettonWallet(0, address, myAddress());
+    return initOf JettonWallet(address, myAddress(), 0);
 }
 
 inline fun getJettonWalletByOwner(jettonWalletOwner: Address): Address {
diff --git a/src/benchmarks/contracts/jetton-wallet.tact b/src/benchmarks/contracts/jetton-wallet.tact
index 6512fa65..ded7775f 100644
--- a/src/benchmarks/contracts/jetton-wallet.tact
+++ b/src/benchmarks/contracts/jetton-wallet.tact
@@ -1,9 +1,9 @@
 import "./messages";
 
 contract JettonWallet(
-    balance: Int as coins,
     owner: Address,
     master: Address,
+    balance: Int as coins,
 ) {
     const minTonsForStorage: Int = ton("0.01");
     const gasConsumption: Int = ton("0.015");
@@ -40,7 +40,7 @@ contract JettonWallet(
                 forwardTonAmount: msg.forwardTonAmount,
                 forwardPayload: msg.forwardPayload
             }.toCell(),
-            init: initOf JettonWallet(0, msg.destination, self.master),
+            init: initOf JettonWallet(msg.destination, self.master, 0),
         });
     }
 
@@ -48,7 +48,7 @@ contract JettonWallet(
         self.balance += msg.amount;
 
         // This message should come only from master, or from other JettonWallet
-        let wallet: StateInit = initOf JettonWallet(0, msg.sender, self.master);
+        let wallet: StateInit = initOf JettonWallet(msg.sender, self.master, 0);
         if (!wallet.hasSameBasechainAddress(sender())) {
             throwUnless(707, self.master == sender());
         }
diff --git a/src/benchmarks/contracts/sbt/constants.tact b/src/benchmarks/contracts/sbt/constants.tact
new file mode 100644
index 00000000..5d6f7fb6
--- /dev/null
+++ b/src/benchmarks/contracts/sbt/constants.tact
@@ -0,0 +1,11 @@
+// ExitCodes
+const IncorrectDeployer: Int = 405;
+const InvalidMethod: Int = 413;
+
+const AlreadyRevoked: Int = 403;
+const IncorrectSender: Int = 401;
+const InvalidData: Int = 65535; // not handled
+const NotInit: Int = 9;
+
+// GasConstants
+const minTonsForStorage: Int = ton("0.05");
diff --git a/src/benchmarks/contracts/sbt/messages-sbt.tact b/src/benchmarks/contracts/sbt/messages-sbt.tact
new file mode 100644
index 00000000..2039115e
--- /dev/null
+++ b/src/benchmarks/contracts/sbt/messages-sbt.tact
@@ -0,0 +1,79 @@
+message(0x5fcc3d14) Transfer {
+    queryId: Int as uint64;
+}
+
+struct NFTData {
+    init: Int;
+    itemIndex: Int as uint64;
+    collectionAddress: Address;
+    owner: Address?;
+    content: Cell?;
+}
+
+struct InitNFTData {
+    collectionAddress: Address;
+    itemIndex: Int as uint64;
+}
+
+message(0xd0c3bfea) RequestOwner {
+    queryId: Int as uint64;
+    destination: Address;
+    body: Cell;
+    withContent: Bool;
+}
+
+message(0x0dd607e3) RequestOwnerOut {
+    queryId: Int as uint64;
+    index: Int as uint256;
+    senderAddress: Address;
+    ownerAddress: Address;
+    body: Cell;
+    revokedAt: Int as uint64;
+    content: Cell?;
+}
+
+message(0x04ded148) ProveOwnership {
+    queryId: Int as uint64;
+    destination: Address;
+    body: Cell;
+    withContent: Bool;
+}
+
+message(0x0524c7ae) ProveOwnershipOut {
+    queryId: Int as uint64;
+    index: Int as uint256;
+    ownerAddress: Address;
+    body: Cell;
+    revokedAt: Int as uint64;
+    content: Cell?;
+}
+
+message(0xc18e86d2) ProveOwnershipBounced {
+    queryId: Int as uint64;
+}
+
+message(0x2fcb26a2) GetStaticData {
+    queryId: Int as uint64;
+}
+
+message(0x8b771735) ReportStaticData {
+    queryId: Int as uint64;
+    index: Int as uint256;
+    collectionAddress: Address;
+}
+
+message(0xd136d3b3) TakeExcess {
+    queryId: Int as uint64;
+}
+
+message(0xd53276db) ExcessOut {
+    queryId: Int as uint64;
+}
+
+message(0x1f04537a) Destroy {
+    queryId: Int as uint64;
+}
+
+message(0x6f89f5e3) Revoke {
+    queryId: Int as uint64;
+}
diff --git a/src/benchmarks/contracts/sbt/sbt-item.tact b/src/benchmarks/contracts/sbt/sbt-item.tact
new file mode 100644
index 00000000..604cbd54
--- /dev/null
+++ b/src/benchmarks/contracts/sbt/sbt-item.tact
@@ -0,0 +1,180 @@
+import "./constants";
+import "./messages-sbt";
+
+contract SBTItem {
+    itemIndex: Int as uint64;
+    collectionAddress: Address;
+
+    // state init data don't have this parameters
+    owner: Address? = null;
+    content: Cell? = null;
+    authorityAddress: Address? = null;
+    revokedAt: Int = 0;
+
+    init(initNFTData: InitNFTData) {
+        self.itemIndex = initNFTData.itemIndex;
+        self.collectionAddress = initNFTData.collectionAddress;
+    }
+
+    receive() {} //to ignore empty messages 
+
+    receive(msg: Slice) {
+        // Check if owner == null, which means the contract hasn't been initialized yet
+        // owner is used as an initialization flag: if null - contract is not initialized,
+        // if not null - contract has been initialized before
+        // This prevents contract re-initialization since initialization should happen only once
+        // We use similar checks in other methods (e.g., Transfer, GetStaticData) with throwUnless(NotInit, self.owner != null)
+        // to ensure the contract is properly initialized before executing any operations
+        throwUnless(InvalidData, self.owner == null);
+        throwUnless(IncorrectDeployer, sender() == self.collectionAddress);
+
+        self.owner = msg.loadAddress();
+        self.content = msg.loadRef();
+        self.authorityAddress = msg.loadAddress();
+        self.revokedAt = msg.loadUint(64);
+    }
+
+    receive(msg: RequestOwner) {
+        throwUnless(NotInit, self.owner != null);
+
+        let content: Cell? = msg.withContent ? self.content!! : null;
+
+        let msgBody = RequestOwnerOut {
+            queryId: msg.queryId,
+            index: self.itemIndex,
+            senderAddress: sender(),
+            ownerAddress: self.owner!!,
+            body: msg.body,
+            revokedAt: self.revokedAt,
+            content: content,
+        };
+
+        message(MessageParameters {
+            to: msg.destination,
+            value: 0,
+            mode: SendRemainingValue,
+            bounce: true,
+            body: msgBody.toCell(),
+        });
+    }
+
+    receive(msg: ProveOwnership) {
+        throwUnless(NotInit, null != self.owner);
+        throwUnless(IncorrectSender, self.owner == sender());
+
+        let content: Cell? = msg.withContent ? self.content!! : null;
+
+        let msgBody = ProveOwnershipOut {
+            queryId: msg.queryId,
+            index: self.itemIndex,
+            ownerAddress: self.owner!!,
+            body: msg.body,
+            revokedAt: self.revokedAt,
+            content: content,
+        };
+
+        message(MessageParameters {
+            to: msg.destination,
+            value: 0,
+            mode: SendRemainingValue,
+            bounce: true,
+            body: msgBody.toCell(),
+        });
+    }
+
+    bounced(msg: bounced<ProveOwnershipOut>) {
+        message(MessageParameters {
+            to: self.owner!!,
+            value: 0,
+            mode: SendRemainingValue,
+            bounce: false,
+            body: ProveOwnershipBounced {
+                queryId: msg.queryId,
+            }.toCell(),
+        });
+    }
+
+    receive(msg: GetStaticData) {
+        throwUnless(NotInit, self.owner != null);
+
+        let msgBody = ReportStaticData {
+            queryId: msg.queryId,
+            index: self.itemIndex,
+            collectionAddress: self.collectionAddress,
+        };
+
+        message(MessageParameters {
+            bounce: false,
+            to: sender(),
+            value: 0,
+            body: msgBody.toCell(),
+            mode: SendRemainingValue,
+        });
+    }
+
+    receive(msg: TakeExcess) {
+        throwUnless(NotInit, self.owner != null);
+        throwUnless(IncorrectSender, self.owner == sender());
+
+        nativeReserve(minTonsForStorage, ReserveExact);
+
+        let msgBody = ExcessOut { queryId: msg.queryId };
+
+        message(MessageParameters {
+            bounce: false,
+            to: sender(),
+            value: 0,
+            body: msgBody.toCell(),
+            mode: SendRemainingBalance,
+        });
+    }
+
+    receive(msg: Destroy) {
+        throwUnless(NotInit, self.owner != null);
+        throwUnless(IncorrectSender, self.owner == sender());
+
+        let msgBody = ExcessOut { queryId: msg.queryId };
+
+        message(MessageParameters {
+            bounce: false,
+            to: sender(),
+            value: 0,
+            body: msgBody.toCell(),
+            mode: SendRemainingBalance,
+        });
+
+        self.owner = null;
+        self.authorityAddress = null;
+    }
+
+    receive(msg: Revoke) {
+        throwUnless(NotInit, self.owner != null);
+        throwUnless(IncorrectSender, self.authorityAddress == sender());
+        throwUnless(AlreadyRevoked, self.revokedAt == 0);
+
+        self.revokedAt = now();
+    }
+
+    receive(msg: Transfer) {
+        throwUnless(NotInit, self.owner != null);
+        throw(InvalidMethod);
+    }
+
+    get fun get_nft_data(): NFTData {
+        return NFTData {
+            init: self.owner != null ? -1 : 0, // -1 is true
+            itemIndex: self.itemIndex,
+            collectionAddress: self.collectionAddress,
+            owner: self.owner,
+            content: self.content,
+        };
+    }
+
+    get fun get_authority_address(): Address? {
+        return self.authorityAddress;
+    }
+
+    get fun get_revoked_time(): Int {
+        return self.revokedAt;
+    }
+}
diff --git a/src/benchmarks/jetton/results_code_size.json b/src/benchmarks/jetton/results_code_size.json
index 9dd96062..45d1570a 100644
--- a/src/benchmarks/jetton/results_code_size.json
+++ b/src/benchmarks/jetton/results_code_size.json
@@ -109,6 +109,16 @@
         "wallet cells": "16",
         "wallet bits": "8211"
       }
+    },
+    {
+      "label": "1.6.5 with reordered wallet fields",
+      "pr": "https://github.com/tact-lang/tact/pull/2699",
+      "size": {
+        "minter cells": "30",
+        "minter bits": "14688",
+        "wallet cells": "16",
+        "wallet bits": "8211"
+      }
     }
   ]
 }
diff --git a/src/benchmarks/jetton/results_gas.json b/src/benchmarks/jetton/results_gas.json
index 09320fe8..01c0d2aa 100644
--- a/src/benchmarks/jetton/results_gas.json
+++ b/src/benchmarks/jetton/results_gas.json
@@ -341,6 +341,15 @@
         "burn": "11830",
         "discovery": "6161"
       }
+    },
+    {
+      "label": "1.6.5 with reordered wallet fields",
+      "pr": "https://github.com/tact-lang/tact/pull/2699",
+      "gas": {
+        "transfer": "15143",
+        "burn": "11812",
+        "discovery": "6161"
+      }
     }
   ]
 }
diff --git a/src/benchmarks/sbt/results_code_size.json b/src/benchmarks/sbt/results_code_size.json
new file mode 100644
index 00000000..3137a2b0
--- /dev/null
+++ b/src/benchmarks/sbt/results_code_size.json
@@ -0,0 +1,20 @@
+{
+  "results": [
+    {
+      "label": "FunC",
+      "size": {
+        "item cells": "21",
+        "item bits": "7055"
+      },
+      "pr": "https://github.com/getgems-io/nft-contracts/blob/24ae5e9c7a6c78b592c01be32fee2edb12424be8/packages/contracts/sources/sbt-item.fc"
+    },
+    {
+      "label": "Tact",
+      "pr": "https://github.com/tact-lang/tact/pull/2560",
+      "size": {
+        "item cells": "22",
+        "item bits": "12899"
+      }
+    }
+  ]
+}
diff --git a/src/benchmarks/sbt/results_gas.json b/src/benchmarks/sbt/results_gas.json
new file mode 100644
index 00000000..8307916f
--- /dev/null
+++ b/src/benchmarks/sbt/results_gas.json
@@ -0,0 +1,30 @@
+{
+  "results": [
+    {
+      "label": "FunC",
+      "pr": "https://github.com/getgems-io/nft-contracts/blob/24ae5e9c7a6c78b592c01be32fee2edb12424be8/packages/contracts/sources/sbt-item.fc",
+      "gas": {
+        "deploy": "3204",
+        "request owner": "5548",
+        "prove ownership": "5667",
+        "get static data": "5291",
+        "destroy": "6685",
+        "revoke": "4203",
+        "take excess": "6211"
+      }
+    },
+    {
+      "label": "Tact",
+      "pr": "https://github.com/tact-lang/tact/pull/2560",
+      "gas": {
+        "deploy": "4877",
+        "request owner": "7338",
+        "prove ownership": "7636",
+        "get static data": "7303",
+        "destroy": "7796",
+        "revoke": "5253",
+        "take excess": "8132"
+      }
+    }
+  ]
+}
diff --git a/src/benchmarks/sbt/sbt.spec.ts b/src/benchmarks/sbt/sbt.spec.ts
new file mode 100644
index 00000000..0a8fcdc4
--- /dev/null
+++ b/src/benchmarks/sbt/sbt.spec.ts
@@ -0,0 +1,561 @@
+import "@ton/test-utils";
+import { Cell, beginCell, toNano, contractAddress, SendMode } from "@ton/core";
+
+import type { Sender } from "@ton/core";
+import { Blockchain, type BlockchainSnapshot } from "@ton/sandbox";
+import type { SandboxContract, TreasuryContract } from "@ton/sandbox";
+import {
+    generateResults,
+    getStateSizeForAccount,
+    generateCodeSizeResults,
+    getUsedGas,
+    printBenchmarkTable,
+} from "@/benchmarks/utils/gas";
+import { join, resolve } from "path";
+import { readFileSync } from "fs";
+import { posixNormalize } from "@/utils/filePath";
+import { type Step, writeLog } from "@/test/utils/write-vm-log";
+import {
+    SBTItem,
+    storeReportStaticData,
+    storeRequestOwnerOut,
+    storeProveOwnershipOut,
+    storeExcessOut,
+} from "@/benchmarks/contracts/sbt/output/sbt-item_SBTItem";
+
+import type {
+    ReportStaticData,
+    RequestOwner,
+    ProveOwnership,
+    ProveOwnershipOut,
+    GetStaticData,
+    Destroy,
+    Revoke,
+    TakeExcess,
+    RequestOwnerOut,
+    ExcessOut,
+} from "@/benchmarks/contracts/sbt/output/sbt-item_SBTItem";
+
+import benchmarkResults from "@/benchmarks/sbt/results_gas.json";
+import benchmarkCodeSizeResults from "@/benchmarks/sbt/results_code_size.json";
+
+const loadFunCSBTBoc = () => {
+    const bocItem = readFileSync(
+        posixNormalize(
+            resolve(__dirname, "../contracts/func/output/sbt-item.boc"),
+        ),
+    );
+
+    return { bocItem };
+};
+
+describe("itemSBT", () => {
+    let blockchain: Blockchain;
+
+    let owner: SandboxContract<TreasuryContract>;
+
+    let snapshot: BlockchainSnapshot;
+
+    let itemSBT: SandboxContract<SBTItem>;
+    let funcItemSBT: SandboxContract<SBTItem>;
+
+    let step: Step;
+    const results = generateResults(benchmarkResults);
+    const codeSizeResults = generateCodeSizeResults(benchmarkCodeSizeResults);
+    const expectedCodeSize = codeSizeResults.at(-1)!;
+    const funcCodeSize = codeSizeResults.at(0)!;
+
+    const expectedResult = results.at(-1)!;
+    const funcResult = results.at(0)!;
+
+    let funcInit: { code: Cell; data: Cell };
+
+    const sendDeploy = async (
+        via: SandboxContract<TreasuryContract>,
+        itemSBT: SandboxContract<SBTItem>,
+        init: { code: Cell; data: Cell } | null,
+    ) => {
+        return await via.send({
+            to: itemSBT.address,
+            value: toNano("1"),
+            init,
+            body: beginCell()
+                .storeAddress(via.address) // owner
+                .storeRef(beginCell().endCell()) // content
+                .storeAddress(via.address) // authority
+                .storeUint(0, 64) // revoke
+                .endCell(),
+            sendMode: SendMode.PAY_GAS_SEPARATELY,
+        });
+    };
+
+    const deployFuncSBTItem = async (
+        blockchain: Blockchain,
+        via: SandboxContract<TreasuryContract>,
+        index: bigint = 0n,
+    ) => {
+        const sbtData = loadFunCSBTBoc();
+        const itemCode = Cell.fromBoc(sbtData.bocItem)[0]!;
+
+        const initData = beginCell()
+            .storeUint(index, 64) // itemIndex
+            .storeAddress(via.address) // collectionAddress
+            .endCell();
+
+        const init = { code: itemCode, data: initData };
+
+        const itemAddress = contractAddress(0, init);
+        const itemSBTScope: SandboxContract<SBTItem> = blockchain.openContract(
+            await SBTItem.fromAddress(itemAddress),
+        );
+
+        return { itemSBTScope, init };
+    };
+
+    beforeAll(async () => {
+        blockchain = await Blockchain.create();
+        owner = await blockchain.treasury("owner");
+
+        step = writeLog({
+            path: join(__dirname, "output", "log.yaml"),
+            blockchain,
+        });
+
+        // ITEM
+        {
+            itemSBT = blockchain.openContract(
+                await SBTItem.fromInit({
+                    $$type: "InitNFTData",
+                    itemIndex: 0n,
+                    collectionAddress: owner.address,
+                }),
+            );
+
+            const deployResult = await sendDeploy(
+                owner,
+                itemSBT,
+                itemSBT.init!,
+            );
+
+            expect(deployResult.transactions).toHaveTransaction({
+                from: owner.address,
+                to: itemSBT.address,
+                deploy: true,
+                success: true,
+            });
+
+            expect((await itemSBT.getGetNftData()).owner).toEqualAddress(
+                owner.address,
+            );
+            // deploy func
+            const funcResult = await deployFuncSBTItem(blockchain, owner);
+            funcItemSBT = funcResult.itemSBTScope;
+            funcInit = funcResult.init;
+
+            const deployFuncItem = await sendDeploy(
+                owner,
+                funcItemSBT,
+                funcInit,
+            );
+            expect(deployFuncItem.transactions).toHaveTransaction({
+                from: owner.address,
+                to: funcItemSBT.address,
+                success: true,
+                deploy: true,
+            });
+
+            expect(await funcItemSBT.getGetAuthorityAddress()).toEqualAddress(
+                owner.address,
+            );
+        }
+        snapshot = blockchain.snapshot();
+    });
+
+    beforeEach(async () => {
+        await blockchain.loadFrom(snapshot);
+    });
+
+    afterAll(() => {
+        printBenchmarkTable(results, codeSizeResults, {
+            implementationName: "FunC",
+            printMode: "full",
+        });
+    });
+
+    it("deploy", async () => {
+        const runDeployTactTest = async () => {
+            const newItemSBT = blockchain.openContract(
+                await SBTItem.fromInit({
+                    $$type: "InitNFTData",
+                    itemIndex: 1n,
+                    collectionAddress: owner.address,
+                }),
+            );
+            const sendResult = await step("request owner", async () =>
+                sendDeploy(owner, newItemSBT, newItemSBT.init!),
+            );
+
+            expect(sendResult.transactions).toHaveTransaction({
+                deploy: true,
+            });
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const runDeployFuncTest = async () => {
+            const funcResult = await deployFuncSBTItem(blockchain, owner, 1n);
+            const newFuncItemSBT = funcResult.itemSBTScope;
+            const newFuncInit = funcResult.init;
+
+            const sendResult = await step("deploy", async () =>
+                sendDeploy(owner, newFuncItemSBT, newFuncInit),
+            );
+
+            expect(sendResult.transactions).toHaveTransaction({
+                deploy: true,
+            });
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const deployGasUsedTact = await runDeployTactTest();
+        const deployGasUsedFunC = await runDeployFuncTest();
+
+        expect(deployGasUsedTact).toEqual(expectedResult.gas["deploy"]);
+        expect(deployGasUsedFunC).toEqual(funcResult.gas["deploy"]);
+    });
+
+    it("request owner", async () => {
+        const sendRequestOwner = async (
+            itemSBT: SandboxContract<SBTItem>,
+            from: Sender,
+            value: bigint,
+        ) => {
+            const msg: RequestOwner = {
+                $$type: "RequestOwner",
+                queryId: 0n,
+                destination: owner.address,
+                body: beginCell().endCell(),
+                withContent: true,
+            };
+
+            return await itemSBT.send(from, { value }, msg);
+        };
+
+        const runRequestOwnerTest = async (
+            scopeItemSBT: SandboxContract<SBTItem>,
+        ) => {
+            const sendResult = await step("request owner", async () =>
+                sendRequestOwner(scopeItemSBT, owner.getSender(), toNano(1)),
+            );
+
+            const expectedBody: RequestOwnerOut = {
+                $$type: "RequestOwnerOut",
+                queryId: 0n,
+                index: 0n,
+                senderAddress: owner.address,
+                ownerAddress: owner.address,
+                body: beginCell().endCell(),
+                revokedAt: 0n,
+                content: beginCell().endCell(),
+            };
+
+            expect(sendResult.transactions).toHaveTransaction({
+                from: scopeItemSBT.address,
+                to: owner.address,
+                body: beginCell()
+                    .store(storeRequestOwnerOut(expectedBody))
+                    .endCell(),
+                inMessageBounceable: true,
+            });
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const requestOwnerGasUsedTact = await runRequestOwnerTest(itemSBT);
+        const requestOwnerGasUsedFunC = await runRequestOwnerTest(funcItemSBT);
+
+        expect(requestOwnerGasUsedTact).toEqual(
+            expectedResult.gas["request owner"],
+        );
+        expect(requestOwnerGasUsedFunC).toEqual(
+            funcResult.gas["request owner"],
+        );
+    });
+
+    it("prove ownership", async () => {
+        const sendProveOwnership = async (
+            itemSBT: SandboxContract<SBTItem>,
+            from: Sender,
+            value: bigint,
+        ) => {
+            const msg: ProveOwnership = {
+                $$type: "ProveOwnership",
+                queryId: 0n,
+                destination: owner.address,
+                body: beginCell().endCell(),
+                withContent: true,
+            };
+
+            return await itemSBT.send(from, { value }, msg);
+        };
+
+        const runProveOwnershipTest = async (
+            scopeItemSBT: SandboxContract<SBTItem>,
+        ) => {
+            const sendResult = await step("prove ownership", async () =>
+                sendProveOwnership(scopeItemSBT, owner.getSender(), toNano(1)),
+            );
+
+            const expectedBody: ProveOwnershipOut = {
+                $$type: "ProveOwnershipOut",
+                queryId: 0n,
+                index: 0n,
+                ownerAddress: owner.address,
+                body: beginCell().endCell(),
+                revokedAt: 0n,
+                content: beginCell().endCell(),
+            };
+
+            expect(sendResult.transactions).toHaveTransaction({
+                from: scopeItemSBT.address,
+                to: owner.address,
+                body: beginCell()
+                    .store(storeProveOwnershipOut(expectedBody))
+                    .endCell(),
+                inMessageBounceable: true,
+            });
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const proveOwnershipGasUsedTact = await runProveOwnershipTest(itemSBT);
+        const proveOwnershipGasUsedFunC =
+            await runProveOwnershipTest(funcItemSBT);
+
+        expect(proveOwnershipGasUsedTact).toEqual(
+            expectedResult.gas["prove ownership"],
+        );
+        expect(proveOwnershipGasUsedFunC).toEqual(
+            funcResult.gas["prove ownership"],
+        );
+    });
+
+    it("get static data", async () => {
+        const sendGetStaticData = async (
+            itemSBT: SandboxContract<SBTItem>,
+            from: Sender,
+            value: bigint,
+        ) => {
+            const msg: GetStaticData = {
+                $$type: "GetStaticData",
+                queryId: 0n,
+            };
+
+            return await itemSBT.send(from, { value }, msg);
+        };
+
+        const runGetStaticTest = async (
+            scopeItemSBT: SandboxContract<SBTItem>,
+        ) => {
+            const sendResult = await step("get static data", async () =>
+                sendGetStaticData(scopeItemSBT, owner.getSender(), toNano(1)),
+            );
+
+            const expectedBody: ReportStaticData = {
+                $$type: "ReportStaticData",
+                queryId: 0n,
+                index: 0n,
+                collectionAddress: owner.address,
+            };
+
+            expect(sendResult.transactions).toHaveTransaction({
+                from: scopeItemSBT.address,
+                to: owner.address,
+                body: beginCell()
+                    .store(storeReportStaticData(expectedBody))
+                    .endCell(),
+                inMessageBounceable: false,
+            });
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const getStaticGasUsedTact = await runGetStaticTest(itemSBT);
+        const getStaticGasUsedFunC = await runGetStaticTest(funcItemSBT);
+
+        expect(getStaticGasUsedTact).toEqual(
+            expectedResult.gas["get static data"],
+        );
+        expect(getStaticGasUsedFunC).toEqual(funcResult.gas["get static data"]);
+    });
+
+    it("take excess", async () => {
+        const sendTakeExcess = async (
+            itemSBT: SandboxContract<SBTItem>,
+            from: Sender,
+            value: bigint,
+        ) => {
+            const msg: TakeExcess = {
+                $$type: "TakeExcess",
+                queryId: 0n,
+            };
+
+            return await itemSBT.send(from, { value }, msg);
+        };
+
+        const runTakeExcessTest = async (
+            scopeItemSBT: SandboxContract<SBTItem>,
+        ) => {
+            const sendResult = await step("take excess", async () =>
+                sendTakeExcess(scopeItemSBT, owner.getSender(), toNano(1)),
+            );
+
+            const expectedBody: ExcessOut = {
+                $$type: "ExcessOut",
+                queryId: 0n,
+            };
+
+            expect(sendResult.transactions).toHaveTransaction({
+                from: scopeItemSBT.address,
+                to: owner.address,
+                body: beginCell().store(storeExcessOut(expectedBody)).endCell(),
+                inMessageBounceable: false,
+            });
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const takeExcessGasUsedTact = await runTakeExcessTest(itemSBT);
+        const takeExcessGasUsedFunC = await runTakeExcessTest(funcItemSBT);
+
+        expect(takeExcessGasUsedTact).toEqual(
+            expectedResult.gas["take excess"],
+        );
+        expect(takeExcessGasUsedFunC).toEqual(funcResult.gas["take excess"]);
+    });
+
+    it("destroy", async () => {
+        const sendDestroy = async (
+            itemSBT: SandboxContract<SBTItem>,
+            from: Sender,
+            value: bigint,
+        ) => {
+            const msg: Destroy = {
+                $$type: "Destroy",
+                queryId: 0n,
+            };
+
+            return await itemSBT.send(from, { value }, msg);
+        };
+
+        const runDestroyTest = async (
+            scopeItemSBT: SandboxContract<SBTItem>,
+        ) => {
+            const sendResult = await step("destroy", async () =>
+                sendDestroy(scopeItemSBT, owner.getSender(), toNano(1)),
+            );
+
+            const expectedBody: ExcessOut = {
+                $$type: "ExcessOut",
+                queryId: 0n,
+            };
+
+            expect(sendResult.transactions).toHaveTransaction({
+                from: scopeItemSBT.address,
+                to: owner.address,
+                body: beginCell().store(storeExcessOut(expectedBody)).endCell(),
+                inMessageBounceable: false,
+            });
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const destroyGasUsedTact = await runDestroyTest(itemSBT);
+        const destroyGasUsedFunC = await runDestroyTest(funcItemSBT);
+
+        expect(destroyGasUsedTact).toEqual(expectedResult.gas["destroy"]);
+        expect(destroyGasUsedFunC).toEqual(funcResult.gas["destroy"]);
+    });
+
+    it("revoke", async () => {
+        const sendRevoke = async (
+            itemSBT: SandboxContract<SBTItem>,
+            from: Sender,
+            value: bigint,
+        ) => {
+            const msg: Revoke = {
+                $$type: "Revoke",
+                queryId: 0n,
+            };
+
+            return await itemSBT.send(from, { value }, msg);
+        };
+
+        const runRevokeTest = async (
+            scopeItemSBT: SandboxContract<SBTItem>,
+        ) => {
+            const sendResult = await step("revoke", async () =>
+                sendRevoke(scopeItemSBT, owner.getSender(), toNano(1)),
+            );
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                from: scopeItemSBT.address,
+            });
+
+            expect(sendResult.transactions).not.toHaveTransaction({
+                success: false,
+            });
+
+            return getUsedGas(sendResult, "internal");
+        };
+
+        const revokeGasUsedTact = await runRevokeTest(itemSBT);
+        const revokeGasUsedFunC = await runRevokeTest(funcItemSBT);
+
+        expect(revokeGasUsedTact).toEqual(expectedResult.gas["revoke"]);
+        expect(revokeGasUsedFunC).toEqual(funcResult.gas["revoke"]);
+    });
+
+    it("item cells", async () => {
+        expect(
+            (await getStateSizeForAccount(blockchain, itemSBT.address)).cells,
+        ).toEqual(expectedCodeSize.size["item cells"]);
+        expect(
+            (await getStateSizeForAccount(blockchain, funcItemSBT.address))
+                .cells,
+        ).toEqual(funcCodeSize.size["item cells"]);
+    });
+
+    it("item bits", async () => {
+        expect(
+            (await getStateSizeForAccount(blockchain, itemSBT.address)).bits,
+        ).toEqual(expectedCodeSize.size["item bits"]);
+        expect(
+            (await getStateSizeForAccount(blockchain, funcItemSBT.address))
+                .bits,
+        ).toEqual(funcCodeSize.size["item bits"]);
+    });
+});
diff --git a/src/bindings/typescript/writeStruct.ts b/src/bindings/typescript/writeStruct.ts
index 7d66e1b1..c5c0d003 100644
--- a/src/bindings/typescript/writeStruct.ts
+++ b/src/bindings/typescript/writeStruct.ts
@@ -149,7 +149,7 @@ function writeSerializerField(gen: number, s: AllocationOperation, w: Writer) {
 }
 
 export function writeTupleParser(s: ABIType, w: Writer) {
-    w.append(`function loadTuple${s.name}(source: TupleReader) {`);
+    w.append(`export function loadTuple${s.name}(source: TupleReader) {`);
     w.inIndent(() => {
         if (s.fields.length <= maxTupleSize) {
             for (const f of s.fields) {
@@ -177,7 +177,7 @@ export function writeTupleParser(s: ABIType, w: Writer) {
 }
 
 export function writeGetterTupleParser(s: ABIType, w: Writer) {
-    w.append(`function loadGetterTuple${s.name}(source: TupleReader) {`);
+    w.append(`export function loadGetterTuple${s.name}(source: TupleReader) {`);
     w.inIndent(() => {
         for (const f of s.fields) {
             writeTupleFieldParser("_" + f.name, f.type, w, true);
@@ -211,7 +211,7 @@ function writeTupleFieldParser(
 }
 
 export function writeTupleSerializer(s: ABIType, w: Writer) {
-    w.append(`function storeTuple${s.name}(source: ${s.name}) {`);
+    w.append(`export function storeTuple${s.name}(source: ${s.name}) {`);
     w.inIndent(() => {
         w.append(`const builder = new TupleBuilder();`);
         for (const f of s.fields) {
diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts
index b69acf1f..536e2b0c 100644
--- a/src/generator/writers/writeFunction.ts
+++ b/src/generator/writers/writeFunction.ts
@@ -617,9 +617,12 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) {
             resolveFuncType(self, ctx, isSelfOpt) + " " + funcIdOf("self"),
         );
     }
-    for (const a of f.params) {
-        params.push(resolveFuncType(a.type, ctx) + " " + funcIdOf(a.name));
-    }
+
+    f.params.forEach((a, index) => {
+        const name =
+            a.name.kind === "wildcard" ? `_${index}` : funcIdOf(a.name);
+        params.push(resolveFuncType(a.type, ctx) + " " + name);
+    });
 
     const fAst = f.ast;
     switch (fAst.kind) {
@@ -706,7 +709,8 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) {
                             !resolveFuncPrimitive(
                                 resolveTypeRef(ctx.ctx, a.type),
                                 ctx,
-                            )
+                            ) &&
+                            a.name.kind !== "wildcard"
                         ) {
                             ctx.append(
                                 `var (${resolveFuncTypeUnpack(resolveTypeRef(ctx.ctx, a.type), funcIdOf(a.name), ctx)}) = ${funcIdOf(a.name)};`,
diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap
index bc697602..de11b084 100644
--- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap
+++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap
@@ -261,6 +261,15 @@ exports[`resolveDescriptors should fail descriptors for contract-duplicates-in-t
 "
 `;
 
+exports[`resolveDescriptors should fail descriptors for contract-field-with-trait-type 1`] = `
+"<unknown>:6:5: Fields with a trait type are not supported
+  5 | contract TestContract {
+> 6 |     data: Test? = null;
+          ^~~~~~~~~~~~~~~~~~
+  7 | }
+"
+`;
+
 exports[`resolveDescriptors should fail descriptors for contract-getter-override-virtual-no-keyword 1`] = `
 "<unknown>:9:3: Function "getter" is defined as virtual in trait "T": you are probably missing "override" keyword
    8 | contract TestContract with T {
@@ -446,6 +455,24 @@ exports[`resolveDescriptors should fail descriptors for message-decl-remainder-i
 "
 `;
 
+exports[`resolveDescriptors should fail descriptors for message-decl-remaining-field-with-optional-type 1`] = `
+"<unknown>:8:8: The "as remaining" field cannot have optional type
+  7 |     a: Int;
+> 8 |     s: Cell? as remaining;
+             ^~~~~
+  9 | }
+"
+`;
+
+exports[`resolveDescriptors should fail descriptors for message-decl-remaining-field-with-optional-type-2 1`] = `
+"<unknown>:7:5: The "as remaining" field can only be the last field of the message
+  6 | message Test {
+> 7 |     s: Cell? as remaining;
+          ^~~~~~~~~~~~~~~~~~~~~
+  8 |     a: Int;
+"
+`;
+
 exports[`resolveDescriptors should fail descriptors for message-field-with-contract-type 1`] = `
 "<unknown>:6:5: Fields with a contract type are not supported yet
   5 | message TakeEscrowData {
@@ -464,6 +491,24 @@ exports[`resolveDescriptors should fail descriptors for message-field-with-optio
 "
 `;
 
+exports[`resolveDescriptors should fail descriptors for message-field-with-optional-trait-type 1`] = `
+"<unknown>:6:5: Fields with a trait type are not supported
+  5 | message TakeData {
+> 6 |     data: Test?;
+          ^~~~~~~~~~~
+  7 | }
+"
+`;
+
+exports[`resolveDescriptors should fail descriptors for message-field-with-trait-type 1`] = `
+"<unknown>:6:5: Fields with a trait type are not supported
+  5 | message TakeData {
+> 6 |     data: Test;
+          ^~~~~~~~~~
+  7 | }
+"
+`;
+
 exports[`resolveDescriptors should fail descriptors for message-negative-opcode-1 1`] = `
 "<unknown>:1:9: Opcode of message "Foo" is negative ('-1') which is not allowed
 > 1 | message(-1) Foo { }
@@ -933,6 +978,15 @@ exports[`resolveDescriptors should fail descriptors for struct-field-with-contra
 "
 `;
 
+exports[`resolveDescriptors should fail descriptors for struct-field-with-trait-type 1`] = `
+"<unknown>:6:5: Fields with a trait type are not supported
+  5 | struct TakeData {
+> 6 |     data: Test;
+          ^~~~~~~~~~
+  7 | }
+"
+`;
+
 exports[`resolveDescriptors should fail descriptors for ton-with-blank-string 1`] = `
 "<unknown>:3:22: ton() function requires a non-empty value
   2 | 
diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts
index 2bbf36c8..cbd9ccbb 100644
--- a/src/types/resolveSignatures.ts
+++ b/src/types/resolveSignatures.ts
@@ -199,6 +199,12 @@ export function resolveSignatures(ctx: CompilerContext, Ast: FactoryAst) {
                     field.loc,
                 );
             }
+            if (t.kind === "trait") {
+                throwCompilationError(
+                    `Fields with a trait type are not supported`,
+                    field.loc,
+                );
+            }
         }
 
         // Check for no "as remaining" in the middle of the struct or contract
@@ -217,6 +223,18 @@ export function resolveSignatures(ctx: CompilerContext, Ast: FactoryAst) {
             }
         }
 
+        for (const field of t.fields) {
+            if (field.as === "remaining") {
+                const type = field.type;
+                if (type.kind === "ref" && type.optional) {
+                    throwCompilationError(
+                        `The "as remaining" field cannot have optional type`,
+                        field.ast.type.loc,
+                    );
+                }
+            }
+        }
+
         const fields = t.fields.map((v) => createTLBField(v.abi));
 
         // Calculate signature and method id
diff --git a/src/types/test-failed/contract-field-with-trait-type.tact b/src/types/test-failed/contract-field-with-trait-type.tact
new file mode 100644
index 00000000..cca80fe1
--- /dev/null
+++ b/src/types/test-failed/contract-field-with-trait-type.tact
@@ -0,0 +1,7 @@
+trait BaseTrait {}
+
+trait Test {}
+
+contract TestContract {
+    data: Test? = null;
+}
diff --git a/src/types/test-failed/message-decl-remaining-field-with-optional-type-2.tact b/src/types/test-failed/message-decl-remaining-field-with-optional-type-2.tact
new file mode 100644
index 00000000..5bee44cc
--- /dev/null
+++ b/src/types/test-failed/message-decl-remaining-field-with-optional-type-2.tact
@@ -0,0 +1,9 @@
+trait BaseTrait {}
+
+primitive Int;
+primitive Cell;
+
+message Test {
+    s: Cell? as remaining;
+    a: Int;
+}
diff --git a/src/types/test-failed/message-decl-remaining-field-with-optional-type.tact b/src/types/test-failed/message-decl-remaining-field-with-optional-type.tact
new file mode 100644
index 00000000..afdcf8e5
--- /dev/null
+++ b/src/types/test-failed/message-decl-remaining-field-with-optional-type.tact
@@ -0,0 +1,9 @@
+trait BaseTrait {}
+
+primitive Int;
+primitive Cell;
+
+message Test {
+    a: Int;
+    s: Cell? as remaining;
+}
diff --git a/src/types/test-failed/message-field-with-optional-trait-type.tact b/src/types/test-failed/message-field-with-optional-trait-type.tact
new file mode 100644
index 00000000..c330dffc
--- /dev/null
+++ b/src/types/test-failed/message-field-with-optional-trait-type.tact
@@ -0,0 +1,7 @@
+trait BaseTrait {}
+
+trait Test {}
+
+message TakeData {
+    data: Test?;
+}
diff --git a/src/types/test-failed/message-field-with-trait-type.tact b/src/types/test-failed/message-field-with-trait-type.tact
new file mode 100644
index 00000000..3eefd1c1
--- /dev/null
+++ b/src/types/test-failed/message-field-with-trait-type.tact
@@ -0,0 +1,7 @@
+trait BaseTrait {}
+
+trait Test {}
+
+message TakeData {
+    data: Test;
+}
diff --git a/src/types/test-failed/struct-field-with-trait-type.tact b/src/types/test-failed/struct-field-with-trait-type.tact
new file mode 100644
index 00000000..85a835c2
--- /dev/null
+++ b/src/types/test-failed/struct-field-with-trait-type.tact
@@ -0,0 +1,7 @@
+trait BaseTrait {}
+
+trait Test {}
+
+struct TakeData {
+    data: Test;
+}



Test patch:
diff --git a/src/test/e2e-emulated/functions/wildcard-parameters.spec.ts b/src/test/e2e-emulated/functions/wildcard-parameters.spec.ts
new file mode 100644
index 00000000..c23f033b
--- /dev/null
+++ b/src/test/e2e-emulated/functions/wildcard-parameters.spec.ts
@@ -0,0 +1,53 @@
+import { toNano } from "@ton/core";
+import type { SandboxContract, TreasuryContract } from "@ton/sandbox";
+import { Blockchain } from "@ton/sandbox";
+import { Test } from "./output/wildcard-parameters_Test";
+import "@ton/test-utils";
+
+describe("wildcard-parameters", () => {
+    let blockchain: Blockchain;
+    let treasure: SandboxContract<TreasuryContract>;
+    let contract: SandboxContract<Test>;
+
+    beforeEach(async () => {
+        blockchain = await Blockchain.create();
+        blockchain.verbosity.print = false;
+        treasure = await blockchain.treasury("treasure");
+
+        contract = blockchain.openContract(await Test.fromInit());
+
+        const deployResult = await contract.send(
+            treasure.getSender(),
+            { value: toNano("10") },
+            null,
+        );
+        expect(deployResult.transactions).toHaveTransaction({
+            from: treasure.address,
+            to: contract.address,
+            success: true,
+            deploy: true,
+        });
+    });
+
+    it("should run get methods correctly", async () => {
+        expect(Number(await contract.getContractAdd())).toEqual(0);
+        expect(Number(await contract.getGlobalAdd())).toEqual(0);
+        expect(Number(await contract.getGlobalAsmAdd())).toEqual(3);
+
+        expect(Number(await contract.getGlobalAddThreeWildcards())).toEqual(0);
+        expect(Number(await contract.getGlobalAddMixedParams())).toEqual(3);
+        expect(Number(await contract.getGlobalAddMixedParams2())).toEqual(3);
+        expect(Number(await contract.getGlobalAddMixedParams3())).toEqual(3);
+        expect(Number(await contract.getGlobalAsmAddThreeWildcards())).toEqual(
+            3,
+        );
+        expect(Number(await contract.getGlobalAsmAddMixedParams())).toEqual(3);
+        expect(Number(await contract.getGlobalAsmAddMixedParams2())).toEqual(3);
+
+        expect(Number(await contract.getContractAddThreeWildcards())).toEqual(
+            0,
+        );
+        expect(Number(await contract.getContractAddMixedParams())).toEqual(4);
+        expect(Number(await contract.getContractAddMixedParams2())).toEqual(4);
+    });
+});
diff --git a/src/test/e2e-emulated/functions/wildcard-parameters.tact b/src/test/e2e-emulated/functions/wildcard-parameters.tact
new file mode 100644
index 00000000..a1d5a796
--- /dev/null
+++ b/src/test/e2e-emulated/functions/wildcard-parameters.tact
@@ -0,0 +1,54 @@
+fun globalAdd(_: Int, _: Int): Int { return 0 }
+fun globalAddThreeWildcards(_: Int, _: Int, _: Int): Int { return 0 }
+fun globalAddMixedParams(a: Int, _: Int, b: Int): Int { return a + b }
+fun globalAddMixedParams2(a: HoldInt, _: Int, b: Int): Int { return a.val + b }
+fun globalAddMixedParams3(a: HoldInt, _: Int, b: Int, _: HoldInt): Int { return a.val + b }
+
+asm fun globalAsmAdd(_: Int, _: Int): Int { ADD }
+asm fun globalAsmAddThreeWildcards(_: Int, _: HoldInt, _: Int): Int { DROP ADD }
+asm fun globalAsmAddMixedParams(a: Int, _: Int, b: Int): Int { DROP ADD }
+asm fun globalAsmAddMixedParams2(a: Int, _: Int, b: Int, _: HoldInt): Int { DROP DROP ADD }
+
+struct HoldInt {
+    val: Int;
+}
+
+contract Test {
+    receive() {}
+
+    fun add(_: Int, _: Int): Int {
+        return 0;
+    }
+
+    get fun contractAdd(): Int { return self.add(1, 2) }
+
+    get fun globalAdd(): Int { return globalAdd(1, 2) }
+
+    get fun globalAsmAdd(): Int { return globalAsmAdd(1, 2) }
+
+    fun addThreeWildcards(_: Int, _: HoldInt, _: Int): Int { return 0 }
+
+    fun addMixedParams(a: Int, _: Int, b: HoldInt): Int { return a + b.val }
+
+    fun addMixedParams2(a: Int, _: Int, b: HoldInt, _: Int): Int { return a + b.val }
+
+    get fun contractAddThreeWildcards(): Int { return self.addThreeWildcards(1, HoldInt { val: 2 }, 3) }
+
+    get fun contractAddMixedParams(): Int { return self.addMixedParams(1, 2, HoldInt { val: 3 }) }
+
+    get fun contractAddMixedParams2(): Int { return self.addMixedParams2(1, 2, HoldInt { val: 3 }, 10) }
+
+    get fun globalAddThreeWildcards(): Int { return globalAddThreeWildcards(1, 2, 3) }
+
+    get fun globalAddMixedParams(): Int { return globalAddMixedParams(1, 2, 3) }
+
+    get fun globalAddMixedParams2(): Int { return globalAddMixedParams2(HoldInt { val: 1 }, 2, 3) }
+
+    get fun globalAddMixedParams3(): Int { return globalAddMixedParams3(HoldInt { val: 1 }, 2, 3, HoldInt { val: 10 }) }
+
+    get fun globalAsmAddThreeWildcards(): Int { return globalAsmAddThreeWildcards(1, HoldInt { val: 2 }, 3) }
+
+    get fun globalAsmAddMixedParams(): Int { return globalAsmAddMixedParams(1, 2, 3) }
+
+    get fun globalAsmAddMixedParams2(): Int { return globalAsmAddMixedParams2(1, 2, 3, HoldInt { val: 10 }) }
+}
diff --git a/src/test/utils/all-in-folder.build.ts b/src/test/utils/all-in-folder.build.ts
index 12eb83dc..c28768ca 100644
--- a/src/test/utils/all-in-folder.build.ts
+++ b/src/test/utils/all-in-folder.build.ts
@@ -75,14 +75,14 @@ const runFuncBuild = async (folder: string, globs: string[]) => {
 
     const logger = new Logger();
 
-    const importRegex = /#include\s+"([^"]+)"/g;
-    const isContractRegex = /\(\)\s+recv_internal/g;
-
     const compileFuncContract = async (contractInfo: {
         name: string;
         path: string;
         output: string;
     }) => {
+        const importRegex = /#include\s+"([^"]+)"/g;
+        const isContractRegex = /\(\)\s+recv_internal/g;
+
         const stdlibPath = stdlib.resolve("std/stdlib.fc");
         const stdlibCode = stdlib.readFile(stdlibPath).toString();
         const stdlibExPath = stdlib.resolve("std/stdlib_ex.fc");