Smart Contract Vulnerabilities that ChatGPT cannot uncover (Part 2)

TechJD
4 min readMar 13, 2024

Welcome back to the series. In the last part, we talked about sneaky Denial of Service attacks. In this edition, we will examine an outdated cache exploit from a famous DeFi example, Cover Protocol.

I was curious, so I asked ChatGPT 4 if it could audit the exploited function:

function deposit(address _lpToken, uint256 _amount) external override {
require(block.timestamp >= START_TIME , "Blacksmith: not started");
require(_amount > 0, "Blacksmith: amount is 0");
Pool memory pool = pools[_lpToken];
require(pool.lastUpdatedAt > 0, "Blacksmith: pool does not exists");
require(IERC20(_lpToken).balanceOf(msg.sender) >= _amount, "Blacksmith: insufficient balance");
updatePool(_lpToken);

Miner storage miner = miners[_lpToken][msg.sender];
BonusToken memory bonusToken = bonusTokens[_lpToken];
_claimCoverRewards(pool, miner);
_claimBonus(bonusToken, miner);

miner.amount = miner.amount.add(_amount);

miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER);
miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER);
IERC20(_lpToken).safeTransferFrom(msg.sender, address(this), _amount);
emit Deposit(msg.sender, _lpToken, _amount);
}

I’ll spare you the word salad that it spit back out at me, but you can imagine it wasn’t very helpful. There were general suggestions about reentrancy and adding more validation checks for safety, followed by an admission that more information is needed to see how some of the other functions are implemented. So, I asked again:

The closest to the correct answer ChatGPT got was here:

But this still isn’t what’s wrong with the contract. And no, more information is not really needed. The issue, which was spotted in the attack on Cover Protocol, is staring us right in the face. I am adding the same code as above, this time with the in-line comments I made added back (I wasn’t going to give ChatGPT any hints):

function deposit(address _lpToken, uint256 _amount) external override {
require(block.timestamp >= START_TIME , "Blacksmith: not started");
require(_amount > 0, "Blacksmith: amount is 0");
Pool memory pool = pools[_lpToken]; //caches data
require(pool.lastUpdatedAt > 0, "Blacksmith: pool does not exists");
require(IERC20(_lpToken).balanceOf(msg.sender) >= _amount, "Blacksmith: insufficient balance");
updatePool(_lpToken); //updates the pool. updatePool checks the current balance of the lptoken in the contract

Miner storage miner = miners[_lpToken][msg.sender];
BonusToken memory bonusToken = bonusTokens[_lpToken];
_claimCoverRewards(pool, miner); //still uses the old pool values
_claimBonus(bonusToken, miner); //still uses the old pool values

miner.amount = miner.amount.add(_amount);
// update writeoff to match current acc rewards/bonus per token
miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER); //still uses the old pool values
miner.bonusWriteoff = miner.amount.mul(bonusToken.accBonusPerToken).div(CAL_MULTIPLIER); //still uses the old pool values

IERC20(_lpToken).safeTransferFrom(msg.sender, address(this), _amount);
emit Deposit(msg.sender, _lpToken, _amount);
}

The attacker exploited the outdated cache value of pool to dramatically increase the value of the rewards received.

According to this breakdown by Mudit Gupta:

  1. A new pool was approved for liquidity mining, merely hours before the hack. This pool is perfectly normal but since it was new, the blacksmith contract didn’t have any LP token of this pool.
  2. The attacker deposited some tokens of this pool into the Blacksmith contract.
  3. The Blacksmith contract keeps track of rewards on a per token basis. If a lot of tokens are locked, the per token reward will be small. If very few tokens are locked, the per token reward will be large. The relevant variable is called accRewardsPerToken and is calculated as totalPoolRewards / totalTokenBalance.
  4. The attacker then withdrew almost all of the LP tokens from the Blacksmith contract, reducing the totalTokenBalance amount to almost zero.
  5. The attacker then deposited some tokens of this pool again into the Blacksmith contract. This is where the bug showed its true colors. Since the totalTokenBalance was reduced a lot in the previous transaction, the newly calculated accRewardsPerToken shot up. The contract uses rewardWriteoff to keep the affect of accRewardsPerToken in check. However, due to the bug, the old (small) value of accRewardsPerToken was used when calculating the rewardWriteoff value. Due to this, the large value of accRewardsPerToken remained unchecked.
  6. The attacker then withdrew their rewards. Since there was a large, unchecked value in accRewardsPerToken, the total reward paid out of the system got inflated and the contract ended up minting 40,796,131,214,802,500,000 COVER tokens.

Moral of the story here is, always check to make sure you’re using the correct values in your function. Because ChatGPT won’t catch it!

If you found this article helpful and enjoy learning about all sorts of cool tricks with Solidity and DeFi hacks, star our CryptoCadet Academy repo on GitHub for more!

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.

https://twitter.com/ascendantproj

https://twitter.com/cryptocadetapp

https://twitter.com/thetechjd

--

--

TechJD

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