$2.1 MILLION HACK ON GYM NETWORK
Audited by Certik and PeckShield with excellent reputation, but still getting hacked.
ChainZoom investigated and compiled the logic of all GymNetwork modules before being hacked at the repos: https://github.com/ChainZoom-Security/gym-network-exploit
Introduction
Gym Network, a platform touted as a farming-based interest optimization platform for users, was hacked for $2.1M on June 8th. The incident caused the GYMNET token to drop from $0.2 to $0.026, now the token has recovered to a price of $0.08
The Hacking process
Jun-August 2022 05:18:03 AM +UTC
Hacker attacks the GymSinglePool module through a contract he coded himself
=> The result of this transaction is that the hacker is noted to have deposited a lot of $GYMNET into the GymSinglePool even though he actually did not deposit a single $GYMNET.
Jun-August 2022 05:18:45 AM +UTC)
Hacker withdraw 8,000,000 GYMNET from GymSinglePool to his contract. This is the transaction.
Jun-August 2022 05:18:45 AM +UTC)
Hacker transferred 320,000 GYMNET to wallet 0xef6afbb3e43a1289bd6b96252d372058106042f6, 480,000 GYMNET to wallet 0x7e8413065775e50b0b0717c46118b2e6c87e960a, and swapped the remaining 7,200,000 GYMNET for more than 3300 BNB. This is the transaction.
So how did Hacker attack GymSinglePool?
Contract GymSinglePool is actually a Proxy, before being attacked, it executes according to the logic implemented in this contract.
The code can be viewed here
Basically, GymSinglePool allows users to farm in blocks, it is the logic of sushi farm extended with lockingPeriods and levels selection function along with interest optimization through a series of modules such as GymAccountant, GymMLM, GymVaultBank, LiquidityProvider, NetGymFarming.
Let's take a look at the GymSimglePool's deposit function:
/**
* @notice Deposit in given pool
* @param _depositAmount: Amount of want token that user wants to deposit
*/
function deposit(
uint256 _depositAmount,
uint8 _periodId,
uint256 _referrerId,
bool isUnlocked
) external {
require(isPoolActive, "Contract is not running yet");
IGymMLM(relationship).addGymMLM(msg.sender, _referrerId);
_deposit(_depositAmount, _periodId, isUnlocked);
}IGymMLM().addGymMLM() is just a step to save user information participating in the Pool through referrals by another user.
In the _deposit() function: (lines 386 to 450):
updatePool(), will be quite familiar to those who have learned about sushi farmtoken.safeTransferFrom(msg.sender, address(this), amountToDeposit), transfer money from user tologics for calculating timestamps and updating information for the user
Here the most important step is token.safeTransferFrom(msg.sender, address(this), amountToDeposit), to make sure the user actually deposits into the pool.
After deposit(), the user can withdraw() based on the information recorded in the deposit step, from lines 528-585.
function withdraw(uint256 _depositId) external {
require(_depositId >= 0, "Value is not specified");
updatePool();
_withdraw(_depositId);
}There is one more type of deposit in the depositFromOtherContract() function.
/**
* @notice Deposit in given pool
* @param _depositAmount: Amount of want token that user wants to deposit
*/
function depositFromOtherContract(
uint256 _depositAmount,
uint8 _periodId,
bool isUnlocked,
address _from
) external {
require(isPoolActive, "Contract is not running yet");
_autoDeposit(_depositAmount, _periodId, isUnlocked, _from);
}The _autoDeposit() function also has:
updatePool()the logic for calculating and updating information for the user
But where is
token.safeTransferFrom()? Why not transfer money from the user to the pool but the pool approves itself to spend its own money? (line 446)
token.approve(address(this), _depositAmount);The hacker took advantage of this to attack, he did not deposit a single GYMNET coin but was recorded to have deposited 8,000,000 GYMNET into the pool, then he withdrew 8,000,000 GYMNET and sold it.
This raises a lot of questions. Is the mistake really because of the team's carelessness, or is the hack totally scripted?


