文章

后端与合约交互安全点梳理

常见安全点&最佳实践

1. 私钥安全

  • 不要在代码、GitHub或云端明文存储私钥,避免暴露资产。

  • 尽量用环境变量、硬件钱包(Ledger、Trezor)、或专门的密钥管理服务(如 HashiCorp Vault)。

  • 不要将助记词/私钥硬编码在脚本中。

2. RPC 端点安全

  • 不要将敏感资产连到公开或不信任的节点。选用受信任的节点服务商(如 Infura/Alchemy)。

  • 如果自己搭节点,注意加密/白名单访问。

3. 合约 ABI 与地址校验

  • 指定的 ABI 必须与部署的合约匹配,否则会导致调用异常甚至损失资金。

  • 合约地址必须核对清楚,防止钓鱼攻击。

合约地址校验方法

  1. 地址格式基本校验

  • Ethereum 地址为 0x 开头的 40 位十六进制字符串。

  • 使用 web3 工具内置的 isAddress 方法:

from web3 import Web3
Web3.isAddress("0x...")  # 返回 True/False
  1. 地址是否合约地址判断

  • 查询该地址在链上有没有已部署合约(即代码长度是否大于 0):

code = web3.eth.get_code("0x...")  # 返回 Bytecode
is_contract = len(code) > 2  # 空合约返回 '0x'
  • 可以用 Etherscan、区块浏览器多重确认是不是已部署的合约。

  1. 聚合认证:

  • 查区块浏览器(如 Etherscan、OKLink)、项目官网、官方文档或公告的合约地址,避免钓鱼或山寨合约。


ABI 校验方法

  1. 与合约实际接口对比

  • 拿到的 ABI 应与链上实际合约源代码匹配(可对比函数名、参数数量和类型)。

  • 优选来自可信渠道的 ABI(Etherscan 验证过的合约有“合约 ABI”公开页面)。

  • 建立自己的热点合约库,保证数据的独立性。

  1. 动态 ABI 验证(接口探查法)

  • 用 web3 或 ethers 连接合约后,尝试静态 call 函数看能否正常返回。

  • 调用已知无风险 view/pure 方法,验证函数响应情况。

  1. hash 校验(针对升级合约/源码校验)

  • 可用编译工具对源码输出的 ABI 进行 hash(如 SHA256),与团队或社区公开 hash 校验一致性。

  • Sourcify 可自动校验某地址代码和公开源码一致性。

补充

Sourcify 是一个智能合约源码验证和合约溯源的开源服务和生态,由 Ethereum Foundation 支持,其核心目标是提升智能合约链上透明度和可审计性。它对于开发者安全地确认、比对和追溯链上合约的源代码有重要作用。下面详细说明其功能和作用:


一、Sourcify 的核心功能和作用

1. 智能合约源码验证与溯源

  • 用户可以上传自己合约的源码、编译器参数、ABI 等元信息,Sourcify 系统会将其与链上已部署的合约字节码(Bytecode)做自动比对。

  • 如果完全一致,就“验证”通过,表明这个链上地址确实就是由该源码编译部署的。

2. 链上“溯源标签”

  • 成功验证的合约会获得一个“verified”或“perfect”标记,任何人都可以公开查阅其对应的源代码。

  • 这种机制防止开发者发布无法追溯的合约,从而降低对用户和外部审计的透明度风险。

3. 去中心化开源验证

  • 支持包括以太坊主网、测试网和很多 EVM 兼容链,由社区共同维护并可自建验证节点。

4. 一键识别 & 检索

  • 任何人可以在 Sourcify 上输入一个合约地址自动检索其是否已被验证,并查看源代码和合约依赖(import 路径等)。

  • 集成了“合约元数据”(metadata),便于开发者与安全人员更方便地查阅所有历史版本和依赖库。

5. 自动生成 ABI、调用参数等开发辅助

  • 已验证的合约可一键下载对应 ABI/interface,加快开发与检测流程,也易于安全分析和自动化调用。

6. 跨链和代理合约检测

  • 支持代理合约实际逻辑地址的解析和比对,可提升安全工程师和用户对 proxy pattern 的可验证性。


二、Sourcify 和 Etherscan 有何区别?

  • Etherscan 是中心化区块链浏览器,提供源码验证等服务,但其数据库和审核归网站所有。

  • Sourcify 是完全开源、分布式的生态系统,主打社区驱动的验证和索引,且支持多条链(很多链原生不依赖 Etherscan)。

  • 如果一个合约被 Sourcify 验证过,其他任何 DApp、前端或安全工具都可以更自动化地“溯源”其源码,重现部署过程。


三、典型使用场景

  1. 合约开发者 / DApp 团队发布合约时上传源码进行验证 (促进透明可信)。

  2. 普通用户与审计员查询任意地址对应的源码和版本,核实项目安全性

  3. 安全工具集成 Sourcify 查询,自动判断合约与源码匹配,防止调用未知/假合约

4. 调用方式与 Gas 策略

  • 明确调用 read(view/pure)方法与 write(非 view 方法);写操作需消耗 Gas。

  • 合理设置 gas limit,避免 OOG(Out of Gas)。

  • 注意重入与 re-entrancy 风险(比如 ERC-20 approve+transferFrom 执行时)。

重入攻击原理:

核心原理

重入攻击是攻击者通过外部合约调用链上的目标合约,在目标合约还没完成本次操作(如还未更新余额、状态还没变)时,反复重新进入目标合约关键函数,致使一些代码被重复执行,从而造成意料之外的结果(如大量盗取资金)。


攻击发生的典型场景

1. 目标合约中的典型漏洞代码

// 不安全的提款函数
function withdraw(uint256 _amount) public {
    require(balances[msg.sender] >= _amount, "Insufficient balance");
    // 步骤1:先给调用者转账
    (bool sent, ) = msg.sender.call{value: _amount}("");
    require(sent, "Failed to send Ether");
    // 步骤2:再扣除余额(状态更新)
    balances[msg.sender] -= _amount;
}
  • 关键问题:先给用户打钱,然后才扣余额。如果msg.sender是一个合约账户,这个转账会触发对方的receive或者fallback函数。


2. 攻击者构造的钓鱼合约结构

contract Attack {
    address public victim;
    constructor(address _victim) { victim = _victim; }

    // 用于收到ETH时反复调用victim合约
    receive() external payable {
        if (victim.balance >= 1 ether) {
            // 重入调用漏洞函数
            Victim(victim).withdraw(1 ether);
        }
    }

    function attack() external payable {
        // 先存钱到目标合约
        Victim(victim).deposit{value: 1 ether}();
        // 然后发起提取
        Victim(victim).withdraw(1 ether);
    }
}

interface Victim {
    function deposit() external payable;
    function withdraw(uint256) external;
}

3. 攻击流程详解

假如攻击者在Attack.attack()中存入1 ETH,然后调用withdraw(1 ether)

步骤解析(每一步是 EVM 执行序列):

  1. Victim.withdraw(1) 检查攻击者余额(满足)。

  2. 执行转账 (bool sent, ) = msg.sender.call{value: 1 ether}("");

  • 这里msg.sender其实就是攻击者的合约,EVM 在转账时调用了Attack.receive()

  1. Attack.receive() 里,发现 victim.balance >= 1 ether,又调用了一遍目标合约的 withdraw(1 ether)

  2. 由于Victim 的 balances 还没“减1”,检查通过,重复步骤2…

  3. 如此循环N次(取决于Gas限制),每次都转1 ETH走,相当于攻击者1 ETH能多次提取(而实际上只有1 ETH本金)。

最后:

  • 状态变量balances[msg.sender] -= _amount总是在所有交易转账完成之后才执行,这就是被反复利用的“窗口”。


可视化流程

一张简单的流程图:

Attack合约.attack() 
  |
  |--> Victim.withdraw() 
           |
           |-----> Attack合约.receive() 
                         |
                         |-----> Victim.withdraw() 
                                       |
                                       ...(嵌套继续)

为什么攻击会得逞?

  • EVM 允许递归外部调用(合约对合约),并且转账时可以执行fallback/receive逻辑。

  • 只要Victim合约的状态更新排在“外部调用”之后,就让攻击者有机可乘。

  • Solidity 在低版本强烈建议用transfer,因其固定位gas 2300,限制复杂逻辑执行。但call没有这个限制,若直接用call+外部调用,重入风险极高。

5. 代币授权 Approve/Allowance

  • 避免将 allowance 设置为最大值(如 2**256-1),授予最小必要权限。

  • 经常 revoke(0 allowance 重置)已不再使用的 DApp 授权。

6. 重放攻击与事务签名

  • 使用 chainId 避免跨链重放攻击(EIP-155)。

  • 签名数据要检查使用的 domain separator,防范签名钓鱼。

补充Domain Separator

Domain Separator(域分隔符),本质上是一段哈希数据,相当于用户签名“属于哪个DApp、哪个合约、哪个网络”的数字标签。

在 EIP-712(结构化数据签名标准)中,签名不是单纯的消息文本,而是“域数据”+“实际内容数据”打包后的哈希,结构大致如下:

EIP-712 结构签名 = keccak256(
  "\x19\x01"
  domainSeparator        // 用于分隔“域”
  structHash            // 具体消息内容的Hash
)

domainSeparator 通常包含:

  • 合约名称 (name)

  • 合约版本 (version)

  • 合约地址 (verifyingContract)

  • 链ID (chainId)

如:MyDapp,v1,0x5101...,chainId 1


为何要有 Domain Separator?

1. 防止签名钓鱼

举例说明:

  • 假如用户在A DApp网站签名了“我同意转账100 USDT给 XX 地址”的结构化数据。

  • 如果没有 domainSeparator,B DApp或恶意合约可能获取到你原始签名,重新拼装到自己环境里“验过签”,实现完全不同的行为(比如A上只是抽奖签名,B上变成授权提币),即“签名重放”。

domainSeparator强制把DApp/合约/链等信息和数据混合后打包签名,导致签名只能在原来的场景解包生效,跨协议(DApp、链、合约)就自然验证失败。

2. 提升签名的数据唯一性/安全隔离

这样即便签名的数据内容完全一样,如果是在不同合约的 domain 下,签名 hash 也完全不同。


开发和审计上如何“检查 domain separator”

1. 对前端/合约开发者

  • 前端准备标准 EIP-712 消息对象时,domain 字段要覆盖 name/version/chainId/verifyingContract 这些关键信息,并且与合约实际地址对应。

  • 后端/合约端验证签名时,也要按照同样 domain 生成 domainSeparator,确保和前端一直。

示例 EIP-712 TypeScript签名配置:

const domain = {
  name: "MyDapp",
  version: "1",
  chainId: 1,
  verifyingContract: "0x123456...789"
};
const types = { /* ... */ };
const value = { /* ... */ };

// 用户签名出去的数据(MetaMask等会弹窗展示Domain信息,提升用户识别风险)
signTypedData(domain, types, value);

2. 对安全审计/内核开发:

  • 明确合约端 verifySignature 函数里用的 domain separator 和前端保持一致。

  • 避免项目只用简单 hash 消息或 EIP-191 非结构化签名,容易遇见被复用的“签名钓鱼”手法。

3. 对普通用户:

  • 签名前注意钱包弹窗的“域名确认”内容(如 MetaMask 现在会弹出:你正在为哪个 DApp,哪个链,哪个合约地址签名),

  • 如果出现“你签名的不是本 DApp/本合约”的内容,一定要警惕。


典型的签名钓鱼风险案例

  • 钓鱼网站获取用户签名后,恶意拼装到其他平台上乱用。

  • 或如未加 domain 的普通 eth_sign、personal_sign,被黑客利用伪造授权。

  • 也有NFT市场、DAO投票方案历史上因为没用 EIP-712 domain,导致用户签名容易被复用或被盗。

7. 数据验证

  • 与合约交互的输入参数要做本地校验,避免参数注入、数据溢出等风险。

8. 防止钓鱼合约

  • 确认证实合约来源和代码开源可读,因为前端可以随意伪造 ABI 诱导操作危险合约。(校验或建立合约库)

9. 交易回执核查

  • 要检查合约交互返回的交易 receipts 和状态,否则可能以为执行成功实际失败。

10. 多线程/批量操作处理

  • 并行交易时要处理 nonce 分配、并发写冲突问题,避免意外的 nonce 重复或者交易失败。

11. 链上数据同步延迟

  • 监听事件/读取状态时,要考虑到区块确认延迟及链上状态不一致。

12. 权限管理/合约升级

  • 与代理合约交互时确认当前逻辑合约的实现(防范升级漏洞)。

  • 调用 sensitive 方法需确保对方身份和权限。

13. 接口限额/防 DDoS

  • 自建 RPC 需防止批量恶意调用造成资产/服务损耗。

许可协议:  CC BY 4.0