这是TON401K 系列的第四篇文章,以下为其他相关内容
在此之前我们已经完整介绍了TON401K 的前端开发过程,而且也把代码整理后开源成为 Ton-Telegram Mini App 前端模板 供各位参考。現在我們開始討論有關TON401K 智能合約的部分。按之前的計劃,把这个项目分为三个主要阶段,并为每个阶段推出一个最简可行产品MVP。在首阶段,我们会推出一个最简单的储蓄产品,让用户可以每月进行储蓄,然后在一定的期限之后一次全数取出。这个阶段我们主要的工作是编写一个简单的锁仓合约,这方面我们之后可以继续探讨。完成后我们的APP 为用户制定退休计划和进行自愿锁仓,就好像一个链上的存钱罐一样,用智能合约来帮用户养成储蓄的习惯。
快速索引
TON401K合約功能概述
TON401k 智能合约是一个在 Telegram 与 TON 生态中的创新链上社保解决方案。以下是其主要功能概述:
一、基础设置与权限管理
- 合约具备明确的所有权设置,可确保对合约的关键操作由合法的所有者或被授权的操作者进行。所有者和操作者可以对合约进行管理和控制。
- 设置了停止和恢复功能,可在必要时暂停合约的运行以应对特殊情况,同时也能在合适的时候恢复合约的正常运作。
二、Jetton 管理
- 可以添加和移除特定的 Jetton 合约地址到支持的列表中。这使得合约能够有选择性地支持特定的 Jetton,为用户提供更多的资产选择。
- 对支持的 Jetton 进行严格的权限管理,只有所有者或操作者有权进行添加和移除操作,确保了合约的安全性和稳定性。
三、储蓄功能
- 支持存储 TON,当接收到特定的 “saving” 消息时,合约会处理存储操作,将发送者作为储蓄计划的所有者,并将存储的 TON 数量累加到总存储的 TON 数量中。
- 提供启动 TON 储蓄计划和 Jetton 储蓄计划的功能。用户可以根据自己的需求选择不同的储蓄计划,为自己的未来进行规划。在启动储蓄计划时,合约会进行严格的条件检查,确保计划的合理性和可行性。
- 对于储蓄计划,用户可以设置储蓄目的、目标金额、截止日期和是否锁定等参数,以满足个性化的需求。
四、强制提取功能
- 合约的所有者或操作者可以啟動强制提取程序,讓存款沿路返回到用戶錢包。
- 这一功能可以确保资金的安全性下增加在特殊情况时的可操作性,例如用户遇到突发情况急需取回资金。
智能合约分片概念
在 TON 的开发中,智能合约需分片处理。以 Jetton 代幣合約为例,我們不能像以太坊那样用一个合约处理所有事,在 TON 上要进行分片,每个用户需专属的 Jetton 钱包合约,主合约负责代币参数和铸造程序等。套用到 Ton401k 也如此,处理用户儲蓄计划时,用户便會通过主合约部署专属退休計劃合約,该钱包负责记录儲蓄计划所需参数。所以你能夠看到我們的智能合約結構如下:
- ton401k_main.tact (TON401K 主合约,负责开立退休计划,基础设置与权限管理和强制提取功能)
- saving_ton_wallet.tact (TON 储蓄计划子合约,负责纪录个别储蓄计划所需参数,也会负责保管TON,在合约中也订明了TON只能回到用户钱包以保障用户资金安全 )
- messages.tact(自定义消息管理)
ton401k_main.tact
这里定义了一个名为Ton401kMain
的智能合约,并继承了Deployable
、OwnableTransferable
和Resumable
这三个特质。这意味着这个合约可以被部署、进行所有权转移以及可以被停止和恢复。
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.tact 和 saving_ton_wallet.tact 的代码,并展示如何用blueprint 自带的回测功具进行测试。
我是TonPanda,TON和ICP的早期投资者,资深以太坊开发者,也是一名热爱去中心化的小韭菜,现时主要开发Telegram和TON生态应用。欢迎关注我们的 X:https://x.com/ton_org 和 飞机群 @tonchina!