DeFiFoundry
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

Funds for an auction can be drained by an attacker via a hash collision.

Summary

The function createAuction() from AuctionFactory compute the salt solely from a parameter set by a msg.sender which means, the final address where the contract will be deployed relies solely on the parameters of the function.

Description

An attacker can use brute-force to find two private keys that create EOAs that matches with the potential auctionAddress before The attacker approves auction tokens to an EOA which would enable him drain all the funds.

salt is set derived from parameter set by msg.sender, thus, the final address is determined only by the four parameters passed to the function (address auctionToken, uint256 biddingTime, uint256 totalTokens, bytes32 salt). By brute-forcing many address values, we have obtained many different (undeployed) address accounts for (1). The user can know the address of an auction before deploying it,

(2) can be searched the same way. The contract just has to be deployed using CREATE2, and the salt is in the attacker's control by definition. An attacker can find any single address collision between (1) and (2) with high probability of success using the following meet-in-the-middle technique, a classic brute-force-based attack in cryptography:

Brute-force a sufficient number of values of delegate address (2^80), pre-compute the resulting account addresses, and efficiently store them e.g. in a Bloom filter data structure.

Brute-force contract pre-computation to find a collision with any address within the stored set in step 1. The feasibility, as well as detailed technique and hardware requirements of finding a collision, are sufficiently described in multiple references:

PoC: Finding a collision
Note that in createAuction, CREATE2 salt is user-supplied

address auctionAddress = address(
new FjordAuction{ salt: salt }(fjordPoints, auctionToken, biddingTime, totalTokens)
); //@audit address collision to steal all the auction points by allowances

The address collision an attacker will need to find are:

One undeployed auction address (1).
Arbitrary attacker-controlled wallet contract (2).
Both sets of addresses can be brute-force searched because:

As shown above, salt is a user-supplied parameter. By brute-forcing many salt values, we have obtained many different (undeployed) wallet accounts for (1).
(2) can be searched the same way. The contract just has to be deployed using CREATE2, and the salt is in the attacker's control by definition.
An attacker can find any single address collision between (1) and (2) with high probability of success using the following meet-in-the-middle technique, a classic brute-force-based attack in cryptography:

Brute-force a sufficient number of values of salt (2^80), pre-compute the resulting account addresses, and efficiently store them e.g. in a Bloom filter data structure.
Brute-force contract pre-computation to find a collision with any address within the stored set in step 1.
The feasibility, as well as detailed technique and hardware requirements of finding a collision, are sufficiently described in multiple references:

Coded unit-PoC

While we cannot provide an actual hash collision due to infrastructural constraints, we provide a coded PoC to prove the following properties of the EVM enabling this attack:

  1. A contract can be deployed on top of an address that already had a contract before.

  2. By deploying a contract and calling selfdestruct in the same transaction, allowances can be set for an address that has no bytecode.

Steps to recreate:

  1. Paste the following file onto Remix:

pragma solidity ^0.8.20;
contract Token {
mapping(address => mapping(address => uint256)) public allowance;
function increaseAllowance(address to, uint256 amount) public {
allowance[msg.sender][to] += amount;
}
}
contract InstantApprove {
function setApprove(Token ts, uint256 amount) public {
ts.increaseAllowance(msg.sender, amount);
}
function destroy() public {
selfdestruct(payable(tx.origin));
}
}
contract Test {
Token public ts;
uint256 public constant APPROVE_AMOUNT = 2e18;
constructor() {
ts = new Token();
}
function test(uint _salt) public returns (address) {
InstantApprove ia = new InstantApprove{salt: keccak256(abi.encodePacked(_salt))}();
address ia_addr = address(ia);
ia.setApprove(ts, APPROVE_AMOUNT);
ia.destroy();
return ia_addr;
}
function getCodeSize(address addr) public view returns (uint) {
uint size;
assembly {
size := extcodesize(addr)
}
return size;
}
function getAllowance(address from) public view returns (uint) {
return ts.allowance(from, address(this));
}
}
  1. Deploy the contract Test.

  2. Run the function Test.test() with a salt of your choice and record the returned address. The results will be:

    • Test.getAllowance() for that address will return exactly APPROVE_AMOUNT.

    • Test.getCodeSize() for that address will return exactly zero.

  3. Using the same salt, run Test.test() again. The transaction will go through, and the result will be:

    • Test.test() returns the same address as the first run.

    • Test.getAllowance() for that address will return twice APPROVE_AMOUNT.

    • Test.getCodeSize() for that address will still return zero.

Impact

Complete draining of an account's funds if allowances are set and selfdestruct is called.

The advancement of computing hardware shows that the cost of an attack is just a few million dollars, and the current Bitcoin network hashrate allows brute-forcing in about half an hour. The cost of the attack may be offset by longer brute-force times.

For DeFi protocols, it is normal for account balances to reach significant values. Such an attack is massively profitable and poses a severe risk to the protocol's users.

Tools Used

Manual Audit

Recommended Mitigation Steps

To mitigate this vulnerability, avoid allowing user-supplied salt, ensuring salts are securely generated on-chain, or implement additional verification steps before deploying contracts to deterministic addresses.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.