On 26 April Pike Finance was exploited for $299k. The project announced via their X account that the root cause was due to a forged CCTP message which allowed the attacker to drain funds. As a result of the exploit they paused their contract which required it to be upgraded first.
The upgrade caused a knock on effect and on 30 April 2024 Pike Finance was exploited for a second time with approximate losses of $1.68m. Due to the way in which delegatecall works with upgradeable contracts, the addition of the pause() and unpause() function caused a collision in storage. The contract was then able to be initialized for a second time by the attacker, giving them elevated access.
Incident 1
https://arbiscan.io/tx/0x979ad9b7f5331ea8034305a83b5cd50aea88adec395fff8298dd90eb1b87667f
https://etherscan.io/tx/0x167c97b5897d11b23a6694f30acb71e8deead771d22fe88aee04ca8696a2bffa
Incident 2
https://arbiscan.io/tx/0xdac6af5695ba00b3d229574dbf7fcc326d16b9f8a52ad2620637d3022956d157
https://etherscan.io/tx/0xe2912b8bf34d561983f2ae95f34e33ecc7792a2905a3e317fcc98052bce
The Build Up
26 April: Pike Finance was exploited for ~$299k. The project paused the contract while they investigated and addressed the vulnerability. As the project didn't contain a pause function it needed to be upgraded first.
When comparing the proxy contracts initial implementation (0x634683d7079af2EBeC84637BBC29dbD6FE817564) against the new upgrade (0xD167A1893e8F108572826dAbAe19663A9131b0c2), we can see the newly added Unpaused() and Pause() functions.
The Second Exploit
30 April: Though the contract had remained paused, Pike Finance was exploited for ~$1.68m.
The attack flow is based on the following transaction. https://etherscan.io/tx/0xe2912b8bf34d561983f2ae95f34e33ecc7792a2905a3e317fcc98052bce66431
The vulnerability is due to complexities with using delegateCall and upgradeable proxy contracts. Delegatecall allows a contract to load code from a different address at runtime.
"Storage, current address and balance still refer to the calling contract, only the code is taken from the called address."
Any change to the state by the called contract affects the calling contract state. If the state of the called contract isn’t identical to the calling contract, it can lead to incorrect behaviour. In the case of Pike Finance the state variable showing the contract was initialized returned false instead of true.
The Paused() and Unpaused() functions that were added in an upgrade after the initial incident return a boolean flag (true of false) which only occupies one bit of storage. This means they can fit and be included in a storage slot that isn’t using up its allocated space, creating a difference between the two contracts. This difference then causes the collision. In Pike Finance, the upgrade assigned 0 bytes to the initialization flag, erasing its state. Below is the state change from txn 0xf3f where the newly added unpause() function was called.
The attacker could therefore call initialize() again as the contract believed it had never been initialized.
Summary of Losses
(Approximate at time of exploit)
Incident 1
Arbitrum: $299,107
Incident 2
Arbitrum: $100,070.00
Ethereum: $1,433,977.23
Optimism: $150,458.95
Total: $1,983,633.18
The attack wallet from the first incident withdrew 1 BNB withdrawal from Tornado Cash on BSC before bridging a small amount to Arbitrum via Orbiter bridge to cover fees. After the exploit $299k was bridged from Arbitrum to Ethereum via Stargate bridge and deposited into Tornado Cash from the attacker’s wallet, 0xAdaF1626aEC26A7937aE7d1Fa0664e6E0904C1d0. After the Tornado deposits a small amount of left over ETH 0.043, too small to deposit to Tornado Cash, was sent to a Binance account.
In the second incident the attacker funded their Arbitrum wallet via Railgun which then bridged to Optimism. The exploited funds from both Arb and OP were then bridged to Ethereum via Li.Fi bridge before the exploit transaction on Ethereum was executed. 562 ETH was then transferred back to a Railgun wallet, at which point the funds are shielded, hiding their trail.
This incident is a rare case of a reaction to one exploit leading to the cause of a second exploit and highlights the cutthroat nature of web3. The issue exploited in the second incident is a widely known one but easy to overlook, especially when dealing with the pressure of having been exploited for $299k. The $1.68 million loss from the second exploit was the 5th largest incident we recorded in April.
To find out more about CertiK’s audit process and how we can help protect your smart contracts visit certik.com/products/smart-contract-audit or contact us via the social media listed on certik.com.