2022-11-bond-judging's People
Forkers
aviggiano2022-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.
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
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
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
bases/BondBaseTeller.sol
import the abstract contractERC20
fromsolmate/tokens/ERC20.sol
;- The calling chain: function
purchase
--> function_handleTransfers
--> functionERC20.safeTransfer
orERC20.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 - However, the solmate library of
ERC20
contract has no declaration ofsafeTransfer
andsafeTransferFrom
. When aERC20
token does not implementsafeTransfer
andsafeTransferFrom
, functionpurchase
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
- Replace these lines (See BondFixedExpiryTeller.sol#L107-L108) with
if (bondToken == ERC20BondToken(address(0x00))) {
deploy(underlying_, expiry_);
}
- 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:
-
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 withblock.number > blockSet ? referralFee : oldReferralFee
. -
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
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:
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
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
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 100market.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
= 110maxPayout
= 100market.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:
- 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
- Tokens can expire at times that don't align with the time included in the tokenId
- 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/
- 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 andN
as the method arguments- After some time, Alice decides to change from
N
toM
(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 andM
as the method arguments- 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- 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 anotherM
tokens- 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
toM
(N > 0
andM > 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
-
Implement functions similar to OpenZeppelin’s increaseAllowance or decreaseAllowance
-
Reduce the spender’s allowance to 0. Subsequently, set the spender's allowance to the desired value. Reference: ethereum/EIPs#20 (comment)
-
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
bases/BondBaseCallback.sol
imports the abstract contractERC20
fromsolmate/tokens/ERC20.sol
, and using the library inlib/TransferHelper.sol
;- Function
withdraw
callstoken_.safeTransfer(to_, amount_)
, anddeposit
callstoken_.safeTransferFrom(msg.sender, address(this), amount_)
inlib/TransferHelper.sol
, and finally call virtual functiontransfer
andtransferFrom
in solmate library; - Thus, the
ERC20
token could re-implements an evil callback intransfer
andtransferFrom
function, doing exploitation using the owner permission of Bond protocol.
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.
- IBondTeller, BondFixedExpiryTeller, and BondFixedTermTeller need to have a new mapping integer variable: bond minted quantity for every market id.
- 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.
- 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
- Alice wants to purchase a bond. She calls
purchase()
from the frontend with Mallory as thereferrer_
. - Mallory sees this and also notices that Alice has used a low
minAmountOut_
. So she frontruns Alice to up herreferrerFees
. - Alice's purchase still goes through but has had to paid more fees then expected.
Code Snippet
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:
- Save
initialCapacity
up front to ensure these calculations are happening with the correct value. - 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
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.