Re-constructing the GDS Hack || Things that you should actually know to code safe protocols


On January 3, 2023, the GDS project on Binance Smart Chain was hacked. Soon after that followed the post-mortem articles from:

While all of them more or less wrote the same thing (someone checks for plagiarism? 😉); I do not believe even a single one of them was successful in explaining how the hacker executed his / her work. Quite a shame!

How do I know, you wonder 🤔? Well, I am a security researcher in this field. I have spent some time understanding how these hacks are executed and best of all, I try and re-create them so that I exactly know the what and the how!

So if you are a smart contract developer, intending to make sure that you write “safe code”, and you are keeping a tab on the hacks that happen; you need to make sure that you understand this hack very carefully

Now, this hack is not one of the biggest hack (in terms of $$ lost); but it is one fine job done over multiple blocks and hence you should understand that this hack is NOT the job of the FLASHLOAN ALONE!

The other auditors f***ed up in not being able to understand and explain the hack that was executed over 1370 blocks [24451036 less 24449667 (including the last block)] and NOT through a SINGLE transaction that has been identified.

To prove the above, I have documented the exact manner in which the hack was executed, by (a) explaining the events and (b) re-creating this hack.

Before Transaction 1:

Transaction 1 (Epoch Computation Transaction):

The Base Attacker Contract does the following things in a loop for almost 30 times:

  • Creates the first “Attacker” contract (eg:0x0f8D735c0b67f845068Bb31684707851f9D2767D)
  • Transfers ~0.19 LP tokens to this attacker contract.
  • Swaps 120USDC from Base Attacker for GDS token and feed into this Attacker Contract
  • Transfers the GDS Token to the dead account (0x000000000000000000000000000000000000dEaD) (from the Attacker Contract):
  • Transfers Back 0.19 LP tokens to his wallet.
  • Repeating the above steps for the second “attacker” contract and so on for all others.

Intention behind transaction 1:

  • The intention behind this transaction is to exploit a vulnerability in the GDS contract’s “_settlementLpMining” function. [Yes, this was identified by the other auditor’s blog post also]
  • The function has a check that the “lastEpoch” of the “from” address should be greater than zero.
  • By transferring GDS tokens to a dead account, the contract sets the lastEpoch of the attacker contract to the current epoch(in this case, from 0 to 29). This allows the attacker to use the updated lastEpoch(now equal to the current epoch, 29) to take advantage of the flaw in the contract when the attacker does the transfer in Transaction2.
  • This means that if there are LP tokens in the caller’s account (ie the Attacker Contract) and the currentEpoch is greater than the lastEpoch, the “_lpRewardAmount” is calculated and transferred to the caller.
  • The transaction 1 sets the first condition true for all attacker contracts in a loop by transferring GDS token to dead account.

Transaction 2 (Flash Loan Transaction):

The one that is believed to be the only transaction in which the hack was executed. ROFL 🤦‍♂️

  • Take flash loan of all USDC tokens available in the FlashLoan Contract, in this case 2M USDC was borrowed to the Base contract (0x0B995C08ABddC0442BEE87d3a7C96b227f8e7268).
  • In the callback of the flash loan call:
    - Take another flash loan of ~315k USDC from another source.
    - Swap 600K USDC for GDS tokens.
    - Add liquidity to the USDC/ GDS pool to acquire a large amount of LP tokens.
    - Perform the following steps in the loop for previously created Attackers contracts:
    1. Transfer all acquired LP tokens to the previously deployed Attacker 1 contract.
    2. Call a function withdraw created in the attacker 1 contract, which does the following:
    2.1 Transfer 10000 wei [please note: wei only] of GDS tokens to dead account
    2.2 Gets the reward from the GDS token
    2.3 Transfer back all LP tokens to the base contract
    3. Continue to transfer LP tokens to the next Attacker contracts and call withdraw functions of same.

Explanation of Transaction 2:

Due to the Transaction 1, the lastEpoch of the caller (Attacker contracts in this case) was set to the currentEpoch. Now when the GDS Tokens is transferred to the dead account, the second condition currentEpoch > lastEpoch[_from] satisfies. This will NOT be possible if the attack was executed ONLY through the flash loan contract. This is where all the other audit companies focused only.

And as the large amount of LP tokens are acquired, the calculated rewards are also high and the caller ends up with the good amount of GDS tokens.

Code explanation:

  • _refreshDestroyMiningAccount will be called in _afterTokenTransfer which eventually gets called after token transfer of GDS.
  • Since the to address of the transfer is equal to the dead account, the first branch gets executed.
  • isOpenLpMining is true at the time of attack, it will enter the nested branch inside and execute _settlementLpMining
  • Now the first condition gets satisfied after Transaction 1 and the second & third conditions gets satisfied after Transaction 2, the rewards get calculated and the caller is rewarded with GDS tokens. The more LP tokens the caller have, the more rewards is emitted.

Attack simulation:

I have re-created parts of the hack in this GitHub Repo


Base Attacker contract : Used in Transaction 1 (Epoch Computation Transaction):

Attacker1 contract: Used in both transactions 1 and 2:

  • In the attack, Transaction 1 uses this “Attacker 1” contract to transfer initial GDS tokens and sets its current epoch as the last epoch. This contract serves as a blueprint for over 50 similar contracts to increase the impact of the attack. Transaction 2 also leverages the Attacker 1 contract, along with a flash loan, to acquire large amounts of LP tokens and receive substantial rewards from the GDS token contract.
  • transferToken() called in Txn1: burn GDS tokens equivalent to 100USDC.
  • withdraw() called in Txn2: burn 10k wei of GDS tokens.

FlashLoanExample contract: The FlashLoanExample contract performs the following steps:

  • It takes a flash loan of USDC tokens from a pool to obtain a large amount.
  • Half of the obtained USDC tokens are swapped to GDS tokens and used to add liquidity to the USDC-GDS pool to get a large number of LP tokens.
  • The LP tokens are then transferred to the Attacker1 contract and the withdraw() function is called. This function burns a small amount of GDS tokens to obtain large rewards.
  • Finally, all the tokens are transferred back to the FlashLoanExample contract and swapped back to USDC to repay the flash loan debt along with the fees.

It’s important to note that in this demonstration, only one attacker contract was used, resulting in a loss due to the incurred fees for internal transfers. If more than 50 attacker contracts were used, as the attacker in the actual attack did, the rewards would have been higher. But it can be clearly seen in the above log that by burning only 10k wei of GDS token, a large amount of GDS tokens are received as reward, hence an PROFIT!

Scripts used:

  • deploy.ts : a simple script to deploy all contracts
  • flash.ts: creates instances of the contracts and perform Transaction 1 and Transaction 2. Note that some arbitrary values are being used in the script to perform the desired job, such as transfer amount, swapping amount, etc.

Steps taken to re-create the hack:

  1. Creating a forked testnest, forking from BSC at blocknumber: 24449913, allow unlimited contract size

2. Fork this Github Repo:

3. Update your .env file for the RPC from buildbear and your private key:

4. Visit the faucet, Add Network to Metmask, and get a good amount of Native tokens (BB ETH) and the ERC20 token 0x55d398326f99059fF775485246999027B3197955

5. Run npm i in your terminal, and then the command

npx hardhat run scripts/deploy.ts --network buildbear

You should see something like this:

6. Update the addresses in scripts/flash.ts file

7. Visit the Faucet again, for getting more Binance-Pegged USD Token 0x55d398326f99059fF775485246999027B3197955 into the baseAttacker contract and the flash loan example contract. See the example image below:
NOTE that I have updated the address in the first box in which I want the USD token. That is the address of the baseAttacker contract.
This is where I could not find anything better than BuildBear (shout out 🎉🔈). It is easy to get any ERC20 token that I might need in a matter of seconds.

Do the same for the Attacker_contract and also the Flash Loan Example Contract.

8. Job done. Just run the script npx hardhat run scripts/flash.ts --network buildbear

At the end of the script, you should see something like this:

You have successfully hacked the smart contract of GDS for the computation error.


Summary of the simulation:

  • The simulation demonstrated how the GDS contract can emit more GDS rewards through an attack.
  • The demonstration used only one attack contract to show the potential reward that could be earned.
  • Using only one attack contract resulted in a high cost in terms of transaction fees. However, if the attacker had used more than 50 attack contracts, they would have been able to earn higher rewards.
  • Example:
    Let’s say the attacker had 50 attack contracts each with 1 million LP tokens. If each attack contract earned 10,000 GDS tokens as rewards, the total reward would be 500,000 GDS tokens. This is assuming that each transfer incurs the same amount of fees, which is likely to be the case since the same attack function is used in all contracts. However, the attacker would have been able to earn more rewards if they had used more than 50 attack contracts.

Let’s get started then, Shall we?