Canto Application Specific Dollars and Bonding Curves for 1155s - 0xVolcano's results

Tokenizable bonding curves using a Stablecoin-as-a-Service token

General Information

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

Canto

Findings Distribution

Researcher Performance

Rank: 35/120

Findings: 1

Award: $118.64

Gas:
grade-a

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

Awards

118.6406 USDC - $118.64

Labels

bug
G (Gas Optimization)
grade-a
high quality report
selected for report
sponsor confirmed
G-13

External Links

Gas Report

Table Of Contents

Note: The following findings were not found by the bot: for max savings please implement the changes found by bot

We can optimize the function buy(): (save 287 Gas from tests included)

https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L150-L169

MinAverageMedianMax
Before157773157773157773157773
After157486157486157486157486
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;

We can optimize the function sell(): save 234 Gas from tests included

https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L174-L189

MinAverageMedianMax
Before42142421424214242142
After41908419084190841908
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);

We can optimize the function mintNFT(): (save 215 Gas from tests included)

https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L203-L221

MinAverageMedianMax
Before68280682806828068280
After68065680656806568065
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, "");

We can optimize the function burnNFT(): (save 230 Gas from tests included)

https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L226-L241

MinAverageMedianMax
Before49135491354913549135
After48905489054890548905
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);

We can optimize the function 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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter