All audits

Web3 · Smart contract audit

BloomBeans

softstack completed a smart contract audit of BloomBeans's Core Protocol in April 2026, reporting 17 findings (1 high, 7 medium and 9 low), of which 17 were resolved or mitigated before the final report. The full report is published in the softstack public audit archive.

Key facts

Client
BloomBeans
Completed
Type
Smart contract audit
Chains
Ethereum
Languages
Solidity
Commit
f3c0efc3cb68

About the project

BloomBeans is a decentralised on-chain financial protocol on Ethereum, established in January 2024 and run by the BloomBeans Foundation in Panama. Its contract code is audited and then locked, so the rules of the system stay immutable. The native BEAN token is a utility token under MiCA Regulation (EU) 2023/1114 and denominates protocol interactions.

Scope

Source repositorygithub.com/BloomBeans/bb-contracts

Components reviewed

6 modules · 6 files
  1. Income.sol
  2. SavingsData.sol
  3. Savings.sol
  4. Airdrop.sol
  5. Bean.sol
  6. GlobalMarker.sol

Findings

0Critical
1High
7Medium
9Low
0Informational

17 of 17 findings resolved before the final report.

IDTitleSeverityStatus
6.2.5SavingsData Table Mutability Retroactively Breaks Savings Token Accounting

SavingsData.savingsTable can be modified at any time via setSavingsTable()/ setSavingsTableBatch(). Savings tokens store only lookup keys (productLife, markerIndex) at mint -- not the actual multiplier.

Mediumfixed
6.2.1Principal Permanently Locked When All Interest Claimed at Maturity

withdrawIncome() gates the entire function -- including principal return -- behind a NothingToWithdraw check on withdrawable interest.

Highfixed
6.2.2Income Table Mutability Retroactively Breaks Existing Token Accounting

setIncomeTable() can be modified at any time by the owner. Existing Income tokens have earningsRemaining frozen at mint time, but getWithdrawableReward() reads the live incomeTable at withdrawal.

Mediumfixed
6.2.3Token Transferable While on Loan (Income) -- Loan Obligation Dumping

ERC1155 transfer overrides (safeTransferFrom/safeBatchTransferFrom) only check whenNotPaused but never check loan[id].onLoan. A borrower can take a loan (receive 25% of principal as minted BEAN), then transfer the token to a victim.

Mediumfixed
6.2.4Missing Pause Checks on createLoan and repayLoan (Income)

emergencyPause is enforced on mintIncome, withdrawIncome, migrateIncome, and transfer overrides, but not on createLoan() (L868) or repayLoan() (L917).

Mediumfixed
6.2.6Token Transferable While on Loan (Savings) -- Loan Obligation Dumping

Same pattern as H-02. ERC1155 transfer overrides in Savings.sol only check emergencyPause, with no loan[id].onLoan check. A borrower takes a loan, transfers the token to a victim, and keeps the loaned BEAN.

Mediumfixed
6.2.7Missing Pause Checks on createLoan and repayLoan (Savings)

Same pattern as H-03. createLoan() and repayLoan() in Savings.sol do not check emergencyPause. Four other functions enforce the pause, but loan operations remain callable -- minting new Bean tokens and modifying state during a supposedly…

Mediumfixed
6.2.8Zero dropAmt Allows Permanent Claim Lockout

If dropAmt is 0 (its uninitialized default), users who call claim() have their claimedDefi/claimedZeely flag set permanently but receive 0 tokens.token.transfer(msg.sender, 0) does not revert.

Mediumfixed
6.2.9Merkle Proof Uses Non-Standard Single-Hash Leaf Encoding

The Merkle leaf is computed as keccak256(abi.encodePacked(msg.sender)) -- a single hash. OpenZeppelin recommends double-hashing (keccak256(bytes.concat(keccak256(abi.encode(msg.sender))))) to prevent second pre-image attacks where an…

Lowfixed
6.2.10Merkle Root Can Be Changed After Users Start Claiming

changeMerkleRoot() is callable by the owner at any time with no timelock, event, or delay -- even while the airdrop is active. Users with pending claims in the mempool may have their proofs invalidated.

Lowfixed
6.2.11Centralization Risk -- Single EOA Owner Can Add Contract Addresses

Before setRegistry() is called and Registry.initializeMultiSig() completes, the onlyMultiSig modifier falls back to owner().

Lowfixed
6.2.12getMarker() Returns Wrong Tier at Last Boundary Value

When totalSupply equals exactly markers[markers.length - 1] (the last marker value), getMarker() returns 0 (first tier / highest interest) instead of the last tier.

Lowfixed
6.2.13Silent Fallthrough Returns Default 0 on Unmatched Supply

getMarker() has a code path where the function completes without an explicit return statement, producing an implicit return of 0 (tier 0 = highest interest).

Lowfixed
6.2.14Partial setInterest Update Leaves Stale Marker/Interest Data

The constructor allocates 41-element arrays. setInterest() writes index-by-index without clearing remaining slots. If fewer than 41 entries are provided, stale values persist.

Lowfixed
6.2.15setInterest Uses onlyOwner Instead of onlyMultiSig

setInterest() -- the protocol's interest rate oracle configuration -- uses onlyOwner instead of the onlyMultiSig pattern used for lesser functions elsewhere. A compromised owner key can unilaterally set all interest rates to 0% or maximum.

Lowfixed
6.2.16Input Validation After External Calls in mintIncome (CEI Violation)

mintIncome validates _paymentFrequency and _period inside the minting loop (lines 213-214), after Referral.addReferrer(), Referral.eligibleForReward(), Bean.mint(), and IERC20.transferFrom() have already executed.

Lowfixed
6.2.17mintSavings Allows qty=0

Same pattern as M-14. Calling mintSavings with _qty = 0 passes the max check. The referral registration executes before the loop, but no tokens are minted. Enables free referral chain gaming without economic commitment.

Lowfixed

Methodology

Review process

  1. Scoping and threat modelling

    The commit hash is frozen and the attack surface is mapped: asset flows, trust assumptions, privileged roles and the external systems the code depends on.

  2. Automated analysis

    Static analysers sweep the full codebase to surface known weakness patterns, unreachable states and deviations from established conventions.

  3. Manual code review

    Independent engineers read the code line by line, concentrating on business logic, access control, arithmetic and the handling of external calls.

  4. Adversarial testing

    Property-based fuzzing and invariant tests attack the assumptions the protocol relies on, targeting states that ordinary unit tests never reach.

  5. Reporting

    Every finding is documented with a severity rating, its impact, the steps to reproduce it and a concrete remediation the team can act on.

  6. Remediation verification

    Submitted fixes are re-reviewed against the original finding, and the final report records the resolved state of each one.

FAQ

The review reported 17 findings: 1 high, 7 medium and 9 low. 17 of 17 were resolved or mitigated before the final report.

6 source files were reviewed in github.com/BloomBeans/bb-contracts at commit f3c0efc. The published report lists every file covered.

This report covers the code as reviewed in April 2026 at commit f3c0efc. Any code changed after that point is outside the reviewed scope.