Platform: Code4rena
Start Date: 27/11/2023
Pot Size: $60,500 USDC
Total HM: 7
Participants: 72
Period: 7 days
Judge: Picodes
Total Solo HM: 2
Id: 309
League: ETH
Rank: 7/72
Findings: 2
Award: $1,662.48
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: LokiThe5th
Also found by: kfx, lil_eth, sivanesh_808
1651.1636 USDC - $1,651.16
This is likely to be a contentious issue, so the reviewer will deviate from the usual submission template to make a clear case for this issue.
The birthday paradox refers to the counterintuitive fact that in a room of only 23 people there is a probability exceeding 50% that any two people share a birthday.
ELI5 (as much for my own benefit as for the reader's.)
Where there are random numbers generated there is a much greater likelihood that there will be any number that is drawn at least twice, than a single predetermined number. In other words, for a set of 0 <= x <= 99
possible numbers, if we only make 10 draws (and not excluding this number from subsequent draws) looking for the number x
, then there is for each draw a 0.01
probability to get x
, and so for a sample of 10
draws the probability of finding x
is 0.1
.
But, the chance of drawing any two values that are the same, in those 10
draws is about 0.39
. For 25
draws the odds are about 0.95
. See more about this calculation and this attack vector here.
For the keccak256 hashing used in the EVM, it means that there are 2^256
possible hashes for all possible inputs. Of course, all the possible inputs are much greater than 2^256
. This brings us to the pigeonhole problem (which we run into when we cast values from uint256
to uint64
, as an example). To simplify, where there are functions that have a greater number of valid inputs than the number possible outcomes, then logically there must be multiple inputs that have the same output.
Consider this code snippet from the contest, which follows a well-known pattern:
address( uint160( uint256( keccak256( abi.encodePacked( bytes1(0xff), factory, keccak256(abi.encode(features)), Constants.V3POOL_INIT_CODE_HASH ) ) ) ) )
We know that keccak256
provides a 256-bit result, but then that 256-bit result is truncated to a 160-bit value. The preceeding 96-bit value is lost. Meaning that for every 160-bit address derived by the code above, there would have been 2^96
possible values that could have led to that address. For our N
of 160, this means an attacker would need to compute sqrt(2^160)
, which is 2^80
, to be likely to find a collision.
The validateCallback
function validates that a sender
address conforms to the expected counterfactual address for a Uni-V3 pool given the specified features and the appropriate FACTORY
address.
The check assumes that the only way a sender
address can be derived is through deployment via the V3 Factory contract, which uses counterfactual deployments. This is problematic, given that the uniswapV3MintCallback
does not check that a pool address exists, only that it conforms to the address derivation rules from CallBackLib::validateCallback()
.
Given the pigeonhole problem, and an attacker's ability to generate inputs by varying the parameters supplied in the data
input of the uniswapV3Callback
, it is theoretically feasible that an attacker can generate some combination of token0
, token1
, and fee
, where the token0
is a valid address for a token to be stolen (e.g. USDC
or WETH
), and the token1
and fee
can be varied arbitrarily to create addresses. The attacker would then generate EOA (or deterministic contract) addresses with varying parameters until they generated an address that matches ANY of the possible addresses generated by part 1.
The impact would be that the attacker then deploys the EOA which has been found to collide with a given set of pool parameters. The attacker would then be able to call uniswapV3MintCallback
directly, passing in the seed values used to create the collision in the data
parameters. Because this will ensure the validateCallback
check passes, the attacker would be able to specify ANY address that has given approval to the SemiFungiblePositionManager
and transfer the approved amount of token0
to the attacker address. This is problematic as the SemiFungiblePositionManager
is likely to accrue significant approvals, as it needs approvals from users to safeTransferFrom
from the user
to the uniswapV3Pool
when creating positions.
Consider the following:
Therefore, there must be hash outputs where the last 160 bits of the 256 bit hash output are the same. There are 2^96 different keccak256 hash outputs that would share the same last 160 bits.
The odds of person creating an address using counterfactual deployment (or normal EOA generation) and so create an account with the same address as a specific target
address in normal on-chain use are so small that it's practically impossible.
But the probability of getting a usable address from address deployment is the probability of ANY address derived using the method in the validateCallback
function being the same as ANY address (EOA or deterministic) derived by the attacker through some seed values. This greatly reduces the amount of compute necessary, but it is still not trivial.
The attacker would need to compute at least two sets of 2^80 - one set for the EOA and the other for the combination of PoolFeatures
that would clash. With that there is a 39% chance of a clash.
As the submission by 0x52 showed, drawing 2^81 samples would raise the success rate to 86%.
As the feasibility of this attack is probably the greatest limiting factor, it would be wise to get some real-world bounds for this.
Please note that these are rough estimates to provide context
Given these parameters it would take a single antminer (at 255Th/s) about 300 years to generate the first set. (2^81 hashes at 255e12 hashes generated per second). So to do it in one year would require about 300 antminers, thus a capital outlay of USD 1.26 million. Then you need the second set, so it would take another year for a total of two sets. In terms of power you would need 5.4kw (single machine power draw) * 300 (number of miners) * 0.12 (cost per kwh) * 8784 (hours in a year) * 2
(using here the average cost to businesses of 0.12 USD per kWh), which gives about USD 3 415 219.2 of energy costs in total.
So here we have an attack that will cost about 4.6 million dollars in capex + energy over a two year period.
The SemifungiblePositionManager
requires token approvals from users to allow it to safeTransferFrom
the payer
to the uniswapV3Pool
and would have accrued enough approvals within two years that should be multiples of this cost, thus the attack is expected to be profitable.
The reviewer has here not taken into account the requirement of a reliable power supply of 1.62MWh that 300 antminers running in concert would need, nor the other operational expenses related to such a massive enterprise, nor the storage problem of storing 2^81
outputs to check the second set against. Importantly, it is assumed that the machines used are hashing at equivalent efficiencies for keccak256
hashes.
But the continued improvement of technology makes this more and more feasible within the next few years (there is already a new antminer slated for early 2024 release that is expected to reach 335TH/s without greater power draw).
Considering the broader context of hacks within the DeFi industry and known nation-state actors operating in the space, and, considering that the fix for this is also relatively simple, it seems a prudent idea to add a fix to mitigate this issue that may become very real within the next decade.
Manual review.
In the callback, obtain the fee
and use that to query the FACTORY
to ensure the msg.sender
is a pool that has been deployed.
function validateCallback( address sender, address factory, PoolFeatures memory features ) internal pure { // compute deployed address of pool from claimed features and canonical factory address // then, check against the actual address and verify that the callback came from the real, correct pool - if ( - address( - uint160( - uint256( - keccak256( - abi.encodePacked( - bytes1(0xff), - factory, - keccak256(abi.encode(features)), - Constants.V3POOL_INIT_CODE_HASH - ) - ) - ) - ) - ) != sender - ) revert Errors.InvalidUniswapCallback(); + if (sender != IFactory(factory).getPool( + features.token0, + features.token1, + features.fee + )) revert Errors.InvalidUniswapCallback(); }
Invalid Validation
#0 - c4-sponsor
2023-12-17T19:37:39Z
dyedm1 (sponsor) disputed
#1 - dyedm1
2023-12-17T19:37:54Z
valid, but dup #128
#2 - c4-judge
2023-12-24T14:46:51Z
Picodes marked the issue as duplicate of #128
#3 - c4-judge
2023-12-26T23:05:45Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-12-26T23:06:39Z
Picodes marked the issue as selected for report
🌟 Selected for report: osmanozdemir1
Also found by: 0xCiphky, Audinarey, Banditx0x, CRYP70, Cryptor, D1r3Wolf, KupiaSec, LokiThe5th, Sathish9098, Skylice, ThenPuli, Topmark, Udsen, ZanyBonzy, baice, ether_sky, fatherOfBlocks, foxb868, grearlake, hihen, hubble, hunter_w3b, lanrebayode77, leegh, lsaudit, minhtrng, nocoder, onchain-guardians, ptsanev, ro1sharkm, seaton0x1, sivanesh_808, t4sk, tapir, tpiliposian, ustas
11.3163 USDC - $11.32
There are no payable functions within the ERC1155Minimal.sol
or the SemiFungiblePositionManager.sol
contracts. There are also no functions that would allow the retrieval of any ether
that's sent to these contracts. Thus any ether accidentally sent to these contracts via the payable
multicall
would be stuck in the contract.
Remove the payable
keyword from Multicall::multicall()
.
Some zkEVMs derive addresses using a different method than what is used on Mainnet and most L2's.
Given this, the code in the contest repo which relies on deterministic computation of addresses (CalbackLib::validateCallback()
specifically) is unlikely to function correctly in such zkEVM contexts.
It is impossible (currently) to support all L2's relying on deterministic deployments. Please take care in the future when deploying to zk-L2's to ensure code compatibility.
#0 - c4-judge
2023-12-14T16:49:25Z
Picodes marked the issue as grade-c
#1 - c4-judge
2023-12-26T23:07:16Z
Picodes marked the issue as grade-b