TON 代币:Jetton教学(一)

这是TON Panda 上第一篇面向开发者的入门教学,会用我们自己的实验专案Trump Vs Harris 为例子和大家一起讨论TON 智能合约的开发体验和心得,设计一款带有特殊功能的Jetton合约。这个合约也会部署在TON主网上,完成后大家可以一起体验一下。

以下是这合约的一些功能:

  1. Jetton会通过投票来MINT
  2. 投票需要支付TON
  3. 每一票要支付的TON会愈来愈多
  4. 当大选结果公布前的1小时前投票会被禁止
  5. 当美国大选公布后,输的一方的票会全部作废,投入的TON为当成池子加入胜出的一方

在开始之前,我们要先了解Jetton和ERC20的智能合约的区别。和以太坊不同,TON的代币标准名为Jetton,并且在智能合约上有着一币一地址的特性,这是由于TON的合约有储存上限,不可以像以太坊一样由一个合约控制所有持有人的余额,Jetton 主智能合约上只存储了有关代币的常见信息(包括总供应量、元数据链接或元数据本身)。所以在智能合约的写法上,Jetton和ERC20 有着显著的差异。

开发流程

參考TON官方的 TON 文档:https://docs.ton.org/mandarin/develop/overview 们会使用Blueprint SDK 和 Tact 语言来进行开发。TON的智能合约一般都用FunC或Tact来开发,FunC 是一种针对那些想深入了解 TON 架构的开发者的底层语言,由于 FunC 是较底层的语言,使用上有一定难度,也不容易上手。而 Tact 的设计类似于流行的编程语言,如 JavaScript、Python 和 Solidity。这显著简化了对语言的学习和理解,用 Tact 来开发,你可以更专注于写代码,而不必太担心区块链的底层细节。

使用Blueprint SDK 快速开始

npm create ton@latest
blueprint
npm create ton@latest

这样便可以生成一个完整的智能合约开发环境了,内容包含了开发,测试及部署。

Jetton主智能合约

基本的Jetton可参考 Howard Pen 的例子(https://github.com/howardpen9/jetton-implementation-in-tact/tree/main),𥚃面的代码包括了:

  1. Jetton 主智能合约 (jetton.tact)
  2. Jetton 钱包智能合约(jetton.tact)
  3. Jetton 讯息 (messages.tact)
    中文区其中一位技术大佬LaDoger Bark,曾指出TON 其实是一个通讯工具,TON其实是Telegram的去中心化版本。TON上面的所有”交易”其实本质上都是智能合约彼此之间”传讯”而已。别忘了TON的”钱包”本身也是一个智能合约。
  4. 我们的代币合约 (contract.tact)

jetton.tact (1)

import "@stdlib/ownable";
import "./messages";

// ============================================================================================================ //
@interface("org.ton.jetton.master")
trait Jetton with Ownable {
    total_supply: Int;
    mintable: Bool;
    owner: Address;
    content: Cell;

    receive(msg: TokenUpdateContent){
        self.requireOwner(); // Allow changing content only by owner
        self.content = msg.content; // Update content
    }

    receive(msg: TokenBurnNotification){
        self.requireSenderAsWalletOwner(msg.sender); // Check wallet
        self.total_supply = (self.total_supply - msg.amount); // Update supply
        if (msg.response_destination != null) {
            // Cashback
            send(SendParameters{
                    to: msg.response_destination!!,
                    value: 0,
                    bounce: false,
                    mode: SendRemainingValue,
                    body: TokenExcesses{query_id: msg.query_id}.toCell()
                }
            );
        }
    }

    // https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md
    receive(msg: ProvideWalletAddress){
        // 0x2c76b973
        require(context().value >= ton("0.0061"), "Insufficient gas");
        let init: StateInit = initOf JettonDefaultWallet(msg.owner_address, myAddress());
        if (msg.include_address) {
            send(SendParameters{
                    to: sender(),
                    value: 0,
                    mode: SendRemainingValue,
                    body: TakeWalletAddress{
                        query_id: msg.query_id,
                        wallet_address: contractAddress(init),
                        owner_address: beginCell().storeBool(true).storeAddress(msg.owner_address).endCell().asSlice()
                    }.toCell()
                }
            );
        } else {
            send(SendParameters{
                    to: sender(),
                    value: 0,
                    mode: SendRemainingValue,
                    body: TakeWalletAddress{ // 0xd1735400
                        query_id: msg.query_id,
                        wallet_address: contractAddress(init),
                        owner_address: beginCell().storeBool(false).endCell().asSlice()
                    }.toCell()
                }
            );
        }
    }

    // Private Methods //
    // @to The Address receive the Jetton token after minting
    // @amount The amount of Jetton token being minted
    // @response_destination The previous owner address
    fun mint(to: Address, amount: Int, response_destination: Address) {
        require(self.mintable, "Can't Mint Anymore");
        self.total_supply = (self.total_supply + amount); // Update total supply

        let winit: StateInit = self.getJettonWalletInit(to); // Create message
        send(SendParameters{
                to: contractAddress(winit),
                value: 0,
                bounce: true,
                mode: SendRemainingValue,
                body: TokenTransferInternal{
                    query_id: 0,
                    amount: amount,
                    from: myAddress(),
                    response_destination: response_destination,
                    forward_ton_amount: 0,
                    forward_payload: beginCell().endCell().asSlice()
                }.toCell(),
                code: winit.code,
                data: winit.data
            }
        );
    }

    fun requireSenderAsWalletOwner(owner: Address) {
        let ctx: Context = context();
        let winit: StateInit = self.getJettonWalletInit(owner);
        require(contractAddress(winit) == ctx.sender, "Invalid sender");
    }

    virtual fun getJettonWalletInit(address: Address): StateInit {
        return initOf JettonDefaultWallet(address, myAddress());
    }

    // ====== Get Methods ====== //

    get fun get_jetton_data(): JettonData {
        return
            JettonData{
                total_supply: self.total_supply,
                mintable: self.mintable,
                owner: self.owner,
                content: self.content,
                wallet_code: initOf JettonDefaultWallet(self.owner, myAddress()).code
            };
    }

    get fun get_wallet_address(owner: Address): Address {
        return contractAddress(initOf JettonDefaultWallet(owner, myAddress()));
    }
}

导入ownable 和 messages,ownable属于标准的trait,可以使合约可以设置拥有者角色,拥有者拥有比其他用户更高的权限。例如,如果你的合约支持升级,那么将升级权限限制为只有拥有者可以执行就非常合理,否则任何人都有可能破坏合约。而messages 则是我们自定义的讯息标准。上面的代码我们可以直接引用,也可以重写,但要留意必须要跟从 TEP74 的标准,不然的话其他钱包/交易所/区块链浏览器就有可能不支援你的代币了。上面的代碼中,total_supply是Howard 自定义的,本身不在TEP74的标准中。

Jetton 钱包智能合约

jetton.tact (2)

// ============================================================ //
@interface("org.ton.jetton.wallet")
contract JettonDefaultWallet
{
    const minTonsForStorage: Int = ton("0.019");
    const gasConsumption: Int = ton("0.013");
    balance: Int as coins = 0;
    owner: Address;
    master: Address;
    init(owner: Address, master: Address){
        self.balance = 0;
        self.owner = owner;
        self.master = master;
    }

    receive(msg: TokenTransfer){
        // 0xf8a7ea5
        let ctx: Context = context(); // Check sender
        require(ctx.sender == self.owner, "Invalid sender");
        let final: Int =
            (((ctx.readForwardFee() * 2 + 2 * self.gasConsumption) + self.minTonsForStorage) + msg.forward_ton_amount); // Gas checks, forward_ton = 0.152
        require(ctx.value > final, "Invalid value");
        // Update balance
        self.balance = (self.balance - msg.amount);
        require(self.balance >= 0, "Invalid balance");
        let init: StateInit = initOf JettonDefaultWallet(msg.sender, self.master);
        let wallet_address: Address = contractAddress(init);
        send(SendParameters{
                to: wallet_address,
                value: 0,
                mode: SendRemainingValue,
                bounce: true,
                body: TokenTransferInternal{ // 0x178d4519
                    query_id: msg.query_id,
                    amount: msg.amount,
                    from: self.owner,
                    response_destination: msg.response_destination,
                    forward_ton_amount: msg.forward_ton_amount,
                    forward_payload: msg.forward_payload
                }.toCell(),
                code: init.code,
                data: init.data
            }
        );
    }

    receive(msg: TokenTransferInternal){
        // 0x178d4519
        let ctx: Context = context();
        if (ctx.sender != self.master) {
            let sinit: StateInit = initOf JettonDefaultWallet(msg.from, self.master);
            require(contractAddress(sinit) == ctx.sender, "Invalid sender!");
        }
        // Update balance
        self.balance = (self.balance + msg.amount);
        require(self.balance >= 0, "Invalid balance");
        // Get value for gas
        let msg_value: Int = self.msg_value(ctx.value);
        let fwd_fee: Int = ctx.readForwardFee();
        if (msg.forward_ton_amount > 0) {
            msg_value = ((msg_value - msg.forward_ton_amount) - fwd_fee);
            send(SendParameters{
                    to: self.owner,
                    value: msg.forward_ton_amount,
                    mode: SendPayGasSeparately,
                    bounce: false,
                    body: TokenNotification{ // 0x7362d09c -- Remind the new Owner
                        query_id: msg.query_id,
                        amount: msg.amount,
                        from: msg.from,
                        forward_payload: msg.forward_payload
                    }.toCell()
                }
            );
        }
        // 0xd53276db -- Cashback to the original Sender
        if (msg.response_destination != null && msg_value > 0) {
            send(SendParameters{
                    to: msg.response_destination!!,
                    value: msg_value,
                    bounce: false,
                    body: TokenExcesses{query_id: msg.query_id}.toCell(),
                    mode: SendPayGasSeparately
                }
            );
        }
    }

    receive(msg: TokenBurn){
        let ctx: Context = context();
        require(ctx.sender == self.owner, "Invalid sender"); // Check sender

        self.balance = (self.balance - msg.amount); // Update balance
        require(self.balance >= 0, "Invalid balance");
        let fwd_fee: Int = ctx.readForwardFee(); // Gas checks
        require(ctx.value > ((fwd_fee + 2 * self.gasConsumption) + self.minTonsForStorage), "Invalid value - Burn");
        // Burn tokens
        send(SendParameters{
                to: self.master,
                value: 0,
                mode: SendRemainingValue,
                bounce: true,
                body: TokenBurnNotification{
                    query_id: msg.query_id,
                    amount: msg.amount,
                    sender: self.owner,
                    response_destination: msg.response_destination
                }.toCell()
            }
        );
    }

    fun msg_value(value: Int): Int {
        let msg_value: Int = value;
        let ton_balance_before_msg: Int = (myBalance() - msg_value);
        let storage_fee: Int = (self.minTonsForStorage - min(ton_balance_before_msg, self.minTonsForStorage));
        msg_value = (msg_value - (storage_fee + self.gasConsumption));
        return msg_value;
    }

    bounced(msg: bounced<TokenTransferInternal>){
        self.balance = (self.balance + msg.amount);
    }

    bounced(msg: bounced<TokenBurnNotification>){
        self.balance = (self.balance + msg.amount);
    }

    get fun get_wallet_data(): JettonWalletData {
        return
            JettonWalletData{
                balance: self.balance,
                owner: self.owner,
                master: self.master,
                code: initOf JettonDefaultWallet(self.owner, self.master).code
            };
    }
}

我们可以看到很多 receive() 开头的代码,这是因为所有交易都是讯息,智能合约中的代码则会判断收到讯息后会执行什么逻辑,同时更新链上储存的数据。有几点非常特别的地方,TON链上储存数据是需要费用的,这个费用在这里minTonsForStorage 定义了,据文档资料显示,0.1TON可以在链上储存 10KB 资料 2.5年。另外TON 异步链,我们在编写智能合约的时候,也需要注意这一点,一个交易当中可能包含数个不同的讯息,每个讯息都不会同步完成的,所以我们不能理所当然的在不同合约上加入线性逻辑。以 receive(msg: TokenTransfer) receive(msg: TokenTransferInternal) 为例,假设 BOB 要转一笔Jetton给ALICE,BOB是通过自己的钱包地址(其实也是合约),向属于BOB的JettonWallet 发送TokenTransfer 的讯息,BOB的JettonWallet在收到讯息后,便扣减BOB在JettonWallet 中的余额,然后再向ALICE 的JettonWallet发送 TokenTransferInternal 的讯息,ALICE 的JettonWallet在收到讯息后再更新ALICE的余额,这样便完成转帐。(Excess的部分我们先忽略)

你可以看到,按照这个写法,代币的转账是完全不需要经过Jetton的主合约的,只需要两个钱包合约之间来回沟通便可以完成。下一篇我们会尝试在这Jetton 范本的基础上加入自定义的MINT 功能,欢迎一起参与讨论。

如果你只需要一个简单的代币合约,只需到 https://minter.ton.org/ 上输入表单即可。这个方法无需任何代码!

Leave a Comment