Platform: Code4rena
Start Date: 13/11/2023
Pot Size: $24,500 USDC
Total HM: 3
Participants: 120
Period: 4 days
Judge: 0xTheC0der
Id: 306
League: ETH
Rank: 20/120
Findings: 3
Award: $278.33
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: chaduke
Also found by: 0xpiken, Bauchibred, Matin, MohammedRizwan, MrPotatoMagic, OMEN, Pheonix, SandNallani, T1MOH, Topmark, ZanyBonzy, adriro, aslanbek, ayden, bareli, bart1e, bin2chen, btk, cheatc0d3, codynhat, critical-or-high, d3e4, erebus, firmanregar, hunter_w3b, jasonxiale, kaveyjoe, ksk2345, lsaudit, max10afternoon, merlinboii, nailkhalimov, osmanozdemir1, peanuts, pep7siup, pontifex, sbaudh6, shenwilly, sl1, tourist, wisdomn_, young, zhaojie
47.8152 USDC - $47.82
QA issues | Issues | Instances |
---|---|---|
[L-01] | Mapping shareBondingCurves is not updated when creating new share | 1 |
[L-02] | Missing shareData[_id].creator != msg.sender check in sell() function | 1 |
[L-03] | Revert with an error message instead of letting the underflow revert in sell() function | 1 |
[L-04] | Share holder can spam off-chain tracking with 0 amount event emission in fuction claimHolderFee() | 1 |
[N-01] | Missing natspec dev comment to approve underlying tokens to Market.sol for transfer | 1 |
[N-02] | Rename LinearBondingCurve.sol contract to InverseLogarithmicCurve.sol contract | 1 |
The mapping shareBondingCurves is supposed to store the bonding curve being used for a shareID. But when createNewShare() is called, the function does not update the mapping for the shareID created. Although the mapping is not used anywhere in the current contract, if any external protocols or contracts use the value from that mapping, it will return an incorrect value i.e. address(0)
We can observe from Line 125-128 no update is made to the mapping.
File: Market.sol 117: function createNewShare( 118: string memory _shareName, 119: address _bondingCurve, 120: string memory _metadataURI 121: ) external onlyShareCreator returns (uint256 id) { 122: require(whitelistedBondingCurves[_bondingCurve], "Bonding curve not whitelisted"); 123: require(shareIDs[_shareName] == 0, "Share already exists"); 124: id = ++shareCount; 125: shareIDs[_shareName] = id; 126: shareData[id].bondingCurve = _bondingCurve; //@audit shareBondingCurves mapping is not updated here 127: shareData[id].creator = msg.sender; 128: shareData[id].metadataURI = _metadataURI; 129: emit ShareCreated(id, _shareName, _bondingCurve, msg.sender); 130: }
Solution: Update the mapping for that shareID to store the address of the bonding curve
shareData[_id].creator != msg.sender
check in sell() functionUnlike the buy() function, the sell function() does not include a check to see if the msg.sender is not the creator of the shareID for which tokens are being sold.
As mentioned in the publicly known issues section in the README, "This check can be circumvented easily (buying from a different address or buying an ERC1155 token on the secondary market and unwrapping it). The main motivation behind this check is to make clear that the intention of the system is not that creators buy a lot of their own tokens (which is the case for other bonding curve protocols), it is not a strict security check. "
In our case, it would be to prevent the creator from mass selling tokens from his/her original address. Though market-making cannot be prevented (such as creator mass selling tokens to short on an exchange), the check should be implemented as done in the buy() function.
Solution:
File: Market.sol 178: function sell(uint256 _id, uint256 _amount) external { 179: require(shareData[_id].creator != msg.sender, "Creator cannot sell"); 180: (uint256 price, uint256 fee) = getSellPrice(_id, _amount);
In the line below from the sell() function we can observe that if the user does not have enough tokens to sell, the subtraction would underflow. This is not the best practice and is not recommended since it leaves the user without a reason while giving an underflow error.
File: Market.sol 188: tokensByAddress[_id][msg.sender] -= _amount; // Would underflow if user did not have enough tokens
Solution: Add a check to see if amount >= tokensByAddress[_id][msg.sender]
. If not then revert with an insufficient balance error
A holder with 0 holder fees can claim 0 amount and spam event emissions. This will spam the off-chain monitoring system with 0 amount claim entries. Though no risk, the call should be reverted in case amount == 0
to ensure proper error handling and preventing event emission spamming.
File: Market.sol 271: function claimHolderFee(uint256 _id) external { 272: uint256 amount = _getRewardsSinceLastClaim(_id); 273: rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; 274: if (amount > 0) { 275: SafeERC20.safeTransfer(token, msg.sender, amount); 276: } 277: emit HolderFeeClaimed(msg.sender, _id, amount); 278: }
The buy() function transfers the underlying tokens from the msg.sender to the Market.sol contract when purchasing tokens of a shareID. But for this the users need to approve the Market.sol with the underlying amount of tokens.
File: Market.sol 150: /// @notice Buy amount of tokens for a given share ID 151: // @dev User needs to approve the contract with price + fee or more amount of tokens 152: /// @param _id ID of the share 153: /// @param _amount Amount of shares to buy
The LinearBondingCurve.sol contract does not act as a linear bonding curve but instead an inverse logarithmic curve due to the formula 0.1 / log2(shareCount)
being used. This contract can be confusing to understand initially since the contract name is differing from the implementation.
#0 - c4-pre-sort
2023-11-24T06:51:35Z
minhquanym marked the issue as high quality report
#1 - c4-sponsor
2023-11-27T09:25:41Z
OpenCoreCH (sponsor) confirmed
#2 - c4-judge
2023-11-29T23:12:22Z
MarioPoneder marked the issue as grade-a
🌟 Selected for report: 0xVolcano
Also found by: 0xAnah, 0xhex, 0xta, 100su, JCK, K42, MrPotatoMagic, chaduke, cheatc0d3, hunter_w3b, lsaudit, mgf15, parlayan_yildizlar_takimi, sivanesh_808, tabriz, tala7985
8.2749 USDC - $8.27
Gas Optimizations | Issues | Instances |
---|---|---|
[G-01] | Remove else block to save gas | 1 |
[G-02] | Remove redundant mapping shareBondingCurves to save gas | 1 |
[G-03] | Parameter can be made calldata to save gas | 1 |
[G-04] | Remove redundant check shareData[_id].creator != msg.sender to save gas | 1 |
Function execution cost: 860 - 848 = 12 gas saved (per call)
Instead of this:
File: LinearBondingCurve.sol 27: 28: function getFee(uint256 shareCount) public pure override returns (uint256) { 29: uint256 divisor; 30: if (shareCount > 1) { 31: divisor = log2(shareCount); 32: } else { 33: divisor = 1; 34: } 35: // 0.1 / log2(shareCount) 36: return 1e17 / divisor; 37: }
Use this:
File: LinearBondingCurve.sol 27: 28: function getFee(uint256 shareCount) public pure override returns (uint256) { 29: uint256 divisor = 1; 30: if (shareCount > 1) { 31: divisor = log2(shareCount); 32: } 33: // 0.1 / log2(shareCount) 34: return 1e17 / divisor; 35: }
The mapping shareBondingCurves is not used anywhere in the contract and can thus be removed.
Deployment cost: 2499354 - 2487364 = 11990 gas saved
Remove the mapping shareBondingCurves below:
File: Market.sol 45: /// @notice Stores the bonding curve per share 46: mapping(uint256 => address) public shareBondingCurves;
Parameters that are only read can be marked calldata.
Function execution cost: 10457 - 10068 = 389 gas saved (per call)
Instead of this:
File: Market.sol 115: function createNewShare( 116: string memory _shareName, 117: address _bondingCurve, 118: string memory _metadataURI
Use this:
File: Market.sol 115: function createNewShare( 116: string calldata _shareName, 117: address _bondingCurve, 118: string calldata _metadataURI
shareData[_id].creator != msg.sender
to save gasThe check require(shareData[_id].creator != msg.sender, "Creator cannot buy");
on Line 151 can be circumvented by the creator by using another address as mentioned in the README's publicly known issues section. This check has no use if it can be circumvented and should be removed.
Deployment cost: 2499354 - 2479531 = 19823 gas saved
Function execution cost: 157773 - 157550 = 223 gas saved (per call)
Remove the check shown below:
File: Market.sol 151: require(shareData[_id].creator != msg.sender, "Creator cannot buy");
#0 - c4-pre-sort
2023-11-24T06:52:30Z
minhquanym marked the issue as high quality report
#1 - c4-sponsor
2023-11-27T09:27:30Z
OpenCoreCH (sponsor) confirmed
#2 - c4-judge
2023-11-29T19:44:33Z
MarioPoneder marked the issue as grade-b
#3 - mcgrathcoutinho
2023-11-30T12:15:23Z
Hi @MarioPoneder , here are some points I'd like to point out as to why I think this report is grade-a deserving:
On observing this comparison based on both number of optimizations and gas savings, I believe this report should fall under the grade-a category. I would appreciate it if you would reconsider my report and provide some feedback.
Thank you
#4 - MarioPoneder
2023-11-30T14:57:35Z
🌟 Selected for report: 0xSmartContract
Also found by: 0xbrett8571, 0xepley, Bauchibred, K42, Kose, MrPotatoMagic, Myd, Sathish9098, aariiif, cats, clara, emerald7017, fouzantanveer, hunter_w3b, invitedtea, unique
222.2364 USDC - $222.24
Day 1
Day 2
The codebase can be divided into two protocols, namely, the asD protocol and the 1155Tech protocol. The asD protocol serves the 1155Tech protocol by providing the application specific dollars (asDs) created as the underlying currency for buying/selling shares and minting/burning ERC1155 tokens.
The codebase is quite straightforward to understand after reading the README.
Here are certain aspects which can be improved:
Other than this, the most important aspects of the codebase have been implemented correctly such as ensuring correct fee accounting while buying, selling, minting, burning and updating fee rewards after claiming. This implementation prevents an user/attacker from profiting from the fee rewards by continuously buying and selling back-to-back.
There are only two important centralization risks in the codebase:
Credits to sponsor for explanation:
Formula: 0.1 / log2(shareCount)
The intuition behind this is that the fee should decrease for larger markets (hence the division by the share count), but it should only do so slowly / sub-linear (hence the log2).
15 hours
#0 - c4-pre-sort
2023-11-24T06:53:13Z
minhquanym marked the issue as high quality report
#1 - c4-sponsor
2023-11-27T09:29:46Z
OpenCoreCH (sponsor) acknowledged
#2 - c4-judge
2023-11-29T20:44:43Z
MarioPoneder marked the issue as grade-a