起因在日常的链上浏览中,笔者留意到一个VerifiedContract。其中有了5ETH(价值约$10,183)的资金。令人警觉的是,该高净值合约的公开交互记录极为异常,仅存在一条历史交易。分析基本信息首先给出合约相关信息:合约地址:0xf327CC34562ac0857C2Ff0
在日常的链上浏览中,笔者留意到一个Verified Contract。其中有了 5 ETH(价值约 $10,183)的资金。令人警觉的是,该高净值合约的公开交互记录极为异常,仅存在一条历史交易。
首先给出合约相关信息:
合约地址:0xf327CC34562ac0857C2Ff033Ce8D8B3713725cFD Etherscan:https://etherscan.io/address/0xf327cc34562ac0857c2ff033ce8d8b3713725cfd 合约代码:
/**
*Submitted for verification at Etherscan.io on 2026-03-30
*/
pragma solidity ^0.8.0;
contract LETS_Play
{
function Try(string memory _response) public payable
{
require(msg.sender == tx.origin);
if(responseHash == keccak256(abi.encode(_response)) && msg.value > 1 ether)
{
payable(msg.sender).transfer(address(this).balance);
}
}
string public question;
bytes32 responseHash;
mapping (bytes32=>bool) admin;
function Start(string calldata _question, string calldata _response) public payable isAdmin{
if(responseHash==0x0){
responseHash = keccak256(abi.encode(_response));
question = _question;
}
}
function Stop() public payable isAdmin {
payable(msg.sender).transfer(address(this).balance);
responseHash = 0x0;
}
function New(string calldata _question, bytes32 _responseHash) public payable isAdmin {
question = _question;
responseHash = _responseHash;
}
constructor(bytes32[] memory admins) {
for(uint256 i=0; i< admins.length; i++){
admin[admins[i]] = true;
}
}
modifier isAdmin(){
require(admin[keccak256(abi.encodePacked(msg.sender))]);
_;
}
fallback() external {}
}
合约实现:一个竞猜合约,admin可以设置一个问题和答案,参与者可以通过try函数提交答案,
首先看全局变量有三个:
string public question:存储admin提出的问题
bytes32 responseHash:存储admin预设答案的hash值
mapping(bytes32 => bool) admin:使用bytes -> Bool来对应是否为管理员,但是实际存储中是使用的address的hash值作为对应关系也就是这里的bytes,防止找到哪些是管理员
再看合约代码只有4个函数
function Try(string memory _response) public payable
function Start(string calldata _question, string calldata _response) public payable isAdmin
function Stop() public payable isAdmin
function New(string calldata _question,bytes32 _responseHash) public payable isAdmin
逐个对函数逻辑进行分析:
function Try(string memory _response) public payable {
require(msg.sender == tx.origin);
if (
responseHash == keccak256(abi.encode(_response)) &&
msg.value > 1 ether
) {
payable(msg.sender).transfer(address(this).balance);
}
}
可以说是整个合约最重要的一个函数,首先判断了msg.sender == tx.origin,阻止外部合约调用,防止闪电贷,然后就是判断传入的_response是否与存储的responseHash相等,如果相等而且转入的以太币多于1 ether就把整个合约的钱都转给调用者。该函数仅设置了条件成立时的转账逻辑,在未通过校验时没有任何信息给出
function Start(
string calldata _question,
string calldata _response
) public payable isAdmin {
if (responseHash == 0x0) {
responseHash = keccak256(abi.encode(_response));
question = _question;
}
}
Start函数为设置问题,如果当前responseHash为0,则设置responseHash为答案的hash,问题则直接改为新提交的问题
function Stop() public payable isAdmin {
payable(msg.sender).transfer(address(this).balance);
responseHash = 0x0;
}
Stop函数:admin可以随时任意结束游戏,存在极大安全风险
function New(
string calldata _question,
bytes32 _responseHash
) public payable isAdmin {
question = _question;
responseHash = _responseHash;
}
New函数:admin可以随时任意更改question和答案hash,存在极大安全风险
constructor(bytes32[] memory admins) {
for (uint256 i = 0; i < admins.length; i++) {
admin[admins[i]] = true;
}
}
构造器中设置admin账户有哪些,将admins数组中的每一个设置true,这里提交的bytes32应是属于admin address的hash
modifier isAdmin() {
require(admin[keccak256(abi.encodePacked(msg.sender))]);
_;
}
检查是否为管理员
对于以上代码逻辑,没有设置任何的event事件
由于区块链上一切信息都是公开的,我们可以直接从链上查到问题和答案为:


inputdata:0xc76de3e90000...[省略]...206c657474655220572000...
问题为:"What is at the end of a rainbow?" 答案为:" letteR W "
按照常规逻辑,调用者只需构造 Try(" letteR W ") 即可提取合约余额。然而,笔者基于 Anvil Fork 主网状态进行本地测试时,发现交易虽被成功打包(Status: Success),但未触发任何底层 CALL 转账指令。
显然是没有进入if的逻辑,显然问题就出现在responseHash == keccak256(abi.encode(_response)。虽然responseHash是非public的状态变量,但是一切数据都是存储在链上,依然可以用storageAt来查看,由于这里是bytes32变量,位于第二个位置,所以直接查slot 1
slot 0 -> question (string)
slot 1 -> responseHash (bytes32)

得到了0xf152950bed091c9854229d3eecb07fae4c84127704751a692c8409543dc02bd3

letteR W 却是得到的0x9f16d1bc522fb263ce6d2593130671f3038b388331c87fa555be65b8979e3513
调用new初始化过responseHash,导致Start修改并未生效
交易Hash:0x62a1c781701c93b4d7a2aace3c42cf75ec540078ff6753824ab65a67a0806b12

说明链上的responseHash其实是被修改了
既然公开的 Start函数调用 交易并未修改状态,那么真实的 responseHash 是何时被注入的?通过深挖交易执行栈(Call Stack),笔者在 Etherscan 的 Advanced Mode 中提取到了一笔隐蔽的Internal Transaction。

inputData:
0x9278a35a000000000000000000000000f327cc34562ac0857c2ff033ce8d8b3713725cfd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000084ed8df1640000000000000000000000000000000000000000000000000000000000000040f152950bed091c9854229d3eecb07fae4c84127704751a692c8409543dc02bd30000000000000000000000000000000000000000000000000000000000000020576861742069732061742074686520656e64206f662061207261696e626f773f00000000000000000000000000000000000000000000000000000000
这就说的通了0xf152950bed091c9854229d3eecb07fae4c84127704751a692c8409543dc02bd3的缘由
本地模拟过程:
启动anvil:
anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/token-xxxxxx
切换到本地账户:
cast rpc anvil_impersonateAccount 0x038405665520AC945D26eB7936ffe0115B2f2BBd \ --rpc-url http://127.0.0.1:8545
给自己印钞三个ETH:
cast rpc anvil_setBalance 0x038405665520AC945D26eB7936ffe0115B2f2BBd 0x29a2241af62c0000 \
--rpc-url http://127.0.0.1:8545
但是其实这里是无法完成的,因为0xf152950bed091c9854229d3eecb07fae4c84127704751a692c8409543dc02bd3这个哈希值不可能被还原为字符串。所以这个方法肯定是无法实现的。
这个合约的Internal Transaction普通模式下看不到这条交易,只有在Advanced Mode下才能看到,容易被误导只有一次交易,从而错误调用Try给合约转账导致大量损失!

在start交易中,可以看到,调用栈中,并没有任何storage被修改,其实这里就可以看出这里的start函数只是一个幌子,是这个蜜罐的关键:https://app.blocksec.com/phalcon/explorer/tx/eth/0x2fffcd94fd19e2202223f1ce3f5e0ad44636a18e4d0e6ff960181e4982167e1e

在Internal Transaction中的New调用:https://app.blocksec.com/phalcon/explorer/tx/eth/0x62a1c781701c93b4d7a2aace3c42cf75ec540078ff6753824ab65a67a0806b12

可以看到这里是确实修改了question和responseHash
由于存在New函数和Stop函数,管理员可随时调用,无条件修改答案哈希,或者随时撤走所有资金,而没有任何限制条件
管理员可监听mempool情况下高Gas随时抢跑退款离场
① 用户发现正确答案,提交 Try("correct_answer") + 2 ETH
│
│ 交易进入 Mempool(公开可见)
▼
② 管理员监控 Mempool,立刻发送高 Gas 交易:
New("same question", 0xDEAD...) ← 更改答案哈希
│
▼
③ 管理员交易先被打包 (Gas 更高)
④ 用户交易后被打包 → 哈希不匹配 → 2 ETH 被吞
⑤ 管理员调用 Stop() 提走所有资金
管理员地址被刻意混淆
constructor(bytes32[] memory admins) {
for (uint256 i = 0; i < admins.length; i++) {
admin[admins[i]] = true;
}
}
构造函数接收 预计算的哈希值,而非地址。无法从合约代码或构造参数直接反推管理员地址,隐藏管理员身份
阻止用户保护性调用措施
require(msg.sender == tx.origin); // 禁止合约调用
禁止用户通过保护合约调用,禁止使用闪电贷策略,而只能EOA调用。
强制要求 msg.sender == tx.origin,意味着用户必须使用 EOA直接调用,无法通过编写一个带有 require(balance > oldBalance) 的探测合约来进行安全包裹。
经过以上分析,可以得出这是一个完完全全的有目的精心设计的蜜罐合约,一个猜谜蜜罐/钓鱼合约。
┌─────────────────────────────────────────────────────┐
│ 蜜罐骗局完整生命周期 │
├─────────────────────────────────────────────────────┤
│ │
│ Phase 1: 布局 │
│ ├─ 部署合约,设定简单问题和答案 │
│ ├─ 向合约注入 5 ETH 作为"奖池" │
│ └─ 答案故意设置为容易猜到/链上可查 │
│ │
│ Phase 2: 引诱 │
│ ├─ 在社交媒体/论坛宣传"猜谜赢 ETH" │
│ ├─ 受害者分析合约,发现答案似乎可以获取 │
│ └─ 受害者认为这是一个"漏洞"可以利用 │
│ │
│ Phase 3: 收割 │
│ ├─ 受害者调用 Try() 并发送 > 1 ETH │
│ ├─ 管理员前端运行 New() 修改答案哈希 │
│ ├─ 受害者交易完成,ETH 被吞 │
│ └─ 管理员调用 Stop() 提走所有资金 │
│ │
│ Phase 4: 重复 │
│ └─ 重新注入 ETH,更换问题,寻找下一个受害者 │
│ │
| 风险 | 等级 | 说明 |
|---|---|---|
| 蜜罐骗局设计 | 严重 | 合约本质为诈骗工具 |
| 管理员抢跑改答案 | 严重 | 用户永远无法真正获胜 |
| 管理员身份隐藏 | 高 | 阻碍追责 |
| tx.origin限制 | 高 | 阻止保护性措施 |
| 无事件日志 | 中 | 降低透明度 |
| 无时间锁/多签 | 中 | 管理员单点控制 |
蜜罐设计者的伪装逻辑:
Q: 为什么Try()失败不revert? A: "这是猜谜游戏,猜错要赔钱,revert了就没人投注了" ← 合理
Q: 为什么require(msg.sender == tx.origin)? A: "防止闪电贷攻击和合约自动化套利" ← 看似合理
Q: 为什么admin用hash而不是address? A: "增加安全性,防止管理员地址被针对攻击" ← 看似合理
Q: 为什么没有event? A: "节省gas" ← 看似合理
→ 每个单独的设计都有"合理"的解释 → 但组合在一起就形成了完美的蜜罐 → 这正是高级蜜罐区别于低级蜜罐的地方
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!