Smart Contract Vulnerabilities that ChatGPT cannot uncover (Part 1)

TechJD
4 min readFeb 28, 2024

This is the beginning of a new series I’m running about some of the sneakiest DeFi hacks I’ve come across in my career. There are several reasons why many of them are so hard to catch.

One reason is that although these hacks are known exploits, the way these exploits are taught are with impractical scenarios that know one would ever do and so in practice, one may not even recognize when this particular exploit is staring them in the face.

Another reason has to do with the accessibility of Solidity. One of Solidity’s best features is that it is very easy for the common person to understand and begin writing their own smart contracts. A person with little experience can load up RemixIDE and deploy a contract pretty easily. This “from zero-to-deployed” setup is important because it promotes the widespread usage of smart contracts. However, it is deceptive. Without much experience in Solidity development and how smart contracts work, one can fall victim to one of these hacks, not even recognizing the vulnerability was there in the first place. Furthermore, most AI tools or other software are not yet advanced enough to point out all of these vulnerabilities (I’ve tried). So, without further adieu, I’d like to introduce an exploit you’ve probably learned about or heard of:

Denial Of Service Attack (DoS)

The examples I always see for DoS either involve multiple users depositing to a bank but apparently only one person can hold their deposit in the bank at a time (who does that?) or some instance where a bot is repeatedly calling a function in such a way that other callers cannot complete a call. But I’ve seen the potential for a DoS attack in situations that were much more practical, namely, auctions.

When a person conducts an auction and allows participants to bid, they are creating a situation where only one user can have their bid held at a time and everyone else gets refunded. What makes DoS so sinister in this case is that on its face, the code looks solid.

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Auction {
address payable public highestBidder;
uint256 public highBid;


function bid() public payable {

require(msg.value > highBid, "bid too low");

require(highestBidder.send(highBid), "unable to refund");


highestBidder = payable (msg.sender);

highBid = msg.value;
}


}

The auction contract works like this: users can each bid, but the bid has to be higher than the previous bid. If this condition is met, the contract must send the previous high bid to the previous highest bidder. The variables highestBidder and highBid are then both updated with the new highest bidder and their bid.

There are actually a few things wrong with this code. One is that it fails the checks-effects-interactions flow, which requires highestBidder to be stored in a new variable (call it “previousHighestBidder”) and highBid (“previousHighBid”) and then the send call should come last using the updated variables. Writing the code in such a way makes the function vulnerable to reentrancy, which I’ll talk about in another installment.

Believe it or not, when I see the code written like this, it’s often because for whatever reason, the developer who wrote it didn’t want to make the extra effort of creating those new variables. It looks cleaner this way, but it’s wrong. What makes this even more egregious is that out of the two common examples I gave, this is the one that can be easily caught with AI or other auditing tools.

So then how do prevent a bot from making repeated calls, so that other callers cannot bid? I’ve got 2 solutions.

  1. Disallow Bots
function isContract (address _addr) public view returns (bool){
uint32 size;
assembly {
size := extcodesize(_addr)
}
return (size > 0);

}

Solidity recognizes to types of address: EOA (externally-owned address) and smart contracts. An EOA is basically a person’s wallet. By including this function, it will return true if the address belongs to a smart contract.

function bid() public payable  {

require(msg.value > highBid, "bid too low");

//New require statement
require(!isContract(msg.sender), "no bots allowed");

require(highestBidder.send(highBid), "unable to refund");


highestBidder = payable (msg.sender);

highBid = msg.value;
}

}

By including this addition, require statement, we are only allowing real people to make bids.

2. Set a Gas Limit

If you want to still allow bots, but not allow them to make repeated calls, you can place the send call inside of an if-statement that is conditioned upon the amount of gas remaining in the function. In other words, there will likely be enough gas to complete one call, but not multiple calls.

function bid() public payable  {

require(msg.value > highBid, "bid too low");


if(gasleft() > 10000){
require(highestBidder.send(highBid), "unable to refund");
}

highestBidder = payable (msg.sender);

highBid = msg.value;
}

}

GasLeft is a built-in Solidity function that checks how much gas is left. The cherry on top to this method is that it creates a honeypot for the attacker. The attacker’s bid gets stuck while all other participants are unaffected.

TechJD is the Founder of Ascendant.Finance, which assists web2 businesses to transition to web3, consulting on all facets including SEC compliance, tokenomics, development, and connecting with angel investors. For more information check out ascendant.finance or join the Discord.

twitter: https://twitter.com/ascendantproj

--

--

TechJD

Law, programming, and everything in-between! Coming up with fun coding projects with real-world application.