Lucene search

K
code423n4Code4renaCODE423N4:2023-11-CANTO-FINDINGS-ISSUES-482
HistoryNov 17, 2023 - 12:00 a.m.

Unchecked Bonding Curve Lookups in Market validation of _id in buy() and sell() absent.

2023-11-1700:00:00
Code4rena
github.com
2
bonding curve
lookup validation
market functions

AI Score

7

Confidence

High

Lines of code
<https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L141-L145&gt;
<https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L132-L136&gt;
<https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L141-L145&gt;

Vulnerability details

Impact

buy() and sell() functions pass _id to to getBuyPrice()/getSellPrice() without validating it is a valid share ID. This can cause transactions to revert if invalid ID is provided.

Attackers can disrupt trades and deny service by intentionally passing invalid IDs that don’t map to shares.

Proof of Concept

Likely, invalid IDs can be guessed/brute-forced buy() and sell().

    function buy(uint256 _id, uint256 _amount) external {
        require(shareData[_id].creator != msg.sender, "Creator cannot buy");
        (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID
        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);
        // Split the fee among holder, creator and platform
        _splitFees(_id, fee, shareData[_id].tokensInCirculation);
        rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled;


        shareData[_id].tokenCount += _amount;
        shareData[_id].tokensInCirculation += _amount;
        tokensByAddress[_id][msg.sender] += _amount;


        if (rewardsSinceLastClaim &gt; 0) {
            SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim);
        }
        emit SharesBought(_id, msg.sender, _amount, price, fee);
    }



    function sell(uint256 _id, uint256 _amount) external {
        (uint256 price, uint256 fee) = getSellPrice(_id, _amount);
        // 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;


        shareData[_id].tokenCount -= _amount;
        shareData[_id].tokensInCirculation -= _amount;
        tokensByAddress[_id][msg.sender] -= _amount; // Would underflow if user did not have enough tokens


        // Send the funds to the user
        SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim + price - fee);
        emit SharesSold(_id, msg.sender, _amount, price, fee);
    }

These functions pass the _id parameter to getBuyPrice() and getSellPrice() without first validating _id is a valid share ID.

This allows attackers to provide any arbitrary _id value.

If an invalid _id is provided, getBuyPrice() will look up an empty Share struct.

    function getBuyPrice(uint256 _id, uint256 _amount) public view returns (uint256 price, uint256 fee) {
        // If id does not exist, this will return address(0), causing a revert in the next line
        address bondingCurve = shareData[_id].bondingCurve;
        (price, fee) = IBondingCurve(bondingCurve).getPriceAndFee(shareData[_id].tokenCount + 1, _amount);
    }

And will revert when trying to read data from the empty struct.

Market.sol#getSellPrice

    function getSellPrice(uint256 _id, uint256 _amount) public view returns (uint256 price, uint256 fee) {
        // If id does not exist, this will return address(0), causing a revert in the next line
        address bondingCurve = shareData[_id].bondingCurve;
        (price, fee) = IBondingCurve(bondingCurve).getPriceAndFee(shareData[_id].tokenCount - _amount + 1, _amount);
    }

Imagine, an attacker could exploit this to disrupt trades because invalid IDs can be guessed/brute-forced.

  1. Calling buy() or sell() with a random ID like 729:
buy(729, 1);
  1. This passes invalid ID 729 to getBuyPrice()

  2. getBuyPrice() reverts due to empty share lookup

  3. Repeated calls with random IDs repeatedly disrupts buy/sell

  4. Legitimate traders for real share IDs denied service and lose funds

Failing to validate IDs allows attackers to easily disrupt critical platform functions.

Recommended Mitigation Steps

Validate _id refers to existing share before passing to lookup functions.

require(_id &gt; 0 && _id &lt;= totalShares, "Invalid id");
getBuyPrice(_id, _amount);

Assessed type

Invalid Validation


The text was updated successfully, but these errors were encountered:

All reactions

AI Score

7

Confidence

High