How Was Hashflow Exploited?

4 min read

Learn how Hashflow was exploited, resulting in a loss of approximately $605,000.

TL;DR#

On June 14, 2023, Hashflow was exploited on multiple chains owing to a smart contract vulnerability, which resulted in a loss of approximately $605,000.

Introduction to Hashflow#

Hashflow is a DEX designed for zero slippage, interoperability, and MEV-protected trades.

Vulnerability Assessment#

The root cause of the exploit is a vulnerable transferFrom function that allows anyone to arbitrarily transfer the assets authorized by the users.

Steps#

Step 1:

We attempted to analyse one of the attack transactions executed by the exploiter on the Ethereum Mainnet.

Step 2:

It appears that the event is a coordinated white-hat operation in an attempt to ensure a swift recovery of user assets.

Step 3:

There existed a vulnerable `0x1ce5` function in the exploited contract containing the transferFrom invocation, which allowed for the arbitrary transfer of the tokens authorized by the users.

function 0x1ce5(uint256 varg0, uint256 varg1, uint256 varg2, uint256 varg3) private { 
    v0 = address(varg2);
    v1 = address(varg1);
    v2 = address(varg3);
    if (this.balance >= 0) {
        if ((address(v2)).code.size) {
            v3 = v4 = 0;
            while (v3 < 100) {
                MEM[v3 + MEM[64]] = MEM[v3 + (MEM[64] + 32)];
                v3 += 32;
            }
            if (v3 <= 100) {
                goto 0x30d1B0x247dB0x237b0x1ce5;
            } else {
                MEM[100 + MEM[64]] = 0;
            }
            v5, v6, v7, v8 = address(v2).transferFrom(v0, v1, varg0).gas(msg.gas);
            if (RETURNDATASIZE() == 0) {
                v9 = v10 = 96;
            } else {
                v9 = v11 = new bytes[](RETURNDATASIZE());
                RETURNDATACOPY(v11.data, 0, RETURNDATASIZE());
            }
            if (!v5) {
                require(!MEM[v9]v8, MEM[v9]);
                v12 = new array[](v13.length);
                v14 = v15 = 0;
                while (v14 < v13.length) {
                    v12[v14] = v13[v14];
                    v14 += 32;
                }
                if (v14 <= v13.length) {
                    goto 0x2d0bB0x30dbB0x25e4B0x25aaB0x247dB0x237b0x1ce5;
                } else {
                    v12[v13.length] = 0;
                }
                revert(Error(v12, v16, 'SafeERC20: low-level call failed'));
            } else {
                if (MEM[v9]) {
                    require(32 + v9 + MEM[v9] - (32 + v9) >= 32);
                    require(MEM[32 + v9] == MEM[32 + v9]);
                    require(MEM[32 + v9], Error('SafeERC20: ERC20 operation did not succeed'));
                }
                return ;
            }
        } else {
            MEM[MEM[64] + 4] = 32;
            revert(Error('Address: call to non-contract'));
        }
    } else {
        MEM[MEM[64] + 4] = 32;
        revert(Error('Address: insufficient balance for call'));
    }
}

Step 4:

In the attack contract, it can be seen that there are two ways in which users can choose to recover their assets: 

  • first, by calling the `recover` function, which allows for 100% of the assets recovery,
function recover(address token) external payable {
  uint256 amount = userMap[token][msg.sender];
  userMap[token][msg.sender] = 0;
  IERC20(token).safeTransfer(msg.sender, amount);
}
  • and another, by calling the `recoverWithDonate` function, in which 10% of the user's assets are held back as a bribe.
function recoverWithDonate(address token) external payable {
  uint256 amount = userMap[token][msg.sender];
  userMap[token][msg.sender] = 0;
  uint256 transferAmount = amount * 90 / 100;
  uint256 donate = amount - transferAmount;
  IERC20(token).safeTransfer(msg.sender, transferAmount);
  IERC20(token).safeTransfer(owner, donate);
}

 Step 5:

The deployer of the attack contract also left an on-chain message for users to first revoke their permissions on the affected contracts before attempting to recover their assets. Without revoking the permissions, the same or another hacker can easily drain the funds yet again.

Step 6:

The other vulnerable contracts across different chains on multiple pools are also listed alongside for reference: BNB ChainPolygonAvalanche-C ChainArbitrum One.

Aftermath#

The team acknowledged the occurrence of the exploit, and stated that all of the affected users will be reimbursed. The Hashflow DEX remained unaffected and was fully operational.

The team also shared the process for recovering funds in full for all of the affected users.

Solution#

The exploit could have been prevented to a greater extent had the users revoked their permissions for all of the affected Hashflow contracts.

Moreover, the integration of parametric cover policies offered by Neptune Mutual could have significantly alleviated the impact of this attack on its users. These policies protect users against financial losses resulting from smart contract vulnerabilities without the need to provide explicit evidence of individual losses. Upon the resolution of an incident, policyholders can claim their payouts promptly, reducing the financial distress and time spent in recovery procedures. At the moment, the marketplace is available on two popular blockchain networks, Ethereum, and Arbitrum.

Neptune Mutual's security team would also have evaluated the platform for DNS and web-based security, frontend and backend security, intrusion detection and prevention, and other security considerations.

Reference Source CertiK

By

Tags