Guardian's review of BracketFi, Round 1 for Bracket, published January 2025. The report records 34 findings across 2 review rounds, including 3 critical and 2 high.
- Published
- Review window
- December 19, 2024 to January 13, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum
- Sector
- Yield and vaults
- 3 Critical
- 2 High
- 9 Medium
- 20 Low
- 0 Informational
Scope
4 files in scope · 505 nSLOC
| File | nSLOC | Lines |
|---|---|---|
src/factory/VaultFactory.sol | 43 | 56 |
src/token/BrktETH.sol | 212 | 363 |
src/vaults/RebasingToken.sol | 31 | 40 |
src/vaults/BracketVault.sol | 219 | 345 |
Findings 34
Main Review
28 findings · December 19 to 23, 2024-
C-01 Critical Incorrect Amount Of Collateral Returned Logical Error Resolved
Description
The existing
calculateBurnlogic incorrectly calculates how much of thetokenshould be returned to the user for the given amount of brktETH value.For example:
(1) User deposits 10 WSTETH and receives 10 brktETH. Assume this is the entire brktETH supply.
(2) ETH rate of WSTETH goes from 1 ETH to 1.1 ETH.
(3) User burns all 10 brktETH shares, but does not receive 10 WSTETH. Instead they get (1.1 ether * 11 ether / 1 ether) which is 12.1 WSTETH, more than the 10 WSTETH they put in.
This can be extremely detrimental to protocol as a user may extract more funds than appropriate, as well as prevent a depositor from withdrawing all their shares. Furthermore, this issue will occur every single time
burnis called. To accurately calculate how muchtokenshould be returned, instead the ETH value of the redeemed brktETH should be divided by the ETH value of 1token.Recommendation
Change the calculation to
return Math.mulDiv(value, 1 ether, oracle.getRate(token)); -
C-02 Critical Inaccurate Share Calculation On Mint Logical Error Resolved
Description
The
mintfunction in theBrktETHcontract currently deposits tokens into the contract—updating the token’stotalDeposit—before calculating the amount to mint. As a result, the user’s newly deposited tokens are included in the total value during the share calculation, causing the user to receive fewer shares than intended.For example:
(1) Alice deposits 10 WSTETH and mints 10 brktETH
(2) Bob deposits 10 WSTETH directly after, and mints 5 brktETH (10 brktETH * 10 ETH / 20 ETH), although he should receive the same amount of brktETH as Alice due to 50-50% supply of the pool.
Consequently, this causes loss of assets for the depositor as they receive less shares than necessary, and will occur each time after the first mint.
Recommendation
Modify the mint function so that it calculates the amount of tokens to be minted before depositing them into the contract.
-
C-03 Critical Incorrect Collateral Calculation During Burn Logical Error Resolved
Description
The burn function currently burns the user’s brktETH tokens before calculating the amount of collateral they are entitled to. By doing so, the calculation for
colAmounttakes place after the user’s share has already been removed from the supply, which may cause them to receive more collateral than they should or end up with nothing at all if they are the only remaining shareholder.Recommendation
Modify the burn function to calculate the collateral amount before burning the user’s brktETH.
-
H-01 High Missing lastNavUpdate Update Logical Error Resolved
Description
In the
updateNavfunction there is no update to thelastNavUpdatevariable, therefore the confirmation period validation will never apply past the initial 1 day period after the vault starts.Recommendation
Update the
lastNavUpdatevariable in theupdateNavfunction -
M-01 Medium Incomplete Check In calculateMint Function Validation Resolved
Description
The current
calculateMintimplementation assumes that iftotalSupplyis zero, there is no existing pool of assets and thus setsbrktAmount = value:function calculateMint(uint256 value) public view returns (uint256 brktAmount) { uint256 supply = totalSupply(); if (supply != 0) { brktAmount = Math.mulDiv(supply, value, getTotalValue()); } else { brktAmount = value; } }However, in a very specific edge case scenario, it could be possible that
totalSupplyis non-zero butgetTotalValueis actually zero, for example, due to a sudden asset devaluation. In that case, a division by zero would occur blocking any new deposits (once the issue where you deposit before calculating the mintedbrktAmountis corrected).Recommendation
Update the
calculateMintfunction as shown below:function calculateMint(uint256 value) public view returns (uint256 brktAmount) { uint256 supply = totalSupply(); uint256 totalValue = getTotalValue(); if ((supply != 0) && (totalValue != 0)) { brktAmount = Math.mulDiv(supply, value, totalValue); } else { brktAmount = value; } } -
M-02 Medium Decreased NAV Frontrunning Frontrunning Resolved
Description
If the NAV_UPDATER calls
updateNavwith anewNavthat is lower than the previous one, a user can frontrun the update and trigger a withdrawal. If the delay is only 1 epoch, then the user's shares will be valued at the previous epoch's NAV which is higher, withdrawing more assets and avoiding the loss.Recommendation
Consider enforcing that the delay is greater than 1 epoch, otherwise clearly document this risk and using a private RPC.
-
M-03 Medium Manager Avoids Negative Performance Fees Unexpected behavior Acknowledged
Description
The
claimManagerPerformanceFeesfunction may be called once every 90 days and each time theaccruedManagerPerformanceFeesare reset to zero.This way a manager does not have to continue to pay down a large negative performance fee if it has accrued and has been being paid off for a long duration of time.
However if a manager submits an
updateNavcall with a negative performance fee directly before the 90 period is over they can almost immediately avoid the negative performance fee by calling theclaimManagerPerformanceFeesfunction again, whether on purpose or by accident.Recommendation
Consider using a separate interval for clearing negative performance fees which resets every time a new negative performance fee is added.
-
M-04 Medium Wrong Index For Whitelist/Blacklist Logical Error Resolved
Description
Within functions
whitelistCollateralandblacklistCollateralthe index of the token is retrieved to whitelist/blacklist the token respectively. The issue is that thecollateralsIndexstarts from 1, so the whitelist/blacklist will be set for the wrong token:uint256 index = collateralsIndex[token];Recommendation
Use function
_getIndexinstead since it subtracts the returned index by 1. -
M-05 Medium Incorrect Withdrawable Assets Logical Error Resolved
Description
The
BracketVault.withdrawableAssets()should allow users to query the max amount of assets they can withdraw from the vault.However, this function will incorrectly subtract pending deposit shares when the
lastDeposithas occurred in a previous epoch. The assets for a lastDeposit that occurred in a previous epoch should be represented as withdrawable though since they will be minted to the user in thewithdrawfunction.Recommendation
Modify the
withdrawableAssetsview function such that thependingDepositSharesare only removed from the withdrawable amount when the epoch of thelastDepositis the same as the current epoch:function withdrawableAssets(address account) external view returns (uint256) { uint256 lastNav = epoch == 0 ? 1e18 : navs[epoch - 1]; Deposit memory _deposit = lastDeposit[user]; if (_deposit.epoch == epoch) return convertToAssets(sharesOf(account) - convertToShares(_deposit.assets, vanityNav), lastNav); else return convertToAssets(sharesOf(account), lastNav); } -
M-06 Medium totalValue DoS DoS Acknowledged
Description
The BrktETH contract relies on the getTotalValue function to determine the total value of all collateral denominated in ETH, which is critical for functions like minting and burning. The process involves fetching each collateral’s value and summing the results. However, if any one token’s price retrieval fails, the entire getTotalValue function reverts—causing all dependent operations (e.g., minting, burning) to fail as well. For instance, the EZETH token uses an external oracle that performs certain validations when fetching its price, which could revert if those validations fail. This creates a single point of failure that disrupts the contract’s functionality whenever a single token’s price feed encounters an issue.
Recommendation
Implement a fallback oracle for each token so that if the primary method reverts (e.g., due to external validation errors), the contract can still fetch the token’s price. This prevents the entire getTotalValue function—and consequently critical operations like minting or burning—from halting when a single price feed fails.
-
M-07 Medium Manager Fees Withdrawn By Users Logical Error Resolved
Description
In the
_processDepositsWithdrawalsfunction the available balance for withdrawal by the users is based on the_getManagerAvailableBalance, which does not set aside the accruedManagerPerformanceFees (if positive), accruedManagerTvlFees, and accruedBrktTvlFees.If a manager has not claimed these fees in a significant amount of time, but then submits an updateNav where they cannot fully cover all withdrawals for this epoch then the full manager balance or approval amount is used up.
The fee amounts will still be tracked in the accruedBrktTvlFees, accruedManagerTvlFees, and accruedManagerPerformanceFees variables, but the manager will not be able to claim these amounts as the brktEth and brktEth approval has been removed from the manager address.
Recommendation
Consider reducing the available amount by the accruedManagerPerformanceFees (if positive), accruedManagerTvlFees, and accruedBrktTvlFees in the
_processDepositsWithdrawalsfunction. -
L-01 Low Swap Can Have Equal In and Out Tokens Validation Resolved
Description
Function
swapCollaterallacks validation thattokenIn != tokenOut. In such a case, there could be an early return not to waste gas on calculating the rebalance and transfers.Recommendation
Early return if
tokenIn == tokenOut -
L-02 Low Misvaluation Due to stETH-ETH Peg Assumption Oracles Acknowledged
Description
The _getWstethRate function calls the stEthPerToken() function in the WSTETH contract, which returns the amount of stETH per wstETH. This value is then treated as if stETH were pegged 1:1 to ETH. However, stETH can and has previously depegged from ETH (Ref), making this assumption unreliable.
Recommendation
Use a reliable price feed, such as Chainlink’s stETH-ETH feed (Link), to accurately determine the ETH value instead of assuming a one-to-one peg.
-
L-03 Low Fees Unavailable For Claim Logical Error Acknowledged
Description
Over time fees are accrued within the BracketVault:
accruedManagerPerformanceFees,accruedManagerTvlFees, andaccruedBrktTvlFees.However, there is no guarantee that there is sufficient brktETH balance for the fees to be claimed because user withdrawals may decrease the manager's balance.
For example:
- Nav is 1 ether,
- Alice deposits 10 brktETH and gets 10 shares. She is the sole depositor.
- Nav increases to 2 ether, with >0 fees accumulated.
- Alice burns 10 shares to get 20 brktETH, which comes from the manager's balances.
- Fee claim is attempted but manager does not have sufficient balance for the fee and causes a
ERC20InsufficientBalancerevert.
Recommendation
Ensure managers are aware to set aside brktETH for fees.
-
L-04 Low Lack of Slippage Control Slipagge Resolved
Description
The mint and burn functions in the BrktETH contract rely on the vault’s total collateral value to determine the number of shares or collateral token a user will receive. Because the vault uses tokens that can change in value at any time—for example, through rebases or slashings. This can lead to unexpected outcomes, such as the user ending up with fewer shares minted, or returning a different ratio of collateral when burning tokens.
Recommendation
Implement slippage control that allows users to specify acceptable thresholds.
-
L-05 Low Unused Params Optimization Resolved
Description
The following param/error is not used in
BracketVaultcontract:feeClaimerstate variableDepositIsCurrentEpocherror
Recommendation
Remove unused code or consider adding it to the current implementation.
-
L-06 Low Increased Gas Cost For Multiple Collaterals Configuration Acknowledged
Description
The
BracketOraclecurrently supports 10 collateral tokens. In case all 10 are added and deposits are non zero, thegetTotalValuefunction will need to iterate through every token to fetch the rate, adding around 270,000 gas, with Renzo's Staked ETH rate being the most gas intensive.As a result, main actions will have an increased gas consumption:
- burn: 325671
- mint: 362535
As the protocol is deployed on the Ethereum mainnet , this gas cost can become a barrier for users to invest in the
BrktEthvault.Recommendation
Consider this scenario when adding supported collaterals.
-
L-07 Low Adding Collateral Without Oracle Support Validation Resolved
Description
There are ten collaterals currently supported in
BracketOracle, butBrktETH.addCollateraldoes not check if the collateral being added is currently supported.As the
addCollateralis only called once, it will be wise to make a call togetRate(token)to ensure its supported.In the future, when new collaterals are added (i.e. apxETH, ETHx), this check will ensure
BracketOracleis updated first.Recommendation
When adding a new collateral, consider executing
getRate(token)to verify if the oracle supports it. -
L-08 Low Vault Does Not Validate Zero Amounts Validation Resolved
Description
During
depositandwithdraw, theassetsparam is not validated, so it can be 0. Although there is no major impact, the function does not revert and events are emitted, potentially causing issues in the UI.Recommendation
Consider validating for zero asset amount during
depositandwithdraw. -
L-09 Low Missing Admin Functions Configuration Resolved
Description
The
manageraddress is set during vault initialization. However, if there is an issue with manager or it's compromised, there is no admin function to update this address.Similarly,
withdrawaldelay is set at initialization but can't be updated again.Recommendation
Consider adding admin functions to update
managerandwithdrawalDelay. -
L-10 Low Unlicensed Smart Contracts Best practices Resolved
Description
The
BracketVault,BracketOracleandBrktETH, contracts are currently marked as unlicensed, as indicated by the SPDX license identifier at the top of the file:SPDX-License-Identifier: UNLICENSEDUsing unlicensed contracts can lead to legal uncertainties and conflicts regarding the usage, modification and distribution rights of the code.
Recommendation
It is recommended to choose and apply an appropriate open-source license to the smart contract. Some options are:
- MIT License: A permissive license that allows for reuse with minimal restrictions.
- GNU General Public License (GPL): A copyleft license that ensures derivative works are also open-source.
- Apache License 2.0: A permissive license that provides an express grant of patent rights from contributors to users.
-
L-11 Low balanceOf Includes Pending Amounts Unexpected behavior Acknowledged
Description
The
balanceOffunction relies on thesharesOfto determine the balance of a user. ThesharesOffunction includes thependingDepositShareswhich can include funds that the user has not yet been minted from thelastDepositand may not be able to presently mint in the case where thelastDeposit.epoch == epoch.As a result the
balanceOfandsharesOffunction reflects funds that are not available to the user and may lead to confusion for users and integrators.Recommendation
Be aware of this behavior, if it is expected then be sure to document this clearly for users and integrators.
-
L-12 Low Vanity Nav Updated In Epoch 0 Validation Resolved
Description
The
updateVanityNavfunction may be called to set thevanityNavto something other than 1e18 before the vault has been started with thestartVaultfunction.This may lead to unexpected behavior and should not be a supported interaction.
Recommendation
Consider validating that the epoch is greater than 0 in the
updateVanityNavfunction. -
L-13 Low Lacking Pause Mechanism Best practices Resolved
Description
The owner of
BrktETHis able to blacklist collaterals, preventing any further deposits. However, this does not prevent collateral withdrawals.If one of the external protocols is compromised, it will create a bank run, as users will try to withdraw the other collaterals not affected. In this case, a pausing mechanism will help to address the situation and implement the appropriate fixes.
Pausing the protocol can also allow owner to prevent user actions during an upgrade or fix.
Recommendation
Consider adding a pausing mechanism, inheriting from OZ
PausableUpgradeableand implement the admin functionspauseandunpause. -
L-14 Low Unnecessary Address(0) Check Optimization Resolved
Description
In the
initializefunction there is an address 0 check against all entries of thetokensarray, however theaddCollateralfunction already implements such a check.Recommendation
Remove the redundant address zero check in the
initializefunction for thetokensentries. -
L-15 Low Excessive Wait For Manager Withdrawals Logical Error Resolved
Description
The
managerwill usebrktETHfrom vault depositors for certain investment strategies. Therefore, thebrktETHtokens will need to be burned to extract the underlying collateral.The issue relies on the withdrawal mechanism, as users will need to wait 5 days to be able to claim the collateral. This creates a barrier for the manager and affects investments during this waiting period.
Recommendation
Consider adding an exception for managers, either receiving the collateral immediately or reduce the waiting period.
-
L-16 Low Collateral DoS Risk DoS Resolved
Description
The
addCollateralfunction does not validate a maximum number of collateral tokens that can be added to the array.As a result the owner may on accident add too many collateral tokens over time which must all be looped over in the
getTotalValuefunction. This may cause operations to require more than the block gas limit or at least be quite expensive to operate.Recommendation
Consider adding a validation against a maximum number of supported collaterals. Additionally, consider implementing functionality to be able to remove collateral tokens from the
collateralslist when they are blacklisted and have no deposits so that this does not become an issue over time. -
L-17 Low Lacking Slippage Check Allows Griefing Griefing Resolved
Description
In the
swapCollateralfunction there is no way for the caller to specify the worst rate they are willing to accept for a swap.There is no slippage at the bracket level, however the rate reported by some tokens may be manipulated by a malicious actor who wishes to grief the owner who is making the swap through bracket.
For example, the Renzo EZEth price may be manipulated by force sending Ether to the
depositQueueandwithdrawQueueaddress.Recommendation
Consider adding a
maxAmountInparameter to theswapCollateralfunction to allow the caller to protect themselves from any potential griefing attacks that may apply to any arbitrary collateral token.
Remediation Review
6 findings · January 12 to 13, 2025-
H-01 High Missing collateralsIndex Update Logical Error Acknowledged
Description
In the
removeCollateralfunction thecollateralsIndexentry for the token that is moved from the back of the list to replace the token being moved is not updated. Furthermore the index entry of thecollateralsIndexfor the token being removed is not cleared.This will perturb the accounting of the system and validation of collaterals.
Recommendation
Delete the entry for the token being removed from the
collateralsIndexmapping.Furthermore update the
collateralsIndexentry for the token at the back of the list that is being moved to overwrite the removed token.The
removeCollateralfunction should look as follows:function removeCollateral(address token) external onlyRole(DEFAULT_ADMIN_ROLE) { uint256 index = _getIndex(token); if (collaterals[index].whitelisted) revert CollateralNotBlacklisted(); if (collaterals[index].totalDeposit != 0) revert CollateralNotEmpty(); address moveToken = collaterals[collaterals.length - 1]; collaterals[index] = moveToken; collateralsIndex[moveToken] = index + 1; delete collateralsIndex[token]; collaterals.pop(); } -
M-01 Medium Invalid Index Checks Logical Error Acknowledged
Description
Throughout the
BrktEthcontract several invalid index checks are made which errantly assume that a 0 index indicates that the token is not a supported collateral.In the
whitelistCollateral,blacklistCollateral, andremoveCollateralfunctions the result of the_getIndexis compared against 0 to see if the collateral token has been configured. However this is incorrect as the returned index from the_getIndexfunction is already reduced by 1.Recommendation
Remove the errant index 0 check in the
whitelistCollateral,blacklistCollateral, andremoveCollateralfunctions. -
M-02 Medium lastNavUpdate Updated Before Conditional Check Logical Error Acknowledged
Description
The lastNavUpdate variable is updated to block.timestamp before the conditional check that uses this variable. Consequently, the condition lastNavUpdate + NAV_NO_CONFIRMATION_PERIOD > block.timestamp will always evaluate to true. As a result, only accounts with the NAV_CONFIRMER_ROLE will be able to call the updateNav function.
Recommendation
Update lastNavUpdate only after the condition is validated.
-
L-01 Low Missing Libraries Initialization Best Practices Acknowledged
Description
Several upgradeable contracts inherit OpenZeppelin contracts/libraries that require calling their respective
__[LibraryName]_initfunctions inside the contract’s owninitializefunction.Although some contracts do make certain initializer calls (e.g.
__Ownable_init(msg.sender),__ERC20Permit_init("Bracket Liquid Staked ETH"),__ERC20_init(...), etc.), others do not consistently call all the initializers of their inherited contracts. For example:BracketPricinginheritsUUPSUpgradeablebut never calls__UUPSUpgradeable_init().BrktETHinheritsUUPSUpgradeable,PausableUpgradeable, etc. and only partially initializes some of them (e.g.,__AccessControlDefaultAdminRules_initis called, but there is no corresponding call to__UUPSUpgradeable_init()or__Pausable_init()).
Recommendation
Review each upgradeable contract’s parent classes to identify all required initializers (e.g.
__UUPSUpgradeable_init,__Ownable_init,__AccessControl_init,__Pausable_init, etc.) and ensure they are properly invoked in the contract’s initialize function. -
L-02 Low Unimplemented _getPythPrice Function Warning Acknowledged
Description
In the
BracketPricingcontract, the function_getPythPriceis declared but not implemented, making anyOracle.Pythconfiguration inoperable.Recommendation
Implement
_getPythPriceto properly fetch and decode the Pyth price. -
L-03 Low _getChainlinkPrice Function Is Only Compatible With 8 decimals Price Feeds Warning Acknowledged
Description
The
_getChainlinkPricefunction inBracketPricingcontract multiplies the price by1e10to convert it to 18 decimals(uint256(price) * 1e10). This works only if the Chainlink aggregator returns an 8-decimal price. If an aggregator uses any other number of decimals, the result becomes incorrect.Recommendation
Retrieve the feed’s actual number of decimals via priceFeed.decimals() and scale the value accordingly. A more robust example:
function _getChainlinkPrice(address oracle) internal view returns (uint256) { AggregatorV2V3Interface priceFeed = AggregatorV2V3Interface(oracle); uint8 decimals = priceFeed.decimals(); ( , int256 price, , , ) = priceFeed.latestRoundData(); if (price <= 0) revert ZeroPrice(); // Convert the price to 18 decimals uint256 scaledPrice = uint256(price); if (decimals < 18) { scaledPrice *= 10 ** (18 - decimals); } else if (decimals > 18) { scaledPrice /= 10 ** (decimals - 18); } return scaledPrice; }
No findings match.
More from Bracket
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
