← Back to Reports
Bearn yBGT
Electisec yBGT Review
Review Resources:
- yBGT repo: https://github.com/Bearn-Sucks/yBGT
- bera chain docs: https://docs.berachain.com/
Auditors:
- Watermelon
- Panda
Table of Contents
Review Summary
yBGT
yBGT provides a liquid wrapper for BGT.
The yBGT Repo and two contracts from the yearn tokenized-strategy-periphery codebase were reviewed over six days. Two auditors performed the code review between the 24th and 31st of March 2025. The repository was under active development during the review, but the review was limited to the latest commit at the start. This was commit 5f6584bfd6f0d17a6bc143c0fd2f569c1ce23b26 for the yBGT repo and 7d8559d6e6efd5e9d7d43b7354f9926e2ff9f63b for the yearn tokenized-strategy-periphery codebase.
Scope
The scope of the review consisted of the following contracts at the specific commit:
src/
├── BearnAuctionFactory.sol
├── BearnBGT.sol
├── BearnBGTEarnerVault.sol
├── BearnCompoundingVault.sol
├── BearnVaultFactory.sol
├── BearnVoter.sol
├── BearnVoterManager.sol
├── StakedBearnBGT.sol
From the yearn tokenized-strategy-periphery codebase, the following contracts were reviewed:
src/
├── Bases/
│ ├── Staker/
│ │ └── TokenizedStaker.sol
│ └── Hooks/
│ └── BaseHooks.sol
After the findings were presented to the yBGT team, fixes were made and included in several PRs.
This review is a code review to identify potential vulnerabilities in the code. The reviewers did not investigate security practices or operational security and assumed that privileged accounts could be trusted. The reviewers did not evaluate the security of the code relative to a standard or specification. The review may not have identified all potential attack vectors or areas of vulnerability.
Electisec and the auditors make no warranties regarding the security of the code and do not warrant that the code is free from defects. Electisec and the auditors do not represent nor imply to third parties that the code has been audited nor that the code is free from defects. By deploying or using the code, yBGT and users of the contracts agree to use the code at their own risk.
Code Evaluation Matrix
Category | Mark | Description |
---|---|---|
Access Control | Good | The codebase implements proper authorization patterns and role-based access control. Privileged functions are protected with appropriate modifiers. |
Mathematics | Good | The mathematical operations in the contracts appear sound, with proper consideration for precision and edge cases. |
Complexity | Average | While most functions are reasonably straightforward, some components, like the reward distribution and voting management, introduce moderate complexity that requires careful implementation. |
Libraries | Good | The project appropriately uses established libraries and components, particularly from the Yearn ecosystem. |
Decentralization | Average | The protocol has some centralized components managed by privileged roles but also incorporates decentralized governance mechanisms through BGT voting. |
Code stability | Good | The codebase appears stable with well-defined interfaces and consistent implementation patterns. |
Documentation | Good | Functions and contracts have appropriate comments explaining their purpose and behavior. The overall architecture is well documented. |
Monitoring | Good | The contracts implement sufficient events for tracking important state changes and user interactions. |
Testing and verification | Low | Test coverage exists but could benefit from more comprehensive testing of edge cases and integration scenarios. |
Findings Explanation
Findings are broken down into sections by their respective impact:
- Critical, High, Medium, Low impact
- These are findings that range from attacks that may cause loss of funds, impact control/ownership of the contracts, or cause any unintended consequences/actions that are outside the scope of the requirements.
- Gas savings
- Findings that can improve the gas efficiency of the contracts.
- Informational
- Findings including recommendations and best practices.
Critical Findings
None
High Findings
1. High - BearnVoterManager
sets incorrect address as auction's receiver
BearnVoterManager's constructor
creates an instance of Yearn's Auction
contract, which is used to create dutch auctions that sell an arbitrary asset, selected by an authorized party, for HONEY
.
Technical Details
When creating the Auction
instance, the BearnVoterManager
provides its own address as the receiver of the auction's proceeds. Given that the contract is only used to manage the BearnVoter
contract, whose HONEY
balance is manipulated by the BearnVoterManager
contract, the auction's receiver address is incorrect.
Because the BearnVoterManager
contract cannot manipulate its own HONEY
balance, any auction proceeds will be locked within the contract.
Impact
High. All auction proceeds are directed to the incorrect receiver, locking them perpetually.
Recommendation
Set address(_bearnVoter)
as the receiver of the auction's proceeds instead of address(this)
.
@@ -60,7 +60,7 @@ contract BearnVoterManager is Authorized {
AuctionFactory(0xCfA510188884F199fcC6e750764FAAbE6e56ec40)
.createNewAuction(
address(honey),
- address(this),
+ address(_bearnVoter),
styBGT.management(),
1 days,
5_000
Developer Response
Fixed https://github.com/Bearn-Sucks/yBGT/commit/d693c8e4ec2a2bcfa045b171896252a75d7b9671
Medium Findings
1. Medium - BearnVoterManager.getReward
reads incorrect account's HONEY
balance
BearnVoterManager.getReward
is called by StakedBearnBGT._claimAndNotify
to claim HONEY
rewards accrued by the BearnVoter
's staked BGT
position.
Technical Details
BearnVoterManager.getReward
first makes the BearnVoter
call BGTStaker.getReward()
, whose current implementation claims the caller's rewards and pushes them to the same account. Because the HONEY
rewards are pushed to the BearnVoter
contract, the read at BearnVoterManager.sol#L261
is incorrectly executed. Subsequently, the BearnVoter
contract is made to transfer the HONEY.balanceOf(bearnVoterManager)
amount of HONEY
.
Three distinct scenarios can arise at this point:
HONEY.balanceOf(bearnVoterManager) == HONEY.balanceOf(bearnVoter)
: In this case, no malfunction is caused by the incorrect balance read.HONEY.balanceOf(bearnVoterManager) < HONEY.balanceOf(bearnVoter)
: In this case, theBearnVoter
contract is left holding a non-zero amount ofHONEY
, which in turn implies a lower-than-expected amount of funds being reported as profit toStakedBearnBGT
, lowering its reward rate.HONEY.balanceOf(bearnVoterManager) > HONEY.balanceOf(bearnVoter)
: In this case, theHONEY
transfer will revert as theBearnVoter
contract holds insufficient funds to execute such transfer.
Case 3 is the worst case: it implies a partial DoS on StakedBearnBGT
's reward claim flow, which will be resolved once the BearnVoter
instance has accrued sufficient rewards for its balance to surpass that of BearnVoterManager
.
Impact
Medium.
Recommendation
Execute the balance read correctly by reading the BearnVault
's balance.
@@ -258,7 +258,7 @@ contract BearnVoterManager is Authorized {
);
// Transfer Full balance of honey to styBGT to account for auctions.
- uint256 amount = honey.balanceOf(address(this));
+ uint256 amount = honey.balanceOf(address(bearnVoter));
// Send rewards to styBGT
if (amount > 0) {
Developer Response
Fixed https://github.com/Bearn-Sucks/yBGT/commit/d693c8e4ec2a2bcfa045b171896252a75d7b9671
2. Medium - Users of BearnBGT can't signal intention of withdraw to bera
Currently, BearnBGT (yBGT) users face a significant limitation: they are locked into the protocol without a reliable way to signal their intention to withdraw or exit when their BGT tokens are being used for voting. The existing redeem()
function only allows the withdrawal of unboosted (non-voting) BGT tokens. This means users can be effectively trapped in the protocol if most or all of their yBGT is used for governance voting.
This creates several problems:
- Users cannot reliably exit their positions
- The protocol lacks transparency on withdrawal demand
- There's no mechanism to prioritize and fulfill withdrawal requests as tokens become available
- Voter operators have no visibility into user withdrawal intentions
Technical Details
The current implementation in BearnBGT.sol
has a redeem()
function that checks the available unboosted balance and only allows withdrawal up to that amount:
function maxRedeem() public view returns (uint256 maxAmount) {
return IBGT(bearnVoter.bgt()).unboostedBalanceOf(address(bearnVoter));
}
function redeem(uint256 amount) external returns (uint256 outputAmount) {
uint256 fee;
(outputAmount, fee) = feeModule.redeem(msg.sender, amount);
// burn and redeem to msg.sender
if (outputAmount > 0) {
_burn(msg.sender, outputAmount);
bearnVoter.redeem(msg.sender, outputAmount);
}
// transfer fees to fee recipient
if (fee > 0) {
_transfer(msg.sender, treasury, fee);
}
return outputAmount;
}
This means that if all tokens are being used for voting (boosted), users cannot withdraw them.
Impact
Medium. Decreases trust in the protocol due to uncertainty about liquidity
Recommendation
Implement a Withdrawal Queue similar to Lido's approach with the following components:
- Withdrawal Request Queue
- Allow users to enter a queue for withdrawals that exceed current unboosted balance
- Users specify the amount to withdraw and receive a withdrawal NFT/receipt as proof
- This signals intent without immediate execution
- Withdrawal Fulfillment Logic
- Newly emitted BGT should first fulfill pending withdrawal requests before being used for voting
- The vote operator should be able to see pending withdrawal requests and their total amount
- Implement logic to gradually unboost to fulfill withdrawal requests over time
Developer Response
Acknowledged
As of now, users are expected to exit yBGT through the AMM pools. We anticipate yBGT trading at a premium for a while, meaning redemptions should not be used.
The logic for the withdrawal queue can be added to the current system, and we plan to implement this functionality in the future.
Even if redemptions become necessary now, reports are also permissionless. This is when BGT is claimed and becomes unboosted, so redemptions could be paired with permissionless reports to free up unboosted BGT.
Low Findings
1. Low - Unbounded rewardTokens
array in addReward
can lead to DOS
Technical Details
The addReward function in the TokenizedStaker contract allows the management role to add new reward tokens to the contract.
There's no limit to how many reward tokens can be added, and the _updateReward
function iterates through all tokens in the rewardTokens array:
File: TokenizedStaker.sol
362: for (uint256 i; i < rewardTokens.length; ++i) {
363: address _rewardToken = rewardTokens[i];
Impact
Low.
Recommendation
Add an upper limit to the number of tokens in rewardTokens
.
Developer Response
Acknowledged
2. Low - Inconsistent default behaviour in BaseHooks
withdraw methods
BaseHooks.withdraw
and BaseHooks.redeem
provide overloaded methods which define different maxLoss
hardcoded values.
Technical Details
While BaseHooks.withdraw
inserts a maxLoss = 0
to disallow any potential loss to the user within the withdrawal process, BaseHooks.redeem
inserts maxLoss = MAX_BPS
, effectively disabling the user loss check completely.
Impact
Low. Enabling a strict loss check on BaseHooks.withdraw
by default might mislead contract users into believing the same is true for BaseHooks.redeem
, exposing them to a potential 100% loss of funds.
Recommendation
Maintain consistent behavior across both methods: ideally, both methods should employ maxLoss = 0
.
Developer Response
Acknowledged
This is done to match the same default functionality as the TokenizedStrategy itself has. Which is dont to match the 4626 standard. Since 'withdraw' must return the exact amount of asset requested (0 max loss), but redeem must just burn the amount of shares.
We allow loss on the redeem calls to make downstream integrations easier specifically when staking 4626 calls in order to not revert from simple rounding losses of a few wei
Gas Saving Findings
1. Gas - recoverERC20
can disrupt reward token distribution
Technical Details
The recoverERC20
function allows management to recover tokens from the contract, with a safeguard to prevent recovering reward tokens until 90 days after the reward period ends:
function recoverERC20(
address _tokenAddress,
uint256 _tokenAmount
) external virtual onlyManagement {
// Logic to check if it's a reward token and enforce 90-day wait
if (isRewardToken) {
require(
block.timestamp > maxPeriodFinish + 90 days,
"wait >90 days"
);
// Takes all of the token balance
_tokenAmount = ERC20(_tokenAddress).balanceOf(address(this));
}
// Transfer tokens to management
}
The issue is that even after recovering a reward token, it remains in the rewardTokens
array and continues to be processed during reward updates and claims. This creates several inefficiencies:
- Failed token transfers may occur in
_getRewardFor
if all of a reward token was recovered - Gas is wasted on processing tokens that can no longer be distributed
- The contract maintains reward accounting for tokens that have been removed
Impact
Gas savings.
Recommendation
- Add a
deactivated
flag to theReward
struct - Set this flag when recovering all of a reward token
- Modify
_updateReward
and_getRewardFor
to skip deactivated tokens
Example implementation:
// Add to Reward struct
bool deactivated;
// In recoverERC20, when taking all tokens
if (_tokenAmount == ERC20(_tokenAddress).balanceOf(address(this))) {
rewardData[_tokenAddress].deactivated = true;
}
// In _updateReward and _getRewardFor
if (rewardData[rewardToken].deactivated) continue;
Developer Response
Acknowledged
2. Gas - BearnVoteManager.activateBoost
and BearnVoteManager.dropBoost
route external calls to BearnVoter
needlessly
BearnVoteManager.activateBoost
and BearnVoteManager.dropBoost
are methods which implement no access control and simply execute an external call to BearnVoter.execute
, making the contract execute an external call to Berachain's BGT
token.
Technical Details
Because BGT.activateBoost
and BGT.dropBoost
do not require for the holder of a boost to be the calling account, any account is allowed to activate and drop a boost on behalf of another account if enough time has passed since the boost activation or deactivation has been requested.
It is thus unnecessary for BearnVoterManager
to route such calls via BearnVoter
, when it is allowed to call into BGT
directly.
Impact
Gas savings.
Recommendation
Modify BearnVoteManager.activateBoost
and BearnVoteManager.dropBoost
to interact directly with BGT
, invoking the corresponding methods, taking care to specify the BearnVoter
contract as the user
parameter for such calls.
Developer Response
Fixed https://github.com/Bearn-Sucks/yBGT/commit/d693c8e4ec2a2bcfa045b171896252a75d7b9671
Informational Findings
1. Informational - TokenizedStaker
misses a method for an authorized party to claim a single reward on a user's behalf
TokenizedStaker
implements logic for a user to approve another account to claim and receive its rewards via the setClaimForSelf
method.
Technical Details
While the authorized account can claim all of the user's rewards by calling getRewardFor
, it doesn't dispose of a method to claim a single reward, similarly to how the rewards owner can do by invoking getOneReward
.
Impact
Informational.
Recommendation
Implement a getOneRewardFor
method to offer authorized accounts the same functionality, allowing them to claim only one reward token on a user's behalf.
Developer Response
Acknowledged
2. Informational - Missing event logs
Technical Details
- When modifying the
TokenizedStaker.claimForRecipient
mapping viaTokenizedStaker.setClaimFor
orTokenizedStaker.setClaimForSelf
, no event log is emitted to signal the storage modification to off-chain components. - When adding a reward via
TokenizedReward.addReward
, no event is emitted.
Impact
Informational.
Recommendation
- Define a
ClaimSetFor(address staker, address recipient)
event and log it at the end of_setClaimFor
. - Define a
RewardTokenAdded(address _rewardToken, address _rewardsDistributor, uint256 _rewardsDuration)
event and log it at the end of_addReward
.
Developer Response
Acknowledged
Final remarks
The yBGT codebase demonstrates solid engineering practices with good access control and documentation. However, fixes are required for the identified issues related to reward handling and withdrawal mechanisms to ensure proper functionality and user experience.