Solidity中payable函数msg.value接收以太行为差异问题
问题背景
- 两个独立智能合约分别实现了
payable类型函数:BuyLand与depositEthers,两个函数内部均编写了条件校验逻辑 - 实际测试观测到资金接收表现存在明显差异:
BuyLand函数无论内部校验逻辑是否通过,都会接收到调用时传入的以太depositEthers函数仅在所有校验条件通过时才会接收以太,校验失败回滚时不会接收对应资金
- 核心疑问:调用
payable函数并在交易的value字段传入指定数额的以太时,是否无论函数内部的校验条件、分支判断是否通过,函数所属合约都一定会接收到该笔传入的以太?
两个函数的实现代码如下:
// 1st Function function depositEthers() public payable { require(users[msg.sender].flag != 0, "You are not a registered user, get yourself registered first"); require(msg.value > 0, "No Ethers was sent, Please send Ethers"); users[msg.sender].balance += msg.value; } // 2nd Function function BuyLand(uint _landId) public payable { require(landOwnerMapping[_landId] != msg.sender, "You can not Buy Land because you are the Owner"); require(BuyerMapping[msg.sender].isVerified == true, "Buyer is not verified"); require(SellerMapping[landOwnerMapping[_landId]].isVerified == true, "Seller is not verified"); require(Lands[_landId].isVerified == true, "Land is not verified"); if (msg.value > Lands[_landId].LandPrice*1000000000000000000) { //payable(msg.sender).transfer(address(this).balance); emit buyingLand("Land not bought, sent more Ethers than Land price", _landId, Lands[_landId].LandPrice, landOwnerMapping[_landId], msg.sender); } else if (msg.value < Lands[_landId].LandPrice*1000000000000000000) { //payable(msg.sender).transfer(address(this).balance); emit buyingLand("Land not bought, sent less Ethers than Land price", _landId, Lands[_landId].LandPrice, landOwnerMapping[_landId], msg.sender); } else { payable(landOwnerMapping[_landId]).transfer(msg.value); landOwnerMapping[_landId] = msg.sender; ownerMapping[msg.sender] = _landId; emit buyingLand("Land bought successfully", _landId, Lands[_landId].LandPrice, landOwnerMapping[_landId], msg.sender); } }
核心规则
EVM的交易执行遵循原子性原则:只有当交易整体执行成功、没有触发回滚(revert)时,交易中附带的以太才会真正转入合约地址;只要交易触发回滚,所有状态变更(包括资金转移)都会被完全撤销,合约不会收到任何资金。
两个payable函数表现不一致,本质是两个函数在校验失败时的处理逻辑完全不同,和payable关键字本身无关。payable关键字的作用仅仅是允许函数在被调用时接收附带msg.value的交易——如果非payable函数被调用时传入了value会直接报错回滚,但它本身不决定资金最终是否会留在合约里。
行为差异原因
depositEthers 函数
这个函数的校验全部通过require实现:
- 只要任意一个
require条件不满足,就会直接触发交易回滚,所有状态变更(包括msg.value对应的资金转入)都会被撤销,资金不会留在合约里 - 只有所有
require都通过,函数执行完整流程不触发回滚,资金才会真正计入合约余额,和观测到的表现完全一致。
BuyLand 函数
这个函数的逻辑和前者存在本质区别:
- 函数最开头的4个
require如果不满足,同样会触发回滚,此时合约也收不到资金 - 观测到的「校验不通过也收到以太」,实际是走到了后面
msg.value大于/小于土地价格的两个if/else if分支:这两个分支只触发了事件,没有调用revert/require抛出异常回滚交易,交易本身是执行成功的,只是没有走购买成功的逻辑。此时传入的以太已经因为交易成功留在了合约里,而你注释掉了这两个分支里给用户退钱的transfer代码,所以资金就永久滞留在合约里了。
风险提示
当前BuyLand函数存在明显的资金漏洞:用户传错金额时不会回滚交易,也不会退款,多余/不足的资金都会被锁在合约里无法取出。如果要实现「金额不对就不收钱」的逻辑,要么在金额不匹配的分支里加revert终止交易,要么保留注释掉的退款逻辑,把多收的钱转回给用户。
内容的提问来源于stack exchange,提问作者M Husnain Abbas
相关产品推荐
相关产品推荐

