TON 链上社保 (TON401K)合约开发 3 – 储蓄计划子合约解释

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


TON401k 一共由 ton401k_main.tact(TON401K 主合约)saving_ton_wallet.tact(TON 储蓄计划子合约) 和 messages.tact(自定义消息管理)三部分組成,在上篇文章中,我們分析了ton401k_main.tact 代碼的內容,這一次我們會分析子合約,并展示如何用blueprint 自带的回测功具进行测试。

引入模块变量

和主合约差不多,首先引入模块与变量

import "@stdlib/deploy";
import "./messages";
struct SavingPlan {
...
}

struct SavingStat {
...
}

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 

    // fields 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


    // fields 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
    ...
}

定义struct

  • struct SavingPlan:用于表示储蓄计划的结构体,包含了锁定期状态、计划是否正在进行、目标储蓄金额、储蓄目的、开始时间、截止时间、最后储蓄日期、存款次数等字段。
  • struct SavingStat:用于表示储蓄统计信息的结构体,包含了总储蓄金额、完成的计划数量、失败的计划数量、总存款次数、所有计划的总时间、链上财务评分(OFS)等字段。

变量定义

  1. owner: Address:存储钱包的所有者地址,只有所有者可以控制钱包中的资金。
  2. parent: Address:表示发起储蓄计划的主合约地址,ton401k_main.tact
  3. 储蓄计划相关变量:
    • lockup: Bool:表示是否有锁定期。
    • ongoing: Bool:表示储蓄计划是否正在进行。
    • targetReached: Bool:表示是否达到了目标储蓄金额。
    • target: Int as coins:用户想要储蓄的 TON 数量目标。
    • purpose: String:储蓄目的。
    • startingTime: Int as uint32:储蓄计划的开始时间。
    • deadline: Int as uint32:储蓄计划的截止时间。
    • lastSavingDate: Int as uint32:最后一次储蓄的日期。
    • numberOfDeposit: Int as uint16:存款次数。
    • minDeposit: Int as coins:每次存款的最小金额。
  4. 储蓄统计相关变量:
    • totalSaving: Int as coins:总储蓄金额。
    • planFinished: Int as uint16:完成的计划数量。
    • planFailed: Int as uint16:失败的计划数量。
    • totalNumberOfDeposit: Int as uint32:总存款次数。
    • planTime: Int as uint32:所有计划的总时间。
    • OFS: Int as uint32:链上财务评分。

接收函数与消息处理

receive(msg: M2CSavingPlan)

receive(msg: M2CSavingPlan){
    require(sender() == self.parent, "please use the default function");
    require(!self.ongoing, "no on going plan");
    require(msg.deadline > now(), "your can only plan for your future!");

    self.purpose = msg.purpose;
    self.target = msg.target;
    self.deadline = msg.deadline;
    self.lockup = msg.lockup;
    self.ongoing = true;
    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))
    }
}
  • M2C 是自定的消息,意为Master to Child
  • 首先,检查发送者是否为主合约地址,确保只有主合约可以创建储蓄计划。
  • 检查是否已经有正在进行的计划,如果有则不能创建新的计划。
  • 检查截止时间是否在未来。
  • 如果满足条件,设置储蓄计划的目的、目标金额、截止时间、锁定期等信息。
  • 根据截止时间和目标金额计算最小存款金额。

receive(“saving”)

这个接收函数用于处理存款操作。

receive("saving"){
    let ctx: Context = context();
    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 ){
        self.numberOfDeposit = self.numberOfDeposit + 1;
        self.OFS = self.OFS + self.numberOfDeposit * savingAmt/ ton("0.1");
    }

    self.lastSavingDate = now();
    if(contractBalance >= self.target){
        self.targetReached = true;
    }
}
  • 检查发送者是否为主合约地址,确保只有主合约可以进行存款操作。
    • 其实可以直接允许用户存款,然后再由子合约发送消息到主合约以作记录,但我们这次选择的是用户存款到主合约,再由主合约把存款发送到子合约。
  • 检查存款金额是否大于等于最小存款金额。
  • 如果当前时间与最后一次存款日期相差超过一天,则增加存款次数,并更新 OFS(链上财务评分)。
  • 更新最后一次存款日期。
  • 如果当前合约余额大于等于目标金额,则设置目标达成状态为真。

receive(“withdraw”)

这个接收函数用于处理用户的取款操作。

receive("withdraw"){
    let ctx: Context = context();
    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
        }
    );
}
  • 检查发送者是否为钱包所有者。
  • 检查是否有正在进行的储蓄计划。
  • 如果有锁定期,检查是否已经达到目标金额或者截止时间已过。
  • 获取当前合约余额。
  • 调用checkPlanWhenWithdraw函数进行取款后的计划检查和统计更新。
  • 发送包含取款金额的消息给钱包所有者。

receive(“forceWithdraw”)

这个接收函数用于处理紧急取款操作,只能由主合约的所有者或操作员触发(owner or operator,可参阅上一篇文章)。

receive("forceWithdraw"){
    let ctx: Context = context();
    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
        }
    );
}
  • 检查发送者是否为主合约地址。
  • 获取当前合约余额。
  • 调用checkPlanWhenWithdraw函数进行取款后的计划检查和统计更新。
  • 发送包含取款金额的消息给钱包所有者。

辅助函数和Getter

fun checkPlanWhenWithdraw()

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;
        roundScore = roundScore + planDuration / (60*60*24);
    }
    else{
        self.planFailed = self.planFailed + 1;
    }

    self.planTime = self.planTime+ planDuration;
    self.totalNumberOfDeposit = self.totalNumberOfDeposit + self.numberOfDeposit;
    self.totalSaving = self.totalSaving +  myBalance();

    if(self.numberOfDeposit>6){
        roundScore = roundScore  + myBalance() / ton("1") * self.numberOfDeposit;
    }
    if(self.lockup && self.deadline > self.lastSavingDate && self.targetReached){
        roundScore = roundScore*2;
    }

    self.OFS = self.OFS + roundScore;
}
  • 将正在ongoing设置为 false。
  • 计算储蓄计划的持续时间。
  • 根据不同情况计算得分。
  • 如果截止时间大于最后一次存款日期且目标已达成,则增加完成计划数量,并根据计划持续时间计算得分。
  • 如果不满足上述条件,则增加失败计划数量。
  • 更新总存款次数、总储蓄金额和计划总时间。
  • 如果存款次数大于 6,则根据存款次数和余额计算得分。
  • 如果有锁定期且目标已达成和截止时间未过,则得分加倍。
  • 更新链上财务评分。

get fun balance(): Int

这个获取函数用于获取当前合约的余额。

get fun savingPlan(): SavingPlan

这个获取函数用于返回当前的储蓄计划状态。

get fun savingStat(): SavingStat

这个获取函数用于返回当前的储蓄统计信息。

测试TON 智能合约

TON Blueprint 的TypeScript SDK 类似以太坊的truffle,都有内建的沙盒测试功能,可以不用每次都部署到测试网进行测试,加快开发流程。一般的开发测试程都是这样,先在本地沙盒运行不同的test cases,都成功后再部署到Testnet 进行链上交互。

要執行測試也很簡單,在 tests/Ton401kMain.spec.ts 中編寫測試代碼,然後運行:

npx blueprint test

之后马上可以知道合约有没有异常


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

Leave a Comment