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: 21/120
Findings: 2
Award: $321.04
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: sashik_eth
Also found by: 0x4non, 0x6980, 0xAgro, Cryptor, Kaysoft, Kenshin, Madalad, SaeedAlipoor01988, Sathish9098, W0RR1O, adriro, ayden, btk, catellatech, codeslide, devscrooge, georgits, giovannidisiena, lukris02, matrix_0wl, sayan, tnevler, tsvetanovv
23.0813 USDC - $23.08
Judge has assessed an item in Issue #326 as 3 risk. The relevant finding follows:
[L-05] Integer overflow by unsafe casting Description Keep in mind that the version of solidity used, despite being greater than 0.8, does not prevent integer overflows during casting, it only does so in mathematical operations.
It is necessary to safely convert between the different numeric types.
Lines of code virtualBaseTokenReserves += uint128(netInputAmount - feeAmount - protocolFeeAmount); PrivatePool.sol#L230 virtualNftReserves -= uint128(weightSum); PrivatePool.sol#L231 virtualBaseTokenReserves -= uint128(netOutputAmount + protocolFeeAmount + feeAmount); PrivatePool.sol#L323 virtualNftReserves += uint128(weightSum); PrivatePool.sol#L324 Recommended Mitigation Steps Use OpenZeppelin safeCast library.
#0 - c4-judge
2023-05-04T16:55:38Z
GalloDaSballo marked the issue as duplicate of #167
#1 - c4-judge
2023-05-04T16:56:29Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: AkshaySrivastav
Also found by: 0x5rings, 0xbepresent, ABA, Bauchibred, BenRai, DadeKuma, ElKu, RaymondFam, Rolezn, adriro, btk, chaduke, devscrooge, dingo2077, minhtrng, nemveer, p0wd3r, rbserver, ulqiorra
297.9612 USDC - $297.96
The Caviar Private Pools protocol is a decentralized platform on Ethereum that allows users to create and join private investment pools. The smart contract manages the funds within the pool, ensures compliance with the specified rules, and distributes rewards to members. The protocol democratizes investment opportunities, promotes decentralization, and fosters innovation in the investment industry.
Total Low issues |
---|
Risk | Issues Details | Number |
---|---|---|
[L-01] | Critical changes should use-two step procedure | 1 |
[L-02] | No Storage Gap for Upgradeable contracts | 1 |
[L-03] | Loss of precision due to rounding | 1 |
[L-04] | Lack of nonReentrant modifier | 4 |
[L-05] | Integer overflow by unsafe casting | 4 |
[L-06] | Add address(0) check for the critical changes | 2 |
[L-07] | Array lengths not checked | 4 |
[L-08] | Unused receive() Function Will Lock Ether In Contract | 1 |
[L-09] | Misleading comment | 1 |
[L-10] | Fees are not capped | 1 |
Total Non-Critical issues |
---|
Risk | Issues Details | Number |
---|---|---|
[NC-01] | Include return parameters in NatSpec comments | 6 |
[NC-02] | Lack of event emit | 3 |
[NC-03] | Mark visibility of initialize() functions as external | 1 |
[NC-04] | Use bytes.concat() and string.concat() | 7 |
[NC-05] | Use a more recent version of OpenZeppelin dependencies | 1 |
[NC-06] | Constants in comparisons should appear on the left side | 8 |
[NC-07] | revert() Statements Should Have Descriptive Reason Strings | 1 |
[NC-08] | Lock pragmas to specific compiler version | All Contracts |
[NC-09] | There is no need to cast a variable that is already an address, such as address(x) | 2 |
Total Stylistic issues |
---|
Risk | Issues Details | Number |
---|---|---|
[S-01] | Lines are too long | 5 |
[S-02] | Function writing does not comply with the Solidity Style Guide | 2 Contracts |
[S-03] | Generate perfect code headers every time | All Contracts |
[S-04] | For functions, follow Solidity standard naming conventions | 1 Contracts |
Total Recommendations |
---|
Risk | Issues Details | Number |
---|---|---|
[R-01] | Missing checks for address(0) | 4 |
[R-02] | Contracts should have full test coverage | |
[R-03] | Value is not validated to be different than the existing one | 5 |
[R-04] | Add a timelock to critical functions | 5 |
[R-05] | Need Fuzzing test | All Contracts |
[R-06] | Use SMTChecker | |
[R-07] | Events that mark critical parameter changes should contain both the old and the new value | 5 |
The following contract Factory.sol
inherit Owned.sol
which have a function that allows the owner to transfer ownership to a different address. If the owner accidentally uses an invalid address for which they do not have the private key, then the system will gets locked.
function transferOwnership(address newOwner) public virtual onlyOwner { owner = newOwner; emit OwnershipTransferred(msg.sender, newOwner); }
Consider adding two step procedure on the critical functions where the first is announcing a pending new owner and the new address should then claim its ownership.
A similar issue was reported in a previous contest and was assigned a severity of medium: code-423n4/2021-06-realitycards-findings#105
For upgradeable contracts, inheriting contracts may introduce new variables. In order to be able to add new variables to the upgradeable contract without causing storage collisions, a storage gap should be added to the upgradeable contract.
Consider adding a storage gap at the end of the upgradeable contract:
/** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ uint256[50] private __gap;
Loss of precision due to the nature of arithmetics and rounding errors.
uint256 outputAmount = inputAmount * virtualBaseTokenReserves / (virtualNftReserves + inputAmount);
nonReentrant
modifierIt is a best practice to use the nonReentrant
modifier when you make external calls to untrustable entities because even if an auditor did not think of a way to exploit it, an attacker just might.
function buy(uint256[] calldata tokenIds, uint256[] calldata tokenWeights, MerkleMultiProof calldata proof)
function sell(
function change(
function withdraw(address _nft, uint256[] calldata tokenIds, address token, uint256 tokenAmount) public onlyOwner {
Add reentrancy guard to the functions mentioned above.
Keep in mind that the version of solidity used, despite being greater than 0.8, does not prevent integer overflows during casting, it only does so in mathematical operations.
It is necessary to safely convert between the different numeric types.
virtualBaseTokenReserves += uint128(netInputAmount - feeAmount - protocolFeeAmount);
virtualNftReserves -= uint128(weightSum);
virtualBaseTokenReserves -= uint128(netOutputAmount + protocolFeeAmount + feeAmount);
virtualNftReserves += uint128(weightSum);
Use OpenZeppelin safeCast library.
address(0)
check for the critical changesCheck of address(0)
to protect the code from (0x0000000000000000000000000000000000000000)
address problem just in case. This is best practice or instead of suggesting that they verify _address != address(0)
, you could add some good NatSpec comments explaining what is valid and what is invalid and what are the implications of accidentally using an invalid address.
function setPrivatePoolMetadata(address _privatePoolMetadata) public onlyOwner { privatePoolMetadata = _privatePoolMetadata; }
function setPrivatePoolImplementation(address _privatePoolImplementation) public onlyOwner { privatePoolImplementation = _privatePoolImplementation; }
Add checks for address(0)
when assigning values to address state variables.
If the length of the arrays are not required to be of the same length, user operations may not be fully executed. Therefore, it's important to ensure that the arrays being operated on are properly aligned to avoid these issues.
function buy(uint256[] calldata tokenIds, uint256[] calldata tokenWeights, MerkleMultiProof calldata proof)
function sell( uint256[] calldata tokenIds, uint256[] calldata tokenWeights, MerkleMultiProof calldata proof, IStolenNftOracle.Message[] memory stolenNftProofs // put in memory to avoid stack too deep error ) public returns (uint256 netOutputAmount, uint256 feeAmount, uint256 protocolFeeAmount) {
function change( uint256[] memory inputTokenIds, uint256[] memory inputTokenWeights, MerkleMultiProof memory inputProof, IStolenNftOracle.Message[] memory stolenNftProofs, uint256[] memory outputTokenIds, uint256[] memory outputTokenWeights, MerkleMultiProof memory outputProof ) public payable returns (uint256 feeAmount, uint256 protocolFeeAmount) {
function sumWeightsAndValidateProof( uint256[] memory tokenIds, uint256[] memory tokenWeights, MerkleMultiProof memory proof ) public view returns (uint256) {
Check that the arrays length are the same.
receive()
Function Will Lock Ether In ContractEthRouter.sol
has a payable receive()
function for receiving ETH. But there is no way to withdraw it, thus the funds will be locked in the contract.
receive() external payable {}
If the intention is for the Ether to be used, the function should call another function, otherwise it should revert, or add a function to rescue the locked Eth.
The current comment in the PrivatePool
contract at line 376-377 is misleading, as it states that the sum of the caller's NFT weights must be less than or equal to the sum of the output pool NFTs weights. However, the implementation of the contract does not reflect this comment, and it actually revert if the input weight sum is less than the output weight sum, which is the opposite of what the comment implies. This issue could potentially lead to confusion for developers and result in incorrect implementation of the contract.
To address this issue, we recommend changing the comment at line 376-377 to better reflect the intended functionality of the contract:
// Old comment: The sum of the caller's NFT weights must be less than or equal to the sum of the output pool NFTs weights. // New comment: The sum of the caller's NFT weights must be greater than or equal to the sum of the output pool NFTs weights.
The current implementation of the protocol allows fees to be set without any limit which means fees can be set to an arbitrarily high value, potentially harming users and reducing the protocol's overall health.
function setProtocolFeeRate(uint16 _protocolFeeRate) public onlyOwner { protocolFeeRate = _protocolFeeRate; }
To address this issue, we recommend implementing a fee cap.
If Return parameters are declared, you must prefix them with /// @return
.
Some code analysis programs do analysis by reading NatSpec details, if they can't see the @return
tag, they do incomplete analysis.
) public payable returns (uint256 feeAmount, uint256 protocolFeeAmount) {
function flashFeeToken() public view returns (address) {
function tokenURI(uint256 tokenId) public view returns (string memory) {
function attributes(uint256 tokenId) public view returns (string memory) {
function svg(uint256 tokenId) public view returns (bytes memory) {
function trait(string memory traitType, string memory value) internal pure returns (string memory) {
Include @return
parameters in NatSpec comments
The below methods do not emit an event when the state changes, something that it's very important for dApps and users.
function setPrivatePoolMetadata(address _privatePoolMetadata) public onlyOwner { privatePoolMetadata = _privatePoolMetadata; }
function setPrivatePoolImplementation(address _privatePoolImplementation) public onlyOwner { privatePoolImplementation = _privatePoolImplementation; }
function setProtocolFeeRate(uint16 _protocolFeeRate) public onlyOwner { protocolFeeRate = _protocolFeeRate; }
Emit event.
initialize()
functions as externalIf someone wants to extend via inheritance, it might make more sense that the overridden initialize()
function calls the internal {...}_init function, not the parent public initialize()
function.
External instead of public would give more the sense of the initialize()
functions to behave like a constructor (only called on deployment, so should only be called externally)
Security point of view, it might be safer so that it cannot be called internally by accident in the child contract
It might cost a bit less gas to use external over public
It is possible to override a function from external to public ("opening it up") but it is not possible to override a function from public to external ("narrow it down").
Ref: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3750
function initialize( address _baseToken, address _nft, uint128 _virtualBaseTokenReserves, uint128 _virtualNftReserves, uint56 _changeFee, uint16 _feeRate, bytes32 _merkleRoot, bool _useStolenNftOracle, bool _payRoyalties ) public {
Change the visibility of initialize()
functions to external.
bytes.concat()
and string.concat()
Solidity version 0.8.4 introduces:
bytes.concat()
vs abi.encodePacked(<bytes>,<bytes>)
string.concat()
vs abi.encodePacked(<string>,<string>)
bytes memory metadata = abi.encodePacked(
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(metadata)));
bytes memory _attributes = abi.encodePacked(
_svg = abi.encodePacked(
_svg = abi.encodePacked(
_svg = abi.encodePacked(
abi.encodePacked(
Use bytes.concat()
and string.concat()
For security, it is best practice to use the latest OpenZeppelin dependencies version.
"dependencies": { "@openzeppelin/merkle-tree": "^1.0.2", "ethers": "^5.7.2" }
Old version of OpenZeppelin/merkle-tree is used (^1.0.2)
, newer version can be used (1.0.4)
.
Constants in comparisons should appear on the left side, doing so will prevent typo bug.
if (_baseToken == address(0)) {
if ((_baseToken == address(0) && msg.value != baseTokenAmount) || (_baseToken != address(0) && msg.value > 0)) {
if (_baseToken == address(0)) {
if (token == address(0)) {
if (baseToken == address(0)) {
if ((baseToken == address(0) && msg.value != baseTokenAmount) || (msg.value > 0 && baseToken != address(0))) {
if (token == address(0)) {
if (baseToken == address(0) && msg.value < fee) revert InvalidEthAmount();
if (merkleRoot == bytes32(0)) {
Constants should appear on the left side:
if (address(0) == _baseToken) {
revert()
Statements Should Have Descriptive Reason StringsThe correct and clear error description explains to the user why the function reverts, but the error descriptions below in the project are not self-explanatory. These error descriptions are very important in the debug features of DApps like Tenderly.
revert();
Error definitions should be added to the revert("...")
block.
Pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case with contracts in a library or EthPM package. Otherwise, the developer would need to manually update the pragma in order to compile locally.
pragma solidity ^0.8.19;
Ethereum Smart Contract Best Practices: Lock pragmas to specific compiler version.
address(x)
There is no need to cast a variable that is already an address
, such as address(x)
, x
is also address
.
if (protocolFeeAmount > 0) ERC20(baseToken).safeTransfer(address(factory), protocolFeeAmount);
if (protocolFeeAmount > 0) ERC20(baseToken).safeTransfer(address(factory), protocolFeeAmount);
if (protocolFeeAmount > 0) ERC20(baseToken).safeTransfer(address(factory), protocolFeeAmount);
Use directly variable:
if (protocolFeeAmount > 0) ERC20(baseToken).safeTransfer(factory, protocolFeeAmount);
Usually lines in source code are limited to 80 characters. Today's screens are much larger so it's reasonable to stretch this in some cases. Since the files will most likely reside in GitHub, and GitHub starts using a scroll bar in all cases when the length is over 164 characters, the lines below should be split when they reach that length.
Ref: https://docs.soliditylang.org/en/v0.8.10/style-guide.html#maximum-line-length
trait("Base token balance", Strings.toString(privatePool.baseToken() == address(0) ? address(privatePool).balance : ERC20(privatePool.baseToken()).balanceOf(address(privatePool))))
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" style="width:100%;background:black;fill:white;font-family:serif;">',
"Base token balance: ", Strings.toString(privatePool.baseToken() == address(0) ? address(privatePool).balance : ERC20(privatePool.baseToken()).balanceOf(address(privatePool))),
event Initialize(address indexed baseToken, address indexed nft, uint128 virtualBaseTokenReserves, uint128 virtualNftReserves, uint56 changeFee, uint16 feeRate, bytes32 merkleRoot, bool useStolenNftOracle, bool payRoyalties);
event Change(uint256[] inputTokenIds, uint256[] inputTokenWeights, uint256[] outputTokenIds, uint256[] outputTokenWeights, uint256 feeAmount, uint256 protocolFeeAmount);
Solidity Style Guide
Ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
Functions should be grouped according to their visibility and ordered:
constructor()
receive()
fallback()
external / public / internal / private
view / pure
Follow Solidity Style Guide.
I recommend using header for Solidity code layout and readability
/*////////////////////////////////////////////////////////////// TESTING 123 //////////////////////////////////////////////////////////////*/
The protocol don't follow solidity standard naming convention.
Ref: https://docs.soliditylang.org/en/v0.8.17/style-guide.html#naming-conventions
Follow solidity standard naming convention.
address(0)
Check of the address for the critical changes to protect the code from address(0) problem in case of mistakes.
constructor(address _royaltyRegistry) { royaltyRegistry = _royaltyRegistry; }
constructor(address _factory, address _royaltyRegistry, address _stolenNftOracle) { factory = payable(_factory); royaltyRegistry = _royaltyRegistry; stolenNftOracle = _stolenNftOracle; }
Add address(0) check.
While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.
Line coverage percentage should be 100%.
While the value is validated to be in the constant bounds, it is not validated to be different than the existing one. Queueing the same value will cause multiple abnormal events to be emitted, will ultimately result in a no-op situation.
function setVirtualReserves(uint128 newVirtualBaseTokenReserves, uint128 newVirtualNftReserves) public onlyOwner {
function setMerkleRoot(bytes32 newMerkleRoot) public onlyOwner {
function setFeeRate(uint16 newFeeRate) public onlyOwner {
function setUseStolenNftOracle(bool newUseStolenNftOracle) public onlyOwner {
function setPayRoyalties(bool newPayRoyalties) public onlyOwner {
Add a require()
statement to check that the new value is different than the current one.
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate.
function setProtocolFeeRate(uint16 _protocolFeeRate) public onlyOwner { protocolFeeRate = _protocolFeeRate; }
function setVirtualReserves(uint128 newVirtualBaseTokenReserves, uint128 newVirtualNftReserves) public onlyOwner { // set the virtual base token reserves and virtual nft reserves virtualBaseTokenReserves = newVirtualBaseTokenReserves; virtualNftReserves = newVirtualNftReserves; // emit the set virtual reserves event emit SetVirtualReserves(newVirtualBaseTokenReserves, newVirtualNftReserves); }
function setFeeRate(uint16 newFeeRate) public onlyOwner { // check that the fee rate is less than 50% if (newFeeRate > 5_000) revert FeeRateTooHigh(); // set the fee rate feeRate = newFeeRate; // emit the set fee rate event emit SetFeeRate(newFeeRate); }
function setUseStolenNftOracle(bool newUseStolenNftOracle) public onlyOwner { // set the use stolen NFT oracle flag useStolenNftOracle = newUseStolenNftOracle; // emit the set use stolen NFT oracle event emit SetUseStolenNftOracle(newUseStolenNftOracle); }
function setPayRoyalties(bool newPayRoyalties) public onlyOwner { // set the pay royalties flag payRoyalties = newPayRoyalties; // emit the set pay royalties event emit SetPayRoyalties(newPayRoyalties); }
Consider adding a timelock to the critical changes.
As Alberto Cuesta Canada said:
Fuzzing is not easy, the tools are rough, and the math is hard, but it is worth it. Fuzzing gives me a level of confidence in my smart contracts that I didn’t have before. Relying just on unit testing anymore and poking around in a testnet seems reckless now.
Ref: https://medium.com/coinmonks/smart-contract-fuzzing-d9b88e0b0a05
Use should fuzzing test like Echidna.
The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification.
Ref: https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19
Events that mark critical parameter changes should contain both the old and the new value.
emit SetVirtualReserves(newVirtualBaseTokenReserves, newVirtualNftReserves);
emit SetMerkleRoot(newMerkleRoot);
emit SetFeeRate(newFeeRate);
emit SetUseStolenNftOracle(newUseStolenNftOracle);
emit SetPayRoyalties(newPayRoyalties);
Add the old value to the event.
#0 - GalloDaSballo
2023-04-30T19:47:41Z
[L-01] Critical changes should use-two step procedure 1 NC
[L-02] No Storage Gap for Upgradeable contracts 1 Invalid -3 [L-03] Loss of precision due to rounding 1 L
[L-04] Lack of nonReentrant modifier 4 L
[L-05] Integer overflow by unsafe casting 4 L due to lack of explanation
[L-06] Add address(0) check for the critical changes 2 L
[L-07] Array lengths not checked 4 R
[L-08] Unused receive() Function Will Lock Ether In Contract 1 Invalid
[L-09] Misleading comment 1 NC
[L-10] Fees are not capped 1 L
[NC-01] Include return parameters in NatSpec comments 6 NC
[NC-02] Lack of event emit 3 NC
[NC-03] Mark visibility of initialize() functions as external 1 NC
[NC-04] Use bytes.concat() and string.concat() 7 NC
[NC-05] Use a more recent version of OpenZeppelin dependencies 1 NC
[NC-06] Constants in comparisons should appear on the left side 8 NC
[NC-07] revert() Statements Should Have Descriptive Reason Strings 1 Ignoring
[NC-08] Lock pragmas to specific compiler version All Contracts Nc
[NC-09] There is no need to cast a variable that is already an address, such as address(x) 2 NC
[S-01] Lines are too long 5 NC
[S-02] Function writing does not comply with the Solidity Style Guide 2 Contracts NC
[S-03] Generate perfect code headers every time All Contracts Ignoring
[S-04] For functions, follow Solidity standard naming conventions 1 Contracts NC
[R-01] Missing checks for address(0) 4 [R-02] Contracts should have full test coverage [R-03] Value is not validated to be different than the existing one 5 R
[R-04] Add a timelock to critical functions 5 -3
[R-05] Need Fuzzing test All Contracts [R-06] Use SMTChecker [R-07] Events that mark critical parameter changes should contain both the old and the new value 5 NC
#1 - GalloDaSballo
2023-05-01T07:40:10Z
1L from dups
-6
5L 2R 15NC
#2 - c4-judge
2023-05-01T09:16:16Z
GalloDaSballo marked the issue as grade-a