Rediscovering Smart Contract Honeypots targeting Solidity Devs
How to watch out for common Smart Contract Honeypots and their tactics
This post is mainly for Solidity devs, but I’ve dumbed it down so that even those without dev experience can probably get a rudimentary idea of the concepts. But if you don’t have dev experience, you will almost certainly get lost in the 2nd half of the post when I go over the 5 examples of Honeypots.
Table of Contents
Contract Honeypots (intro)
Common traits of Honeypots
Common trap tactics
How to avoid getting scammed by Honeypots
Samples of my Favorite Honeypots
Honeypot #1 - King of the Hill
Honeypot #2 - An Unsecured Gift
Honeypot #3 - Copy-paste code
Honeypot #4 - Address Balance
Honeypot #5 - Decoy Contract
Contract Honeypots (intro)
Smart Contract Honeypots are contracts that seemingly have a vulnerability. However, this vulnerability is an intentionally-placed bait used to entice attackers into getting trapped by the contracts.
There are many types of contract honeypots. They can target scammers, greedy investors, blackhats, and other devs. This article is only going to cover the last category: trap contracts targeting other devs.
There's a good older summary here that covers many of the older ones and another semi-updated summary of the other known honeypots. Honeypots are fun to study because it's like solving a 2-layer puzzle: find the obvious vulnerability first, and then find the trap.
Common traits of Honeypots
There is a bait vulnerability. In contracts, this can be a typo/mistake, a seemingly-forgotten validation check, or a known common vulnerability like a reentrancy bug.
There is a requirement to add more funds in order to exploit the vulnerability. If there's no practical explanation for that requirement, then it’s a giant red flag that it's a trap.
There is a trap that's much harder to find than the bait vulnerability. Usually something easy to miss unless you're really paying attention to the code. Sometimes, there is a seemingly-random function or random line of code placed in the contract without any obvious purpose. That's usually how the trap designer secretly modifies the contract's variable.
Common trap tactics
Constructors: Constructors are functions that run only when the contract in initially deployed. People sometimes forget that constructors can set state variables in the contract. On Etherscan, you'll sometimes see contract code published as a "Similar match" instead of "Exact match". That means the published code on the block explorer does not reveal the actual constructor function. Unless it says “Exact match”, do not trust the that the block explorer has listed the constructor’s code.
Secret calls by other contracts: A contract’s state variables can be secretly set by calls from other contracts. These actions are often invisible to block explorers. If you look at a bait contract on a block explorer like Etherscan, it will not reveal whether other contracts have called it to set variables.
For example, a contract <Secret_Manipulator> could set a state variable on contract <Honeypot>. You won't be able to find any records of that change by studying <Honeypot> on Etherscan. You have to have prior knowledge about <Secret_Manipulator> to find the transaction record. Unless the scammer is careless and left a trail, you won't be able to find it. And even then, the <Secret_Manipulator> contract is always left as closed source so you will never know exactly what it did.
Obfuscated code. There are contracts with hundreds of lines of useless code, but one of them hides the true function of the contract. Or it might hide the code in a location that isn’t visible on most editors.
Older code that use previous compiler versions of Solidity have deprecated mechanisms and code syntax that might trick newer developers. I can still deploy new contracts using old versions from years ago. Examples of tricky changes:
State variable shadowing: In contracts before 0.6.0, you could have the same variable name in the base contract and the derived contract. Even though they have the same name, they are different variables. Traps often abused state variable shadowing for the "owner" variable, leading you to think the owner was someone else. Newer devs likely aren't aware of this vulnerability.
Integer overflow and underflow existed before 0.8.0. If an integer increases above a certain size, it would overflow into a negative integer (or wrap back to 0 for unsigned integers). Older contracts had to use SafeMath functions to prevent overflow/underflow, which don't exist on newer contracts.
Decoy contracts: One of the most popular honeypots uses a decoy contract with the same name as an object that gets called in the main contract. However, this decoy contract is never actually used. Instead a secret contract is deployed ahead of time, and that’s the one that is actually referenced.
How to avoid getting scammed by Honeypots
Leave it to the devs: First off, you shouldn't be attacking vulnerabilities in other people's contracts unless you are a decent Solidity dev. Most people don’t have to worry about this particular category of honeypots. But they do have to worry about
Always thoroughly test the code in a dev environment first. Use the same Solidity version as the contract when it was deployed.
Be wary of requirements to send ETH that seem out of place. Any contract arbitrarily requiring to add large amounts of funds in order to interact with it is likely a trap
Don't trust the block explorer to show everything. Remember that contracts can be called by other contracts without leaving an obvious record on the bait contract. Many actions can be hidden from block explorers.
Be careful with OLD contracts. Check the Solidity version. Some traps require you to know knowledge about previous Solidity versions because the compiler behavior is different.
Samples of my Favorite Honeypots
Of the 20+ honeypots I’ve reviewed, here are my favorite 5.
I've uploaded simplified versions of several of them at https://github.com/mal-plankton/solidity/tree/main/Scams. That link also includes explanations in the code for how the exploits work.
I’d like to say that no experienced Solidity Dev would fall for any of these, but I’d probably be committing a No True Scotsman fallacy.
Honeypot #1 - King of the Hill
See Code sample
The Bait: None
The Trap: This requires you to know about State Variable Shadowing, which was disallowed after Solidity 0.6.0.
Prior to 0.6.0, you could declare a variable of the same name in both the base contract (Owned) and the derived contract (KingOfTheHill). The <owner> variable used in the <onlyOwner> modifier of the Owned contract is NOT the same as the public <owner> in the KingOfTheHill contract.
Those interacting with the contract think the public <owner> is being changed when the true private <owner> used by the onlyOwner modifier for the takeAll() function is the original deployer of the contract.
There is nothing preventing a developer from tricking others by deploying new contracts using older versions of Solidity
Lessons Learned:
Be careful playing with contracts using old Solidity versions
Always do testing with the same Solidity version of the contract
Honeypot #2 - An Unsecured Gift
See Code sample
The Bait: This contract pretends to be a gift left by one person to another with a weak vulnerability. In the "SetPass()" function, they replaced <pass> with <hash> to try to make it look like they made an accidental typo.
"
SetPass(bytes32 hash)
" is supposed to be "SetPass(bytes32 pass)
""
hashPass = hash;
" should be "hashPass = keccak256(abi.encodePacked(pass));
"<pass> is actually a hash itself, so you need to know the original password to use "GetGift()"
The Trap: A dev might think that they could run "SetPass()" again with a new hash to reset the password. However, the value of <passHasBeenSet> is private and unknown.
Since internal transactions are hidden on blockchain explorers, the scammer was able to secretly call PassHasBeenSet() from another contract, like the "Secretly_Set_PassHasBeenSet" contract below.
So <passHasBeenSet> was secretly already set to true by the scammer. * If <passHasBeenSet> were public, it would've been easy to see the scam.
Lessons Learned:
Keep an eye out of strange functions that seem to have no purpose like PassHasBeenSet(). Normally, you wouldn't even create a separate function to set the <passHasBeenSet> boolean. If there is a weird function, it's a red flag that it could be called by another secret contract.
The requirement that you need to send 1 ETH to change the password is a red flag. It serves no purpose except to extract value from the victim.
Honeypot #3 - Copy-paste code
See Code sample
The Bait: It looks like a completely-normal contract in most text editors.
The Trap: Many code editors like Github and Remix do not have word-wrap enabled by default.
Open the Code sample and scroll all the way to the right
This particular sample sets the real owner to the scammer’s address
Lessons Learned:
Be careful with copy-pasting other people’s code. Always test it first.
Be aware that code that can be hidden off-screen
Honeypot #4 - Address Balance
See Code sample
The Bait: Anyone can call the Take_balance() function, and there are no restrictions in it except that the value sent has to be greater than its current balance.
The Trap:
Devs might not realize that "
address(this).balance
" always includes any ETH funds that are currently being sent to it.So
address(this).balance
is always >= msg.value and that if() statement will never be true unless the balance were 0.
Lessons Learned:
Test contracts in a dev environment before interacting with them
The requirement that the msg.value sent needs to be greater than the current balance is a giant red flag. It serves no purpose other than to make the sender deposit more ETH to the contract.
Honeypot #5 - Decoy Contract
See Code sample
This is one of the most famous dev Honeypots. It’s been covered in on /r/EthDev multiple times
It’s been reported on media sites.
And it has made it to the Solidity-by-example Solidity tutorial website.
The Bait: There is a reentrancy bug in the withdraw() function
The Trap:
The Logger contract is a complete decoy. It never gets called.
The real Logger contract, called HoneyPot, gets deployed separately ahead of time. It shares the same log() function as the decoy Logger contract.
When the Bank contract’s constructor is called, it uses the HoneyPot’s address instead of the decoy address.
Lessons Learned:
Be careful when a contract has the option to set the address for another contract
Be careful with decoys. Just because something exists in code doesn’t mean that it’s the version being used.
There are many more types of Honeypot contracts. Be safe out there.
MPlankton great job man truly appreciate the in-depth details happy holidays!
MPlankton great job man truly appreciate the in-depth details happy holidays!