BetterBank Custom Contracts - BetterBank


Prepared by:

Halborn Logo

HALBORN

Last Updated 10/14/2025

Date of Engagement: September 11th, 2025 - September 16th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

16

Critical

0

High

0

Medium

4

Low

6

Informational

6


1. Introduction

Better Bank engaged Halborn to conduct a security assessment on their smart contracts beginning on September 12th, 2025 and ending on September 19th, 2025. The scope of this assessment was limited to the smart contracts provided to the Halborn team. Commit hashes and additional details are documented in the Scope section of this report.


Better Bank protocol creates and manages an onchain dual-token economy centered on Favor token and Esteem token. Through contracts like PulseMinter, users can mint Esteem with supported assets or redeem it for Favor, while the Treasury governs supply expansion using price oracles and epoch-based seigniorage. Staking (Grove) distributes Favor rewards to Esteem stakers via a snapshot-based mechanism, and a Zapper simplifies user flows by batching multi-step mint, stake, and claim actions. The architecture combines minting, treasury control, staking rewards, and UX tooling into a cohesive incentive loop for sustainable token distribution and protocol growth.

2. Assessment Summary

Halborn assigned a full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.


The purpose of the assessment is to:

    • Identify potential security issues within the smart contracts.

    • Ensure that smart contract functionality operates as intended.

 

In summary, Halborn identified several areas for improvement to reduce both the likelihood and impact of potential risks, which were mostly addressed by the Better Bank team. The main recommendations were:

    • Ensure proper epoch checks in seigniorage allocation to prevent minting against stale epochs.

    • Improve oracle price cap handling to prevent frozen or stale low-price exploitation.

    • Enforce a minimum stake lock period until the end of the next epoch to prevent last-minute staking solely to capture current rewards.

    • Scale and normalize the LP token price.


3. Test Approach and Methodology

Halborn performed a combination of manual code review and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is essential to uncover flaws in logic, process, and implementation, automated testing techniques enhance coverage of smart contracts and can quickly identify issues that do not follow security best practices.


The following phases and associated tools were used throughout the assessment:

    • Research into the architecture, purpose, and use of the platform.

    • Manual code review and walkthrough of the smart contracts to identify potential logic issues.

    • Manual testing of all core functions, including deposit, withdraw, repay, and borrow, to validate expected behavior and identify edge-case vulnerabilities.

    • Local testing to simulate contract interactions and validate functional and security assumptions.

    • Local deployment and testing with Foundry.


4. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

4.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

4.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (C:N)
Low (C:L)
Medium (C:M)
High (C:H)
Critical (C:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

4.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

5. SCOPE

REPOSITORY
(a) Repository: BB-Custom-contracts
(b) Assessed Commit ID: 43fba5e
(c) Items in scope:
  • contracts/usingFetch/usingFetch.sol
  • contracts/Epoch.sol
  • contracts/Esteem.sol
↓ Expand ↓
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

4

Low

6

Informational

6

Security analysisRisk levelRemediation Date
Epoch desynchronization causes seigniorage to be minted with stale dataMediumSolved - 09/19/2025
Price cap mechanism can freeze oracle values and enable arbitrageMediumSolved - 09/22/2025
Reward allocation timing enables strategic staking advantageMediumSolved - 09/20/2025
LP token price returned as a constant value instead of scaling with inputMediumSolved - 09/27/2025
Missed epoch updates can leave Esteem rate stale and undervaluedLowSolved - 09/27/2025
Absence of slippage checks and strict deadlines exposes operations to front-runningLowSolved - 09/22/2025
Ownership transfers rely on single-step pattern without acceptanceLowSolved - 09/22/2025
Tax bypass via intermediate tax-exempt addressLowSolved - 09/27/2025
Pause mechanism does not cover reward claimingLowSolved - 09/27/2025
Epoch counters are initialized inconsistently across contractsLowSolved - 09/27/2025
Array-based removal of excluded addresses may fail with large input sizeInformationalSolved - 09/27/2025
Duplicate Favor registrations can overwrite existing mappingsInformationalSolved - 09/21/2025
Constructor-assigned variables not marked as immutableInformationalSolved - 09/27/2025
Missing zero-address validation in function parametersInformationalSolved - 09/27/2025
ETH refund process lacks reentrancy protectionInformationalSolved - 09/21/2025
Potential LP yield extraction through share price reset if flash loans are enabledInformationalAcknowledged - 09/22/2025

7. Findings & Tech Details

7.1 Epoch desynchronization causes seigniorage to be minted with stale data

//

Medium

Description

In the FavorTreasury contract, the allocateSeigniorage() function suffers from an epoch desynchronization issue caused by the checkEpoch modifier, which increments the epoch after the function body executes. This means that when allocateSeigniorage() is called at the start of a new epoch, it still runs using the previous epoch’s state:

modifier checkEpoch {
    require(block.timestamp >= nextEpochPoint(), "Treasury: not opened yet");
    _;
    epoch = epoch + 1;
}

As a result, supply expansion and reward distribution are calculated for the wrong epoch, and seigniorage for the current epoch is minted based on outdated data such as lastEpochCirculatingSupply.


In this protocol, seigniorage is the process by which new Favor tokens are minted and distributed at the beginning of each epoch, determined by circulating supply and oracle price. If this calculation is performed with stale epoch data, it directly affects the number of tokens minted and leads to incorrect reward allocation to users, as well as a desynchronization between real time and the protocol’s epoch accounting.


This creates a situation where seigniorage for the new epoch is calculated and minted while the contract still considers the previous epoch active.


Example:

  • PERIOD = 1 hour

  • startTime = 10:00, epoch = 5nextEpochPoint = 15:00.

  • At 15:00:05, Alice calls allocateSeigniorage().

  • block.timestamp >= nextEpochPoint passes.

  • Function runs with epoch = 5 (stale).

  • Seigniorage is minted as if still in epoch 5.

  • After function finishes then epoch = 6.


During execution, lastEpochCirculatingSupply() and other epoch-dependent variables are read using the old epoch (5). As a result, supply expansion and rewards are calculated for the previous epoch instead of the current one. This causes incorrect reward distribution and users receive rewards as if still in the previous epoch.

BVSS
Recommendation

It is recommended to increment the epoch before executing the function body.


Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.2 Price cap mechanism can freeze oracle values and enable arbitrage

//

Medium

Description

The update function in the LPOracle contract enforces a maximum cap on price growth by comparing the new LP price with the last recorded price multiplied by the cap:

uint256 sqrt0 = Math.sqrt(uint256(usd0Average._x));
uint256 sqrt1 = Math.sqrt(uint256(usd1Average._x));
uint256 kx = uint256(kAverage._x);
uint256 part = kx * 2 * sqrt0 / 2 ** 56;
uint256 lpQ112 = part * sqrt1 / 2 ** 56;
uint256 newLpPrice = lpQ112 * 1e18 >> 112;

if (lastLpPrice == 0 || lpPriceCap == 0) {
    lastLpPrice = newLpPrice;
} else {
    uint256 maxAllowed = lastLpPrice * lpPriceCap / 1e18;
    if (newLpPrice <= maxAllowed) {
        lastLpPrice = newLpPrice;
    }
}

If the new LP price exceeds this threshold, the oracle ignores the update and keeps the old value. This can cause the oracle to remain stuck at a stale low price while the actual market LP price is much higher, enabling arbitrage against contracts that rely on the oracle.


Example:

  • Current oracle price = $100.

  • Cap allows at most a 2x increasemaxAllowed = $200.

  • Real LP price moves to $210 (legitimately or via manipulation).

  • Since $210 > $200, the contract rejects the update → oracle remains stuck at $100.


BVSS
Recommendation

It is recommended to clamp the new LP price to the maximum allowed value instead of ignoring the update, ensuring the oracle always progresses and does not freeze at stale prices.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.3 Reward allocation timing enables strategic staking advantage

//

Medium

Description

In the Staking contract, the allocateSeigniorage function computes rewardPerShare using the live totalSupply at the time of execution:

function allocateSeigniorage(uint256 amount) external nonReentrant whenNotPaused {
    require(msg.sender == owner() || msg.sender == treasuryOperator, "Not authorized");
    require(amount > 0, "Grove: Cannot allocate 0");
    require(totalSupply() > 0, "Grove: Cannot allocate when totalSupply is 0");

    // Calculate new reward per share
    uint256 prevRPS = getLatestSnapshot().rewardPerShare;
    uint256 nextRPS = prevRPS + ((amount * 1e18) / totalSupply());

    // Store new snapshot
    historyEnd += 1;
    groveHistory[historyEnd] = GroveSnapshot({
        time: block.timestamp,
        rewardReceived: amount,
        rewardPerShare: nextRPS
    });

    // Enforce maximum snapshot history
    if (historyEnd - historyStart + 1 > MAX_HISTORY) {
        delete groveHistory[historyStart];
        historyStart += 1;
    }

    // Transfer rewards from sender to contract
    favor.safeTransferFrom(msg.sender, address(this), amount);

    emit RewardAdded(msg.sender, amount);
}

Because the total supply is read at allocation time, any newly staked Esteem is immediately included in the reward calculation. This allows users who stake right before allocateSeigniorage is called to capture a significant portion of the distribution, diluting existing stakers. Such strategic staking does not break the contract’s rules, but it creates an imbalance in how rewards are allocated and may incentivize short-term movements rather than stable participation.

BVSS
Recommendation

It is recommended to enforce a minimum stake lock period lasting at least until the end of the next epoch, preventing last-minute staking solely to capture current rewards.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.4 LP token price returned as a constant value instead of scaling with input

//

Medium

Description

In the Oracle contract, the consult function is intended to return the output value for a given _amountIn. For token0 and token1, the result is correctly scaled by the input amount:

function consult(address _token, uint256 _amountIn)
    external view
    returns (uint256 amountOut)
{
        if (_token == token0) {
            amountOut = uint256(price0Average.mul(_amountIn).decode144());
        } else if (_token == token1) {
            amountOut = uint256(price1Average.mul(_amountIn).decode144());
        } else if (_token == address(pair)) {
            amountOut = lastLpPrice;
        } else {
            revert("Oracle: INVALID_TOKEN");
        }

}

However, in the LP token branch, the function ignores _amountIn and always returns lastLpPrice. This causes the output to remain constant regardless of the input, breaking consistency with the intended pricing logic.

BVSS
Recommendation

It is recommended to scale the LP token price by multiplying lastLpPrice with _amountIn and normalizing by 1e18.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.5 Missed epoch updates can leave Esteem rate stale and undervalued

//

Low

Description

In the MintRedeemer.updateEsteemRate() function, the Esteem price is increased by a fixed dailyRateIncrease only when explicitly called by an approved user. If no one calls this function for multiple epochs (e.g., several days), the rate remains stale and underpriced. When users mint or redeem during this period, they transact at an outdated esteemRate, causing systemic under-valuation.


In the MintRedeemer contract, the updateEsteemRate function increases the Esteem rate by a fixed dailyRateIncrease only when explicitly called by an approved user:

function updateEsteemRate() external {
    require(isApprovedUser[msg.sender], "Not approved user");
    require(block.timestamp >= startTime, "not started");
    require(block.timestamp >= nextEpochTime(), "Updater epoch not passed");
    epoch += 1;
    esteemRate += dailyRateIncrease;
    emit RateUpdated(esteemRate);
}

If no one triggers this function for several epochs, the esteemRate remains stale and underpriced. When users mint or redeem during this time, they interact with an outdated rate, leading to systemic undervaluation.


Example:

  • Initial esteemRate = 21.00

  • dailyRateIncrease = 0.25

  • After 3 days, the correct rate should be 21.75

  • If no update call is made, the rate remains 21.00 and all mints/redeems are undervalued


BVSS
Recommendation

It is recommended to allow anyone to call the update function and ensure the rate is refreshed automatically during mint and redeem.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id. MintRedeemer no longer uses epochs, now uses block.timestamp price updates.

Remediation Hash

7.6 Absence of slippage checks and strict deadlines exposes operations to front-running

//

Low

Description

The Zapper contract calls Uniswap's swapExactTokensForTokensSupportingFeeOnTransferTokens and addLiquidity functions with parameters amountOutMin = 0, amountAMin = 0 and amountBMin = 0. It also applies very short deadlines such as block.timestamp + 50. This configuration leaves swaps and liquidity additions fully exposed to front-running and sandwich attacks, allowing execution at highly unfavorable prices.

BVSS
Recommendation

It is recommended to enforce non-zero minimum amounts and user-provided deadlines to ensure swaps and liquidity additions fail if prices move beyond an acceptable tolerance.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.7 Ownership transfers rely on single-step pattern without acceptance

//

Low

Description

The contracts initialize ownership through the single-step Ownable pattern, for example by calling Ownable(_owner) in the constructor. In this model, ownership is either set directly in the constructor or transferred in one transaction with transferOwnership(newOwner), which the new owner implicitly accepts. Because there is no explicit acceptance, a mistaken address or malicious input can cause irreversible loss of control, as there is no handshake mechanism to confirm ownership transfer.

BVSS
Recommendation

It is recommended to replace the single-step Ownable implementation with a two-step process such as OpenZeppelin's Ownable2Step, requiring the new owner to explicitly accept ownership before the transfer is finalized.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.8 Tax bypass via intermediate tax-exempt address

//

Low

Description

In the Favor contract's _update function, the logic checks whether either the from or to address is tax-exempt. If either is exempt, no tax is applied. This creates a loophole where funds can bypass taxation through an intermediate tax-exempt address:

function _update(address _from, address _to, uint256 _value) internal override {
        bool destinationIsContract = _to.code.length != 0;


        if (_isTaxExempt(_to) || _isTaxExempt(_from)) {
            super._update(_from, _to, _value);
            return;
 }

Example:

  • Address A (non-exempt) wants to transfer tokens to Contract C (non-exempt).

  • Instead of sending directly, A sends tokens to Contract B (tax-exempt).

  • B then forwards the tokens to C.

  • The effective flow of funds is from A to C, but since B is tax-exempt, the transfer from B to C avoids taxation.


This undermines the intended tax logic and could be exploited to systematically bypass the tax mechanism.

BVSS
Recommendation

It is recommended to modify the _update logic to ensure that tax is enforced if both sender and receiver are non-exempt, regardless of intermediate transfers.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id. The Favor token is now fully gated. EOA transfers are allowed, and transfers only to contracts we set as tax exempt i.e., the LPZapper and Staking.

Remediation Hash

7.9 Pause mechanism does not cover reward claiming

//

Low

Description

In the Staking contract, most core functions such as deposit and withdraw are protected by the whenNotPaused modifier to ensure the protocol can be halted during emergencies or maintenance.


However, the claimReward function does not include this modifier, allowing users to continue claiming rewards even while the contract is paused:

function claimReward() public updateReward(msg.sender) {
        uint256 reward = grovers[msg.sender].rewardEarned;
        if (reward > 0) {
            grovers[msg.sender].rewardEarned = 0;
            favor.safeTransfer(msg.sender, reward);
            emit RewardPaid(msg.sender, reward);
        }
 }

This issue breaks the intended pause mechanism and reduces the effectiveness of emergency controls.

BVSS
Recommendation

It is recommended to add the whenNotPaused modifier to the claimReward function to align it with other core functions.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id. whenNotPaused was added to claimReward.

Remediation Hash

7.10 Epoch counters are initialized inconsistently across contracts

//

Low

Description

The epoch counter is initialized differently across contracts. In FavorTreasury, the starting epoch is set to 0, while in MintRedeemer it is set to 1. This inconsistency can create confusion for integrators and increase the risk of logic errors when comparing or synchronizing epoch-based calculations between contracts.


BVSS
Recommendation

It is recommended to standardize the initialization of epoch counters across all contracts that rely on epoch-based logic to ensure consistent behavior and prevent discrepancies.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id. MintRedeemer no longer uses epochs, now uses block.timestamp price updates.

Remediation Hash

7.11 Array-based removal of excluded addresses may fail with large input size

//

Informational

Description

In the FavorTreasury contract, the removeExcludedAddress function searches the excludedAddresses array with a for loop to locate and remove an entry:

function removeExcludedAddress(address _address) external onlyOwner {
        require(excludedFromTotalSupply[_address], "Address not excluded");
        
        excludedFromTotalSupply[_address] = false;
        
        // Remove from array
        uint256 n = excludedAddresses.length;
        for (uint256 i = 0; i < n; i++) {
            if (excludedAddresses[i] == _address) {
                excludedAddresses[i] = excludedAddresses[n - 1];
                excludedAddresses.pop();
                break;
            }
        }
        
        emit ExcludedAddressRemoved(_address);
}

As the array grows, the gas cost of iterating increases linearly, and for sufficiently large arrays the function may run out of gas and never succeed. This makes it possible for excluded addresses to remain permanently unremovable once the list becomes too large.


BVSS
Recommendation

It is recommended to enforce a hard limit on the number of excluded addresses added so that removal operations remain feasible within gas limits.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id. It was added MAX_EXCLUDED constant and check.

Remediation Hash

7.12 Duplicate Favor registrations can overwrite existing mappings

//

Informational

Description

In the Zapper contract, the addFavor function allows the owner to register a Favor token with its LP pair and base token. However, the function does not validate whether a Favor token, base token, or LP has already been registered:

function addFavor(
    // favor token
    address _favor,
    // lp
    address _lp,
    // base token
    address _token
    ) external onlyOwner {
        require(_favor != address(0), "Invalid address");
        require(_lp != address(0), "Invalid address");
        require(_token != address(0), "Invalid address");
        favorToToken[_favor] = _token;
        favorToLp[_favor] = _lp;
        tokenToFavor[_token] = _favor;
        emit ActiveFavorTokenSet(_favor, _lp, _token);
 }

As a result, calling addFavor multiple times with different values can overwrite existing mappings in favorToToken, favorToLp, and tokenToFavor, leading to inconsistent state and potential misconfiguration.

BVSS
Recommendation

It is recommended to add explicit checks that prevent registering a Favor, LP, or base token more than once, reverting if the mapping already exists.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.13 Constructor-assigned variables not marked as immutable

//

Informational

Description

Several state variables across the contracts are assigned only once in the constructor and are never modified afterward. Declaring them as mutable storage variables unnecessarily increases gas costs, since they are stored in contract storage rather than being optimized as constants in bytecode.

BVSS
Recommendation

It is recommended to declare variables that are assigned only once in the constructor as immutable to reduce gas costs:

IUniswapV2Router02 public immutable router;
BBToken public immutable esteem;
uint256 public immutable startTime;
IUniswapV2Pair public immutable pair;
address public immutable token0;
address public immutable token1;
IUniswapV2Pair public immutable pair;
address public immutable token0;
address public immutable token1;

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id. Variables were changed to immutable where possible.

Remediation Hash

7.14 Missing zero-address validation in function parameters

//

Informational

Description

Several functions across the contracts accept address parameters without checking against address(0). This can result in unintended behaviors such as minting to the zero address, assigning roles to invalid accounts, or transferring tokens to a non-existent destination. Functions affected include:

  • Esteem: addMinter, removeMinter

  • Favor: addMinter, removeMinter, mint

  • Staking: setTreasuryOperator

  • MintRedeemer: mintEsteemWithPLS, redeemFavor

  • Zapper: requestFlashLoan, zapToken, sellTo, buyTo


BVSS
Recommendation

It is recommended to enforce explicit zero-address checks across all user inputs to prevent unintended behaviors.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id. It was added require(account != address(0), "Cannot be address(0)"); to functions.

Remediation Hash

7.15 ETH refund process lacks reentrancy protection

//

Informational

Description

In the Zapper contract, the zapToken and _zapToken functions eventually call _refundDust, which sends ETH back to the caller using a low-level .call. Without reentrancy protection, this opens the possibility of reentrant calls during the refund step. Other core functions in the contract already use nonReentrant, but it is missing here.


BVSS
Recommendation

It is recommended to add the nonReentrant modifier to zapToken and _zapToken to prevent reentrant calls during ETH refunds.

Remediation Comment

SOLVED: The Better Bank team solved the issue in the specified commit id.

Remediation Hash

7.16 Potential LP yield extraction through share price reset if flash loans are enabled

//

Informational

Description

In the Zapper contract, the executeOperation function deposits freshly minted Uniswap LP tokens directly into Aave, making Aave the effective holder of the LP supply:

function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external returns (bool) {
    ........
    ........

    (, , uint256 lpAmount) = router.addLiquidity(
         asset,
         favorToken,
         amount,
         tokenAmount,
         0,
         0,
         address(this),
         block.timestamp + 50
    );

    IERC20(lpToken).forceApprove(address(POOL), lpAmount);

    POOL.supply(lpToken, lpAmount, user, 0);
    POOL.borrow(asset, amount + premium, 2, 0, user);

    IERC20(asset).forceApprove(address(POOL), amount + premium);

    emit FlashLoanExecuted(user, lpToken, lpAmount);

    return true;
}

If borrowing or flash loans were available against this reserve, an attacker could borrow the entire LP supply in a single transaction, redeem it for the underlying tokens, temporarily reset the on-chain LP price, re-add liquidity, and repay the loan. This sequence would allow the attacker to capture the accrued premium that should have represented yield for Aave depositors, effectively draining their interest. While the current protocol does not expose borrowing or flash loan functionality, enabling such features in the future would make this exploit feasible and could result in significant fund loss and token price resets.

BVSS
Recommendation

It is recommended to lock a minimum amount of LP liquidity so that the entire supply cannot be redeemed and the per-LP price cannot be reset through a flash loan.

Remediation Comment

ACKNOWLEDGED: The Better Bank team acknowledged this finding.

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

© Halborn 2025. All rights reserved.