Skip to main content

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​

IdentifierTitleSeverityMitigated
[H-01]Payments for coupons to Booster are irretrievableHighβœ”οΈ
[H-02]BufferBinaryPool can permanently lock funds on early exerciseHighβœ”οΈ
[H-03]Market direction signature can be abused if privateKeeperMode is disabledHighβœ”οΈ
[H-04]closeAnytime timestamp is never validated against current timestampHighβœ”οΈ
[H-05]closeAnyTime timestamp is never validated against pricing timestampHighβœ”οΈ
[M-01]booster#buy fails to apply discountMediumβœ”οΈ
[M-02]Transferred options can only be closed early by previous ownerMediumβœ”οΈ
[L-01]Users can buy coupons for options that don't existLowβœ”οΈ
[L-02]registerAccount sub-call in BufferRouter#openTrades can revert entire transactionLowβœ”οΈ
[I-01]Booster#buy should support buying multiple boosts at a timeInfoβœ”οΈ
[I-02]OptionMath#_decay is never usedInfoβœ”οΈ

[H-01] Payments for coupons to Booster are irretrievable​

Details​

Booster.sol#L92-L99

    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​

Booster.sol#L80-L100

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​

BufferRouter.sol#L248-L258

        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​

BufferRouter.sol#L167-L260

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​

BufferRouter.sol#L248-L258

        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​

BufferRouter.sol#L167-L260

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​

Booster.sol#L92-L95

    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​

Booster.sol#L80-L100

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​

BufferRouter.sol#L197-L205

        (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:

  1. The previous owner can close the option early to cause damage to the new owner
  2. The new owner cannot close their option early by themselves

Lines of Code​

BufferRouter.sol#L167-L260

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​

Booster.sol#L80-L100

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​

BufferRouter.sol#L144-L150

        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​

BufferRouter.sol#L107-L165

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​

Booster.sol#L80-L100

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​

OptionMath.sol#L24-L32

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.