你可能写好了合约,也部署上了链,
但你有没有想过:用户与合约交互这一环,有多少“暗门”?
用户钱包点了“授权”,结果资产全被转走?
网站前端被注入,合约地址被调包?
前端签名生成数据,后端无法验证合法性?
签名的消息被篡改、复用、二次 broadcast?
本章聚焦合约×前端×钱包交互的安全链条:
风险点类别 | 内容 |
---|---|
授权风险(approve) | ERC20/721 授权机制 × 滥用危害 |
签名风险(sign) | 签名复用、钓鱼、不可验证问题 |
前端集成漏洞 | 合约地址注入、ABI 不匹配、假 wallet |
消息验证机制 | EIP-712 结构化签名安全校验 |
DApp 接入安全指南 | 使用 Ethers.js × 安全通信架构 |
approve()
?允许第三方地址(通常是合约)从你钱包转出指定数量的 Token。
token.approve(spender, amount);
授权钓鱼网站 → 用户资产直接被转出
前端地址篡改 → 合约替换为攻击者部署的 clone
忘记限制额度 / 不重置为 0 → 无限制提取
用户授权“Uniswap 兑换合约”,结果是攻击者 clone
一旦用户点了 approve,攻击者调用 transferFrom
直接清空资产
前端地址白名单(合约地址 hardcoded)
使用 approve(0)
重置再设置新的额度
建议使用 permit()
(EIP-2612)进行签名授权(gasless)
显示明确授权信息(合约名称 / 授权额度 / 合约链接)
前端使用 Ethers.js 发起:
signer.signMessage("I approve to claim reward");
或结构化签名:
signer._signTypedData(domain, types, value);
⚠️ 但如果你签的是一段“伪造的交易数据”,攻击者可用它直接发交易!
用户签署了一份“看起来像登录验证”的消息
实际该消息是 Seaport
协议的订单数据
签名被攻击者 broadcast,直接卖出了用户的 NFT
环节 | 安全建议 |
---|---|
前端提示 | 明确展示签名类型(登录 / 授权 / 上链交易) |
使用 EIP712 | 结构化签名避免消息被复用 |
后端验证 | 验证签名来源 + 内容 hash + 有效时间戳 |
禁止重放 | 签名信息中加入 nonce 或一次性随机 token |
不接受 signMessage 签名用于“高价值动作” |
改为 signTypedData + 结构化 verify |
合约地址 hardcode 错误 / 未更新
用户连上 RPC → 签名 / 合约地址全是假的
ABI 与部署版本不一致,导致参数打包错位
多链合约地址混淆,用户部署错链 / 授权错地址
风险点 | 安全建议 |
---|---|
合约地址配置 | 使用 .env / 网络动态加载 + 地址白名单 |
ABI 数据来源 | 自动与 Etherscan / Hardhat 编译同步 |
钱包连接 | 使用 wallet_client 类型安全封装 |
网络识别 | 限制只允许支持的链(如 mainnet + polygon) |
多签 / 权限账户交互 | 强制使用 Gnosis / 多签流程隔离 |
const domain = {
name: "MyApp",
version: "1",
chainId: 1,
verifyingContract: contractAddress
};
const types = {
Permit: [
{ name: "user", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
const value = {
user: userAddress,
nonce: 1,
deadline: 1717181718
};
const signature = await signer._signTypedData(domain, types, value);
const recovered = ethers.utils.verifyTypedData(domain, types, value, signature);
if (recovered !== userAddress) throw new Error("签名不合法");
function verify(
address user,
uint256 nonce,
uint256 deadline,
bytes calldata signature
) public view returns (bool) {
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, user, nonce, deadline));
bytes32 digest = _hashTypedDataV4(structHash);
return ECDSA.recover(digest, signature) == user;
}
检查点 | 是否通过 |
---|---|
所有授权合约是否加入额度限制 / 防调包? | ✅/❌ |
是否使用结构化签名(EIP-712)验证登录? | ✅/❌ |
是否校验签名 nonce / 有效期? | ✅/❌ |
是否强制限定前端访问链? | ✅/❌ |
是否显示完整授权/签名信息给用户? | ✅/❌ |
是否有前端合约地址与链不一致提示? | ✅/❌ |
写一个结构化登录签名模块,配合后端实现安全登录验证
编写一个 DApp 页面,用户需授权 Token,再调用合约方法
实现授权额度限制
签名内容使用 signTypedData
后端校验签名来源 + nonce
尝试使用 WalletConnect / MetaMask,部署一个“合约连接安全组件”
合约安全 ≠ 前端交互安全,用户点击的每一个按钮,可能都通向危险
授权逻辑、签名消息、合约地址、ABI、链 ID,这些都必须明确验证
推荐使用结构化签名(EIP-712)+ 后端验签 + 多链地址控制体系
流动性挖矿怎么被“吸干”?
DAO 治理提案如何被塞进恶意逻辑?
NFT Game 合约如何反复 claim 奖励?
实战复现:攻击路径 × 修复方式 × 风险评估