CertiK 與 TON 共同發布了最新的 TON 生態開發者指南,旨在幫助開發者在使用 Tact 語言程式設計時避免常見錯誤與陷阱。
作者:Certik
封面: Photo by ilgmyzin on Unsplash
TON(The Open Network)以其創新特性和強大的智慧合約效能,不斷拓寬區塊鏈技術的邊界。基於早期的區塊鏈平台(如以太坊等)的經驗與教訓,TON 為開發者提供了一個更有效率且靈活的開發環境。其中推動這項進步的關鍵要素之一就是 Tact 程式語言。
Tact 是專為 TON 鏈設計的一種全新程式語言,以高效與簡潔為核心目標。它易於學習和使用,並與智能合約完美契合。 Tact 是一種靜態類型語言,擁有簡單的語法和強大的類型系統。
儘管如此,開發者在使用 FunC 時遇到的許多問題,在 Tact 開發中仍然存在。以下將結合審計實務案例,分析 Tact 開發中的一些常見錯誤。

資料結構
可選地址
Tact 語言簡化了聲明、解碼和編碼的資料結構。然而,開發者仍需保持謹慎。我們來看一個例子:

這是根據 TEP-74 [1]標準用於轉移 jetton 的內部傳輸(InternalTransfer)訊息聲明。請注意 response_destination 的聲明,它是一個位址(Address)類型。在 Tact 中,要求位址必須是非零位址。然而,jetton 標準的參考實作 [2]允許零位址(addr_none),它由兩個零位元表示。這表示使用者或其他合約可能會嘗試發送帶有零回應位址的 jetton,而該操作會意外失敗。
此外,如果用戶發送給其錢包的 Transfer 訊息允許設定 response_destination,而從發送方錢包到接收方錢包的 InternalTransfer 訊息卻不支援該參數,那麼 jetton 將會 “飛出”,意味著 jetton 無法到達目標位址,最終導致丟失。稍後,我們將討論一種例外情況,即如何正確處理被退回的訊息。息會被妥善處理。
在這種情況下,允許零地址的更好結構聲明應為 Address?,但在 Tact 中,將可選地址傳遞到下一則訊息目前較為繁瑣。
資料序列化
在 Tact 中,開發者可以指定欄位的序列化方式。

本例中,totalAmount 將序列化為 coins,而 releasedAmount 將序列化為 int257(預設為 Int)。 releasedAmount 可以是負值,並且將佔用 257 位元。在大多數情況下,省略序列化類型不會帶來問題;然而,如果資料涉及通信,這就變得至關重要。
以下是我們審計的項目中的一個例子:

此資料結構是由 NFT 專案用作對鏈上 get_static_data [3]請求的回應。根據標準,回覆應該是:

上述索引是 uint256(而不是 int257),這意味著傳回的資料將被呼叫者錯誤解讀,從而導致不可預測的結果。很可能的結果是 report_static_data 處理程序會發生回滾,訊息流也會因此中斷。這些例子說明了為什麼即使在使用 Tact 時,考慮資料序列化也是至關重要的。
有符號整數
不指定 Int 的序列化類型可能會導致比上述範例更嚴重的後果。與 coins 不同,int257 可以是負值,這常常會讓程式設計師感到驚訝。例如,在 Tact 的即時合約中,看到 amount: Int 是極其常見的。

這種寫法本身並不一定意味著存在漏洞,因為該金額(amount)通常會被編碼到 JettonTransfer 訊息中,或傳遞到 send(SendParameters{value: amount}),後者使用的是 coins 類型,不允許負值。然而,在一個案例中,我們發現一個擁有大量餘額的合約,它允許用戶將所有值設為負數,包括獎勵、手續費、金額、價格等。因此,惡意行為者可能會利用此漏洞進行攻擊。
並行
以太坊鏈的開發者必須注意重入攻擊,即在當前函數執行完成之前,能夠再次呼叫同一個合約的函數。而在 TON 鏈上,重入攻擊是不可能的。
由於 TON 是一個支援非同步和平行智慧合約呼叫的系統,追蹤處理動作的順序可能變得更加困難。任何內部訊息都會被目標帳戶接收,交易結果會在交易本身之後處理,但並沒有其他保證(有關訊息傳遞的更多資訊請參見相關文件 [4])。

我們無法預測訊息 3 或訊息 4 哪個會先被送達。
在這種情況下,中間人攻擊(Man-in-the-Middle Attack)[5]在訊息流中是高發的攻擊類型。為了確保安全,開發者應該設定每個訊息的傳遞時間為 1 到 100 秒,在此期間,任何其他訊息都有可能被傳遞。以下是一些可以提高安全性的其他注意事項:
1. 不要檢查或更新合約狀態以供訊息流的後續步驟使用。
2. 使用攜帶值模式(carry-value pattern)[6]。不要直接發送有關值的信息,而是與訊息一起發送。
以下是一個存在漏洞的真實範例:

在上述範例中,發生了以下步驟:
1. 使用者透過 collection_jetton_wallet 向 NftCollection 發送 jetton。
2. TransferNotification 被傳送到 NftCollection 合約,合約記錄了 received_jetton_amount。
3. 合約將 jetton 轉送給 NftCollection 的所有者。
4. 向 NftCollection 發送 Excesses 訊息,作為 response_destination。
5. NftItem 在 Excesses 處理程序中部署,使用 received_jetton_amount。
這裡有幾個問題要注意:
首先,Excesses 訊息並不能保證按照 jetton 標準被送達。如果沒有足夠的 gas 費用來發送 Excesses 訊息,它將被跳過,訊息流將停止。
其次,更新 received_jetton_amount 並在後續使用它會使系統容易受到並發執行的影響。其他用戶可能會同時發送另一個金額並覆蓋已保存的金額,這也可能會被惡意利用以從中獲利。
在並發的情況下,TON 與傳統的中心化多執行緒系統相似。
處理退回訊息
許多合約忽略了退回訊息的處理。然而,Tact 使這一過程變得簡單明了:

若要決定訊息是否應以可退回模式發送,可以考慮兩個因素:
1. 如果訊息失敗,誰應該收到附加的 Toncoin?如果目標應該接收這些資金,而不是發送合約,那麼就以非可退回模式 [7]發送訊息。
2. 如果下一個訊息被拒絕,訊息流會發生什麼事?如果透過處理退回的訊息可以恢復一致的狀態,那麼最好進行處理。如果不能恢復,最好修改訊息流。
以下是 jetton 標準 [8]中的一個例子:

1. Excesses 訊息以非可退回模式發送,因為合約不需要返回 toncoins。
2. 以非可退回模式發送 TransferNotification 訊息,因為 forward_ton_amount 屬於呼叫者,合約不會保留它。
3. 相反,BurnNotification 是以可退回模式發送,因為如果它被 jetton 主合約退回,錢包需要恢復其餘額,以保持 total_supply 一致。4. InternalTransfer 也是可退回的。如果接收方拒絕資金,發送方的錢包必須更新餘額。請記住以下幾點:
1. 退回訊息僅接收 256 位元 [9]的原始訊息;在訊息識別之後,有效資料僅有 224 位元。因此,你將得到有限的關於失敗操作的信息,通常是儲存為 coins 的某個金額。
2. 如果沒有足夠的 gas 費,退回的訊息將無法送達。3. 退回訊息本身無法再次被退回。
返回 Jetton
在某些情況下,撤銷和處理退回訊息不是一個選項。最常見的例子是當你的合約收到 TransferNotification 關於到達的 jetton 時,退回該訊息可能會導致 jetton 永遠被鎖定。相反,你應該使用 try-catch 區塊 [10]來處理。
讓我們來看一個例子。在 EVM 中,當一筆交易被撤銷時,所有結果都會被回滾(除了 gas——它會被礦工收取)。但在 TVM 中,「交易」被分解為一系列訊息,因此只回滾其中一則訊息很可能會導致「合約群組」狀態不一致。
為了解決這個問題,必須手動檢查所有條件,並在緊急情況下來回發送修正訊息。然而,由於在沒有異常的情況下解析有效載荷非常繁瑣,因此最好使用 try-catch 區塊。
下面是一個典型的 Jetton 接收程式碼範例:

請注意,如果 gas 費用不足,即使是將 jettons 發送回去也無法正常工作。此外,需要注意的是,我們是透過 sender() 的「錢包」回饋 jetton,而不是透過我們合約的實際 jetton 錢包回饋。這是因為任何人都可以手動發送 TransferNotification 訊息來欺騙我們。
管理 Gas 費
在審計 TON 合約時,最常見的問題之一就是 gas 費管理問題。主要原因有兩個:
1. 缺乏 gas 費控制可能導致以下問題:
訊息流執行不完整:部分操作會生效,而另一部分因 gas 不足而被回滾。例如,如果獎勵獲取操作在 jetton 錢包中完成,但銷毀份額操作在 jetton 主合約中被忽略,那麼整個合約組將變得不一致。
使用者可以提取自己的合約餘額:此外合約中可能會累積過多的 Toncoin。2. TON 合約開發者難以管理和控制 gas: Tact 的開發者需要透過測試來獲得 gas 消耗量,並在開發過程中每次更新訊息流時都更新相應的數值。
我們建議的做法如下:
1. 確定「入口點」:這些是所有可以接受來自「外部」訊息的訊息處理器,即來自終端用戶或其他合約(如 Jetton 錢包)。
2. 對於每個入口點,繪製所有可能的路徑併計算 gas 消耗。使用 printTransactionFees()(可在 @ton/sandbox 中找到,工具隨 Blueprint [11]一起提供)。
3. 如果可以在訊息流過程中部署合約,則假設它將被部署。部署將消耗更多的 gas 費和儲存費用。4. 在每個入口點,根據情況增加最低的 gas 要求。

5. 如果處理器不發送更多訊息(訊息流在此終止),那麼最好返回 Excesses,如下所示:

不發送 Excesses 也是可以的,但對於像 Jetton Master 這樣的高吞吐量合約,存在大量 BurnNotification 訊息或大量傳入轉帳的合約,累積金額可能會迅速增長。
6. 如果處理器只發送一條訊息-包括 emit(),實際上是一個外部訊息-最簡單的方式是透過 forward() 傳遞剩餘的 gas 費(見上文)。
7. 如果處理器發送多個訊息,或者如果通訊中涉及 ton 數量,那麼計算應發送金額比計算應剩餘金額要更容易。
在下一個例子中,假設合約希望將 forwardAmount 發送給兩個子合約作為押金:

正如你所看到的,gas 費管理需要高度關注,即使在簡單的情況下。請注意,如果你已經發送了訊息,則不能在 send() 模式中使用 SendRemainingValue 標誌,除非你故意想要從合約餘額中支出資金。
結論
隨著 TON 生態系統的發展,Tact 智能合約的安全開發將變得越來越重要。雖然 Tact 提供了更高的效率和簡潔性,但開發者必須保持警惕,避免常見的陷阱。透過了解常見錯誤並實施最佳實踐,開發者可以充分開發 Tact 的潛力,創建強大且安全的智慧合約。持續學習並遵循安全實踐指南,將確保 TON 生態的創新能力得到安全有效地利用,從而為更安全、可信賴的區塊鏈環境作出貢獻。
[1] TEP-74:https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md#1-transfer [2] 參考實作: https://github.com/ton-blockchain/token-contract/ [3] get_static_data:https://github.com/ton-blockchain/TEPs/blob/master/text/0062-nft-standard.md#2-get_static_data [4] 相關文件: https://docs.ton.org/develop/smart-contracts/guidelines/message-delivery-guarantees#message-delivery [5] 中間人攻擊: https://docs.ton.org/develop/smart-contracts/security/secure-programming#3-expect-a-man-in-the-middle-of-the-message-flow [6] 攜帶值模式: https://docs.ton.org/develop/smart-contracts/security/secure-programming#4-use-a-carry-value-pattern [7] 非可退回模式: https://docs.ton.org/develop/smart-contracts/guidelines/non-bouncable-messages [8] jetton 標準: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md#1-transfer [9] 僅收 256 位元: https://docs.tact-lang.org/book/bounced/#caveats [10] try-catch 區塊: https://docs.tact-lang.org/book/statements#try-catch [11] 藍圖:https://github.com/ton-org/blueprint?tab=readme-ov-file#overview免責聲明:作為區塊鏈資訊平台,本站所發布文章僅代表作者及來賓個人觀點,與 Web3Caff 立場無關。文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。