TON 链上社保 (TON401K)合约开发 1 – 基础结构

这是TON401K 系列的第四篇文章,以下为其他相关内容


在此之前我们已经完整介绍了TON401K 的前端开发过程,而且也把代码整理后开源成为 Ton-Telegram Mini App 前端模板 供各位参考。現在我們開始討論有關TON401K 智能合約的部分。按之前的計劃,把这个项目分为三个主要阶段,并为每个阶段推出一个最简可行产品MVP。在首阶段,我们会推出一个最简单的储蓄产品,让用户可以每月进行储蓄,然后在一定的期限之后一次全数取出。这个阶段我们主要的工作是编写一个简单的锁仓合约,这方面我们之后可以继续探讨。完成后我们的APP 为用户制定退休计划和进行自愿锁仓,就好像一个链上的存钱罐一样,用智能合约来帮用户养成储蓄的习惯。

TON401K合約功能概述

TON401k 智能合约是一个在 Telegram 与 TON 生态中的创新链上社保解决方案。以下是其主要功能概述:

一、基础设置与权限管理

  1. 合约具备明确的所有权设置,可确保对合约的关键操作由合法的所有者或被授权的操作者进行。所有者和操作者可以对合约进行管理和控制。
  2. 设置了停止和恢复功能,可在必要时暂停合约的运行以应对特殊情况,同时也能在合适的时候恢复合约的正常运作。

二、Jetton 管理

  1. 可以添加和移除特定的 Jetton 合约地址到支持的列表中。这使得合约能够有选择性地支持特定的 Jetton,为用户提供更多的资产选择。
  2. 对支持的 Jetton 进行严格的权限管理,只有所有者或操作者有权进行添加和移除操作,确保了合约的安全性和稳定性。

三、储蓄功能

  1. 支持存储 TON,当接收到特定的 “saving” 消息时,合约会处理存储操作,将发送者作为储蓄计划的所有者,并将存储的 TON 数量累加到总存储的 TON 数量中。
  2. 提供启动 TON 储蓄计划和 Jetton 储蓄计划的功能。用户可以根据自己的需求选择不同的储蓄计划,为自己的未来进行规划。在启动储蓄计划时,合约会进行严格的条件检查,确保计划的合理性和可行性。
  3. 对于储蓄计划,用户可以设置储蓄目的、目标金额、截止日期和是否锁定等参数,以满足个性化的需求。

四、强制提取功能

  1. 合约的所有者或操作者可以啟動强制提取程序,讓存款沿路返回到用戶錢包。
  2. 这一功能可以确保资金的安全性下增加在特殊情况时的可操作性,例如用户遇到突发情况急需取回资金。

智能合约分片概念

在 TON 的开发中,智能合约需分片处理。以 Jetton 代幣合約为例,我們不能像以太坊那样用一个合约处理所有事,在 TON 上要进行分片,每个用户需专属的 Jetton 钱包合约,主合约负责代币参数和铸造程序等。套用到 Ton401k 也如此,处理用户儲蓄计划时,用户便會通过主合约部署专属退休計劃合約,该钱包负责记录儲蓄计划所需参数。所以你能夠看到我們的智能合約結構如下:

  1. ton401k_main.tact (TON401K 主合约,负责开立退休计划,基础设置与权限管理和强制提取功能)
  2. saving_ton_wallet.tact (TON 储蓄计划子合约,负责纪录个别储蓄计划所需参数,也会负责保管TON,在合约中也订明了TON只能回到用户钱包以保障用户资金安全 )
  3. messages.tact(自定义消息管理)

ton401k_main.tact

这里定义了一个名为Ton401kMain的智能合约,并继承了DeployableOwnableTransferableResumable这三个特质。这意味着这个合约可以被部署、进行所有权转移以及可以被停止和恢复。

import "@stdlib/deploy";
import "@stdlib/ownable";
import "@stdlib/stoppable";
import "./messages";
import "./saving_ton_wallet";
import "./saving_jetton_wallet";

contract Ton401kMain  with Deployable, OwnableTransferable, Resumable {
    // Empty init() function is present by default in all Tact contracts
    // since v1.3.0, so the following may be omitted:
    const GAS_CONSUMPTION: Int = ton("0.01"); // assuming the minium unit of gasConsumption is 0.01 TON
    owner: Address;
    operator: Address; // This is for owner to assign an operator to manage the contract
    stopped: Bool; // The Resumable trait requires you to add this exact state variable
    listOfJetton: map<Address, Int>; //ton401K will only support selected Jetton
    totalTonSaved: Int as coins;
    totalSavingPlan: Int as uint32;

    init() {
        self.owner = sender();
        self.operator = sender();
        self.stopped = false;
        self.listOfJetton = emptyMap();
        self.totalTonSaved = 0;
        self.totalSavingPlan =0;
    }

    receive(msg: AddJettonToList){
        require(sender() == self.owner || sender() == self.operator, "Only sender and operator can do this");
        self.listOfJetton.set(msg.jettonContract, 1);
        self.reply("jetton added".asComment());
    }    

    receive(msg: RemoveJettonFromList){
        require(sender() == self.owner || sender() == self.operator, "Only sender and operator can do this");
        self.listOfJetton.set(msg.jettonContract, null);
        self.reply("jetton removed".asComment());
    }

    receive("saving"){
        let ctx: Context = context(); // Check sender
        let savingAmt = ctx.value;
        let planOwner: Address = sender();
        let init: StateInit = initOf SavingTonWallet(myAddress(), planOwner);
        self.totalTonSaved = self.totalTonSaved + savingAmt;

        send(SendParameters{
                to: contractAddress(init),
                body: "saving".asComment(),
                value: savingAmt - self.GAS_CONSUMPTION, 
                mode: SendIgnoreErrors,
                code: init.code,
                data: init.data
            }
        );
    }

    //when a user want to start a TON saving plan
    receive(msg: StartTonSavingPlan){
        self.requireNotStopped(); // can only start a plan when the contract is running 
        let planOwner: Address = sender();
        let purpose: String  = msg.purpose;
        let targetAmount: Int = msg.target;
        let deadline: Int = msg.deadline; //with timestamp of the deadline
        let lockup: Bool = msg.lockup;
        let today: Int = now(); // in second
        require(targetAmount > 0, "your plan must have a saving target target!");
        require(deadline > today, "your can only plan for your future!");
        //if everything is good, we will send these parameters to the saving contract!

        let init: StateInit = initOf SavingTonWallet(myAddress(), planOwner);
        self.totalSavingPlan = self.totalSavingPlan+1;
        send(SendParameters{
                to: contractAddress(init),
                body: M2CSavingPlan{purpose: purpose, target: targetAmount, deadline: deadline, lockup: lockup}.toCell(),
                value: self.GAS_CONSUMPTION+self.GAS_CONSUMPTION+self.GAS_CONSUMPTION+self.GAS_CONSUMPTION, 
                mode: SendIgnoreErrors,
                code: init.code,
                data: init.data
            }
        );
        
    }

    receive(msg: ForceWithdrawTON){
        require(sender() == self.owner || sender() == self.operator, "Only owner and operator can do this");
        let init: StateInit = initOf SavingTonWallet(myAddress(), msg.walletOwner);

        send(SendParameters{
                to: contractAddress(init),
                body: "forceWithdraw".asComment(),
                value: self.GAS_CONSUMPTION+self.GAS_CONSUMPTION, 
                mode: SendIgnoreErrors,
                code: init.code,
                data: init.data
            }
        );
    }


    get fun SavingTonWalletAddress(owner: Address): Address {
        let expectedAddress: Address = contractAddress(initOf SavingTonWallet(myAddress(), owner));
        return expectedAddress;
    }

}

saving_ton_wallet.tact

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

struct SavingPlan {
    lockup: Bool;
    ongoing: Bool; // ongoing plan
    target: Int as coins; // number of TON user wants to save
    purpose: String;
    startingTime: Int as uint32;
    deadline: Int as uint32;
    lastSavingDate: Int as uint32;
    numberOfDeposit: Int as uint8;
}

struct SavingStat {
    totalSaving: Int as coins;
    planFinished: Int as uint8;
    planFailed: Int as uint8;
    totalNumberOfDeposit: Int as uint8;
    planTime: Int as uint32; // the total number of second for all plan 
    OFS: Int as uint32; //the on-chain financial score
}

contract SavingTonWallet with Deployable {
    //TON takes storage fee, you need to keep some TON in the contract for the storage
    const MIN_TON_FOR_STORAGE: Int = ton("0.01");// 1KB for 2.5 yr
    const GAS_CONSUMPTION: Int = ton("0.01"); // assuming the minium unit of gasConsumption is 0.01 TON

    owner: Address; // only the wallet own can control the fund in the wallet
    parent: Address; // saving plan can only be initiated by the master contract 

    // field for saving plan
    
    lockup: Bool;
    ongoing: Bool; // ongoing plan
    targetReached: Bool; // ongoing plan
    target: Int as coins; // number of TON user wants to save
    purpose: String;
    startingTime: Int as uint32;
    deadline: Int as uint32;
    lastSavingDate: Int as uint32;
    numberOfDeposit: Int as uint16;
    minDeposit: Int as coins; // the min amount of each deposit


    // field for overall record
    totalSaving: Int as coins;
    planFinished: Int as uint16;
    planFailed: Int as uint16;
    totalNumberOfDeposit: Int as uint32;
    planTime: Int as uint32; // the total number of second for all plan 
    OFS: Int as uint32; //the on-chain financial score

    init(parent: Address, owner: Address) {
        self.owner = owner;
        self.parent = parent;
        self.lockup = false;
        self.ongoing = false;
        self.targetReached = false;
        self.target = 0;
        self.purpose = "no plan";
        self.deadline = now();
        self.startingTime = now();
        self.lastSavingDate = now();
        self.numberOfDeposit = 0;
        self.minDeposit = 0;

        self.totalSaving = 0;
        self.planFinished = 0;
        self.planFailed = 0;
        self.totalNumberOfDeposit = 0;
        self.planTime = 0;
        self.OFS = 0;
    }

    receive(msg: M2CSavingPlan){
        require(sender() == self.parent, "please use the default function"); // create saving plan via the main contract
        require(!self.ongoing, "no on going plan"); // one saving plan at a time
        require(msg.deadline > now(), "your can only plan for your future!"); // the deadline must be in the future

        self.purpose = msg.purpose;
        self.target = msg.target;
        self.deadline = msg.deadline;
        self.lockup = msg.lockup;
        self.ongoing = true; // no more saving plan can be created until this flag is false
        self.targetReached = false;
        self.startingTime = now();

        if((self.deadline - self.startingTime)< (60*60*24)){
            self.minDeposit = self.target;  
        }else{
            self.minDeposit = self.target / ((self.deadline - self.startingTime)/(60*60*24)) //min deposit equals to target / number of days
        }        
    }

    receive("saving"){
        let ctx: Context = context(); // Check sender
        let savingAmt = ctx.value;
        require(sender() == self.parent, "owner wallet can only deposit to the account via the main contract");
        require(savingAmt>= self.minDeposit, "you need to deposit more than the minimum desposit");

        let contractBalance = myBalance() + savingAmt;

        if((now() - self.lastSavingDate) >= 24*60*60 ){
            //we encourage user to save regularly, same deposit on the same day will not be counted
            self.numberOfDeposit = self.numberOfDeposit + 1;
            //udpate the OFS for each deposit
            self.OFS = self.OFS + self.numberOfDeposit * savingAmt/ ton("0.1");
        }

        self.lastSavingDate = now(); //update the last saving date
        if(contractBalance >= self.target){
            self.targetReached = true;
        }

    }

    receive("withdraw"){
        let ctx: Context = context(); // Check sender
        require(sender() == self.owner, "owner wallet owner can withdraw from the account"); 
        require(self.ongoing, "you don't have a saving plan");
        if(self.lockup){
            require((self.targetReached || self.deadline < now()), "you haven't reached the goal yet! You need to reach the goal or wait until the deadline");
        }
        let amount: Int = myBalance();
        self.checkPlanWhenWithdraw();

        send(SendParameters{
                to: self.owner,
                body: "withdrawal from saving account".asComment(),
                value: amount,
                mode: SendIgnoreErrors | SendRemainingBalance
            }
        );
    }

    receive("forceWithdraw"){
        // this is an emergency exit that can only be triggered by contract owner or operater, but the fund will only go to user's wallet
        let ctx: Context = context(); // Check sender
        require(sender() == self.parent, "this is an emergency withdraw that can only be triggered by contract owner or operater"); 
        let amount: Int = myBalance();
        self.checkPlanWhenWithdraw();

        send(SendParameters{
                to: self.owner,
                body: "forced withdrawal from saving account".asComment(),
                value: amount,
                mode: SendIgnoreErrors | SendRemainingBalance
            }
        );
    }

    fun checkPlanWhenWithdraw(){
        self.ongoing = false;
        let planDuration: Int = now() - self.startingTime;
        let roundScore: Int = 0;

        if(self.deadline > self.lastSavingDate && self.targetReached){
            self.planFinished = self.planFinished + 1;
            //you can get scoure from your plan time only if you finished the plan
            roundScore = roundScore + planDuration / (60*60*24);
        }
        else{
            self.planFailed = self.planFailed + 1;
        }

        self.planTime = self.planTime+ planDuration; // update the saving duration 
        self.totalNumberOfDeposit = self.totalNumberOfDeposit + self.numberOfDeposit;
        self.totalSaving = self.totalSaving +  myBalance();

        //your plan must be longer than 7 days and you must make at least 7 deposit to get the OFS for saving
        if(self.numberOfDeposit>6){
            roundScore = roundScore  + myBalance() / ton("1") * self.numberOfDeposit;
        }
        //force locked plan will have double score 
        if(self.lockup && self.deadline > self.lastSavingDate && self.targetReached){
            roundScore = roundScore*2;
        }

        self.OFS = self.OFS + roundScore;
    }

    get fun balance(): Int {
        return myBalance();
    }

    get fun savingPlan(): SavingPlan {
        return SavingPlan {
            lockup: self.lockup,
            ongoing: self.ongoing,
            target: self.target,
            purpose: self.purpose,
            startingTime: self.startingTime,
            deadline: self.deadline,
            lastSavingDate: self.lastSavingDate,
            numberOfDeposit: self.numberOfDeposit
        };
    }

    get fun savingStat(): SavingStat {
        return SavingStat {
            totalSaving: self.totalSaving,
            planFinished: self.planFinished,
            planFailed: self.planFailed,
            totalNumberOfDeposit: self.totalNumberOfDeposit,
            planTime:self.planTime,
            OFS: self.OFS
        };
    }
}

上面是完整的代码以供各位参考,下一篇我们会深入探讨 ton401k_main.tactsaving_ton_wallet.tact 的代码,并展示如何用blueprint 自带的回测功具进行测试。


我是TonPanda,TON和ICP的早期投资者,资深以太坊开发者,也是一名热爱去中心化的小韭菜,现时主要开发Telegram和TON生态应用。欢迎关注我们的 X:https://x.com/ton_org 和 飞机群 @tonchina

Leave a Comment