GithubHelp home page GithubHelp logo

2022-11-bond-judging's People

Contributors

hrishibhat avatar rcstanciu avatar sherlock-admin avatar

Stargazers

 avatar  avatar

Watchers

 avatar

Forkers

aviggiano

2022-11-bond-judging's Issues

zimu - Lack of events for critical arithmetic parameters

zimu

medium

Lack of events for critical arithmetic parameters

Summary

Function BondBaseSDA.setDefaults sets critical arithmetic parameters for bond market. But it has no event emitted, it is difficult to track these critical changes off-chain.

Vulnerability Detail

In bases/BondBaseSDA, critical parameters are set and changed in function BondBaseSDA.setDefaults for bond market.
image

However, no event is emitted, and it is difficult to track these critical changes off-chain. Both Users and Issuers would possibly be unware of these changes.

Impact

Both Users and Issuers would possibly be unware of critical changes on bond market.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L348-L356

Tool used

Manual Review

Recommendation

Add an event in BondBaseSDA.setDefaults to report critical arithmetic changes.

xiaoming90 - Teller Cannot Be Removed From Callback Contract

xiaoming90

medium

Teller Cannot Be Removed From Callback Contract

Summary

If a vulnerable Teller is being exploited by an attacker, there is no way for the owner of the Callback Contract to remove the vulnerable Teller from their Callback Contract.

Vulnerability Detail

The Callback Contract is missing the feature to remove a Teller. Once a Teller has been added to the whitelist (approvedMarkets mapping), it is not possible to remove the Teller from the whitelist.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L59

File: BondBaseCallback.sol
56:     /* ========== WHITELISTING ========== */
57: 
58:     /// @inheritdoc IBondCallback
59:     function whitelist(address teller_, uint256 id_) external override onlyOwner {
60:         // Check that the market id is a valid, live market on the aggregator
61:         try _aggregator.isLive(id_) returns (bool live) {
62:             if (!live) revert Callback_MarketNotSupported(id_);
63:         } catch {
64:             revert Callback_MarketNotSupported(id_);
65:         }
66: 
67:         // Check that the provided teller is the teller for the market ID on the stored aggregator
68:         // We could pull the teller from the aggregator, but requiring the teller to be passed in
69:         // is more explicit about which contract is being whitelisted
70:         if (teller_ != address(_aggregator.getTeller(id_))) revert Callback_TellerMismatch();
71: 
72:         approvedMarkets[teller_][id_] = true;
73:     }

Impact

In the event that a whitelisted Teller is found to be vulnerable and has been actively exploited by an attacker in the wild, the owner of the Callback Contract needs to mitigate the issue swiftly by removing the vulnerable Teller from the Callback Contract to stop it from draining the asset within the Callback Contract. However, the mitigation effort will be hindered by the fact there is no way to remove a Teller within the Callback Contract once it has been whitelisted. Thus, it might not be possible to stop the attacker from exploiting the vulnerable Teller to drain assets within the Callback Contract. The Callback Contract owners would need to find a workaround to block the attack, which will introduce an unnecessary delay to the recovery process where every second counts.

Additionally, if the owner accidentally whitelisted the wrong Teller, there is no way to remove it.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L59

Tool used

Manual Review

Recommendation

Consider implementing an additional function to allow the removal of a Teller from the whitelist (approvedMarkets mapping), so that a vulnerable Teller can be removed swiftly if needed.

function removeFromWhitelist(address teller_, uint256 id_) external override onlyOwner {
    approvedMarkets[teller_][id_] = false;
}

Note: Although the owner of the Callback Contract can DOS its own market by abusing the removeFromWhitelist function, no sensible owner would do so.

xiaoming90 - Debt Decay Faster Than Expected

xiaoming90

medium

Debt Decay Faster Than Expected

Summary

The debt decay at a rate faster than expected, causing market makers to sell bond tokens at a lower price than expected.

Vulnerability Detail

The following definition of the debt decay reference time following any purchases at time t taken from the whitepaper. The second variable, which is the delay increment, is rounded up. Following is taken from Page 15 of the whitepaper - Definition 27

image-20221114170852736

However, the actual implementation in the codebase differs from the specification. At Line 514, the delay increment is rounded down instead.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L514

File: BondBaseSDA.sol
513:         // Set last decay timestamp based on size of purchase to linearize decay
514:         uint256 lastDecayIncrement = debtDecayInterval.mulDiv(payout_, lastTuneDebt);
515:         metadata[id_].lastDecay += uint48(lastDecayIncrement);

Impact

When the delay increment (TD) is rounded down, the debt decay reference time increment will be smaller than expected. The debt component will then decay at a faster rate. As a result, the market price will not be adjusted in an optimized manner, and the market price will fall faster than expected, causing market makers to sell bond tokens at a lower price than expected.

Following is taken from Page 8 of the whitepaper - Definition 8

image-20221114173425259

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L514

Tool used

Manual Review

Recommendation

When computing the lastDecayIncrement, the result should be rounded up.

// Set last decay timestamp based on size of purchase to linearize decay
- uint256 lastDecayIncrement = debtDecayInterval.mulDiv(payout_, lastTuneDebt);
+ uint256 lastDecayIncrement = debtDecayInterval.mulDivUp(payout_, lastTuneDebt);
metadata[id_].lastDecay += uint48(lastDecayIncrement);

rvierdiiev - BondAggregator.liveMarketsBy eventually will revert because of block gas limit

rvierdiiev

medium

BondAggregator.liveMarketsBy eventually will revert because of block gas limit

Summary

BondAggregator.liveMarketsBy eventually will revert because of block gas limit

Vulnerability Detail

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L259-L280

    function liveMarketsBy(address owner_) external view returns (uint256[] memory) {
        uint256 count;
        IBondAuctioneer auctioneer;
        for (uint256 i; i < marketCounter; ++i) {
            auctioneer = marketsToAuctioneers[i];
            if (auctioneer.isLive(i) && auctioneer.ownerOf(i) == owner_) {
                ++count;
            }
        }


        uint256[] memory ids = new uint256[](count);
        count = 0;
        for (uint256 i; i < marketCounter; ++i) {
            auctioneer = marketsToAuctioneers[i];
            if (auctioneer.isLive(i) && auctioneer.ownerOf(i) == owner_) {
                ids[count] = i;
                ++count;
            }
        }


        return ids;
    }

BondAggregator.liveMarketsBy function is looping through all markets and does at least marketCounter amount of external calls(when all markets are not live) and at most 4 * marketCounter external calls(when all markets are live and owner matches. This all consumes a lot of gas, even that is called from view function. And each new market increases loop size.

That means that after some time marketsToAuctioneers mapping will be big enough that the gas amount sent for view/pure function will be not enough to retrieve all data(50 million gas according to this). So the function will revert.

Also similar problem is with findMarketFor, marketsFor and liveMarketsFor functions.

Impact

Functions will always revert and whoever depends on it will not be able to get information.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Remove not active markets or some start and end indices to functions.

Ruhum - Referral system allows user to buy at a discount

Ruhum

high

Referral system allows user to buy at a discount

Summary

The referrer is a user-provided value. They can simply use their own address to buy at a discount.

Vulnerability Detail

Anybody can be a referrer. A user can set themselves as a referrer with the highest possible fee (5e3) and buy tokens at a discount.

Impact

A small loss of funds per purchase per user.

Code Snippet

The setReferrerFee() function is permissionless. Anybody can register as a referrer:

    function setReferrerFee(uint48 fee_) external override nonReentrant {
        if (fee_ > 5e3) revert Teller_InvalidParams();
        referrerFees[msg.sender] = fee_;
    }

When making a purchase, they use their own address as the referrer to get a discount: https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L137

Tool used

Manual Review

Recommendation

You could make the referrer map permissioned so that only specific addresses are allowed (frontends). The user could then choose from them when they interact with the contract directly.

xiaoming90 - `BondAggregator.findMarketFor` Function Will Break In Certain Conditions

xiaoming90

medium

BondAggregator.findMarketFor Function Will Break In Certain Conditions

Summary

BondAggregator.findMarketFor function will break when the BondBaseSDA.payoutFor function within the for-loop reverts under certain conditions.

Vulnerability Detail

The BondBaseSDA.payoutFor function will revert if the computed payout is larger than the market's max payout. Refer to Line 711 below.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L699

File: BondBaseSDA.sol
699:     function payoutFor(
700:         uint256 amount_,
701:         uint256 id_,
702:         address referrer_
703:     ) public view override returns (uint256) {
704:         // Calculate the payout for the given amount of tokens
705:         uint256 fee = amount_.mulDiv(_teller.getFee(referrer_), 1e5);
706:         uint256 payout = (amount_ - fee).mulDiv(markets[id_].scale, marketPrice(id_));
707: 
708:         // Check that the payout is less than or equal to the maximum payout,
709:         // Revert if not, otherwise return the payout
710:         if (payout > markets[id_].maxPayout) {
711:             revert Auctioneer_MaxPayoutExceeded();
712:         } else {
713:             return payout;
714:         }
715:     }

The BondAggregator.findMarketFor function will call the BondBaseSDA.payoutFor function at Line 245. The BondBaseSDA.payoutFor function will revert if the final computed payout is larger than the markets[id_].maxPayout as mentioned earlier. This will cause the entire for-loop to "break" and the transaction to revert.

Assume that the user configures the minAmountOut_ to be 0, then the condition minAmountOut_ <= maxPayout Line 244 will always be true. The amountIn_ will always be passed to the payoutFor function. In some markets where the computed payout is larger than the market's max payout, the BondAggregator.findMarketFor function will revert.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L221

File: BondAggregator.sol
220:     /// @inheritdoc IBondAggregator
221:     function findMarketFor(
222:         address payout_,
223:         address quote_,
224:         uint256 amountIn_,
225:         uint256 minAmountOut_,
226:         uint256 maxExpiry_
227:     ) external view returns (uint256) {
228:         uint256[] memory ids = marketsFor(payout_, quote_);
229:         uint256 len = ids.length;
230:         uint256[] memory payouts = new uint256[](len);
231: 
232:         uint256 highestOut;
233:         uint256 id = type(uint256).max; // set to max so an empty set doesn't return 0, the first index
234:         uint48 vesting;
235:         uint256 maxPayout;
236:         IBondAuctioneer auctioneer;
237:         for (uint256 i; i < len; ++i) {
238:             auctioneer = marketsToAuctioneers[ids[i]];
239:             (, , , , vesting, maxPayout) = auctioneer.getMarketInfoForPurchase(ids[i]);
240: 
241:             uint256 expiry = (vesting <= MAX_FIXED_TERM) ? block.timestamp + vesting : vesting;
242: 
243:             if (expiry <= maxExpiry_) {
244:                 payouts[i] = minAmountOut_ <= maxPayout
245:                     ? payoutFor(amountIn_, ids[i], address(0))
246:                     : 0;
247: 
248:                 if (payouts[i] > highestOut) {
249:                     highestOut = payouts[i];
250:                     id = ids[i];
251:                 }
252:             }
253:         }
254: 
255:         return id;
256:     }

Impact

The find market feature within the protocol is broken under certain conditions. As such, users would not be able to obtain the list of markets that meet their requirements. The market makers affected by this issue will lose the opportunity to sell their bond tokens.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L699

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L221

Tool used

Manual Review

Recommendation

Consider using try-catch or address.call to handle the revert of the BondBaseSDA.payoutFor function within the for-loop gracefully. This ensures that a single revert of the BondBaseSDA.payoutFor function will not affect the entire for-loop within the BondAggregator.findMarketFor function.

zimu - BondBaseTeller.purchase would always fail for some tokens

zimu

medium

BondBaseTeller.purchase would always fail for some tokens

Summary

In bases/BondBaseTeller.sol, function purchase exchanges quote tokens for a bond in a specified market by using safeTransfer and safeTransferFrom. However, the imported abstract contract of ERC20 token from solmate library has no declaration of safeTransfer and safeTransferFrom. When calling a ERC20 token without these implementation, function purchase would always fail.

The same thing could happen to function create in BondFixedTermTeller.sol by calling underlying_.safeTransferFrom.

Vulnerability Detail

  1. bases/BondBaseTeller.sol import the abstract contract ERC20 from solmate/tokens/ERC20.sol;
  2. The calling chain: function purchase --> function _handleTransfers --> function ERC20.safeTransfer or ERC20.safeTransferFrom;
    https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L158
    https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L169-L216
    https://github.com/transmissions11/solmate/blob/dd13c61b5f9cb5c539a7e356ba94a6c2979e9eb9/src/tokens/ERC20.sol
  3. However, the solmate library of ERC20 contract has no declaration of safeTransfer and safeTransferFrom. When a ERC20 token does not implement safeTransfer and safeTransferFrom, function purchase would always fail.

Impact

In bases/BondBaseTeller.sol, function purchase would always fail when a ERC20 token does not implement safeTransfer and safeTransferFrom.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L158
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L169-L216
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L114
https://github.com/transmissions11/solmate/blob/dd13c61b5f9cb5c539a7e356ba94a6c2979e9eb9/src/tokens/ERC20.sol

Tool used

Manual Review

Recommendation

To implement safeTransfer and safeTransferFrom function in Bond protocol, or find other conforming libraries to import

Bnke0x0 - Solmate safetransfer and safetransferfrom does not check the code size of the token address, which may lead to funding loss

Bnke0x0

medium

Solmate safetransfer and safetransferfrom does not check the code size of the token address, which may lead to funding loss

Summary

Vulnerability Detail

Impact

the safetransfer and safetransferfrom don't check the existence of code at the token address. This is a known issue while using solmate's libraries. Hence this may lead to miscalculation of funds and may lead to loss of funds, because if safetransfer() and safetransferfrom() are called on a token address that doesn't have a contract in it, it will always return success, bypassing the return value check. Due to this protocol will think that funds have been transferred successfully, and records will be accordingly calculated, but in reality, funds were never transferred. So this will lead to miscalculation and possibly loss of funds

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L143

    'token_.safeTransfer(to_, amount_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L152

       'token_.safeTransferFrom(msg.sender, address(this), amount_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L108

     'token.safeTransfer(to_, send);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L187

      'quoteToken.safeTransferFrom(msg.sender, address(this), amount_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L195

        'quoteToken.safeTransfer(callbackAddr, amountLessFee);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L210

        'payoutToken.safeTransferFrom(owner, address(this), payout_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L214

            'quoteToken.safeTransfer(owner, amountLessFee);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L89

      'underlying_.safeTransfer(recipient_, payout_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L114

       'underlying_.safeTransferFrom(msg.sender, address(this), amount_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L152

    'underlying.safeTransfer(msg.sender, amount_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L90

      'payoutToken_.safeTransfer(recipient_, payout_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L114

            'underlying_.safeTransferFrom(msg.sender, address(this), amount_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L151

            'meta.underlying.safeTransfer(msg.sender, amount_);'

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondSampleCallback.sol#L42

      'payoutToken_.safeTransfer(msg.sender, outputAmount_);'

Tool used

Manual Review

Recommendation

Use openzeppelin's safeERC20 or implement a code existence check

hansfriese - Circuit breaker could cancel the last transaction to prevent an unnecessary loss

hansfriese

medium

Circuit breaker could cancel the last transaction to prevent an unnecessary loss

Summary

In the protocol, the circuit breaker was introduced to suspend the market and protect the the market owners from a sudden lose in the extreme market conditions.
But the last transaction that triggered the circuit breaker is still processed and I believe this last transaction incurs loss of the owner.

Vulnerability Detail

In the BondBaseSDA.sol#427, the circuit breaker is triggered if the total debt is greater than the maximum debt of the market terms.

function purchaseBond(
    uint256 id_,
    uint256 amount_,
    uint256 minAmountOut_
) external override returns (uint256 payout) {
    ...
    // Circuit breaker. If max debt is breached, the market is closed
    if (term.maxDebt < market.totalDebt) {//@audit-info totalDebt was updated in _decayAndGetPrice
        _close(id_);
    } else {
        // If market will continue, the control variable is tuned to to expend remaining capacity over remaining market duration
        _tune(id_, currentTime, price);
    }
}

And the total debt was updated in the function _decayAndGetPrice that is called at BondBaseSDA.sol#398.

function _decayAndGetPrice(
    uint256 id_,
    uint256 amount_,
    uint48 time_
) internal returns (uint256 marketPrice_, uint256 payout_) {
    ...
    markets[id_].totalDebt =
        decayedDebt.mulDiv(debtDecayInterval, decayOffset + lastDecayIncrement) +
        payout_ +
        1; // add 1 to satisfy price inequality
}

It is possible to void the transaction early after this function returns. (not revert though because we need to close the market)

I don't see a reason of processing the transaction while it is clear that it is going to trigger the circuit breaker.

Although there are several additional options to limit the loss from one transaction (like maxPayout), I believe it is better to suspend the market when it's clear the total debt is going to be greater than the max debt.

Impact

The market owner might get a loss that was possible to prevent by the protocol.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L398
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L427
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L452

Tool used

Manual Review

Recommendation

Check the circuit breaker early after calling _decayAndGetPrice and void the transaction to prevent unnecessary loss for the market owner.

Duplicate of #21

caventa - Fixed-expiry bonds should only be deployed during bond or market creation

caventa

medium

Fixed-expiry bonds should only be deployed during bond or market creation

Summary

Fixed-expiry bonds should only be deployed during bond or market creation.

Vulnerability Detail

Right now, everyone can deploy any fixed-expiry bond contract (See BondFixedExpiryTeller.sol#L158-L163). However, the bond will only be minted during bond creation (See BondFixedExpiryTeller.sol#L126 and BondFixedExpiryTeller.sol#L131). Hence, It is better to deploy the bond during the creation to prevent too many contract addresses without any balance to be deployed.

Impact

Too many fixed-expiry deployed bond contracts were created without any balance

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L158-L163
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L126
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L131
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L107-L108
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpirySDA.sol#L46

Tool used

Manual Review

Recommendation

  1. Replace these lines (See BondFixedExpiryTeller.sol#L107-L108) with
if (bondToken == ERC20BondToken(address(0x00))) {
 deploy(underlying_, expiry_);
}
  1. Do not allow any user to access this deploy function (See BondFixedExpiryTeller.sol#L158-L163) directly. It is fine to allow other functions to call this function like what is suggested in 1 and the createMarket function(See BondFixedExpirySDA.sol#L46)

pashov - A whitelisted address can DoS most `view` methods in `BondAggregator`

pashov

medium

A whitelisted address can DoS most view methods in BondAggregator

Summary

A malicious/compromised account that is whitelisted in BondAggregator can create an infinite amount of markets, resulting in the view methods being in a state of DoS

Vulnerability Detail

Any whitelisted account can call registerMarket() as much times as he wants, only paying for gas. If the argument values are always the same, the method will push a new marketId in the marketsForPayout and marketsForQuote arrays on each call, which arrays are iterated over when calling liveMarketsBetween() or liveMarketsFor() or marketsFor() or findMarketFor(). If any of the arrays gets too big, the gas cost to iterate over it will be more than the block gas limit, so the view functions will always revert since they can't be included in a block, essentially resulting in a state of DoS for them.

Impact

The impact is a state of DoS for protocol's functionality, that can be used by on-chain integrated protocols or front ends. Since this requires a malicious/compromised whitelisted account I think Medium severity is appropriate

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L85

Tool used

Manual Review

Recommendation

Limit the times a whitelisted account can call the registerMarket() functionality, for example to 50 or 100 times per account.

obront - Referrers can front run orders to increase referral fee

obront

medium

Referrers can front run orders to increase referral fee

Summary

The setReferrerFee() function in BondBaseTeller.sol has no authorization and can be called by anyone. This can be used by a referrer to front run a user's transaction to temporarily increase the referral fee for the user's transaction.

Vulnerability Detail

When bonds are purchased from BondBaseTeller.sol, the referrer fee is calculated by taking the individual referrer's fee (represented as a fraction of 1e5) and multiplying it by the amount purchased:

uint256 toReferrer = amount_.mulDiv(referrerFees[referrer_], FEE_DECIMALS);

This fee is set in the setReferrerFee() function:

function setReferrerFee(uint48 fee_) external override nonReentrant {
    if (fee_ > 5e3) revert Teller_InvalidParams();
    referrerFees[msg.sender] = fee_;
}

Because a referrer can set their own fee at any time and there are no validations for a user that the referral fee won't increase above what they expected when they signed their transaction, a referrer can watch the mempool and frontrun user transactions to temporarily increase their fee, earn a higher share of rewards, and lower the fee back.

Impact

Users may submit a transaction with a clear expectation of the referral fees that will be charged, and end up with a different fee than expected.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L87-L91

Tool used

Manual Review

Recommendation

There are a few ways to avoid this issue, most of which have some complexity. The two options I'd recommend:

  1. Along with the referral fee, save the old referral fee and the block at which it was set. Then, in purchase(), you can set the fee with block.number > blockSet ? referralFee : oldReferralFee.

  2. Have users include a "slippage" value that sets the max amount of referral fee they are willing to pay. This can be set to the current referral fee, and will only error if the fee is increased after they signed their transaction.

Zarf - Referrer can frontrun purchases and maximise their fee

Zarf

medium

Referrer can frontrun purchases and maximise their fee

Summary

There is no way for the user to be sure the visible referrer fee is the actual fee which has to be paid upon purchasing bonds. A referrer can frontrun bond purchases and maximise their fee (5% of the amount sent in). Only after the purchase has been successfully performed, the user knows how much fee they paid to the referrer.

Vulnerability Detail

Imagine the current referrer fee for a specific referrer is 0.1% of the amount used to purchase bonds. The referrer might monitor the mempool for those specific contract calls which include their address as the referrer in purchase() of the BondBaseTeller contract.

If one of those transactions are residing in the mempool, the referrer could create a transaction with a higher gas price/fee to set the referrer fee to 5% using the setReferrerFee() in the BondBaseTeller contract. This ensures the fee is set to 5% before the purchase of the user will be performed.

Next, the transaction of the user will be executed, which will purchase bonds for a specific amount of quote tokens. However, first the referrer fee (max 5%) will be deducted from amount, resulting in a loss for the user and benefit for the referrer.

Only after the transaction is confirmed and the user received his/her bonds, it’s clear the referrer took 5% of the sent quote tokens (instead of the 0.1% as shown prior to the purchase).

Impact

Users might receive less tokens as expected when purchasing bonds

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L88-L91

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L121-L166

Tool used

Manual Review

Recommendation

Either make the referrer fee immutable, such that the user can be sure the current fee does not increase prior to purchasing bonds.

Alternatively a timelock could be introduced to change the fee amount. This way, frontrunning wouldn't be possible and users would know be certain on the fees they are agreeing with.

Duplicate of #29

obront - Fixed Term Teller tokens can be created with an expiry in the past

obront

high

Fixed Term Teller tokens can be created with an expiry in the past

Summary

The Fixed Term Teller does not allow tokens to be created with a timestamp in the past. This is a fact that protocols using this feature will expect to hold and build their systems around. However, users can submit expiry timestamps slightly in the future, which correlate to tokenIds in the past, which allows them to bypass this check.

Vulnerability Detail

In BondFixedTermTeller.sol, the create() function allows protocols to trade their payout tokens directly for bond tokens. The expectation is that protocols will build their own mechanisms around this. It is explicitly required that they cannot do this for bond tokens that expire in the past, only those that have yet to expire:

if (expiry_ < block.timestamp) revert Teller_InvalidParams();

However, because tokenIds round timestamps down to the latest day, protocols are able to get around this check.

Here's an example:

  • The most recently expired token has an expiration time of 1668524400 (correlates to 9am this morning)
  • It is currently 1668546000 (3pm this afternoon)
  • A protocol calls create() with an expiry of 1668546000 + 1
  • This passes the check that expiry_ >= block.timestamp
  • When the expiry is passed to getTokenId() it rounds the time down to the latest day, which is the day corresponding with 9am this morning
  • This expiry associated with this tokenId is 9am this morning, so they are able to redeem their tokens instantly

Impact

Protocols can bypass the check that all created tokens must have an expiry in the future, and mint tokens with a past expiry that can be redeemed immediately.

This may not cause a major problem for Bond Protocol itself, but protocols will be building on top of this feature without expecting this behavior.

Let's consider, for example, a protocol that builds a mechanism where users can stake some asset, and the protocol will trade payout tokens to create bond tokens for them at a discount, with the assumption that they will expire in the future. This issue could create an opening for a savvy user to stake, mint bond tokens, redeem and dump them immediately, buy more assets to stake, and continue this cycle to earn arbitrage returns and tank the protocol's token.

Because there are a number of situations like the one above where this issue could lead to a major loss of funds for a protocol building on top of Bond, I consider this a high severity.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L97-L105

Tool used

Manual Review

Recommendation

Before checking whether expiry_ < block.timestamp, expiry should be rounded to the nearest day:

expiry = ((vesting_ + uint48(block.timestamp)) / uint48(1 days)) * uint48(1 days);

xiaoming90 - Auctioneer Cannot Be Removed From The Protocol

xiaoming90

medium

Auctioneer Cannot Be Removed From The Protocol

Summary

If a vulnerable Auctioneer is being exploited by an attacker, there is no way to remove the vulnerable Auctioneer from the protocol.

Vulnerability Detail

The protocol is missing the feature to remove an auctioneer. Once an auctioneer has been added to the whitelist, it is not possible to remove the auctioneer from the whitelist.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L62

File: BondAggregator.sol
62:     function registerAuctioneer(IBondAuctioneer auctioneer_) external requiresAuth {
63:         // Restricted to authorized addresses
64: 
65:         // Check that the auctioneer is not already registered
66:         if (_whitelist[address(auctioneer_)])
67:             revert Aggregator_AlreadyRegistered(address(auctioneer_));
68: 
69:         // Add the auctioneer to the whitelist
70:         auctioneers.push(auctioneer_);
71:         _whitelist[address(auctioneer_)] = true;
72:     }

Impact

In the event that a whitelisted Auctioneer is found to be vulnerable and has been actively exploited by an attacker in the wild, the protocol needs to mitigate the issue swiftly by removing the vulnerable Auctioneer from the protocol. However, the mitigation effort will be hindered by the fact there is no way to remove an Auctioneer within the protocol once it has been whitelisted. Thus, it might not be possible to stop the attacker from exploiting the vulnerable Auctioneer. The protocol team would need to find a workaround to block the attack, which will introduce an unnecessary delay to the recovery process where every second counts.

Additionally, if the admin accidentally whitelisted the wrong Auctioneer, there is no way to remove it.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L62

Tool used

Manual Review

Recommendation

Consider implementing an additional function to allow the removal of an Auctioneer from the whitelist, so that vulnerable Auctioneer can be removed swiftly if needed.

function deregisterAuctioneer(IBondAuctioneer auctioneer_) external requiresAuth {
    // Remove the auctioneer from the whitelist
    _whitelist[address(auctioneer_)] = false;
}

xiaoming90 - Market Price Lower Than Expected

xiaoming90

medium

Market Price Lower Than Expected

Summary

The market price does not conform to the specification documented within the whitepaper. As a result, the computed market price is lower than expected.

Vulnerability Detail

The following definition of the market price is taken from the whitepaper. Taken from Page 13 of the whitepaper - Definition 25

image-20221114132609169

The integer implementation of the market price must be rounded up per the whitepaper. This ensures that the integer implementation of the market price is greater than or equal to the real value of the market price so as to protect makers from selling tokens at a lower price than expected.

Within the BondBaseSDA.marketPrice function, the computation of the market price is rounded up in Line 688, which conforms to the specification.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L687

File: BondBaseSDA.sol
687:     function marketPrice(uint256 id_) public view override returns (uint256) {
688:         uint256 price = currentControlVariable(id_).mulDivUp(currentDebt(id_), markets[id_].scale);
689: 
690:         return (price > markets[id_].minPrice) ? price : markets[id_].minPrice;
691:     }

However, within the BondBaseSDA._currentMarketPrice function, the market price is rounded down, resulting in the makers selling tokens at a lower price than expected.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L631

File: BondBaseSDA.sol
631:     function _currentMarketPrice(uint256 id_) internal view returns (uint256) {
632:         BondMarket memory market = markets[id_];
633:         return terms[id_].controlVariable.mulDiv(market.totalDebt, market.scale);
634:     }

Impact

Loss for the makers as their tokens are sold at a lower price than expected.

Additionally, the affected BondBaseSDA._currentMarketPrice function is used within the BondBaseSDA._decayAndGetPrice function to derive the market price. Since a lower market price will be returned, this will lead to a higher amount of payout tokens. Subsequently, the lastDecayIncrement will be higher than expected, which will lead to a lower totalDebt. Lower debt means a lower market price will be computed later.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L687

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L631

Tool used

Manual Review

Recommendation

Ensure the market price is rounded up so that the desired property can be achieved and the makers will not be selling tokens at a lower price than expected.

function _currentMarketPrice(uint256 id_) internal view returns (uint256) {
    BondMarket memory market = markets[id_];
-   return terms[id_].controlVariable.mulDiv(market.totalDebt, market.scale);
+   return terms[id_].controlVariable.mulDivUp(market.totalDebt, market.scale);
}

xiaoming90 - Rounding Issue In Control Variable

xiaoming90

medium

Rounding Issue In Control Variable

Summary

The rounding error when computing the control variable causes the control variable to be lower, leading to the makers selling tokens at a lower price than expected, as the market price of a token is computed as a product of the control variable and debt.

Vulnerability Detail

The computed control variable at Line 600 is rounded up to achieve the desirable property that the integer implementation of the control variable will be greater than or equal to the real value of the control variable. This ensures that the integer implementation of the price calculated from controlVariable * debt will be greater than or equal to the real value of the price, which protects makers from selling tokens at a lower price than expected.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L598

File: BondBaseSDA.sol
598:             // Derive a new control variable from the target debt
599:             uint256 controlVariable = terms[id_].controlVariable;
600:             uint256 newControlVariable = price_.mulDivUp(market.scale, targetDebt);

However, this is not consistently applied throughout the codebase. In the following code, the control variable is rounded down, which will result in the integer implementation of the control variable to be lower than the real value of the control variable. This, in turn, leads to the makers selling tokens at a lower price than expected.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L280

File: BondBaseSDA.sol
280:         // price = control variable * debt / scale
281:         // therefore, control variable = price * scale / debt
282:         uint256 controlVariable = params_.formattedInitialPrice.mulDiv(scale, targetDebt);

Impact

The market price of a token is computed as a product of the control variable and debt. If the control variable is lower than expected, the tokens will be sold at a lower price than expected, leading to a loss for the market makers.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L280

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L598

Tool used

Manual Review

Recommendation

Ensure that the rounding is consistently applied throughout the codebase when computing the control variable so that the desired property can be achieved.

- uint256 controlVariable = params_.formattedInitialPrice.mulDiv(scale, targetDebt);
+ uint256 controlVariable = params_.formattedInitialPrice.mulDivUp(scale, targetDebt);

bin2chen - findMarketFor() missing check minAmountOut_

bin2chen

medium

findMarketFor() missing check minAmountOut_

Summary

BondAggregator#findMarketFor() minAmountOut_ does not actually take effect,may return a market's "payout" smaller than minAmountOut_ , Causes users to waste gas calls to purchase

Vulnerability Detail

BondAggregator#findMarketFor() has check minAmountOut_ <= maxPayout
but the actual "payout" by "amountIn_" no check greater than minAmountOut_

    function findMarketFor(
        address payout_,
        address quote_,
        uint256 amountIn_,
        uint256 minAmountOut_,
        uint256 maxExpiry_
    ) external view returns (uint256) {
...
            if (expiry <= maxExpiry_) {
                payouts[i] = minAmountOut_ <= maxPayout
                    ? payoutFor(amountIn_, ids[i], address(0))
                    : 0;

                if (payouts[i] > highestOut) {//****@audit not check payouts[i] >= minAmountOut_******//
                    highestOut = payouts[i];
                    id = ids[i];
                }
            }

Impact

The user gets the optimal market through BondAggregator#findMarketFor(), but incorrectly returns a market smaller than minAmountOut_, and the call to purchase must fail, resulting in wasted gas

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L248

Tool used

Manual Review

Recommendation

    function findMarketFor(
        address payout_,
        address quote_,
        uint256 amountIn_,
        uint256 minAmountOut_,
        uint256 maxExpiry_
    ) external view returns (uint256) {
...
            if (expiry <= maxExpiry_) {
                payouts[i] = minAmountOut_ <= maxPayout
                    ? payoutFor(amountIn_, ids[i], address(0))
                    : 0;

-               if (payouts[i] > highestOut) {
+               if (payouts[i] >= minAmountOut_ && payouts[i] > highestOut) {
                    highestOut = payouts[i];
                    id = ids[i];
                }
            }

zimu - The value range of BondTerms.vesting easily makes ambiguous understanding

zimu

medium

The value range of BondTerms.vesting easily makes ambiguous understanding

Summary

BondTerms.vesting has two meaning in Bond: length of time from deposit to expiry, and vesting timestamp for expiry. The distinction between these two meanings is subjective design, easily making ambiguous understanding.

Vulnerability Detail

BondTerms.vesting is defined in interfaces/IBondSDA.sol. Here is one place in bases/BondBaseSDA.sol on how it used:

image

image

The function isInstantSwap determines if the vesting is less or equal than 50 years, it has the meaning of the lenght of time from deposit to expiry, and if more than 50 years, vesting means expiry timestamp.

This would possibly make ambiguous understanding. Suppose someone issues a bond term with length of 49 years, he can see his bond token or bond-quote pair has normal operations seems like a perpetual contract; Then, he decides to issuse a bond term with 51 years length, and after depolyment, he surprisedly find the deal is instantly ended.

Thus, It is better to explicitly declare an indicator variable in BondTerms to point out which meaning is chosen by the issuer.

Impact

A bond has ambiguous meaning on its expiry. Both issuers and users could possibly misunderstand its meaning. Also, the fixed 50 years is a subjective design, and cannot be adjusted by possible new strategies.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/interfaces/IBondSDA.sol#L28
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L98
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L780-L783

Tool used

Manual Review

Recommendation

To explicitly declare an indicator variable in BondTerms to let the issuer chooses the meaning, and remove the fixed 50 years which is a subjective design not easy to adjust.

caventa - Bond market won't be created if it was registered separately earlier before

caventa

medium

Bond market won't be created if it was registered separately earlier before

Summary

The bond market won't be created if it was registered separately earlier before.

Vulnerability Detail

The bond market needs to have a conclusion (See BondBaseSDA.sol#L395) value that is smaller than the current block time in order for the purchaser to participate. However, if the bond market is registered separately from the market creation, there is NO logic in the entire codebase that allows the conclusion value to be set. The only logic in the codebase that set the conclusion value (See iBondBaseSDA.sol#L288) is during the market creation which comes together with market registration.

Impact

The bond market which was registered separately from creation will not be used forever.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L395
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L288
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L75-L88
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L167

Tool used

Manual Review

Recommendation

Disallow registerMarket (See BondAggregator.sol#L75-L88) to be called by anyone. Only allow it to be called from createMarket function (See BondBaseSDA.sol#L167)

xiaoming90 - Arbitrary Code Execution Within Callback Exposes Takers To Risk Of Being Compromised

xiaoming90

medium

Arbitrary Code Execution Within Callback Exposes Takers To Risk Of Being Compromised

Summary

Arbitrary code execution within the callback might expose takers to the risk of being compromised if malicious code is inserted into the callback function.

Vulnerability Detail

Bond Protocol allows whitelisted market makers to specify a custom callback contract when creating the market. The callback contract will be triggered when the takers purchase bond tokens.

The market makers can implement arbitrary code within the _callback function. A market maker can insert malicious code into the callback function (e.g. requesting token approval from users), which might allow the malicious maker to steal the assets from the users.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondSampleCallback.sol#L34

File: BondSampleCallback.sol
34:     function _callback(
35:         uint256 id_,
36:         ERC20 quoteToken_,
37:         uint256 inputAmount_,
38:         ERC20 payoutToken_,
39:         uint256 outputAmount_
40:     ) internal override {
41:         // Transfer new payoutTokens to sender
42:         payoutToken_.safeTransfer(msg.sender, outputAmount_);
43:     }

Impact

A market maker could insert malicious code into the callback function, exposing takers to the risk of their assets being stolen. Even if a market owner is trusted at this point in time, the owner could be compromised or turn rogue later.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondSampleCallback.sol#L34

Tool used

Manual Review

Recommendation

Consider removing the callback feature from the protocol since the risk would likely outweigh the benefits. If users are being compromised due to the callback, the protocol team technically can blame it on the malicious market owner. However, in the real world, any bad news related to assets being stolen while using Bond Protocol, regardless it is the protocol or the market owner's fault, the protocol's reputation will be negatively affected. Most of the time, users who lose their funds due to a hack will always blame the protocol.

If that is not possible, instead of allowing whitelisted market makers to define arbitrary callback contract, a safer approach would be to implement an additional whitelisting mechanism to only allow callback contract that has completed a full audit to be added to the market.

Following are some of the security requirements that should be validated against the callback contract during an audit for reference:

  • Immutable (Not upgradable)
  • Does not contain self-destruct
  • Does not perform delegate calls to external contract
  • Should only contain the minimum code required for carrying out the transaction. No malicious code, such as requesting token approval from users

xiaoming90 - Inconsistency Of Minimum And Maximum Terms Allowed

xiaoming90

medium

Inconsistency Of Minimum And Maximum Terms Allowed

Summary

Inconsistency of the minimum and maximum terms allowed for a bond token deployed through Bond Protocol might cause issues and be error-prone.

Vulnerability Detail

A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry based on the following comment within the codebase.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L98

File: BondBaseSDA.sol
97:     // A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry.
98:     uint48 internal constant MAX_FIXED_TERM = 52 weeks * 50;

Within the BondFixedTermSDA.createMarket function, validation is in place at Line 38 to prevent users from creating a market that issues fixed-term bonds that vest less than 1 day or more than MAX_FIXED_TERM (50 years).

This shows that the protocol does not intend to support fixed-term bonds that vest less than 1 day or more than MAX_FIXED_TERM (50 years).

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermSDA.sol#L33

File: BondFixedTermSDA.sol
33:     function createMarket(bytes calldata params_) external override returns (uint256) {
34:         // Decode params into the struct type expected by this auctioneer
35:         MarketParams memory params = abi.decode(params_, (MarketParams));
36: 
37:         // Check that the vesting parameter is valid for a fixed-term market
38:         if (params.vesting != 0 && (params.vesting < 1 days || params.vesting > MAX_FIXED_TERM))
39:             revert Auctioneer_InvalidParams();
40: 
41:         // Create market and return market ID
42:         return _createMarket(params);
43:     }

If there is any bond that has a vesting period longer than 50 years, it is considered a timestamp for fixed expiry, and the bond will be considered a fixed-expiry bond. Many parts of the protocol rely on this invariant to determine whether a bond is a fixed-term or fixed-expiry.

The BondBaseSDA.isInstantSwap function determines if a bond is fixed-term or fixed-expiry by comparing it against the MAX_FIXED_TERM in Line 782

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L780

File: BondBaseSDA.sol
780:     function isInstantSwap(uint256 id_) public view returns (bool) {
781:         uint256 vesting = terms[id_].vesting;
782:         return (vesting <= MAX_FIXED_TERM) ? vesting == 0 : vesting <= block.timestamp;
783:     }

The BondAggregator.findMarketFor function determines if a bond is fixed-term or fixed-expiry by comparing it against the MAX_FIXED_TERM in Line 241

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondAggregator.sol#L221

File: BondAggregator.sol
221:     function findMarketFor(
222:         address payout_,
223:         address quote_,
224:         uint256 amountIn_,
225:         uint256 minAmountOut_,
226:         uint256 maxExpiry_
227:     ) external view returns (uint256) {
228:         uint256[] memory ids = marketsFor(payout_, quote_);
229:         uint256 len = ids.length;
230:         uint256[] memory payouts = new uint256[](len);
231: 
232:         uint256 highestOut;
233:         uint256 id = type(uint256).max; // set to max so an empty set doesn't return 0, the first index
234:         uint48 vesting;
235:         uint256 maxPayout;
236:         IBondAuctioneer auctioneer;
237:         for (uint256 i; i < len; ++i) {
238:             auctioneer = marketsToAuctioneers[ids[i]];
239:             (, , , , vesting, maxPayout) = auctioneer.getMarketInfoForPurchase(ids[i]);
240: 
241:             uint256 expiry = (vesting <= MAX_FIXED_TERM) ? block.timestamp + vesting : vesting;
242: 
243:             if (expiry <= maxExpiry_) {
244:                 payouts[i] = minAmountOut_ <= maxPayout
245:                     ? payoutFor(amountIn_, ids[i], address(0))
246:                     : 0;
247: 
248:                 if (payouts[i] > highestOut) {
249:                     highestOut = payouts[i];
250:                     id = ids[i];
251:                 }
252:             }
253:         }
254: 
255:         return id;
256:     }

However, the issue is that it is possible for users to deploy a fixed-term bond that is more than 50 years (e.g. 100 years) because the BondFixedTermTeller.deploy function does not verify that the vesting period is less than 50 years before creating a token.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L175

File: BondFixedTermTeller.sol
172:     /* ========== TOKENIZATION ========== */
173: 
174:     /// @inheritdoc IBondFixedTermTeller
175:     function deploy(ERC20 underlying_, uint48 expiry_)
176:         external
177:         override
178:         nonReentrant
179:         returns (uint256)
180:     {
181:         uint256 tokenId = getTokenId(underlying_, expiry_);
182:         // Only creates token if it does not exist
183:         if (!tokenMetadata[tokenId].active) {
184:             _deploy(tokenId, underlying_, expiry_);
185:         }
186:         return tokenId;
187:     }

Once the users create the fixed-term bond (e.g. 100 years fixed-term), they can proceed to call the BondFixedTermTeller.create to mint the fixed-term bond for distribution to the public.

Impact

This inconsistency will cause some issues and be error-prone when implementing logic to handle fixed-term bonds created by users VS fixed-term bonds minted by the market. It might also cause issues and confusion when other protocols attempt to integrate with Bond protocol's tokens. Since the protocol deems any bond that has a vesting period longer than 50 years to be considered a timestamp for fixed expiry, a fixed-term bond with a vesting period longer than 50 years might be wrongly deemed as a fixed expiry bond.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L175

Tool used

Manual Review

Recommendation

Ensure that the minimum and maximum terms allowed for a bond token deployed through Bond Protocol are consistent.

function deploy(ERC20 underlying_, uint48 expiry_)
    external
    override
    nonReentrant
    returns (uint256)
{
+	if (expiry_ != 0 && (expiry_ < 1 days || expiry_ > MAX_FIXED_TERM))
+		revert Auctioneer_InvalidParams();
+
    uint256 tokenId = getTokenId(underlying_, expiry_);
    // Only creates token if it does not exist
    if (!tokenMetadata[tokenId].active) {
    	_deploy(tokenId, underlying_, expiry_);
    }
    return tokenId;
}

Alternatively, instead of determining if a bond is fixed-term or fixed-expiry by comparing it against the MAX_FIXED_TERM that is error-prone, consider having a state variable within the bond token implementation that stores a magic predefined byte4 value that indicates whether a bond token is a fixed-term or fixed-expiry token that is more reliable.

Aits - a single whitelist account can create as many as possible.

Aits

medium

a single whitelist account can create as many as possible.

Summary

a single whitelisted account can create as many as possible.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L59-L65

    function whitelist(address teller_, uint256 id_) external override onlyOwner {
        // Check that the market id is a valid, live market on the aggregator
        try _aggregator.isLive(id_) returns (bool live) {
            if (!live) revert Callback_MarketNotSupported(id_);
        } catch {
            revert Callback_MarketNotSupported(id_);
        }

Tool used

Manual Review

Recommendation

Consider limiting the number of whitelisted user or severely limiting who is allowed to create ,

8olidity - Solmate safetransfer and safetransferfrom doesnot check the codesize of the token address, which may lead to fund loss

8olidity

medium

Solmate safetransfer and safetransferfrom doesnot check the codesize of the token address, which may lead to fund loss

Summary

Solmate safetransfer and safetransferfrom doesnot check the codesize of the token address, which may lead to fund loss

Vulnerability Detail

The whole project is using the 'solmate' library to send tokens.
The safeTransfer() functions used in the contract are wrappers around the solmate library. Solmate will not check for contract existance.

File : src/lib/TransferHelper.sol
library TransferHelper {
    function safeTransferFrom(
        ERC20 token,
        address from,
        address to,
        uint256 amount
    ) internal {
        (bool success, bytes memory data) = address(token).call(
            abi.encodeWithSelector(ERC20.transferFrom.selector, from, to, amount)
        );

        require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FROM_FAILED");
    }

    function safeTransfer(
        ERC20 token,
        address to,
        uint256 amount
    ) internal {
        (bool success, bytes memory data) = address(token).call(
            abi.encodeWithSelector(ERC20.transfer.selector, to, amount)
        );

        require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED");
    }

A lot of the code uses this library to transfer tokens

src/BondFixedExpiryTeller.sol:
   88              // If no expiry, then transfer payout directly to user
   89:             underlying_.safeTransfer(recipient_, payout_);
   90          }

  113          uint256 oldBalance = underlying_.balanceOf(address(this));
  114:         underlying_.safeTransferFrom(msg.sender, address(this), amount_);
  115          if (underlying_.balanceOf(address(this)) < oldBalance + amount_)

  151          token_.burn(msg.sender, amount_);
  152:         underlying.safeTransfer(msg.sender, amount_);
  153      }

src/BondFixedTermTeller.sol:
   89              // If no expiry, then transfer payout directly to user
   90:             payoutToken_.safeTransfer(recipient_, payout_);
   91          }

  113          uint256 oldBalance = underlying_.balanceOf(address(this));
  114:         underlying_.safeTransferFrom(msg.sender, address(this), amount_);
  115          if (underlying_.balanceOf(address(this)) < oldBalance + amount_)

  150          _burnToken(msg.sender, tokenId_, amount_);
  151:         meta.underlying.safeTransfer(msg.sender, amount_);
  152      }

src/BondSampleCallback.sol:
  41          // Transfer new payoutTokens to sender
  42:         payoutToken_.safeTransfer(msg.sender, outputAmount_);
  43      }

src/bases/BondBaseCallback.sol:
  142      ) external onlyOwner {
  143:         token_.safeTransfer(to_, amount_);
  144          priorBalances[token_] = token_.balanceOf(address(this));

  151      function deposit(ERC20 token_, uint256 amount_) external onlyOwner {
  152:         token_.safeTransferFrom(msg.sender, address(this), amount_);
  153          priorBalances[token_] = token_.balanceOf(address(this));

src/bases/BondBaseTeller.sol:
  107                  rewards[msg.sender][token] = 0;
  108:                 token.safeTransfer(to_, send);
  109              }

  186          uint256 quoteBalance = quoteToken.balanceOf(address(this));
  187:         quoteToken.safeTransferFrom(msg.sender, address(this), amount_);
  188          if (quoteToken.balanceOf(address(this)) < quoteBalance + amount_)

  194              // Send quote token to callback (transferred in first to allow use during callback)
  195:             quoteToken.safeTransfer(callbackAddr, amountLessFee);
  196  

  209              uint256 payoutBalance = payoutToken.balanceOf(address(this));
  210:             payoutToken.safeTransferFrom(owner, address(this), payout_);
  211              if (payoutToken.balanceOf(address(this)) < (payoutBalance + payout_))

  213  
  214:             quoteToken.safeTransfer(owner, amountLessFee);
  215          }

Impact

Solmate safetransfer and safetransferfrom doesnot check the codesize of the token address, which may lead to fund loss

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L89
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L114
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L152

Tool used

Manual Review

Recommendation

Use openzeppelin's safeERC20 or implement a code existence check

Duplicate of #8

caventa - Too many unnecessary new fixed-expiry bond contracts could be deployed

caventa

medium

Too many unnecessary new fixed-expiry bond contracts could be deployed

Summary

Too many unnecessary new fixed-expiry bond contracts could be deployed.

Vulnerability Detail

(See ERC20BondToken bondToken = bondTokens[underlying_][expiry_];) Fixed-expiry bonds are differentiated by token type and expiry date. This means that there could be 86400 fixed-expiry bond addresses that could be created FOR EVERY SECOND in a day for an underlying token and this could be very inefficient (See BondFixedExpiryTeller.sol#L158-L184).

Impact

Too many gases are spent to create too many new contracts.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L158-L184
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L168

Tool used

Manual Review

Recommendation

Restrict only 1 new contract can be created for 1 underlying token in a day. Add the following code just before BondFixedExpiryTeller.sol#L168

expiry_ = expiry_ / 1 days * 1 days;

caventa - When purchasing a fixed-expiry bond with no expiry / vesting, payoutToken could be burned if the recipient is set to address 0

caventa

high

When purchasing a fixed-expiry bond with no expiry / vesting, payoutToken could be burned if the recipient is set to address 0

Summary

When purchasing a fixed-expiry bond with no expiry / vesting, payoutToken could be burned if the recipient is set to address(0).

Vulnerability Detail

Added a custom test (See MyTest1.t.sol#L196-L237) to verify this.

teller.purchase(address(0), referrer, id, bondAmount, 0);

(See MyTest1.t.sol#L226) can be executed without error If the vesting / expiry (See MyTest1.t.sol#L131) is set to 0. Then, the payoutToken is sent to address 0 (See BondFixedTermTeller.sol#L90, MyTest1.t.sol#L216 and MyTest1.t.sol#L227) after purchase is made.

Impact

ERC20 token which does not have a burn function treats sending tokens to address 0 as burn. Therefore, I would say PayoutToken will get burned if the user accidentally passes in address 0 as the recipient.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/test/MyTest1.t.sol#L196-L237
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/test/MyTest1.t.sol#L226
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/test/MyTest1.t.sol#L131
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L90
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/test/MyTest1.t.sol#L216
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/test/MyTest1.t.sol#L227
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L121

Tool used

Manual Review and added a foundry test (See MyTest1.t.sol#L196-L237)

Recommendation

Add

if(recipient_ == address(0)) {
  revert Teller_InvalidParams();
}

to the first line of the purchase function (See BondBaseTeller.sol#L121). This can prevent the recipient to be set as address 0.

rvierdiiev - meta.tuneBelowCapacity param is not updated when BondBaseSDA.setIntervals is called

rvierdiiev

medium

meta.tuneBelowCapacity param is not updated when BondBaseSDA.setIntervals is called

Summary

When BondBaseSDA.setIntervals function is called then meta.tuneBelowCapacity param is not updated which has impact on price tuning.

Vulnerability Detail

BondBaseSDA.setIntervals function allows for market owner to change some market interval. One of them is meta.tuneInterval.
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L303-L333

    function setIntervals(uint256 id_, uint32[3] calldata intervals_) external override {
        // Check that the market is live
        if (!isLive(id_)) revert Auctioneer_InvalidParams();


        // Check that the intervals are non-zero
        if (intervals_[0] == 0 || intervals_[1] == 0 || intervals_[2] == 0)
            revert Auctioneer_InvalidParams();


        // Check that tuneInterval >= tuneAdjustmentDelay
        if (intervals_[0] < intervals_[1]) revert Auctioneer_InvalidParams();


        BondMetadata storage meta = metadata[id_];
        // Check that tuneInterval >= depositInterval
        if (intervals_[0] < meta.depositInterval) revert Auctioneer_InvalidParams();


        // Check that debtDecayInterval >= minDebtDecayInterval
        if (intervals_[2] < minDebtDecayInterval) revert Auctioneer_InvalidParams();


        // Check that sender is market owner
        BondMarket memory market = markets[id_];
        if (msg.sender != market.owner) revert Auctioneer_OnlyMarketOwner();


        // Update intervals
        meta.tuneInterval = intervals_[0];
        meta.tuneIntervalCapacity = market.capacity.mulDiv(
            uint256(intervals_[0]),
            uint256(terms[id_].conclusion) - block.timestamp
        ); // don't have a stored value for market duration, this will update tuneIntervalCapacity based on time remaining
        meta.tuneAdjustmentDelay = intervals_[1];
        meta.debtDecayInterval = intervals_[2];
    }

meta.tuneInterval has impact on meta.tuneIntervalCapacity. That means that when you change tuning interval you also change the capacity that is operated during tuning.
There is also one more param that depends on this, but is not counted here.

This is meta.tuneBelowCapacity param and it is needed to say if the market has overselled tokens. In another words it says if meta.tuneIntervalCapacity is already sold. This param is checked while tuning and then is updated after the tuning.
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L576-L621

        if (
            (market.capacity < meta.tuneBelowCapacity && timeNeutralCapacity < initialCapacity) ||
            (time_ >= meta.lastTune + meta.tuneInterval && timeNeutralCapacity > initialCapacity)
        ) {
            // Calculate the correct payout to complete on time assuming each bond
            // will be max size in the desired deposit interval for the remaining time
            //
            // i.e. market has 10 days remaining. deposit interval is 1 day. capacity
            // is 10,000 TOKEN. max payout would be 1,000 TOKEN (10,000 * 1 / 10).
            markets[id_].maxPayout = capacity.mulDiv(uint256(meta.depositInterval), timeRemaining);


            // Calculate ideal target debt to satisty capacity in the remaining time
            // The target debt is based on whether the market is under or oversold at this point in time
            // This target debt will ensure price is reactive while ensuring the magnitude of being over/undersold
            // doesn't cause larger fluctuations towards the end of the market.
            //
            // Calculate target debt from the timeNeutralCapacity and the ratio of debt decay interval and the length of the market
            uint256 targetDebt = timeNeutralCapacity.mulDiv(
                uint256(meta.debtDecayInterval),
                uint256(meta.length)
            );


            // Derive a new control variable from the target debt
            uint256 controlVariable = terms[id_].controlVariable;
            uint256 newControlVariable = price_.mulDivUp(market.scale, targetDebt);


            emit Tuned(id_, controlVariable, newControlVariable);


            if (newControlVariable < controlVariable) {
                // If decrease, control variable change will be carried out over the tune interval
                // this is because price will be lowered
                uint256 change = controlVariable - newControlVariable;
                adjustments[id_] = Adjustment(change, time_, meta.tuneAdjustmentDelay, true);
            } else {
                // Tune up immediately
                terms[id_].controlVariable = newControlVariable;
                // Set current adjustment to inactive (e.g. if we are re-tuning early)
                adjustments[id_].active = false;
            }


            metadata[id_].lastTune = time_;
            metadata[id_].tuneBelowCapacity = market.capacity > meta.tuneIntervalCapacity
                ? market.capacity - meta.tuneIntervalCapacity
                : 0;
            metadata[id_].lastTuneDebt = targetDebt;
        }

If you don't update meta.tuneBelowCapacity when changing intervals you have a risk, that price will not be tuned when tuneIntervalCapacity was decreased or it will be still tuned when tuneIntervalCapacity was increased.

As a result tuning will not be completed when needed.

Impact

Tuning logic will not be completed when needed.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Update meta.tuneBelowCapacity in BondBaseSDA.setIntervals function.

Zarf - Checks-Effects-Interaction pattern not followed in BondBaseCallback

Zarf

low

Checks-Effects-Interaction pattern not followed in BondBaseCallback

Summary

The withdraw() function in the BondBaseCallback contract does not follow the checks-effects-interaction pattern.

Vulnerability Detail

When withdrawing tokens from the BondBaseCallback contract, the checks-effects-interaction pattern is not followed, which could result in a reentrancy attack. In case the token is an ERC777 token masquerading as an ERC20 token, the recipient could reenter in the withdraw() function to withdraw additional tokens.

As the withdraw() is able to withdraw all functions in one go, reentering has no additional benefit. Additionally, this function can only be successfully called by the contract owner.

Impact

As this function is only accessible by the contract owner (thanks to the modifier) and you can drain the contract using this function anyway, the impact is considered low.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L138-L145

Tool used

Manual Review

Recommendation

First update the balance in the priorBalances mapping and afterwards send the tokens to the recipient:

function withdraw(
    address to_,
    ERC20 token_,
    uint256 amount_
) external onlyOwner {
    priorBalances[token_] = token_.balanceOf(address(this));
    token_.safeTransfer(to_, amount_);
}

xiaoming90 - Transferring Ownership Might Break The Market

xiaoming90

medium

Transferring Ownership Might Break The Market

Summary

After the transfer of the market ownership, the market might stop working, and no one could purchase any bond token from the market leading to a loss of sale for the market makers.

Vulnerability Detail

The callbackAuthorized mapping contains a list of whitelisted market owners authorized to use the callback. When the users call the purchaseBond function, it will check at Line 390 if the current market owner is still authorized to use a callback. Otherwise, the function will revert.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L379

File: BondBaseSDA.sol
379:     function purchaseBond(
380:         uint256 id_,
381:         uint256 amount_,
382:         uint256 minAmountOut_
383:     ) external override returns (uint256 payout) {
384:         if (msg.sender != address(_teller)) revert Auctioneer_NotAuthorized();
385: 
386:         BondMarket storage market = markets[id_];
387:         BondTerms memory term = terms[id_];
388: 
389:         // If market uses a callback, check that owner is still callback authorized
390:         if (market.callbackAddr != address(0) && !callbackAuthorized[market.owner])
391:             revert Auctioneer_NotAuthorized();

However, if the market owner transfers the market ownership to someone else. The market will stop working because the new market owner might not be on the list of whitelisted market owners (callbackAuthorized mapping). As such, no one can purchase any bond token.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L336

File: BondBaseSDA.sol
336:     function pushOwnership(uint256 id_, address newOwner_) external override {
337:         if (msg.sender != markets[id_].owner) revert Auctioneer_OnlyMarketOwner();
338:         newOwners[id_] = newOwner_;
339:     }

Impact

After the transfer of the market ownership, the market might stop working, and no one could purchase any bond token from the market leading to a loss of sale for the market makers.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L379

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L336

Tool used

Manual Review

Recommendation

Before pushing the ownership, if the market uses a callback, implement an additional validation check to ensure that the new market owner has been whitelisted to use the callback. This will ensure that transferring the market ownership will not break the market due to the new market owner not being whitelisted.

function pushOwnership(uint256 id_, address newOwner_) external override {
    if (msg.sender != markets[id_].owner) revert Auctioneer_OnlyMarketOwner();
+   if (markets[id_].callbackAddr != address(0) && !callbackAuthorized[newOwner_])
+   	revert newOwnerNotAuthorizedToUseCallback();
    newOwners[id_] = newOwner_;
}

rvierdiiev - BondBaseSDA.setDefaults doesn't validate inputs

rvierdiiev

medium

BondBaseSDA.setDefaults doesn't validate inputs

Summary

BondBaseSDA.setDefaults doesn't validate inputs which can lead to initializing new markets incorrectly

Vulnerability Detail

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L348-L356

    function setDefaults(uint32[6] memory defaults_) external override requiresAuth {
        // Restricted to authorized addresses
        defaultTuneInterval = defaults_[0];
        defaultTuneAdjustment = defaults_[1];
        minDebtDecayInterval = defaults_[2];
        minDepositInterval = defaults_[3];
        minMarketDuration = defaults_[4];
        minDebtBuffer = defaults_[5];
    }

Function BondBaseSDA.setDefaults doesn't do any checkings, as you can see. Because of that it's possible to provide values that will break market functionality.

For example you can set minDepositInterval to be bigger than minMarketDuration and it will be not possible to create new market.

Or you can provide minDebtBuffer to be 100% ot 0% that will break logic of market closing.

Impact

Can't create new market or market logic will be not working as designed.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Add input validation.

obront - Fixed Term Markets can be created with 1 day vesting, even though docs specify 3 day minimum

obront

medium

Fixed Term Markets can be created with 1 day vesting, even though docs specify 3 day minimum

Summary

In the docs, it specifies that markets should have a minimum of 3 day vesting to ensure that token prices aren't pushed down by users dumping. However, in the code, this minimum is set to only 1 day.

Vulnerability Detail

In BondFixedTermSDA.sol, the createMarket() function is implemented, which decodes and validates the parameters to create a new market on an auctioneer.

function createMarket(bytes calldata params_) external override returns (uint256) {
    // Decode params into the struct type expected by this auctioneer
    MarketParams memory params = abi.decode(params_, (MarketParams));

    // Check that the vesting parameter is valid for a fixed-term market
    if (params.vesting != 0 && (params.vesting < 1 days || params.vesting > MAX_FIXED_TERM))
        revert Auctioneer_InvalidParams();

    // Create market and return market ID
    return _createMarket(params);
}

In the docs, the minimum vesting period is stated to be 3 days, but in the code above, we only check to ensure that the vesting parameter is greater than or equal to 1 days.

Impact

Issuers will be able to create markets with a 1 day vesting period, which is less than the minimum the Bond Protocol team has determined to avoid creating too much sell pressure on their payout token.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermSDA.sol#L38

Tool used

Manual Review

Recommendation

Increase the minimum vesting to 3 days:

if (params.vesting != 0 && (params.vesting < 3 days || params.vesting > MAX_FIXED_TERM))
...

xiaoming90 - Debt decay interval can be larger than the total duration

xiaoming90

medium

Debt decay interval can be larger than the total duration

Summary

The debt decay interval can be larger than the total duration of the market, which might cause some issues.

Vulnerability Detail

The following code shows that the debtDecayInterval is calculated by multiplying the params_.depositInterval by 5 in Line 185.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L180

File: BondBaseSDA.sol
180:             // The debt decay interval is how long it takes for price to drop to 0 from the last decay timestamp.
181:             // In reality, a 50% drop is likely a guaranteed bond sale. Therefore, debt decay interval needs to be
182:             // long enough to allow a bond to adjust if oversold. It also needs to be some multiple of deposit interval
183:             // because you don't want to go from 100 to 0 during the time frame you expected to sell a single bond.
184:             // A multiple of 5 is a sane default observed from running OP v1 bond markets.
185:             uint32 userDebtDecay = params_.depositInterval * 5;
186:             debtDecayInterval = minDebtDecayInterval > userDebtDecay
187:                 ? minDebtDecayInterval
188:                 : userDebtDecay;

The debt decay interval determines how long it takes for the price to drop to 0 from the last decay timestamp. However, it might be possible for a market marker to define a params_.depositInterval that results in the derived debtDecayInterval being larger than the total duration of the market.

Assume that the parameters of the SDAM:

  • params_.depositInterval = 5 days (Debt decay interval - ID in whitepaper)
  • secondsToConclusion = 10 days (Total Duration - L in whitepaper)

In this case, the debtDecayInterval will end up being 25 days (5 days * 5), which is larger than the secondsToConclusion .

Impact

The price can never drop to 0 within the market period, and the price will decay at an extremely slow rate in some cases. As a result, the sale of the bond tokens might be affected as the price of the bond tokens will remain high for a long period and will not be able to adjust itself according to the economic condition to attract potential takers.

Additionally, it appears that various parts of the calculation depend on scaling a variable with the ratio of the debt decay interval to the total duration. This issue will cause the scaling ratio to go above one (ratio > 1). If the scaling ratio does not intend to be larger than one (ratio > 1), it might break some of the properties of the market.

The following attempt to scale the capacity by the ratio of the debt decay interval to the total duration, as shown below. Taken from Page 5 of the whitepaper - Definition 8

image-20221116175141093

The following attempt to scale the time-neutral capacity by the ratio of the debt decay interval to the total duration, as shown below. Taken from Page 6 of the whitepaper - Definition 12

image-20221116175108232

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L180

Tool used

Manual Review

Recommendation

Review the following about the market design:

  • Determine if the market design allows the debt decay interval to be larger than the total duration
  • Determine if the market design allows the scaling ratio to go above one.

If the debt decay interval should not be larger than the total duration, implement the following validation check

uint32 secondsToConclusion;
uint32 debtDecayInterval;
{
    // Conclusion must be later than the current block timestamp or will revert
    secondsToConclusion = uint32(params_.conclusion - block.timestamp);
    if (
        secondsToConclusion < minMarketDuration ||
        params_.depositInterval < minDepositInterval ||
        params_.depositInterval > secondsToConclusion
    ) revert Auctioneer_InvalidParams();

    // The debt decay interval is how long it takes for price to drop to 0 from the last decay timestamp.
    // In reality, a 50% drop is likely a guaranteed bond sale. Therefore, debt decay interval needs to be
    // long enough to allow a bond to adjust if oversold. It also needs to be some multiple of deposit interval
    // because you don't want to go from 100 to 0 during the time frame you expected to sell a single bond.
    // A multiple of 5 is a sane default observed from running OP v1 bond markets.
    uint32 userDebtDecay = params_.depositInterval * 5;
    debtDecayInterval = minDebtDecayInterval > userDebtDecay
        ? minDebtDecayInterval
        : userDebtDecay;
+        
+   require(debtDecayInterval <= secondsToConclusion, "Invalid debtDecayInterval")

Additionally, it is recommended to define the possible range of the debt decay interval in the whitepaper (e.g. 0 < ID <= T) so that the reader can understand if the market design intends the debt decay interval to be larger than the total duration.

xiaoming90 - Existing Circuit Breaker Implementation Allow Faster Taker To Extract Payout Tokens From Market

xiaoming90

high

Existing Circuit Breaker Implementation Allow Faster Taker To Extract Payout Tokens From Market

Summary

The current implementation of the circuit breaker is not optimal. Thus, the market maker will lose an excessive amount of payout tokens if a quoted token suddenly loses a large amount of value, even with a circuit breaker in place.

Vulnerability Detail

When the amount of the payout tokens purchased by the taker exceeds the term.maxDebt, the taker is still allowed to carry on with the transaction, and the market will only be closed after the current transaction is completed.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L427

File: BondBaseSDA.sol
426:         // Circuit breaker. If max debt is breached, the market is closed
427:         if (term.maxDebt < market.totalDebt) {
428:             _close(id_);
429:         } else {
430:             // If market will continue, the control variable is tuned to to expend remaining capacity over remaining market duration
431:             _tune(id_, currentTime, price);
432:         }

Assume that the state of the SDAM at T0 is as follows:

  • term.maxDebt is 110 (debt buffer = 10%)
  • maxPayout is 100
  • market.totalDebt is 99

Assume that the quoted token suddenly loses a large amount of value (e.g. stablecoin depeg causing the quote token to drop to almost zero). Bob decided to purchase as many payout tokens as possible before reaching the maxPayout limit to maximize the value he could extract from the market. Assume that Bob is able to purchase 50 bond tokens at T1 before reaching the maxPayout limit. As such, the state of the SDAM at T1 will be as follows:

  • term.maxDebt = 110
  • maxPayout = 100
  • market.totalDebt = 99 + 50 = 149

In the above scenario, Bob's purchase has already breached the term.maxDebt limit. However, he could still purchase the 50 bond tokens in the current transaction.

Impact

In the event that the price of the quote token falls to almost zero (e.g. 0.0001 dollars), then the fastest taker will be able to extract as many payout tokens as possible before reaching the maxPayout limit from the market. The extracted payout tokens are essentially free for the fastest taker. Taker gain is maker loss.

Additionally, in the event that a quoted token suddenly loses a large amount of value, the amount of payout tokens lost by the market marker is capped at the maxPayout limit instead of capping the loss at the term.maxDebt limit. This resulted in the market makers losing more payout tokens than expected, and their payout tokens being sold to the takers at a very low price (e.g. 0.0001 dollars).

The market makers will suffer more loss if the maxPayout limit of their markets is higher.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L427

Tool used

Manual Review

Recommendation

Considering only allowing takers to purchase bond tokens up to the term.maxDebt limit.

For instance, based on the earlier scenario, only allow Bob to purchase up to 11 bond tokens (term.maxDebt[110] - market.totalDebt[99]) instead of allowing him to purchase 50 bond tokens.

If Bob attempts to purchase 50 bond tokens, the market can proceed to purchase the 11 bond tokens for Bob, and the remaining quote tokens can be refunded back to Bob. After that, since the term.maxDebt (110) == market.totalDebt (110), the market can trigger the circuit breaker to close the market to protect the market from potential extreme market conditions.

This ensures that bond tokens beyond the term.maxDebt limit would not be sold to the taker during extreme market conditions.

Zarf - Read-only reentrancy in BondFixedTermTeller

Zarf

medium

Read-only reentrancy in BondFixedTermTeller

Summary

When minting new ERC1155 bonds in the BondFixedTermTeller contract, the total supply of this specific bond is updated after the new bonds are sent to the recipient, which introduces a reentrancy attack.

Vulnerability Detail

Whenever a new ERC1155 bond is minted in the BondFixedTermTeller contract, either through _handlePayout() or create(), the total supply is updated after the bond has been minted.

ERC1155 tokens will perform a callback to the recipient in case the recipient implements the ERC1155TokenReceiver interface. Therefore, the recipient (msg.sender in create() or recipient_ in _handlePayout() ) is able to perform a call to an arbitrary contract before the total supply of the bonds is updated.

While the recipient could enter the current BondFixedTermTeller contract to call any function, there is no interesting function which might result in financial loss in case it gets called in the callback. Alternatively, the recipient could enter a smart contract which uses the the public mapping tokenMetadata in BondFixedTermTeller to calculate the current bond price based on the supply. As the supply is not yet updated, but the tokens are minted, this might result in a miscalculation of the price.

Impact

While the BondFixedTermTeller contract itself is not at risk, any protocols integrating with BondFixedTermTeller and using the total supply of the ERC1155 bond token to calculate the price, might come at risk.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L218-L225

Tool used

Manual Review

Recommendation

Update the total supply and mint the tokens afterwards:

function _mintToken(
    address to_,
    uint256 tokenId_,
    uint256 amount_
) internal {
    tokenMetadata[tokenId_].supply += amount_;
    _mint(to_, tokenId_, amount_, bytes(""));
}

obront - Fixed Term Bond tokens can be minted with non-rounded expiry

obront

medium

Fixed Term Bond tokens can be minted with non-rounded expiry

Summary

Fixed Term Tellers intend to mint tokens that expire once per day, to consolidate liquidity and create a uniform experience. However, this rounding is not enforced on the external deploy() function, which allows for tokens expiring at unexpected times.

Vulnerability Detail

In BondFixedTermTeller.sol, new tokenIds are deployed through the _handlePayout() function. The function calculates the expiry (rounded down to the nearest day), uses this expiry to create a tokenId, and — if that tokenId doesn't yet exist — deploys it.

...
expiry = ((vesting_ + uint48(block.timestamp)) / uint48(1 days)) * uint48(1 days);

// Fixed-term user payout information is handled in BondTeller.
// Teller mints ERC-1155 bond tokens for user.
uint256 tokenId = getTokenId(payoutToken_, expiry);

// Create new bond token if it doesn't exist yet
if (!tokenMetadata[tokenId].active) {
    _deploy(tokenId, payoutToken_, expiry);
}
...

This successfully consolidates all liquidity into one daily tokenId, which expires (as expected) at the time included in the tokenId.

However, if the deploy() function is called directly, no such rounding occurs:

function deploy(ERC20 underlying_, uint48 expiry_)
    external
    override
    nonReentrant
    returns (uint256)
{
    uint256 tokenId = getTokenId(underlying_, expiry_);
    // Only creates token if it does not exist
    if (!tokenMetadata[tokenId].active) {
        _deploy(tokenId, underlying_, expiry_);
    }
    return tokenId;
}

This creates a mismatch between the tokenId time and the real expiry time, as tokenId is calculated by rounding the expiry down to the nearest day:

uint256 tokenId = uint256(
    keccak256(abi.encodePacked(underlying_, expiry_ / uint48(1 days)))
);

... while the _deploy() function saves the original expiry:

tokenMetadata[tokenId_] = TokenMetadata(
    true,
    underlying_,
    uint8(underlying_.decimals()),
    expiry_,
    0
);

Impact

The deploy() function causes a number of issues:

  1. Tokens can be deployed that don't expire at the expected daily time, which may cause issues with your front end or break user's expectations
  2. Tokens can expire at times that don't align with the time included in the tokenId
  3. Malicious users can pre-deploy tokens at future timestamps to "take over" the token for a given day and lock it at a later time stamp, which then "locks in" that expiry time and can't be changed by the protocol

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L175-L187

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L243-L250

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L194-L212

Tool used

Manual Review

Recommendation

Include the same rounding process in deploy() as is included in _handlePayout():

function deploy(ERC20 underlying_, uint48 expiry_)
        external
        override
        nonReentrant
        returns (uint256)
    {
        expiry = ((vesting_ + uint48(block.timestamp)) / uint48(1 days)) * uint48(1 days);
        uint256 tokenId = getTokenId(underlying_, expiry_);
        ...

xiaoming90 - Create Fee Discount Feature Is Broken

xiaoming90

medium

Create Fee Discount Feature Is Broken

Summary

The create fee discount feature is found to be broken within the protocol.

Vulnerability Detail

The create fee discount feature relies on the createFeeDiscount state variable to determine the fee to be discounted from the protocol fee. However, it was observed that there is no way to initialize the createFeeDiscount state variable. As a result, the createFeeDiscount state variable will always be zero.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L118

File: BondFixedExpiryTeller.sol
118:         // If fee is greater than the create discount, then calculate the fee and store it
119:         // Otherwise, fee is zero.
120:         if (protocolFee > createFeeDiscount) {
121:             // Calculate fee amount
122:             uint256 feeAmount = amount_.mulDiv(protocolFee - createFeeDiscount, FEE_DECIMALS);
123:             rewards[_protocol][underlying_] += feeAmount;
124: 
125:             // Mint new bond tokens
126:             bondToken.mint(msg.sender, amount_ - feeAmount);
127: 
128:             return (bondToken, amount_ - feeAmount);
129:         } else {
130:             // Mint new bond tokens
131:             bondToken.mint(msg.sender, amount_);
132: 
133:             return (bondToken, amount_);
134:         }

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L118

File: BondFixedTermTeller.sol
118:         // If fee is greater than the create discount, then calculate the fee and store it
119:         // Otherwise, fee is zero.
120:         if (protocolFee > createFeeDiscount) {
121:             // Calculate fee amount
122:             uint256 feeAmount = amount_.mulDiv(protocolFee - createFeeDiscount, FEE_DECIMALS);
123:             rewards[_protocol][underlying_] += feeAmount;
124: 
125:             // Mint new bond tokens
126:             _mintToken(msg.sender, tokenId, amount_ - feeAmount);
127: 
128:             return (tokenId, amount_ - feeAmount);
129:         } else {
130:             // Mint new bond tokens
131:             _mintToken(msg.sender, tokenId, amount_);
132: 
133:             return (tokenId, amount_);
134:         }

Impact

The create fee discount feature is broken within the protocol. There is no way for the protocol team to configure a discount for the users of the BondFixedExpiryTeller.create and BondFixedTermTeller.create functions. As such, the users will not obtain any discount from the protocol when using the create function.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L118

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L118

Tool used

Manual Review

Recommendation

Implement a setter method for the createFeeDiscount state variable and the necessary verification checks.

function setCreateFeeDiscount(uint48 createFeeDiscount_) external requiresAuth {
    if (createFeeDiscount_ > protocolFee)  revert Teller_InvalidParams();
    if (createFeeDiscount_ > 5e3) revert Teller_InvalidParams();
    createFeeDiscount = createFeeDiscount_;
}

xiaoming90 - Race condition on `ERC20BondToken` approval

xiaoming90

medium

Race condition on ERC20BondToken approval

Summary

The approve() function, which is used to manage allowances, exposes the users of the ERC20BondToken token to frontrunning attacks.

Vulnerability Detail

ERC20BondToken inherits from CloneERC20. The CloneERC20 implements the following approve function.

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/lib/CloneERC20.sol#L49

File: CloneERC20.sol
49:     function approve(address spender, uint256 amount) public virtual returns (bool) {
50:         allowance[msg.sender][spender] = amount;
51: 
52:         emit Approval(msg.sender, spender, amount);
53: 
54:         return true;
55:     }

Note that changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering. Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/b2970b96e5e2be297421cd7690e3502e49f7deff/contracts/token/ERC20/IERC20.sol#L57.

Following is the possible attack scenario taken from https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/

  1. Alice allows Bob to transfer N of Alice's tokens (N > 0) by calling the approve method on a Token smart contract, passing the Bob's address and N as the method arguments
  2. After some time, Alice decides to change from N to M (M > 0) the number of Alice's tokens Bob is allowed to transfer, so she calls the approve method again, this time passing the Bob's address and M as the method arguments
  3. Bob notices the Alice's second transaction before it was mined and quickly sends another transaction that calls the transferFrom method to transfer N Alice's tokens somewhere
  4. If the Bob's transaction will be executed before the Alice's transaction, then Bob will successfully transfer N Alice's tokens and will gain an ability to transfer another M tokens
  5. Before Alice noticed that something went wrong, Bob calls the transferFrom method again, this time to transfer M Alice's tokens.

So, an Alice's attempt to change the Bob's allowance from N to M (N > 0 and M > 0) made it possible for Bob to transfer (N + M) of Alice's tokens, while Alice never wanted to allow so many of her tokens to be transferred by Bob.

Impact

The token is not guarded against approval front-running attacks. If the approve function is called twice, an attacker can perform a front-run attack and double spend, resulting in a loss of assets for the victim.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/ERC20BondToken.sol#L25

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/lib/CloneERC20.sol#L49

Tool used

Manual Review

Recommendation

Following are some of the possible solutions to mitigate the issue

  1. Implement functions similar to OpenZeppelin’s increaseAllowance or decreaseAllowance

  2. Reduce the spender’s allowance to 0. Subsequently, set the spender's allowance to the desired value. Reference: ethereum/EIPs#20 (comment)

  3. Preventing a call to approve if all the previous tokens are not spent by adding a check that the allowed balance is 0: require(allowed[msg.sender][_spender] == 0).

zimu - Functions in BondBaseCallback.sol would possibly let the hacker acquire the owner power

zimu

high

Functions in BondBaseCallback.sol would possibly let the hacker acquire the owner power

Summary

In bases/BondBaseCallback.sol, function withdraw and deposit would call functions safeTransfer and safeTransferFrom in lib/TransferHelper.sol, and finally call virtual function transfer and transferFrom in solmate library. However, when an ERC20 token re-implements transfer and transferFrom function with a call back, and since withdraw and deposit do not have reentrancy protection, the owner power of Bond protocol would be taken to withdraw funds.

Vulnerability Detail

  1. bases/BondBaseCallback.sol imports the abstract contract ERC20 from solmate/tokens/ERC20.sol, and using the library in lib/TransferHelper.sol;
  2. Function withdraw calls token_.safeTransfer(to_, amount_), and deposit calls token_.safeTransferFrom(msg.sender, address(this), amount_) in lib/TransferHelper.sol, and finally call virtual function transfer and transferFrom in solmate library;
  3. Thus, the ERC20 token could re-implements an evil callback in transfer and transferFrom function, doing exploitation using the owner permission of Bond protocol.

image

Impact

Since function withdraw and deposit are executed onlyowner and without reentrancy protection, a hacker can re-implement a ERC20 token contract with a callback in transfer and transferFrom function to do exploitation using the owner power of Bond protocol.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L138-L145
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L151-L154

the version of the ERC20 abstract contract that Bond protocol imported:
https://github.com/transmissions11/solmate/blob/dd13c61b5f9cb5c539a7e356ba94a6c2979e9eb9/src/tokens/ERC20.sol

Tool used

Manual Review

Recommendation

Add reentrancy protection to function withdraw and deposit

caventa - Close market should only be allowed if there is no bond token left in the teller contract

caventa

medium

Close market should only be allowed if there is no bond token left in the teller contract

Summary

Close market should only be allowed if there is no bond token left in the teller contract.

Vulnerability Detail

In this protocol, anyone can purchase bonds by supplying QuoteToken for BondToken. Once vested bondToken matured, it can be used to redeem the payoutToken. However, the market can be closed before all the bonds are redeemed.

Impact

Although users are unable to mint tokens (which is correct), users can still be allowed to redeem tokens after closing the market. (See BondBaseSDA.sol#L371-L374, BondBaseSDA.sol#L428, and BondBaseSDA.sol#L439-L444). Technically, no activity should be allowed once the market is closed.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L371-L374
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L428
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L439-L444
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L440
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/lib/ERC1155.sol

Tool used

Manual Review and writing some test units.

Recommendation

There is quite a lot of code refactoring that needs to be done. Below is the direction.

  1. IBondTeller, BondFixedExpiryTeller, and BondFixedTermTeller need to have a new mapping integer variable: bond minted quantity for every market id.
  2. In BondFixedExpiryTeller and BondFixedTermTeller, whenever the mint functions are called for the market id, increase the integer variable; whenever the burn functions are called for the market id, decrease the integer variable.
  3. Ensure all the minted bond token is burned for the market id just before line BondBaseSDA.sol#L440, the code could look like this
if(_teller.mintQtyById(id_) > 0) revert Auctioneer_MintQtyShouldBeZero();

[Note: Checking total supply is another way to ensure there is no bond left. However, the ERC1155 contract (See ERC1155.sol) does not have a total supply variable

8olidity - The value of `createFeeDiscount` can never be updated

8olidity

medium

The value of createFeeDiscount can never be updated

Summary

The value of createFeeDiscount is always 0. You cannot update the value of createFeeDiscount

Vulnerability Detail

The value of createFeeDiscount is always 0. You cannot update the value of createFeeDiscount,Only in the src/outside/BondBaseTeller.sol defines, but no assignment operation. All createFeeDiscount is always 0.

    /// @notice 'Create' function fee discount in basis points (3 decimal places). Amount standard fee is reduced by for partners who just want to use the 'create' function to issue bond tokens.
    uint48 public createFeeDiscount; //@audit 

The effect of this code is to directly compare whether if (protocofee > 0)

        if (protocolFee > createFeeDiscount) {
            // Calculate fee amount
            uint256 feeAmount = amount_.mulDiv(protocolFee - createFeeDiscount, FEE_DECIMALS);
            rewards[_protocol][underlying_] += feeAmount;

            // Mint new bond tokens
            _mintToken(msg.sender, tokenId, amount_ - feeAmount);

            return (tokenId, amount_ - feeAmount);
        } else {
            // Mint new bond tokens
            _mintToken(msg.sender, tokenId, amount_);

            return (tokenId, amount_);
        }

Impact

The value of createFeeDiscount is always 0. You cannot update the value of createFeeDiscount

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L62

Tool used

Manual Review

Recommendation

Let's add a function

    function setcreateFeeDiscount(uint48 createFeeDiscount_) external override requiresAuth {
        createFeeDiscount = createFeeDiscount_;
    }

Duplicate of #16

0xNazgul - [NAZ-M1] `referrer_ && Protocol` Can Front run `purchase()` To Collect Additional Fees Up To `minAmountOut_`

0xNazgul

medium

[NAZ-M1] referrer_ && Protocol Can Front run purchase() To Collect Additional Fees Up To minAmountOut_

Summary

purchase() is a function used to exchange quote tokens for a bond in a specified market and pay fees to both a referrer and protocol.

Vulnerability Detail

The parameter minAmountOut_ in the function purchase() is meant to prevent frontrunning. However, if a user sets minAmountOut_ to a low amount, referrer_ && Protocol can still frontrun the purchaser to up their fees to collect more.

Impact

  1. Alice wants to purchase a bond. She calls purchase() from the frontend with Mallory as the referrer_.
  2. Mallory sees this and also notices that Alice has used a low minAmountOut_. So she frontruns Alice to up her referrerFees.
  3. Alice's purchase still goes through but has had to paid more fees then expected.

Code Snippet

BondBaseTeller.sol#L88-L94

Tool used

Manual Review

Recommendation

Consider adding a timelock to both setReferrerFee() && setProtocolFee().

Duplicate of #29

obront - _tune() uses incorrect initialCapacity

obront

medium

_tune() uses incorrect initialCapacity

Summary

When a market is tuned in _tune(), part of the calculation is the initialCapacity (standardized to payout token). If the market capacity is measured in quote token, this is calculated by adding current capacity to the product of the amount purchased by the current price. This calculation could be off by quite a bit if the current price is not representative of the past prices at which the tokens were purchased.

Vulnerability Detail

In BondBaseSDA.sol, the _tune() function is used to update the market parameters if the market is oversold or undersold.

In order for these calculations to work correctly, we must calculate the initialCapacity of payout tokens.

// Standardize capacity into an payout token amount
uint256 capacity = market.capacityInQuote
    ? market.capacity.mulDiv(market.scale, price_)
    : market.capacity;

// Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point
uint256 initialCapacity = capacity +
    (market.capacityInQuote ? market.purchased.mulDiv(market.scale, price_) : market.sold);

In the situation where the market capacity is measured in the quote token, this calculation boils down to:

market.capacity.mulDiv(market.scale, price_) + market.purchased.mulDiv(market.scale, price_)

The current capacity in this calculation is correct, but the past capacity assumes that the previously purchased tokens were sold at the current price. This likely is not the case.

In situations where the current price is extremely high or low, this calculation has the potential to largely overestimate or underestimate the initial capacity of the token provided.

Impact

Incorrect tuning parameters may lead to incorrectly assigned control variables and adjustments, which could throw off the prices of future bond purchases.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L561-L567

Tool used

Manual Review

Recommendation

Two options I can see:

  1. Save initialCapacity up front to ensure these calculations are happening with the correct value.
  2. Instead of standardizing capacity to the payment token, split _tune() to perform the calculations in whichever token capacity is stored in.

caventa - Every transferrable amount value should not be zero

caventa

medium

Every transferrable amount value should not be zero

Summary

Every transferrable amount value should not be zero.

Vulnerability Detail

All the amounts (See all the code snippets below) in this protocol can be zero. Also, safeTransfer and safeTransferFrom can move zero balance without throwing an error.

Impact

Although it is not harmful to have 0 amount, ensuring that amount is not equal to 0 in the first line of the function is good to prevent all the remaining code from being executed without modifying the storage variable and without funds being moved. This could save a lot of gas and reduce the chance to face unpredictable behavior in the system.

Code Snippet

https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L68
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedExpiryTeller.sol#L99
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L58
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondFixedTermTeller.sol#L100
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondSampleCallback.sol#L37
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/BondSampleCallback.sol#L39
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L125-L126
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseTeller.sol#L171-L173
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L381-L382
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L454
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseSDA.sol#L700
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L80-L81
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L141
https://github.com/sherlock-audit/2022-11-bond/blob/main/src/bases/BondBaseCallback.sol#L151

Tool used

Manual Review and some testing

Recommendation

Restrict the amount so it cannot be zero at the first line of the functions. For example:

 if(amount_ == 0) revert Teller_amountCannotBeZero();

Recommend Projects

  • React photo React

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

  • Vue.js photo Vue.js

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

  • Typescript photo Typescript

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

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

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

Recommend Topics

  • javascript

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

  • web

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

  • server

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

  • Machine learning

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

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

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

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.