今天介紹一個寫合約的設計模式:Pull Payment Pattern (pull-over-push pattern)
智能合約常常被比喻為自動販賣機,使用者投錢,機器執行 function,吐出選擇的飲料。如果要找零,販賣機會在給飲料的同時即時找零。
然而,智能合約應該避免這樣的設計。好的合約傾向將退款的動作獨立寫成一個 function,當用戶主動領取退款時才轉錢。
以下是一個不良示範:
// bad
contract auction {
address highestBidder;
uint highestBid;
function bid() payable {
require(msg.value > highestBid);
if (highestBidder != address(0)) {
(bool success, ) = highestBidder.call{value: highestBid}("");
require(success); // if this call consistently fails, no one else can bid
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
這是一個第一價格拍賣合約,可以看到 bid function 在使用者出價後,直接將上一個最高出價者的錢退還給他,接著更新合約的狀態。
這個 external call 可能失敗。如果上一個最高出價者使用合約來呼叫 bid,而他的合約 fallback 就是 revert,此時轉錢過程會導致交易失敗,合約狀態會 rollback,使得 bid 將永遠無法被執行。
遇到 external call 要預設有失敗的可能性。因此我們將退款邏輯移至另一個 function,使用 mapping(address => uint) refunds
紀錄退款者的地址和金額,讓用戶主動提出退款請求。
// good
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
require(msg.value > highestBid);
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid; // record the refund that this user can claim
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: refund}("");
require(success);
}
}
不要直接將錢匯到用戶的地址,而是幫他們存起來,讓他們自己來提領。