Ton 區塊鏈的官方 類ERC20-Token 智慧合約程式碼-Transfer部分解析

指尖下的幽灵發表於2024-06-12

作者:林冠宏 / 指尖下的幽靈。轉載者,請: 務必標明出處。

掘金:https://juejin.im/user/1785262612681997

GitHub : https://github.com/af913337456/

出版的書籍:

  • 《1.0-區塊鏈DApp開發實戰》
  • 《2.0-區塊鏈DApp開發:基於公鏈》

Ton 區塊鏈的官方 類ERC20-Token 智慧合約程式碼-Transfer部分解析

最近在學習 Ton 鏈的智慧合約,由於我之前的經驗思維主要是集中在以太坊這條鏈的,即Solidity那套,所以帶著長久偏向的思維去閱讀 Ton 的合約時發現格格不入,Ton 的合約設計與EVM體系的屬於天壤之別。

首先 Ton 的合約是分片的,遵循 Parent-Child 的規則,這裡詳細瞭解見:

https://blog.ton.org/how-to-shard-your-ton-smart-contract-and-why-studying-the-anatomy-of-tons-jettons

其次是合約開發的語言,Ton 有三種,用得最多的是 FunC,這是一種完全的非主流語言,在 GitHub 上都沒有特點標識的那種。


按照最快了解 Token 智慧合約的方式,尋找到官方的合約程式碼專案。由於Ton 的經濟 Token 程式碼目前還沒有類似以太坊的各種模型協議,只能把對應以太坊ERC-20的那部分取下來進行閱讀。

下面我將結合Token的轉賬核心操作的原始碼來對其整個呼叫鏈路 進行細緻的分析講解,所選程式碼片段也有註釋。

先了解合約模式
  • Ton 的合約是分片的,拿 Token 型別的合約做例子,其做法是將一份主合約,被稱為 Master 或 Minter 的合約獨一份進行部署,再將和 User 的子合約在轉賬進行時進行新建形式的一一對應部署。
  • 比如說,釋出一份名叫 NOT 的 Token 合約,它的 Master 合約將被部署在鏈上,然後對於後續每一位收到 NOT token 的使用者地址,若不存在就都會建立一份與該地址對應的子合約,稱為 Wallet 合約。
  • 在 Token 型別的合約中,Master 合約中儲存了 Token 的公共資訊,比如 Name,Metaurl,Supply 等,而Transfer 轉賬行為卻都發生在各自的 Wallet 合約裡面。
  • 為 User 建立 Wallet 合約都要經過 Master 進行。
  • 合約允許各自內部呼叫,A 合約呼叫 B 合約的函式。

客戶端-發起轉賬 Token 的流程

例子取於 Golang 客戶端專案程式碼。

func main() {
   ...
   // 初始化自己的錢包
   w, _ := wallet.FromSeed(api, words, wallet.V3R2)
   // 根據該 Token 的 Master 合約地址初始化 Token
   token := jetton.NewJettonMasterClient(api, address.MustParseAddr("EQD0vdS......"))
   // 呼叫 Master 的合約函式獲取轉賬者的 Wallet 合約
   tokenWallet, _ := token.GetJettonWallet(ctx, w.WalletAddress())
   tokenBalance, _ := tokenWallet.GetBalance(ctx)
   amountTokens := tlb.MustFromDecimal("0.1", 9)
   // 轉賬附帶的資訊
   comment, _ := wallet.CreateCommentCell("Hello from tonutils-go!")
   // 初始化收款者的地址,這不是 Wallet 地址
   to := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")
   // 在 BuildTransferPayloadV2 裡指定了 OP = Transfer
   transferPayload, _ := tokenWallet.BuildTransferPayloadV2(to, to, amountTokens, tlb.ZeroCoins, comment, nil)
   // 構造鏈上請求的訊息結構
   msg := wallet.SimpleMessage(tokenWallet.Address(), tlb.MustFromTON("0.05"), transferPayload)
   // 傳送轉賬交易,然後結束
   tx, _, _ := w.SendWaitTransaction(ctx, msg)
   log.Println("transaction confirmed, hash:", base64.StdEncoding.EncodeToString(tx.Hash))
}

上述程式碼可以看到在發起轉賬的時候,收款地址並不是 User 的錢包地址,而是其對應的 Wallet 合約地址。這一點就和包括以太坊在內的絕大部分公鏈都不一樣。

合約端對應的轉賬入口程式碼解析

內部訊息的入口函式,根據 op 引數指定呼叫入口。

() 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);
  if (flags & 1) {
    on_bounce(in_msg_body);
    return ();
  }
  slice sender_address = cs~load_msg_addr();
  cs~load_msg_addr(); ;; skip dst
  cs~load_coins(); ;; skip value
  cs~skip_bits(1); ;; skip extracurrency collection
  cs~load_coins(); ;; skip ihr_fee
  int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs

  int op = in_msg_body~load_uint(32);

  if (op == op::transfer()) { ;; outgoing transfer
    ;; sender_address 是一開始的轉賬者
    ;; msg_value 是改次轉賬中的 Ton 數額
    send_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
    return ();
  }

  if (op == op::internal_transfer()) { ;; incoming transfer
    ;; my_balance 是當前所執行的合約所有者的 Ton 餘額
    receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value);
    return ();
  }

  if (op == op::burn()) { ;; burn
    burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
    return ();
  }

  throw(0xffff);
}
  • recv_internal 是系統內建的函式入口,相當於 main;
  • 系統函式還有:load_datasave_data,載入的是當前合約的資料,儲存也是儲存到當前合約。程式碼中的變數 jetton_master_address 地址永遠是父合約地址
  • 轉賬發起時,指定 op 是 transfer,走到程式碼處理點 op == op::transfer,進入到 send_tokens
  • send_tokens函式原始碼及其解析註釋內容見下👇
() send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure {
  int query_id = in_msg_body~load_uint(64);
  int jetton_amount = in_msg_body~load_coins();
  slice to_owner_address = in_msg_body~load_msg_addr(); ;; 收款人 
  force_chain(to_owner_address);
  ;; owner_address 轉賬者,jetton_master_address Token主地址
  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
  ;; msg_value 是改次轉賬中的 Ton 數額,不是 token 餘額
  balance -= jetton_amount; ;; balance 是轉賬者餘額,jetton_amount 是要轉的數額

  throw_unless(705, equal_slices(owner_address, sender_address)); ;; 要求發起轉賬的人一致 
  throw_unless(706, balance >= 0); ;; 要求減去 jetton_amount 餘額大於 0,防止超出

  cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code);
  slice to_wallet_address = calculate_jetton_wallet_address(state_init);
  slice response_address = in_msg_body~load_msg_addr(); ;; 轉賬結束後要被通知到的地址 
  cell custom_payload = in_msg_body~load_dict();
  int forward_ton_amount = in_msg_body~load_coins(); ;; 附屬的要轉的 Ton 的數額,可以是 0,客戶端賦值
  throw_unless(708, slice_bits(in_msg_body) >= 1);
  slice either_forward_payload = in_msg_body;
  var msg = begin_cell()
    .store_uint(0x18, 6)
    .store_slice(to_wallet_address) ;; 走到收款人的合約處
    .store_coins(0) 
    .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
    .store_ref(state_init); ;; 如果對方沒被部署過 wallet 合約,那麼這個訊息會觸發部署
  var msg_body = begin_cell()
    .store_uint(op::internal_transfer(), 32) ;; internal_transfer 引導到下一個 op
    .store_uint(query_id, 64)
    .store_coins(jetton_amount)
    .store_slice(owner_address) ;; owner_address 轉賬者
    .store_slice(response_address) ;; 轉賬結束後要被通知到的地址 
    .store_coins(forward_ton_amount)
    .store_slice(either_forward_payload)
    .end_cell();

  msg = msg.store_ref(msg_body);
  int fwd_count = forward_ton_amount ? 2 : 1;
  throw_unless(709, msg_value >
                     forward_ton_amount +
                     ;; 3 messages: wal1->wal2,  wal2->owner, wal2->response
                     ;; but last one is optional (it is ok if it fails)
                     fwd_count * fwd_fee +
                     (2 * gas_consumption() + min_tons_for_storage()));
                     ;; universal message send fee calculation may be activated here
                     ;; by using this instead of fwd_fee
                     ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15)

  send_raw_message(msg.end_cell(), 64); ;; revert on errors
  ;; 如果 send_raw_message 沒出錯,那麼下面就會完成最後一步
  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); ;; 儲存轉賬者的 token 餘額
}
  • send_tokens 裡面,還會進行一次內部合約呼叫,呼叫到收款人的 Wallet 合約,對應到 op 是 internal_transfer,而 internal_transfer 的處理函式是 receive_tokens
  • receive_tokens函式原始碼及其解析註釋內容見下👇
() receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure {
  ;; NOTE we can not allow fails in action phase since in that case there will be
  ;; no bounce. Thus check and throw in computation phase.
  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
  int query_id = in_msg_body~load_uint(64);
  int jetton_amount = in_msg_body~load_coins(); ;; token 代幣餘額
  balance += jetton_amount; ;; token 代幣餘額累加
  slice from_address = in_msg_body~load_msg_addr(); ;; 原始的 Token 轉賬者
  slice response_address = in_msg_body~load_msg_addr(); ;; 轉賬結束後要被通知到的地址
  ;; sender_address 系統地址,意味著這個函式只能由系統內部呼叫,排除了外部呼叫
  throw_unless(707,
      equal_slices(jetton_master_address, sender_address)
      |
      equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address)
  );
  int forward_ton_amount = in_msg_body~load_coins(); ;; 附屬要轉賬 ton 數值

  int ton_balance_before_msg = my_ton_balance - msg_value;
  int storage_fee = min_tons_for_storage() - min(ton_balance_before_msg, min_tons_for_storage());
  msg_value -= (storage_fee + gas_consumption());
  if(forward_ton_amount) { ;; 附屬要轉賬 ton,如果不是 0
    msg_value -= (forward_ton_amount + fwd_fee);
    slice either_forward_payload = in_msg_body;

    var msg_body = begin_cell()
        .store_uint(op::transfer_notification(), 32)
        .store_uint(query_id, 64)
        .store_coins(jetton_amount)
        .store_slice(from_address)
        .store_slice(either_forward_payload)
        .end_cell();

    var msg = begin_cell()
      .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract
      .store_slice(owner_address) ;; 當前 Token 收款人地址
      .store_coins(forward_ton_amount) ;; 收款人加上這部分附屬的 Ton。付款人減去
      .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
      .store_ref(msg_body);

    send_raw_message(msg.end_cell(), 1);
  }

  if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) {
    var msg = begin_cell()
      .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
      .store_slice(response_address) 
      .store_coins(msg_value) ;; 超過的 Ton 手續費退款到這個地址
      .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
      .store_uint(op::excesses(), 32)
      .store_uint(query_id, 64);
    send_raw_message(msg.end_cell(), 2);
  }
  ;; 下面為收款人加上 Token 
  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
}
  • 走完 receive_tokens 之後,整個轉賬行為就在鏈上閉環了。
注意:上面的原始碼實現部分比較繞,務必結合註釋閱讀。

相關文章