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-contractsComponents reviewed
6 modules · 6 files- Income.sol
- SavingsData.sol
- Savings.sol
- Airdrop.sol
- Bean.sol
- GlobalMarker.sol
Findings
17 of 17 findings resolved before the final report.
| ID | Title | Severity | Status |
|---|---|---|---|
6.2.5 | SavingsData 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. | Medium | fixed |
6.2.1 | Principal Permanently Locked When All Interest Claimed at Maturity withdrawIncome() gates the entire function -- including principal return -- behind a NothingToWithdraw check on withdrawable interest. | High | fixed |
6.2.2 | Income 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. | Medium | fixed |
6.2.3 | Token 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. | Medium | fixed |
6.2.4 | Missing 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). | Medium | fixed |
6.2.6 | Token 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. | Medium | fixed |
6.2.7 | Missing 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… | Medium | fixed |
6.2.8 | Zero 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. | Medium | fixed |
6.2.9 | Merkle 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… | Low | fixed |
6.2.10 | Merkle 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. | Low | fixed |
6.2.11 | Centralization Risk -- Single EOA Owner Can Add Contract Addresses Before setRegistry() is called and Registry.initializeMultiSig() completes, the onlyMultiSig modifier falls back to owner(). | Low | fixed |
6.2.12 | getMarker() 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. | Low | fixed |
6.2.13 | Silent 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). | Low | fixed |
6.2.14 | Partial 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. | Low | fixed |
6.2.15 | setInterest 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. | Low | fixed |
6.2.16 | Input 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. | Low | fixed |
6.2.17 | mintSavings 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. | Low | fixed |
Methodology
Review process
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.
Automated analysis
Static analysers sweep the full codebase to surface known weakness patterns, unreachable states and deviations from established conventions.
Manual code review
Independent engineers read the code line by line, concentrating on business logic, access control, arithmetic and the handling of external calls.
Adversarial testing
Property-based fuzzing and invariant tests attack the assumptions the protocol relies on, targeting states that ordinary unit tests never reach.
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.
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.