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: 46/120
Findings: 4
Award: $93.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
34.044 USDC - $34.04
EthRouter.change
will revert, when changes.length
> 1
EthRouter#change()
can pass in multiple changes[]
and loop execute pool.change()
The code is as follows:
function change(Change[] calldata changes, uint256 deadline) public payable { // check deadline has not passed (if any) if (block.timestamp > deadline && deadline != 0) { revert DeadlinePassed(); } .. for (uint256 i = 0; i < changes.length; i++) { Change memory _change = changes[i]; // transfer NFTs from caller for (uint256 j = 0; j < changes[i].inputTokenIds.length; j++) { ERC721(_change.nft).safeTransferFrom(msg.sender, address(this), _change.inputTokenIds[j]); } // approve pair to transfer NFTs from router ERC721(_change.nft).setApprovalForAll(_change.pool, true); // execute change PrivatePool(_change.pool).change{value: msg.value}( //<--------@audit Wrong use 'msg.value' _change.inputTokenIds, _change.inputTokenWeights, _change.inputProof, _change.stolenNftProofs, _change.outputTokenIds, _change.outputTokenWeights, _change.outputProof ); // transfer NFTs to caller for (uint256 j = 0; j < changes[i].outputTokenIds.length; j++) { ERC721(_change.nft).safeTransferFrom(address(this), msg.sender, _change.outputTokenIds[j]); } } ...
The current implementation calls pool.change()
in the loop, and the value
passed in incorrectly uses msg.value
.
If there are multiple changes[]
, the second will fail because the contract balance will be insufficient, the first one consume feeAmount + protocolFeeAmount
and the contract balance will be left msg.value - feeAmount -protocolFeeAmount
Here we should pass in address(this).balance
function change(Change[] calldata changes, uint256 deadline) public payable { // check deadline has not passed (if any) if (block.timestamp > deadline && deadline != 0) { revert DeadlinePassed(); } .. ERC721(_change.nft).setApprovalForAll(_change.pool, true); // execute change - PrivatePool(_change.pool).change{value: msg.value}( + PrivatePool(_change.pool).change{value: address(this).balance}( _change.inputTokenIds, _change.inputTokenWeights, _change.inputProof, _change.stolenNftProofs, _change.outputTokenIds, _change.outputTokenWeights, _change.outputProof ); // transfer NFTs to caller for (uint256 j = 0; j < changes[i].outputTokenIds.length; j++) { ERC721(_change.nft).safeTransferFrom(address(this), msg.sender, _change.outputTokenIds[j]); } } ...
#0 - c4-pre-sort
2023-04-20T16:55:04Z
0xSorryNotSorry marked the issue as duplicate of #873
#1 - c4-judge
2023-05-01T07:11:41Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: adriro
Also found by: 0xNorman, 0xRobocop, Aymen0909, ElKu, GT_Blockchain, Josiah, KrisApostolov, RaymondFam, SpicyMeatball, ToonVH, Voyvoda, anodaram, aviggiano, bin2chen, climber2002, giovannidisiena, jpserrat, minhtrng, rbserver, sashik_eth, shaka, wintermute
8.0283 USDC - $8.03
flashFee()
missing multiplied by some exponent depending on the base token decimals.
When execute flashLoan()
fee will undercharged
When the changeFeeQuote()
is calculated, it is performed multiplied by some exponent depending on the base token decimals.
/// @notice Returns the fee required to change a given amount of NFTs. The fee is based on the current changeFee /// (which contains 4 decimals of precision) multiplied by some exponent depending on the base token decimals. /// @param inputAmount The amount of NFTs to change multiplied by 1e18. /// @return feeAmount The fee amount. /// @return protocolFeeAmount The protocol fee amount. function changeFeeQuote(uint256 inputAmount) public view returns (uint256 feeAmount, uint256 protocolFeeAmount) { // multiply the changeFee to get the fee per NFT (4 decimals of accuracy) uint256 exponent = baseToken == address(0) ? 18 - 4 : ERC20(baseToken).decimals() - 4; uint256 feePerNft = changeFee * 10 ** exponent; feeAmount = inputAmount * feePerNft / 1e18; protocolFeeAmount = feeAmount * Factory(factory).protocolFeeRate() / 10_000; }
but in flashFee()
only return changeFee
function flashFee(address, uint256) public view returns (uint256) { return changeFee; }
missing multiplied by some exponent depending on the base token decimals.
function flashFee(address, uint256) public view returns (uint256) { - return changeFee; + uint256 exponent = baseToken == address(0) ? 18 - 4 : ERC20(baseToken).decimals() - 4; + return changeFee * 10 ** exponent; }
#0 - c4-pre-sort
2023-04-20T15:08:26Z
0xSorryNotSorry marked the issue as duplicate of #864
#1 - c4-judge
2023-05-01T07:08:51Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: AkshaySrivastav
Also found by: 0xRobocop, Koolex, adriro, bin2chen, bshramin, chaduke, cryptonue, rbserver, rvierdiiev, saian, sashik_eth, tallo
40.7364 USDC - $40.74
L-01:EthRouter#buy/sell does not determine whether royaltyRecipient is address(0)
If getRoyalty()
returns royaltyFee>0, but royaltyRecipient==address(0)
this will transfer the funds to address(0)
Suggest adding judgment:
function buy(Buy[] calldata buys, uint256 deadline, bool payRoyalties) public payable { ... if (payRoyalties) { uint256 salePrice = inputAmount / buys[i].tokenIds.length; for (uint256 j = 0; j < buys[i].tokenIds.length; j++) { // get the royalty fee and recipient (uint256 royaltyFee, address royaltyRecipient) = getRoyalty(buys[i].nft, buys[i].tokenIds[j], salePrice); - if (royaltyFee > 0) { + if (royaltyFee > 0 && royaltyRecipient!=address(0)) { // transfer the royalty fee to the royalty recipient royaltyRecipient.safeTransferETH(royaltyFee); } } }
L-02:setProtocolFeeRate() does not limit the maximum value, there is a risk of malicious amplification of the fee It is recommended that this should not exceed, for example, 5%
contract Factory is ERC721, Owned { ... function setProtocolFeeRate(uint16 _protocolFeeRate) public onlyOwner { + if (_protocolFeeRate > 5_000) revert FeeRateTooHigh(); protocolFeeRate = _protocolFeeRate; }
#0 - c4-judge
2023-05-02T07:35:55Z
GalloDaSballo changed the severity to 2 (Med Risk)
#1 - c4-judge
2023-05-02T07:35:55Z
GalloDaSballo changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-05-02T07:36:15Z
GalloDaSballo marked the issue as duplicate of #596
#3 - c4-judge
2023-05-02T18:55:35Z
GalloDaSballo marked the issue as nullified
#4 - c4-judge
2023-05-02T18:55:43Z
GalloDaSballo marked the issue as satisfactory
🌟 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()
Using the passed parameter salt
to create contract may risk DOS attacks and replay attacks.
Anyone can call Factory#create()
to create a pool
The code is as follows:
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) { ... privatePool = PrivatePool(payable(privatePoolImplementation.cloneDeterministic(_salt))); ...
This function will call privatePoolImplementation.cloneDeterministic(_salt)
,and finally use CREATE2 to create the contract
An issue could be that an attacker can frontrun the creation TX with their own creation
request, with the same salt
. This would create the exact address created by the
CREATE2 call, When the victim's transaction would be executed, the address is non-empty so the EVM would reject its creation. This would result in a bad UX for a user, who thinks the creation did not succeed. The result contract would still be usable, but would be hard to track as it was created in another TX.
In more extreme cases users may deposit funds into this address
Another possible risk: cross-chain replay attack, malicious users in other chains can generate the same contract address, but the owner is not the same (the owner becomes the malicious user), and there is a risk of replay deposits.
So it is recommended to combine salt
with msg.sender
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) { ... - privatePool = PrivatePool(payable(privatePoolImplementation.cloneDeterministic(_salt))); + privatePool = PrivatePool(payable(privatePoolImplementation.cloneDeterministic(keccak256(abi.encode(msg.sender,_salt)))));
- function predictPoolDeploymentAddress(bytes32 salt) public view returns (address predictedAddress) { + function predictPoolDeploymentAddress(address owner,bytes32 salt) public view returns (address predictedAddress) { predictedAddress = privatePoolImplementation.predictDeterministicAddress(salt, address(this)); + predictedAddress = privatePoolImplementation.predictDeterministicAddress(keccak256(abi.encode(owner,salt)), address(this)); }
#0 - c4-pre-sort
2023-04-20T17:17:55Z
0xSorryNotSorry marked the issue as duplicate of #419
#1 - c4-judge
2023-05-01T07:23:07Z
GalloDaSballo marked the issue as satisfactory