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: 35/120
Findings: 1
Award: $118.64
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 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
118.6406 USDC - $118.64
buy()
: (save 287 Gas from tests included)sell()
: save 234 Gas from tests includedmintNFT()
: (save 215 Gas from tests included)burnNFT()
: (save 230 Gas from tests included)claimHolderFee()
Note: The following findings were not found by the bot: for max savings please implement the changes found by bot
buy()
: (save 287 Gas from tests included)Min | Average | Median | Max | |
---|---|---|---|---|
Before | 157773 | 157773 | 157773 | 157773 |
After | 157486 | 157486 | 157486 | 157486 |
File: /src/Market.sol 150: function buy(uint256 _id, uint256 _amount) external { 151: require(shareData[_id].creator != msg.sender, "Creator cannot buy"); 152: (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID 153: SafeERC20.safeTransferFrom(token, msg.sender, address(this), price + fee); 156: uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); 157: // Split the fee among holder, creator and platform 158: _splitFees(_id, fee, shareData[_id].tokensInCirculation); 159: rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; 161: shareData[_id].tokenCount += _amount; 162: shareData[_id].tokensInCirculation += _amount; 163: tokensByAddress[_id][msg.sender] += _amount;
To get the rewardsSinceLastClaim
we call an internal function _getRewardsSinceLastClaim()
which has the following implementation
function _getRewardsSinceLastClaim(uint256 _id) internal view returns (uint256 amount) { uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; amount = ((shareData[_id].shareHolderRewardsPerTokenScaled - lastClaimedValue) * tokensByAddress[_id][msg.sender]) / 1e18; }
The internal function reads shareData[_id].shareHolderRewardsPerTokenScaled
and tokensByAddress[_id][msg.sender]
which are state variable and are also being read in the main function.
We can avoid making this state read twice by inlining the internal function and caching one call
diff --git a/1155tech-contracts/src/Market.sol b/1155tech-contracts/src/Market.sol index 59c5c96..233d0ce 100644 --- a/1155tech-contracts/src/Market.sol +++ b/1155tech-contracts/src/Market.sol @@ -153,14 +153,19 @@ contract Market is ERC1155, Ownable2Step { SafeERC20.safeTransferFrom(token, msg.sender, address(this), price + fee); // The reward calculation has to use the old rewards value (pre fee-split) to not include the fees of this buy // The rewardsLastClaimedValue then needs to be updated with the new value such that the user cannot claim fees of this buy - uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); + uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; + uint256 _tokensByAddress = tokensByAddress[_id][msg.sender]; + uint256 _shareHolderRewardsPerTokenScaled = shareData[_id].shareHolderRewardsPerTokenScaled; + uint256 rewardsSinceLastClaim = ((_shareHolderRewardsPerTokenScaled - lastClaimedValue) * _tokensByAddress) / 1e18; + // Split the fee among holder, creator and platform - _splitFees(_id, fee, shareData[_id].tokensInCirculation); - rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; + uint256 _tokensInCirculation = shareData[_id].tokensInCirculation; + _splitFees(_id, fee, _tokensInCirculation); + rewardsLastClaimedValue[_id][msg.sender] = _shareHolderRewardsPerTokenScaled; shareData[_id].tokenCount += _amount; - shareData[_id].tokensInCirculation += _amount; - tokensByAddress[_id][msg.sender] += _amount; + shareData[_id].tokensInCirculation = _tokensInCirculation + _amount; + tokensByAddress[_id][msg.sender] = _tokensByAddress + _amount;
sell()
: save 234 Gas from tests includedMin | Average | Median | Max | |
---|---|---|---|---|
Before | 42142 | 42142 | 42142 | 42142 |
After | 41908 | 41908 | 41908 | 41908 |
File: /src/Market.sol 174: function sell(uint256 _id, uint256 _amount) external { 175: (uint256 price, uint256 fee) = getSellPrice(_id, _amount); 176: // Split the fee among holder, creator and platform 177: _splitFees(_id, fee, shareData[_id].tokensInCirculation); 179: uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); 180: rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; 182: shareData[_id].tokenCount -= _amount; 183: shareData[_id].tokensInCirculation -= _amount; 184: tokensByAddress[_id][msg.sender] -= _amount; // Would underflow if user did not have enough tokens
To get the rewardsSinceLastClaim
we call an internal function _getRewardsSinceLastClaim()
which has the following implementation
function _getRewardsSinceLastClaim(uint256 _id) internal view returns (uint256 amount) { uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; amount = ((shareData[_id].shareHolderRewardsPerTokenScaled - lastClaimedValue) * tokensByAddress[_id][msg.sender]) / 1e18; }
The internal function reads shareData[_id].shareHolderRewardsPerTokenScaled
and tokensByAddress[_id][msg.sender]
which are state variable and are also being read in the main function.
We can avoid making this state read twice by inlining the internal function and caching one call
diff --git a/1155tech-contracts/src/Market.sol b/1155tech-contracts/src/Market.sol index 59c5c96..a73f406 100644 --- a/1155tech-contracts/src/Market.sol +++ b/1155tech-contracts/src/Market.sol @@ -176,12 +176,17 @@ contract Market is ERC1155, Ownable2Step { // Split the fee among holder, creator and platform _splitFees(_id, fee, shareData[_id].tokensInCirculation); // The user also gets the rewards of his own sale (which is not the case for buys) - uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); - rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; + + uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; + uint256 _tokensByAddress = tokensByAddress[_id][msg.sender]; + uint256 _shareHolderRewardsPerTokenScaled = shareData[_id].shareHolderRewardsPerTokenScaled; + uint256 rewardsSinceLastClaim = ((_shareHolderRewardsPerTokenScaled - lastClaimedValue) * _tokensByAddress) / 1e18; + + rewardsLastClaimedValue[_id][msg.sender] = _shareHolderRewardsPerTokenScaled; shareData[_id].tokenCount -= _amount; shareData[_id].tokensInCirculation -= _amount; - tokensByAddress[_id][msg.sender] -= _amount; // Would underflow if user did not have enough tokens + tokensByAddress[_id][msg.sender] = _tokensByAddress - _amount; // Would underflow if user did not have enough tokens // Send the funds to the user SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim + price - fee);
mintNFT()
: (save 215 Gas from tests included)Min | Average | Median | Max | |
---|---|---|---|---|
Before | 68280 | 68280 | 68280 | 68280 |
After | 68065 | 68065 | 68065 | 68065 |
File: /src/Market.sol 203: function mintNFT(uint256 _id, uint256 _amount) external { 204: uint256 fee = getNFTMintingPrice(_id, _amount); 206: SafeERC20.safeTransferFrom(token, msg.sender, address(this), fee); 207: _splitFees(_id, fee, shareData[_id].tokensInCirculation); 208: // The user also gets the proportional rewards for the minting 209: uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); 210: rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; 211: tokensByAddress[_id][msg.sender] -= _amount; 212: shareData[_id].tokensInCirculation -= _amount;
To get the rewardsSinceLastClaim
we call an internal function _getRewardsSinceLastClaim()
which has the following implementation
function _getRewardsSinceLastClaim(uint256 _id) internal view returns (uint256 amount) { uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; amount = ((shareData[_id].shareHolderRewardsPerTokenScaled - lastClaimedValue) * tokensByAddress[_id][msg.sender]) / 1e18; }
The internal function reads shareData[_id].shareHolderRewardsPerTokenScaled
and tokensByAddress[_id][msg.sender])
which are state variables which is also being read in the main function.
We can avoid making this state reads twice by inlining the internal function and caching one call
diff --git a/1155tech-contracts/src/Market.sol b/1155tech-contracts/src/Market.sol index 59c5c96..fc85039 100644 --- a/1155tech-contracts/src/Market.sol +++ b/1155tech-contracts/src/Market.sol @@ -206,9 +206,14 @@ contract Market is ERC1155, Ownable2Step { SafeERC20.safeTransferFrom(token, msg.sender, address(this), fee); _splitFees(_id, fee, shareData[_id].tokensInCirculation); // The user also gets the proportional rewards for the minting - uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); - rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; - tokensByAddress[_id][msg.sender] -= _amount; + + uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; + uint256 _tokensByAddress = tokensByAddress[_id][msg.sender]; + uint256 _shareHolderRewardsPerTokenScaled = shareData[_id].shareHolderRewardsPerTokenScaled; + uint256 rewardsSinceLastClaim =((_shareHolderRewardsPerTokenScaled - lastClaimedValue) * _tokensByAddress) /1e18; + + rewardsLastClaimedValue[_id][msg.sender] = _shareHolderRewardsPerTokenScaled; + tokensByAddress[_id][msg.sender] = _tokensByAddress - _amount; shareData[_id].tokensInCirculation -= _amount; _mint(msg.sender, _id, _amount, "");
burnNFT()
: (save 230 Gas from tests included)Min | Average | Median | Max | |
---|---|---|---|---|
Before | 49135 | 49135 | 49135 | 49135 |
After | 48905 | 48905 | 48905 | 48905 |
File: /src/Market.sol 226: function burnNFT(uint256 _id, uint256 _amount) external { 227: uint256 fee = getNFTMintingPrice(_id, _amount); 229: SafeERC20.safeTransferFrom(token, msg.sender, address(this), fee); 230: _splitFees(_id, fee, shareData[_id].tokensInCirculation); 232: uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); 233: rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; 234: tokensByAddress[_id][msg.sender] += _amount; 235: shareData[_id].tokensInCirculation += _amount; 236: _burn(msg.sender, _id, _amount); 238: SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim); 239: // ERC1155 already logs, but we add this to have the price information 240: emit NFTsBurned(_id, msg.sender, _amount, fee); 241: }
we can the internal function _getRewardsSinceLastClaim()
which has the following implementation
function _getRewardsSinceLastClaim(uint256 _id) internal view returns (uint256 amount) { uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; amount = ((shareData[_id].shareHolderRewardsPerTokenScaled - lastClaimedValue) * tokensByAddress[_id][msg.sender]) / 1e18; }
The internal function reads shareData[_id].shareHolderRewardsPerTokenScaled
a state variable which is also being read in the main function. We also read tokensByAddress[_id][msg.sender]
in the main function as well as in the internal function
diff --git a/1155tech-contracts/src/Market.sol b/1155tech-contracts/src/Market.sol index 59c5c96..7f3c5e1 100644 --- a/1155tech-contracts/src/Market.sol +++ b/1155tech-contracts/src/Market.sol @@ -229,10 +229,18 @@ contract Market is ERC1155, Ownable2Step { SafeERC20.safeTransferFrom(token, msg.sender, address(this), fee); _splitFees(_id, fee, shareData[_id].tokensInCirculation); // The user does not get the proportional rewards for the burning (unless they have additional tokens that are not in the NFT) - uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); - rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; - tokensByAddress[_id][msg.sender] += _amount; - shareData[_id].tokensInCirculation += _amount; + + uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; + + uint256 _tokensByAddress = tokensByAddress[_id][msg.sender]; + uint256 _shareHolderRewardsPerTokenScaled = shareData[_id].shareHolderRewardsPerTokenScaled; + uint256 rewardsSinceLastClaim = + ((_shareHolderRewardsPerTokenScaled - lastClaimedValue) * _tokensByAddress) / + 1e18; + + rewardsLastClaimedValue[_id][msg.sender] = _shareHolderRewardsPerTokenScaled; + tokensByAddress[_id][msg.sender] = _tokensByAddress + _amount; + shareData[_id].tokensInCirculation += _amount; _burn(msg.sender, _id, _amount); SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim);
claimHolderFee()
Not covered by tests so no gas benchmarks https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L263-L270
File: /src/Market.sol 263: function claimHolderFee(uint256 _id) external { 264: uint256 amount = _getRewardsSinceLastClaim(_id); 265: rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; 266: if (amount > 0) { 267: SafeERC20.safeTransfer(token, msg.sender, amount); 268: } 269: emit HolderFeeClaimed(msg.sender, _id, amount); 270: }
To get the amount
we call an internal function _getRewardsSinceLastClaim()
which has the following implementation
function _getRewardsSinceLastClaim(uint256 _id) internal view returns (uint256 amount) { uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; amount = ((shareData[_id].shareHolderRewardsPerTokenScaled - lastClaimedValue) * tokensByAddress[_id][msg.sender]) / 1e18; }
The internal function reads shareData[_id].shareHolderRewardsPerTokenScaled
a state variable which is also being read in the main function.
We can avoid making this state read twice by inlining the internal function and caching one call
function claimHolderFee(uint256 _id) external { - uint256 amount = _getRewardsSinceLastClaim(_id); - rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; + uint256 lastClaimedValue = rewardsLastClaimedValue[_id][msg.sender]; + uint256 _shareHolderRewardsPerTokenScaled = shareData[_id].shareHolderRewardsPerTokenScaled; + uint256 amount = ((_shareHolderRewardsPerTokenScaled - lastClaimedValue) * tokensByAddress[_id][msg.sender]) /1e18; + rewardsLastClaimedValue[_id][msg.sender] = _shareHolderRewardsPerTokenS caled; if (amount > 0) { SafeERC20.safeTransfer(token, msg.sender, amount); }
#0 - c4-pre-sort
2023-11-24T06:52:29Z
minhquanym marked the issue as high quality report
#1 - c4-sponsor
2023-11-27T09:27:06Z
OpenCoreCH (sponsor) confirmed
#2 - c4-judge
2023-11-29T19:42:45Z
MarioPoneder marked the issue as grade-a
#3 - MarioPoneder
2023-11-29T19:49:11Z
Chosen for report due to extensive and well-presented optimization on a per-method level while providing valuable before/after comparisons.
#4 - c4-judge
2023-11-29T19:49:24Z
MarioPoneder marked the issue as selected for report