Platform: Code4rena
Start Date: 07/04/2023
Pot Size: $47,000 USDC
Total HM: 20
Participants: 120
Period: 6 days
Judge: GalloDaSballo
Total Solo HM: 4
Id: 230
League: ETH
Rank: 98/120
Findings: 1
Award: $10.86
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: AkshaySrivastav
Also found by: 0xTheC0der, Dug, GT_Blockchain, Haipls, adriro, bin2chen, carlitox477, dingo2077, fs0c, hasmama, hihen, holyhansss_kr, juancito, ladboy233, philogy, saian, said, sashik_eth, yixxas, zion
10.8554 USDC - $10.86
Factory.create(address,address,uint128,uint128,uint56,uint16,bytes32,bool,bool,bytes32,uint256[] memory,uint256)
: DoS attack on new private pools creation by sending the same saltAs the _salt
parameter is included in the transaction, it is possible for a bot to frontrun the creation of a new pool with the same salt, resulting in the reversal of the intended transaction.
DoS attack on new private pools creation by sending the same salt.
As per the documentation provided by OpenZeppelin, the address of a newly created contract using the create2 bytecode can be computed by the following formula: new_address = hash(0xFF, sender, salt, bytecode)
.
The bytecode is the one used by LibClone
when cloneDeterministic
is call:
// LibClone.cloneDeterministic(address,bytes memory, bytes32) // Create the instance. instance := create2(0, sub(data, 0x4c), add(extraLength, 0x6c), salt)
This line of code has 3 possible outputs:
cloneDeterministic
is call by Factory in function create
:
privatePool = PrivatePool( payable(privatePoolImplementation.cloneDeterministic(_salt)) );
From quoted OpenZeppelin article, it can be deducted that the deterministic address privatePool
relays on 3 values:
sender
: The sender
is always the Factory contract since it is the one that calls the cloneDeterministic function.cloneDeterministic
.bytecode
: It corresponds to privatePoolImplementation
's code._salt
: provided by the caller of the transaction.Given that sender and bytecode cannot be manipulated by Factory.create(address,address,uint128,uint128,uint56,uint16,bytes32,bool,bool,bytes32,uint256[] memory,uint256)
msg.sender
, but _salt
can be set at willing, nothing forbid frontrunning a transaction by sending the same _salt
, forcing the frontrun transaction to revert given LibCode
code:
// LibClone.cloneDeterministic(address,bytes memory, bytes32) // If `instance` is zero, revert. if iszero(instance) { // Store the function selector of `DeploymentFailed()`. mstore(0x00, 0x30116425) // Revert with (offset, size). revert(0x1c, 0x04) }
To summarize, an example of a DOS attack can be:
Factory.create(address,address,uint128,uint128,uint56,uint16,bytes32,bool,bool,bytes32,uint256[] memory,uint256)
with _salt = bytes32(1)
, and send her transactionFactory.create(address,address,uint128,uint128,uint56,uint16,bytes32,bool,bool,bytes32,uint256[] memory,uint256)
with _salt = bytes32(1)
(the rest of the values can be different).DeploymentFailed()
To mitigate the potential risk of a hash collision that could trigger a DeploymentFailed()
error and the possibility of front-running vector attacks, the following measures are recommended:
By adding a unique salt value for each new private pool created, the risk of hash collisions can be greatly reduced, and by providing a function to increase the salt value, authorized users can ensure that the salt value is changed frequently enough to further reduce the risk of front-running vector attacks.
All this means:
// Factory.sol + // New storage value + uint256 _salt; //... constructor() ERC721("Caviar Private Pools", "POOL") Owned(msg.sender) { + _salt = 1; } //... + function increaseSalt() external onlyAuthToIncreaseSalt{ + salt++; + } //.. function create( address _baseToken, address _nft, uint128 _virtualBaseTokenReserves, uint128 _virtualNftReserves, uint56 _changeFee, uint16 _feeRate, bytes32 _merkleRoot, bool _useStolenNftOracle, bool _payRoyalties, - bytes32 _salt, uint256[] memory tokenIds, // put in memory to avoid stack too deep error uint256 baseTokenAmount ) public payable returns (PrivatePool privatePool) { // check that the msg.value is equal to the base token amount if the base token is ETH or the msg.value is equal // to zero if the base token is not ETH // require((_baseToken == address(0) && msg.value == baseTokenAmount) || (_baseToken != address(0) && msg.value == 0)) if ( (_baseToken == address(0) && msg.value != baseTokenAmount) || (_baseToken != address(0) && msg.value > 0) ) { revert PrivatePool.InvalidEthAmount(); } // deploy a minimal proxy clone of the private pool implementation privatePool = PrivatePool( - payable(privatePoolImplementation.cloneDeterministic(_salt)) + payable(privatePoolImplementation.cloneDeterministic(bytes32(_salt))) //.. + _salt++ }
#0 - c4-pre-sort
2023-04-19T06:22:51Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-pre-sort
2023-04-20T17:18:45Z
0xSorryNotSorry marked the issue as duplicate of #419
#2 - c4-judge
2023-05-01T07:22:53Z
GalloDaSballo marked the issue as satisfactory