Knownsec Blockchain Lab | Interesting Smart Contract Honeypot Analysis (PART 2)

1 Overview

In the interesting Smart Contract Honeypot Analysis (PART 1), we explained and reproduced the ancient deception methods and magical logic loopholes.

In this part, we will explain and reproduce the exploits of novel gambling games and hackers, so as to further increase our understanding of smart contract honeypot.

Similarly, all of the smart contract honeypot codes are available on GitHub, again with their URLs:

2 A novel gambling game

Since the gambling industry exists, and blockchain decentralization seems to gambling industry brought new opportunities, the addition of it will make people think gambling is fair. As we all know that gambling is often will lose, the next analysis of four blockchain-based gambling game contracts to introduce how the banker is finally secured.

2.1 CryptoRoulette wheel: CryptoRoulette

2.1.1 Honeypot analysis

The first is CryptoRoulette, which translates as “CryptoRoulette wheel.”

The complete code for honeypot is as follows:

// https://github.com/misterch0c/Solidlity-Vulnerable/blob/master/traps/CryptoRoulette.sol
// https://etherscan.io/address/0x94602b0E2512DdAd62a935763BF1277c973B2758

pragma solidity ^0.4.19;

// CryptoRoulette
//
// Guess the number secretly stored in the blockchain and win the whole contract balance!
// A new number is randomly chosen after each try.
//
// To play, call the play() method with the guessed number (1-20). Bet price: 0.1 ether

contract CryptoRoulette {

uint256 private secretNumber;
uint256 public lastPlayed;
uint256 public betPrice = 0.1 ether;
address public ownerAddr;

struct Game {
address player;
uint256 number;
}
Game[] public gamesPlayed;

function CryptoRoulette() public {
ownerAddr = msg.sender;
shuffle();
}

function shuffle() internal {
// randomly set secretNumber with a value between 1 and 20
secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;
}

function play(uint256 number) payable public {
require(msg.value >= betPrice && number <= 10);

Game game;
game.player = msg.sender;
game.number = number;
gamesPlayed.push(game);

if (number == secretNumber) {
// win!
msg.sender.transfer(this.balance);
}

shuffle();
lastPlayed = now;
}

function kill() public {
if (msg.sender == ownerAddr && now > lastPlayed + 1 days) {
suicide(msg.sender);
}
}

function() public payable { }
}

This contract sets up a private random number ‘secretNumber’, which is specified in the range of 1–20 in the ‘shuffle()’ function. The player can use the ‘play()’ function to blindly guess this random number, and if he guesses correctly, he can withdraw all the money in the contract.

The random number is reset after each call to ‘play()’. As more and more players guess wrong, the balance of the tokens in the contract will accumulate, and if they happen to guess correctly, they will get all the prize money, but is that the truth? As you can see, the two most important functions in this honeypot contract are ‘shuffle()’ and ‘play()’. Let’s examine these two functions.

The initial ‘secretNumber’ calls the ‘shuffle()’ function in the constructor ‘CryptoRoulette. The ‘shuffle()’ function sets the value of ‘ secretNumber’ as a single line of code. It can also be seen from the code that the value of ‘secretNumber’ is related to both the number of blocks and the time. The function code is as follows:

function shuffle() internal { // 设置随机数 secretNumber
// randomly set secretNumber with a value between 1 and 20
secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1; // 对 20 取余 再加 1,所以范围在 1 - 20
}

The ‘play()’ function is provided for the user to gamble to guess this random number. The player carries no less than 0.1 ETH and passes in his guess number ‘number’, which is compared with ‘secretNumber’.

If the number is equal, the user can win. Transfer all ether in the contract, but there is a check require at the beginning of the function, which requires the player to guess the number at the end of the function should not be greater than 10, and ‘secretNumber’, as we said in the above function, is in the range of 1–20, so it seems that although it is more difficult, there is still the possibility of guessing correctly.

However, the truth is that the ‘secretNumber’ is always greater than 10, the player can never guess the number correctly, and the contract owner can take all the ether from the contract by calling the ‘kill()’ function. The function code is as follows:

function play(uint256 number) payable public { // 玩游戏竞猜数字
require(msg.value >= betPrice && number <= 10); // 要求 msg.value 不小于 0.1 eth 且 number 要不大于 10

Game game;
game.player = msg.sender; // 游戏玩家为调用者
game.number = number; // 游戏的数字为 number
gamesPlayed.push(game); // 加入游戏列表

if (number == secretNumber) { // 如果 number 为 secretNumber 则游戏胜利
// win!
msg.sender.transfer(this.balance); // 将合约中所有的以太币转给调用者
}

shuffle(); // 执行 shuffle 重置随机数
lastPlayed = now; // 设置最后一个玩的时间为现在
}

Why must the ‘secretNumber’ be greater than 10? The reason is that the initialization of the struct Game Overwrites the stored data ‘secretNumber’, and we have to add the memory keyword to directly initialize the struct in the function because memory is stored using memory, this avoids using storage bits, while the honeypot contract does not use the memory keyword, resulting in variable coverage.

The problem was only prompted before Solidity 0.5.0 with no error warning, So watch out for this in older compilers, you can see the problem in the code replication below.

2.1.2 Code reproduction

To see the value of ‘secretNumber’, we set the type of ‘secretNumber’ to the public, so that the value can be seen directly in the Remix IDE. Even some honeypot deployers can set it to the public to entice attackers to attack the contract because even if they tell the attacker the value of ‘secretNumber’, he can’t guess it correctly.

Use address 0x5B3 to click on ‘Deploy’ deployment contract and call ‘secretNumber’ to see that the initial random number is 1, which is correct because there is no initialization structure and no random number will be overwritten.

Then the attacker finds the honeypot contract, checks ‘secretNumber’ as 1 and thinks it can be attacked for profit, so pass in the number 1 and carry out the function call with 1 Ether if the first judgment condition in the ‘play()’ function is met.

After the function call was successful, I checked the account balance and found that the account balance not only did not get all the tokens in the contract but also lost the 1 Ether carried by the function call.

To see why we Debug the function call we just made.

Debug click next until the first condition is determined, at which point ‘secretNumber’ is still 1.

‘game. Player = MSG. Sender’ overwrites stored data ‘secretNumber’ due to the initialization of the structure game. As a result, ‘secretNumber’ becomes the uint256 content of msg. sender, which makes the following judgment condition untenable, so that the attacker cannot transfer all the token balance in the contract.

2.2 OpenAddressLottery

2.2.1 Honeypot analysis

The second is OpenAddressLottery, which translates to “Develop address lottery.”

The complete code for honeypot is as follows:

// https://etherscan.io/address/0xd1915A2bCC4B77794d64c4e483E43444193373Fa

pragma solidity ^0.4.19;
/*
* This is a distributed lottery that chooses random addresses as lucky addresses. If these
* participate, they get the jackpot: 1.9 times the price of their bet.
* Of course one address can only win once. The owner regularly reseeds the secret
* seed of the contract (based on which the lucky addresses are chosen), so if you did not win,
* just wait for a reseed and try again!
*
* Jackpot chance: 50%
* Ticket price: Anything larger than (or equal to) 0.1 ETH
* Jackpot size: 1.9 times the ticket price
*
* HOW TO PARTICIPATE: Just send any amount greater than (or equal to) 0.1 ETH to the contract's address
* Keep in mind that your address can only win once
*
* If the contract doesn't have enough ETH to pay the jackpot, it sends the whole balance.
*
* Example: For each address, a random number is generated, either 0 or 1. This number is then compared
* with the LuckyNumber - a constant 1. If they are equal, the contract will instantly send you the jackpot:
* your bet multiplied by 1.9 (House edge of 0.1)
*/

contract OpenAddressLottery{
struct SeedComponents{
uint component1;
uint component2;
uint component3;
uint component4;
}

address owner; //address of the owner
uint private secretSeed; //seed used to calculate number of an address
uint private lastReseed; //last reseed - used to automatically reseed the contract every 1000 blocks
uint LuckyNumber = 1; //if the number of an address equals 1, it wins

mapping (address => bool) winner; //keeping track of addresses that have already won

function OpenAddressLottery() {
owner = msg.sender;
reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
}

function participate() payable {
if(msg.value<0.1 ether)
return; //verify ticket price

// make sure he hasn't won already
require(winner[msg.sender] == false);

if(luckyNumberOfAddress(msg.sender) == LuckyNumber){ //check if it equals 1
winner[msg.sender] = true; // every address can only win once

uint win=(msg.value/10)*19; //win = 1.9 times the ticket price

if(win>this.balance) //if the balance isnt sufficient...
win=this.balance; //...send everything we've got
msg.sender.transfer(win);
}

if(block.number-lastReseed>1000) //reseed if needed
reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
}

function luckyNumberOfAddress(address addr) constant returns(uint n){
// calculate the number of current address - 50% chance
n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; //mod 2 returns either 0 or 1
}

function reseed(SeedComponents components) internal {
secretSeed = uint256(keccak256(
components.component1,
components.component2,
components.component3,
components.component4
)); //hash the incoming parameters and use the hash to (re)initialize the seed
lastReseed = block.number;
}

function kill() {
require(msg.sender==owner);

selfdestruct(msg.sender);
}

function forceReseed() { //reseed initiated by the owner - for testing purposes
require(msg.sender==owner);

SeedComponents s;
s.component1 = uint(msg.sender);
s.component2 = uint256(block.blockhash(block.number - 1));
s.component3 = block.difficulty*(uint)(block.coinbase);
s.component4 = tx.gasprice * 7;

reseed(s); //reseed
}

function () payable { //if someone sends money without any function call, just assume he wanted to participate
if(msg.value>=0.1 ether && msg.sender!=owner) //owner can't participate, he can only fund the jackpot
participate();
}

}

The game logic of the OpenAddressLottery contract is simple. The contract has a state variable ‘LuckyNumber’ with an initial value of 1, and each time a contestant makes a guess, either 0 or 1 will be generated according to their address. If the value is the same as ‘LuckyNumber’.

The winner will receive 1.9 times the prize money, and can only win the game once per address, after which he or she will not be able to participate in the contest. The key points of this honeypot contract are the ‘participate()’, ‘luckyNumberOfAddress()’ and ‘forceReseed()’ functions.

The first is the ‘participate()’ function, which allows users to participate in guesses:

function participate() payable { // 参与竞猜
if(msg.value<0.1 ether) // 要求携带大不小于 0.1 的以太币
return; //verify ticket price

// make sure he hasn't won already
require(winner[msg.sender] == false); // 玩家还未胜利过

if(luckyNumberOfAddress(msg.sender) == LuckyNumber){ //check if it equals 1 // 查看竞猜者地址生产的随机数是否为 1
winner[msg.sender] = true; // every address can only win once // 如果符合判断则该竞猜者竞猜成功

uint win=(msg.value/10)*19; //win = 1.9 times the ticket price // 奖金为带入以太币的 1.9 倍

if(win>this.balance) //if the balance isnt sufficient... // 如果获得的奖金超过合约余额则将奖金设置为当前合约所有余额
win=this.balance; //...send everything we've got
msg.sender.transfer(win); // 将奖金转给获胜的竞猜者
}

if(block.number-lastReseed>1000) //reseed if needed // 生成一个新的种子
reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
}

This is followed by the ‘luckyNumberOfAddress()’ function, which passes the contestant’s address as a parameter, via ‘n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; ‘to calculate the number corresponding to the game, because it is mod 2, so the result can only be 0 or 1. The variable ‘secretSeed’ is used to calculate this number, which is always obtained from the ‘reseed()’ function.

function luckyNumberOfAddress(address addr) constant returns(uint n){ // 根据地址生成 luckyNumber
// calculate the number of current address - 50% chance
n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; //mod 2 returns either 0 or 1 // 对 2 取余只可能为 0 或者 1
}

Finally, the reseed() function, which generates the secretSeed using the keccAK256 algorithm, takes the four parameters passed in.

function reseed(SeedComponents components) internal {
secretSeed = uint256(keccak256(
components.component1,
components.component2,
components.component3,
components.component4
)); //hash the incoming parameters and use the hash to (re)initialize the seed
lastReseed = block.number;
}

Through on in the face of contract analysis, look the contract does not have what problem, is also a 50% chance of winning, but it is a trap, it will be said to the Solidity 0.4 x structure variables covering loopholes caused by local variables, namely to the structure of an uninitialized local variable assignment will override intelligent the first few variables defined in the contract.

After the ‘forceReseed()’ function is called, the fourth parameter ‘LuckyNumber’ will be overridden by ‘s.coment4 = tx.gasprice * 7’ and set to 7. The principle of this honeypot contract is similar to the previous one.

A look at the contract’s transaction content shows that openAddressSlottery has a large number of transactions, which illustrates the honeypot contract openAddresslottery’s deception.

2.2.2 Code reproduction

Copy the code of the honeypot contract into the Remix IDE. In order to check the value of ‘LuckyNumber’, we set the type of ‘LuckyNumber’ to public. This gives you a ‘getter()’ function in the Remix IDE to get its value.

Similarly, the honeypot deployer can set this variable to the public to fool an attacker into thinking it is profitable since the value of ‘LuckyNumber’ will always be overwritten as 7.

Click ‘Deploy’ at address 0x5B3 and call ‘LuckyNumber’ to see that the value is 1. Since the ‘SeedComponent’ structure has not been initialized and will not override the value of ‘LuckyNumber’, it is still 1.

Call ‘forceReseed()’ with contract owner 0x5B3 to initialize the four variables in ‘SeedComponent’. You can see that the value of ‘LuckyNumber’ has changed to 7 as a result of initialization.

The attacker 0x 4B2, seeing the contract as vulnerable, calls the ‘participate ()’ function with 10eth and sees no increase in the balance. Look at ‘Luckynumberofaddress’, which has a value of 1 but is not rewarded, and look at ‘LuckyNumber’, which has always been 7.

The reason is that ‘LuckyNumber’is has overwritten with a value of 7 after the deployer calls the ‘foresight ()’ function and the attacker’s address generates a random number of 0 or 1, which means that no one will ever win. This is taking advantage of a compiler vulnerability that was fixed in Solidity 0.5.0, so this honey pot contract is only available in Solidity 0.4. It only works if it’s in X.

2.3 KingOfTheHill: kind of the hill

2.3.1 Honeypot analysis

The third is KingOfTheHill, which translates as “king of the hills”.

The complete code for honeypot is as follows:

// https://etherscan.io/address/0x4dc76cfc65b14b3fd83c8bc8b895482f3cbc150a#code

pragma solidity ^0.4.11;

// Simple Game. Each time you send more than the current jackpot, you become
// owner of the contract. As an owner, you can take the jackpot after a delay
// of 5 days after the last payment.

contract Owned {
address owner; function Owned() {
owner = msg.sender;
}
modifier onlyOwner{
if (msg.sender != owner)
revert(); _;
}
}

contract KingOfTheHill is Owned {
address public owner;
uint public jackpot;
uint public withdrawDelay;

function() public payable {
// transfer contract ownership if player pay more than current jackpot
if (msg.value > jackpot) {
owner = msg.sender;
withdrawDelay = block.timestamp + 5 days;
}
jackpot+=msg.value;
}

function takeAll() public onlyOwner {
require(block.timestamp >= withdrawDelay);
msg.sender.transfer(this.balance);
jackpot=0;
}
}

The honeypot contract KingOfTheHill is only 38 lines of code, the logic is simple, there is a backoff function and a ‘takeAll()’ function, where the ‘jackpot’ variable is the sum of all the tokens passed into the contract.

Assign ‘owner’ to ‘MSG. Sender’ if ‘mag.value’ is greater than ‘jackpot’ each time a user calls the rollback function. After the user has obtained contract owner privileges, the ‘takeAll()’ function can be called to takeAll contract balances away when the extension time expires. So let’s focus on these two functions.

The first is the fallback function, which is a function for the user to participate in the contract “bug” and has the following code:

function() public payable { // 回退函数,用户通过该函数参与合约
// transfer contract ownership if player pay more than current jackpot
if (msg.value > jackpot) { // 判断用户调用时带入的以太币是否大于 jackpot
owner = msg.sender; // 将 owner 设为用户的地址
withdrawDelay = block.timestamp + 5 days; // 取款时间为 5 天后
}
jackpot+=msg.value; // jackpot 的值加上刚才传入的以太
}

This is followed by the takeAll() function, which takes all the balance out of the contract as follows:

function takeAll() public onlyOwner { // 取走所有余额的前提是要满足修饰器 onlyOwner 的要求
require(block.timestamp >= withdrawDelay); // 判断当前时间是否大于了延迟时间
msg.sender.transfer(this.balance); // 将合约中所有余额转到调用者地址中
jackpot=0; // 将 jackpot 设置为 0
}

It doesn’t seem like there’s anything wrong with this contract from the above two functions, but we said it’s a honeypot, so what’s the catch?

To see why look back at the TestBank Honeypot contracts in “Interesting Smart Contract Honeypot: Part 1.” They work on a similar principle of “who owns the contract?” ‘Owned’ and ‘KingOfTheHill’ contracts exist. ‘Owned’ and ‘KingOfTheHill’ contracts inherit from ‘Owned’ contracts. We rewrite ‘KingOfTheHill’ as a single contract.

pragma solidity ^0.4.11;

contract KingOfTheHill {
address owner1;

function Owned() {
owner1 = msg.sender;
}
modifier onlyOwner{
if (msg.sender != owner1)
revert(); _;
}
}

address public owner2;
uint public jackpot;
uint public withdrawDelay;

function() public payable {
if (msg.value > jackpot) {
owner2 = msg.sender;
withdrawDelay = block.timestamp + 5 days;
}
jackpot+=msg.value;
}

function takeAll() public onlyOwner {
require(block.timestamp >= withdrawDelay);
msg.sender.transfer(this.balance);
jackpot=0;
}
}

You can easily see the problem by rewriting the contract code. The modifier function onlyOwner for permission determination is ‘owner1’, and the rollback function changes the owner defined by the original subclass to ‘owner2’, which means that the contract owner cannot be changed. The person calling ‘takeAll()’ can only be the contract creator. So let’s replicate that in code.

2.3.2 Code reproduction

Copy the honeypot contract code into the Remix IDE. In order to facilitate Remix, the ‘withdrawDelay = block.timestamp + 5 days’ change to ‘withdrawDelay = block. Timestamp + 0 days’ , so we don’t have to wait 5 days to try the withdrawal operation during the test.

Click ‘Deploy’ to Deploy the KingOfTheHill contract using address 0x5B3 and click ‘owner’ to see that the current value is 0.

Call the rollback function with 0x5B3 carrying 10 eth and store 10 Eth into the contract. ‘Jackpot’ is 10 eth and ‘owned’ is 0x5B3.

Attacker 0xAb8 sets msg.value to 20 eth and calls the rollback function. Check that ‘owner’ is 0xAb8.

The attacker finds that ‘owner’ is his own address, which meets the requirements of ‘takeAll()’, so he calls’ takeAll() ‘, only to find that the transaction fails and his balance is still 80 ETH (originally 100 ETH).

The honey pot deployer 0x5B3 finds that someone has taken the bite and the contract already contains 30 eth, even though ‘owner’ is the attacker’s address 0xAb8 (owner2 is not bound by onlyOwner), However, 0x5B3 calls the ‘takeAll()’ function and still transfers all the balance (10 ETH + 20 ETH) from the contract. Check the account balance and indeed increase 30 ETH.

A similar smart contract is RichestTakeAll:

2.4 Ethereum Competition game: RACEFORETH

2.4.1 Honeypot analysis

The fourth is RACEFORETH, which translates as “Ethereum competitive game”.

The complete code for honeypot is as follows:

pragma solidity 0.4.21;

// How fast can you get to 100 points and win the prize?
// First person to deposit 0.1 eth (100 finney) wins the entire prize!
// 1 finney = 1 point

contract RACEFORETH {
// 100 points to win!
uint256 public SCORE_TO_WIN = 100 finney;
uint256 public PRIZE;

// 100 points = 0.1 ether
// Speed limit: 0.05 eth to prevent insta-win
// Prevents people from going too fast!
uint256 public speed_limit = 50 finney;

// Keep track of everyone's score
mapping (address => uint256) racerScore;
mapping (address => uint256) racerSpeedLimit;

uint256 latestTimestamp;
address owner;

function RACEFORETH () public payable {
PRIZE = msg.value;
owner = msg.sender;
}

function race() public payable {
if (racerSpeedLimit[msg.sender] == 0) { racerSpeedLimit[msg.sender] = speed_limit; }
require(msg.value <= racerSpeedLimit[msg.sender] && msg.value > 1 wei);

racerScore[msg.sender] += msg.value;
racerSpeedLimit[msg.sender] = (racerSpeedLimit[msg.sender] / 2);

latestTimestamp = now;

// YOU WON
if (racerScore[msg.sender] >= SCORE_TO_WIN) {
msg.sender.transfer(PRIZE);
}
}

function () public payable {
race();
}

// Pull the prize if no one has raced in 3 days :(
function endRace() public {
require(msg.sender == owner);
require(now > latestTimestamp + 3 days);

msg.sender.transfer(this.balance);
}
}

The RACEFORETH contract has a ‘SCORE_TO_WIN’ parameter with a value of 100 finney. The RACEFORETH contract also has two mappings, where ‘racerScore’ is the current score of the competitor.‘racerSpeedLimit’ is the limit of each step.

Competitors accumulate their own score ‘racerScore’ through each transfer amount. When their score ‘racerScore’ is greater than or equal to ‘SCORE_TO_WIN’, they can win and take away the award ‘PRIZE’ deposited by the contract creator at the beginning.

At the heart of the honeypot, contract are the ‘race()’ and ‘endRace()’ functions. Let’s look at these two functions.

The first is the ‘race()’ function, which looks like this:

function race() public payable { // 竞赛
if (racerSpeedLimit[msg.sender] == 0) { racerSpeedLimit[msg.sender] = speed_limit; } // 如果当前调用者的补偿限制为 0,则将其步长最大限制设置为 speed_limit
require(msg.value <= racerSpeedLimit[msg.sender] && msg.value > 1 wei); // 判断 msg.value 是否小于等于当前步长限制并且 msg.value 要大于 1 wei

racerScore[msg.sender] += msg.value; // 用户总得分加上本次的 msg.value
racerSpeedLimit[msg.sender] = (racerSpeedLimit[msg.sender] / 2); // 用户步长限制设置为当前步长限制的一半

latestTimestamp = now;

// YOU WON
if (racerScore[msg.sender] >= SCORE_TO_WIN) { // 如果用户总得分大于等于目标分数
msg.sender.transfer(PRIZE); // 将奖励金额转给用户
}
}

Msg. value should be greater than 1 wei and smaller than the step limit. Add the value to your racerScore. Then set the new step limit to half of the current step limit.

As long as the total score is greater than or equal to the winning goal, you can withdraw the reward. At first glance, the contract may seem that each increase in the number of steps is decreasing, but one day they will catch up, but is this the case?

This is followed by the endRace() function, which looks like this:

function endRace() public { // 结束竞赛
require(msg.sender == owner); // 要求调用者为当前合约所有者
require(now > latestTimestamp + 3 days); // 要求当前时间大于上次竞赛后的 3 天

msg.sender.transfer(this.balance); // 将合约所有余额转给调用者
}

The contract owner can withdraw the balance of the contract three days after the last contest.

2.4.2 Code reproduction

Copy the code for the honeypot contract into the Remix IDE. Add a ‘public nowScore’ to make it easier for us to reproduce, so that we can see the score after each contest when testing.

Use address 0x5B3 to Deploy the RACEFORETH contract by clicking “Deploy”.

If you use 0xAb8 as the attacker, the maximum value is 50 Finney the first time, so set msg.value to 50 Finney, and then check that the current score is 50 Finney.

The attacker 0xAb8 makes a second attempt to set msg.value to 26 Finney, more than half of the 50 Finney of the previous race. The call to ‘race()’ fails. The reason is that our 26 Finney does not satisfy the requirement that require is less than or equal to half of the previous contest.

Each time we pass in half of the previous maximum, we still don’t get to 100 Finney after multiple executions. Because the following formula can only go to 100 but can’t equal 100.

If it is always less than 2, then 50 times this will never equal 100, and it will never reach the endpoint, so for the honeypot contract, even if we call ‘race()’ many times, each time turning into the maximum value, we will never reach the target score, and then we will not be able to withdraw the reward in the contract.

3 Exploits by hackers

3.1 Just for test? (integer overflow) : For_Test

3.1.1 Honeypot analysis

The fifth is For_Test, which translates as “Just for test?”.

The complete code for honeypot is as follows:

// https://etherscan.io/address/0x2eCF8D1F46DD3C2098de9352683444A0B69Eb229#code

pragma solidity ^0.4.19;

contract For_Test
{
address owner = msg.sender;

function withdraw()
payable
public
{
require(msg.sender==owner);
owner.transfer(this.balance);
}

function() payable {}

function Test()
payable
public
{
if(msg.value> 0.1 ether)
{
uint256 multi =0;
uint256 amountToTransfer=0;


for(var i=0;i<msg.value*2;i++)
{
multi=i*2;

if(multi<amountToTransfer)
{
break;
}
else
{
amountToTransfer=multi;
}
}
msg.sender.transfer(amountToTransfer);
}
}
}

The logic of the honeypot contract For_Test is very simple. There is only one core function, ‘Test()’. In this function, when the ‘msg.value’ passed in is greater than 0.1eth, the value of ‘amountToTransfer’ will be obtained according to the content of the for a loop.

This means that the function caller is rewarded with 4 times the amount of money transferred in. Next, let’s analyze the main content of the function.

The code for the ‘Test()’ function is as follows:

function Test()
payable
public
{
if(msg.value> 0.1 ether) // 要求 msg.value 必须大于 0.1 eth
{
uint256 multi =0;
uint256 amountToTransfer=0;


for(var i=0;i<msg.value*2;i++) // i 小于 msg.value*2 则执行下面内容
{
multi=i*2; // multi 的值为 2 倍的 i

if(multi<amountToTransfer) // multi 小于 amountToTransfer 则 break 跳出循环
{
break;
}
else
{
amountToTransfer=multi; // 否则将 amountToTransfer 设置为 multi
}
}
msg.sender.transfer(amountToTransfer); // 将 amountToTransfer 的值转给调用者
}
}

Careful analysis of the code logic can be found in the for loop if there is a condition when the condition is true, it will jump out of the loop, but this judgment condition is very weird, because ‘amountToTransfer’ is initially 0, before jumping out ‘amountToTransfer=multi’, In the next cycle, ‘multi’ becomes twice ‘I’, which means that ‘multi’ is always greater than ‘amountToTransfer’.

Isn’t the corresponding judgment condition never established? There are a few things we need to know about this honeypot contract before we finally reveal it.

  • ‘msg.value’ is in wei, and 1 eth = 10<sup>18</sup> wei.
  • when a parameter variable is defined as’ var ‘, its data type is’ uint8 ‘and its value range is [0,255].

Value of ‘msg.value’ is the minimum value of 0.1 eth, and the value of ‘msg.value*2’ exceeds the value range of ‘uint8’, which means that there is an integer overflow here.

When ‘I = 255’ is executed, ‘I ++’ will result in ‘I’ overflows to 0. At this time, ‘multi’ is 0 and less than ‘amountToTransfer’. In this way, the judgment condition of IF is met, and the loop will end in advance.

According to the code content, the final amount transferred to the caller is’ amountToTransfer=255*2=510 WEI ‘. No matter the caller passes in any amount greater than 0.1 ETH, it will only get 510 WEI.

3.1.2 Code reproduction

Copy the code for the honeypot contract to the Remix IDE and use address 0x5B3 to Deploy the For_Test contract by clicking “Deploy”. 0x5B3 has an account balance of 100 eth.

Select 0xAb8 as the attacker, set ‘MSG. Value’ to 10 eth, call ‘Test()’ and find that the account balance has not increased but decreased the 10 ETH just passed (but ended up with 510 WEI transfer).

When the attacker transferred the token into the contract, the contract owner called ‘withdraw()’ and withdrew the 10 eth that the attacker just passed in by calling ‘Test()’, increasing the account balance to 110 ETH.

Test1 is a similar smart contract:

Github address: [smart-contract-honeypots/ test1.sol]

3.2 Dividend distribution (older compiler bug) : DividendDistributor

3.2.1 Honeypot analysis

The last one is DividendDistributor, which translates as “dividend distribution.”

The complete code for honeypot is as follows:

// https://etherscan.io/address/0x858c9eaf3ace37d2bedb4a1eb6b8805ffe801bba


pragma solidity ^0.4.0;

contract Ownable {
address public owner;
function Ownable() public {
owner = msg.sender;
}

modifier onlyOwner() {
if (msg.sender != owner)
throw;
_;
}

modifier protected() {
if(msg.sender != address(this))
throw;
_;
}

function transferOwnership(address newOwner) public onlyOwner {
if (newOwner == address(0))
throw;
owner = newOwner;
}
}

contract DividendDistributorv3 is Ownable{
event Transfer(
uint amount,
bytes32 message,
address target,
address currentOwner
);

struct Investor {
uint investment;
uint lastDividend;
}

mapping(address => Investor) investors;

uint public minInvestment;
uint public sumInvested;
uint public sumDividend;

function DividendDistributorv3() public{
minInvestment = 0.4 ether;
}

function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner) protected
{
if(! target.call.value(amount)() )
throw;
Transfer(amount, message, target, currentOwner);
}

function invest() public payable {
if (msg.value >= minInvestment)
{
sumInvested += msg.value;
investors[msg.sender].investment += msg.value;
// manually call payDividend() before reinvesting, because this resets dividend payments!
investors[msg.sender].lastDividend = sumDividend;
}
}

function divest(uint amount) public {
if ( investors[msg.sender].investment == 0 || amount == 0)
throw;
// no need to test, this will throw if amount > investment
investors[msg.sender].investment -= amount;
sumInvested -= amount;
this.loggedTransfer(amount, "", msg.sender, owner);
}

function calculateDividend() constant public returns(uint dividend) {
uint lastDividend = investors[msg.sender].lastDividend;
if (sumDividend > lastDividend)
throw;
// no overflows here, because not that much money will be handled
dividend = (sumDividend - lastDividend) * investors[msg.sender].investment / sumInvested;
}

function getInvestment() constant public returns(uint investment) {
investment = investors[msg.sender].investment;
}

function payDividend() public {
uint dividend = calculateDividend();
if (dividend == 0)
throw;
investors[msg.sender].lastDividend = sumDividend;
this.loggedTransfer(dividend, "Dividend payment", msg.sender, owner);
}

// OWNER FUNCTIONS TO DO BUSINESS
function distributeDividends() public payable onlyOwner {
sumDividend += msg.value;
}

function doTransfer(address target, uint amount) public onlyOwner {
this.loggedTransfer(amount, "Owner transfer", target, owner);
}

function setMinInvestment(uint amount) public onlyOwner {
minInvestment = amount;
}

function () public payable onlyOwner {
}

function destroy() public onlyOwner {
selfdestruct(msg.sender);
}
}

The logic of DividendDistributor in a honeypot contract is not too difficult. It mainly has the functions of investing, drawing money and calculating dividends, etc.

In the contract, there is an investor of structure type, whose function is to store the investment information of investors, including the investment amount and dividend.

In addition, the structure realizes the mapping of account addresses to investors through mapping. There seems to be no problem with the contract in general, and no problem with the contract if the compiler version is set correctly.

Take a look at the key contract functions, ‘invest()’, ‘Divest ()’, ‘loggedTransfer()’ and ‘payDividend()’. Take a look at the key contract functions, ‘invest()’, ‘Divest ()’, ‘loggedTransfer()’ and ‘payDividend()’.

First, the ‘invest()’ function is used for the user to call the function to invest. The amount of investment each time should not be less than the required minimum amount of 0.4 eth. Relevant variables are updated after the investment.

The complete code is as follows:

function invest() public payable { // 投资
if (msg.value >= minInvestment) // 要求 msg.value 不小于最低投资数
{
sumInvested += msg.value; // 总投资数加上该次投资的
investors[msg.sender].investment += msg.value; // 调用者投资数也加上该次投资的
// manually call payDividend() before reinvesting, because this resets dividend payments!
investors[msg.sender].lastDividend = sumDividend; // 调用者最后一次股息分红为总的股息
}
}

The ‘divest()’ function, as the reverse of the above function, retrieves the amount invested by the caller.

The function starts by checking that the caller’s investment or argument is not zero, subtracts the amount of the withdrawal, and transfers the amount from the contract owner’s account to the caller.

The complete code is as follows:

function divest(uint amount) public { // 取钱
if ( investors[msg.sender].investment == 0 || amount == 0) // 要求调用者投资的数量和该次取出的数量都不为 0
throw;
// no need to test, this will throw if amount > investment
investors[msg.sender].investment -= amount; // 调用者投资的数量减去本次取出的数量
sumInvested -= amount; // 总投资数也减去本次取出的数量
this.loggedTransfer(amount, "", msg.sender, owner); // 从合约所有者账户中转走 amount 金额给调调用者
}

The loggedTransfer() function simply transfers money and records transfers. The complete code is as follows:

function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner) protected
{ // 记录转账操作
if(! target.call.value(amount)() ) // 转账给 target 地址
throw; // 失败则抛出异常
Transfer(amount, message, target, currentOwner);
}

The payDividend() function takes a dividend set by the contract owner. The complete code is as follows:

function payDividend() public { // 获取股息
uint dividend = calculateDividend(); // 计算股息
if (dividend == 0) // 股息为 0 则抛出异常
throw;
investors[msg.sender].lastDividend = sumDividend; // 调用者最后一次股息为总股息
this.loggedTransfer(dividend, "Dividend payment", msg.sender, owner); // 从合约所有者账户中转走 dividend 股息给调调用者
}

By analyzing the four functions above, we find that the honeypot contract is attractive because it allows investors to withdraw their money at any time and pay dividends via the ‘payDividend()’ function.

Such a contract may seem lucrative, but it turns out to be a trap that exploits a bug in an older version of the compiler. Before Solidity 0.4.12 there was a bug where the compiler would skip an empty string if it was used as an argument during a function call.

Among the core functions above, the ‘divest ()’ function has such a problem that, according to the bug specification, it calls ‘this. Loggedtransfer (amount,”, msg.sender, owner); ‘then becomes ‘loggedTransfer (uint amount, bytes32 msg. sender, address, owner, address empty)’ eventually transfers money to ‘owner ’ user ‘owner. call. value (amount)()’.

Here’s a code to replicate the honeypot contract and uncover its true nature.

3.2.2 Code replay

Copy the honeypot contract code into the Remix IDE and set the Solidity version of the compiler to 0.4.11.

Select 0x5B3 as the contract deployer and owner, click “Deploy” to Deploy, set VALUE to 10 eth, and call the ‘distributeDividends’ function to set the dividend.

Using 0xAb8 as the attacker, set VALUE to 10 eth and call the ‘invest()’ function to invest.

Use 0xAb8 to call the function shown in the figure below to get information about the honeypot contract, including calculating the dividend, its own investment amount, minimum investment amount, contract owner ‘owner’, total dividend, and total investment amount.

Call ‘divest()’ with 0xAb8 and set its passed parameter to ‘5000000000000000000’ to retrieve half of the 10 eth just invested and find that the transaction is confirmed.

The ‘target’ argument becomes the address of ‘owner’ and the second argument is replaced by ‘msg.sender’, which returns the current balance of the account and finds that the 5 eth retrieved by ‘divest()’ has been transferred to ‘owner’ account 0x5B3.

4 Summary

Through the analysis of the ethereum honeypot smart contract, we can find that these interesting honeypot contracts in the smart contract are more like phishing, using various tricks to induce others to transfer tokens into the contract to further obtain these tokens.

Of course, honeypot contracts are not completely devoid of learning idity. We can see attacking thinking on contracts and many of the old and new features of Solidity in honeypot contracts.

These issues need to be considered in regular contract audits as well, otherwise contracts can be hacked and contract tokens can be stolen. Even now, honeypot contracts are being written to ensnare people, but their thinking is no longer limited to those who want to make money from the pie in the sky, but robots of all kinds have become their targets.

Therefore, we must pay attention to the functional logic of the contract to prevent the contract from being attacked because of the functional logic, but also to prevent the contract owner from running away and other factors.

5 References

--

--

--

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Knownsec Blockchain Lab

Knownsec Blockchain Lab

More from Medium

Knownsec Blockchain Lab | All the past, all for the prologue — 2021 blockchain typical security…

Weekly Blockchain Security Report by Fairyproof- Feb 14 to Feb 20

Wormhole Bridge Records 2022’s Largest DeFi Hack Loss Yet!💰

Beosin Blockchain Security Ecosystem Overview in Q1 2022: Losses From Security Incidents Reach…