Audits
Buffer protocol has been audited extensively by industry-leading smart contract auditor - Sherlock.xyz
All Audits of Buffer smart contracts can be found below:
v2 Smart Contract Audit by Sherlock (Q4 2022)β
https://audits.sherlock.xyz/contests/24
v2.5 Smart Contract Audit by 0x52 (Q3 2023)β
- Review Dates: 7/26/23 - 7/30/23
- Reviewed by: 0x52 (@IAm0x52)
The Buffer V2.5 repo was reviewed at hash 84b6060b44
In-Scope Contracts
- contracts/core/AccountRegistrar.sol
- contracts/core/Booster.sol
- contracts/core/BufferBinaryOptions.sol
- contracts/core/BufferBinaryPool.sol
- contracts/core/BufferRouter.sol
- contracts/core/CreationWindow.sol
- contracts/core/OptionMath.sol
- contracts/core/OptionConfig.sol
- contracts/core/Validator.sol
Deployment Chain(s)
- Arbitrum One Mainnet
Summary of Findingsβ
Identifier | Title | Severity | Mitigated |
---|---|---|---|
[H-01] | Payments for coupons to Booster are irretrievable | High | βοΈ |
[H-02] | BufferBinaryPool can permanently lock funds on early exercise | High | βοΈ |
[H-03] | Market direction signature can be abused if privateKeeperMode is disabled | High | βοΈ |
[H-04] | closeAnytime timestamp is never validated against current timestamp | High | βοΈ |
[H-05] | closeAnyTime timestamp is never validated against pricing timestamp | High | βοΈ |
[M-01] | booster#buy fails to apply discount | Medium | βοΈ |
[M-02] | Transferred options can only be closed early by previous owner | Medium | βοΈ |
[L-01] | Users can buy coupons for options that don't exist | Low | βοΈ |
[L-02] | registerAccount sub-call in BufferRouter#openTrades can revert entire transaction | Low | βοΈ |
[I-01] | Booster#buy should support buying multiple boosts at a time | Info | βοΈ |
[I-02] | OptionMath#_decay is never used | Info | βοΈ |
[H-01] Payments for coupons to Booster are irretrievableβ
Detailsβ
uint256 price = couponPrice - discount;
require(token.balanceOf(user) >= price, "Not enough balance");
token.safeTransferFrom(user, address(this), couponPrice); <- @audit tokens transferred to this
userBoostTrades[tokenAddress][user]
.totalBoostTrades += MAX_TRADES_PER_BOOST;
emit BuyCoupon(tokenAddress, user, couponPrice);
Booster#buy transfers tokens to itself but has no way to recover these tokens, causing them to be permanently trapped in this contract.
Lines of Codeβ
Recommendationβ
Transfer fees to a configurable address such as admin
Remediationβ
Mitigated here by sending fees to admin rather than the booster contract
[H-02] BufferBinaryPool can permanently lock funds on early exerciseβ
Detailsβ
BufferBinaryOptions.sol#L380-L395
profit =
(option.lockedAmount *
OptionMath.blackScholesPriceBinary(
config.iv(),
option.strike,
closingPrice,
option.expiration - closingTime,
true,
isAbove
)) /
1e8;
} else {
profit = option.lockedAmount;
}
pool.send(optionID, user, profit);
When exercising an option early, it is possible to receive a partial payout depending on the factors such as when the contract is unlocked and the current asset price. This will cause the contract to send only a portion of the locked amount.
BufferBinaryPool.sol#L200-L207
uint256 transferTokenXAmount = tokenXAmount > ll.amount
? ll.amount
: tokenXAmount;
ll.locked = false;
lockedPremium = lockedPremium - ll.premium;
lockedAmount = lockedAmount - transferTokenXAmount;
tokenX.safeTransfer(to, transferTokenXAmount);
This is problematic since the amount of tokens sent is also the amount unlocked. This will leave the remainder of the fees perpetually locked in the BufferBinaryPool contract. Example: A user opens an option that locks 100 USDC. After some time they decide to exercise early. Due to current pricing factors their option is exercised for only 50 USDC. This unlocks 50 USDC leaving the other 50 USDC permanently locked, which means they can never be withdrawn by LPs.
Lines of Codeβ
BufferBinaryPool.sol#L191-L212
Recommendationβ
BufferBinaryPool#send should always unlock the entire option amount not just the amount sent.
Remediationβ
Mitigated here. Upon exercise option.lockedAmount is redeemed to the option contract. profit is sent to the user and the remainder (if any) is sent back to the pool.
[H-03] Market direction signature can be abused if privateKeeperMode is disabledβ
Detailsβ
Buffer-Protocol-v2_5/contracts/core/BufferRouter.sol Lines 233 to 246 in 84b6060
if (
!Validator.verifyMarketDirection(|
params,
queuedTrade,
optionInfo.signer
)
) {
emit FailUnlock(
params.optionId,
params.targetContract,
"Router: Wrong market direction"
);
continue;
}
if (
!Validator.verifyMarketDirection(
params,
queuedTrade,
optionInfo.signer
)
) {
emit FailUnlock(
params.optionId,
params.targetContract,
"Router: Wrong market direction"
);
continue;
}
When options are exercised, the market direction is revealed via a signature provided by the opener. This can cause 2 significant issues if privateKeeperMode is disabled: User can change the direction of their trade after to guarantee they win User can open trade and withhold direction signature to indefinitely lock LP funds
Lines of Codeβ
BufferRouter.sol#L233-L246 BufferRouter.sol#L300-L313
Recommendationβ
Instead of using a signature at exercise, consider instead concatenating the direction with a salt then hashing storing that hash with the queued trade. Upon closure the salt and direction can be provided and the hash checked against the stored hash.
Remediationβ
Mitigated here by removing the ability to disable privateKeeperMode
[H-04] closeAnytime timestamp is never validated against current timestampβ
Detailsβ
try
optionsContract.unlock(
params.optionId,
params.closingPrice,
publisherSignInfo.timestamp,
params.isAbove
)
{} catch Error(string memory reason) {
emit FailUnlock(params.optionId, params.targetContract, reason);
continue;
}
When exercising early, the timestamp used for unlocking is extremely important. The current implementation doesn't ever validate the current timestamp is anywhere near the current timestamp. If private keeper mode is disabled, a user could exercise their option in the past when they would get a better payoff.
Lines of Codeβ
Recommendationβ
Validate that exercise timestamp is within some margin of the current timestamp
###Remediation Mitigated here by removing the ability to disable privateKeeperMode. This has only been addressed for non-trusted actors and can still be exploited by a malicious/compromised keeper
[H-05] closeAnyTime timestamp is never validated against pricing timestampβ
Detailsβ
try
optionsContract.unlock(
params.optionId,
params.closingPrice,
publisherSignInfo.timestamp,
params.isAbove
)
{} catch Error(string memory reason) {
emit FailUnlock(params.optionId, params.targetContract, reason);
continue;
}
When an option is exercised early, it uses the timestamp of the pricing data without ever checking the timestamp of the closeAnytime signature. This can lead to 2 potential issues: The user may receive less than expected due to the actual exercise timestamp being different than when they signed If private keeper mode is disabled then transactions may be intercepted and the signature used by someone else to close the position with a much different timestamp
Lines of Codeβ
Recommendationβ
Pricing data timestamp and closeAnytime timestamp should be required to match.
Remediationβ
Mitigated here by removing the ability to disable privateKeeperMode. This has only been addressed for non-trusted actors and can still be exploited by a malicious/compromised keeper
[M-01] booster#buy fails to apply discountβ
Detailsβ
uint256 price = couponPrice - discount;
re[Title](command:workbench.action.addRootFolder)quire(token.balanceOf(user) >= price, "Not enough balance");
token.safeTransferFrom(user, address(this), couponPrice); <- @audit transfers couponPrice rather than price
When buying boosts the contract mistakenly transfers couponPrice rather than price which is reduced by discount. Additionally the event also emits this incorrect value.
Lines of Codeβ
Recommendationβ
Transfer price rather than couponPrice
Remediationβ
Mitigated here by transferring price rather than couponPrice.
[M-02] Transferred options can only be closed early by previous ownerβ
Detailsβ
(address signer, ) = getAccountMapping(queuedTrade.user);
bool isUserSignValid = Validator.verifyCloseAnytime(
optionsContract.assetPair(),
closeParam.userSignInfo.timestamp,
params.optionId,
closeParam.userSignInfo.signature,
signer
);
Options are minted as NFTs allowing options to be transferred to other users. This allows users to potentially sell/transfer their options to other user. This cause 2 issues:
- The previous owner can close the option early to cause damage to the new owner
- The new owner cannot close their option early by themselves
Lines of Codeβ
Recommendationβ
NFT functionality should be removed or closeAnytime should determine the signer based on the owner of the option NFT
Remediationβ
Mitigated here by only allowing positions to be transferred to/from approved addresses.
[L-01] Users can buy coupons for options that don't existβ
Detailsβ
Booster#buy allows users to buy boosts for any tokens, which means that user can pay for and buy useless boost for tokens that aren't listed.
Lines of Codeβ
Recommendationβ
Consider limiting boost purchases to only tokens currently listed for better UX
Remediationβ
Mitigated here the buy function was refactored to be callable only by owner and use permits for approval which negates the impact of this.
[L-02] registerAccount sub-call in BufferRouter#openTrades can revert entire transactionβ
Detailsβ
if (params[index].register.shouldRegister) {
accountRegistrar.registerAccount(
params[index].register.oneCT,
user,
params[index].register.signature
);
}
The openTrades function has been designed to prevent it from reverting under almost every circumstance. The exception to this is that accountRegistrar.registerAccount can revert the entire transaction.
Lines of Codeβ
Recommendationβ
Consider using a try-catch block to prevent it from reverting
Remediationβ
Mitigated here by implementing a try-catch block
[I-01] Booster#buy should support buying multiple boosts at a timeβ
Detailsβ
Booster#buy only allows the purchase of one boost at a time. Consider allowing users to bulk buy boosts to save transactions and gas.
Lines of Codeβ
Recommendationβ
Consider allowing multiple boosts to be purchased in a single transaction
Remediationβ
Mitigated here by allowing the user to buy multiple coupons in a single transaction
[I-02] OptionMath#_decay is never usedβ
Detailsβ
OptionMath#_decay is never used and should be removed
Lines of Codeβ
Recommendationβ
OptionMath#_decay should be removed
Remediationβ
Mitigated here by removing OptionMath#_decay
The core contributor team followed vigorous processes to develop and secure Buffer protocol, including Writing comprehensive unit tests Verifying contracts against written rules (a process known as Formal Verification) Conducting extensive internal code reviews Hiring reputable auditors to audit contracts Running a security bounty program In the future, the core team will audit new versions of the protocol during major upgrades to the architecture of the protocol.