Calculated from recorded token losses using historical USD prices at the incident time.
0x4019890fe5a5bd527cd3b9f7ee6d94e55b331709b703317860d028745e33a8ca0x3541499cda8ca51b24724bb8e7ce569727406e04EthereumOn Ethereum mainnet block 19417823, an unprivileged attacker exploited the BloomBeans BEAN proxy at 0x3541499cda8ca51b24724bb8e7ce569727406e04 by replacing its trusted registry pointer with an attacker-controlled contract, minting arbitrary BEAN, and dumping the minted supply into live Uniswap V2 liquidity. The full exploit occurred in transaction 0x4019890fe5a5bd527cd3b9f7ee6d94e55b331709b703317860d028745e33a8ca.
The root cause is an access-control failure in BBToken::setRegistry(address). Because the setter is public and unguarded, the contract's mint authorization root becomes attacker-controlled. Once the attacker points registry to a fake contract that returns the attacker as Savings, BBToken::mint(address,uint256) accepts arbitrary issuance. The attacker then sells the forged BEAN through public Uniswap routes and extracts 5063858067635448251 wei of WETH-equivalent value from pool liquidity.
The BEAN token is a proxy-backed ERC-20 whose verified implementation lives at 0x74463ed91bfa45bca06d59e8b383a89709842f69. That implementation stores a mutable Registry public registry pointer and does not hard-code any mint role inside mint. Instead, the token asks the external registry whether msg.sender matches one of five named system modules: Savings, , , , or .
ReferralInsuranceIncomeLockedSavingsThe authentic Registry contract is itself owner-gated for setContractAddress, but that protection only matters if the token continues to trust the authentic registry. Once the token allows an arbitrary caller to swap the registry pointer, the registry ceases to be a protocol trust anchor and becomes attacker input.
The profit leg is completely permissionless. After minting BEAN, the attacker can approve the public Uniswap V2 router at 0x7a250d5630b4cf539739df2c5dacb4c659f2488d and sell through the BEAN/WETH pair at 0xdec556c2ddf5d79f56801e8a669a1eeba23af94d and the multihop BEAN -> 0xea0abf7ab2f8f8435e7dc4932ffab37761267843 -> USDC -> WETH route.
The vulnerability class is an attacker-driven trust-boundary failure. mint(address,uint256) relies entirely on _isAuthorizedAddress, and _isAuthorizedAddress relies entirely on the mutable external registry pointer. Because setRegistry(address) is public, any caller can redefine the contract that answers the authorization checks.
The intended invariant is straightforward: only BloomBeans-controlled modules recorded in the authentic registry should be able to mint BEAN, and unprivileged users must not be able to redefine the authorization source used by mint. That invariant is first broken when the attacker calls setRegistry(address) with a malicious registry. It is exploited when the subsequent mint call succeeds because the fake registry returns the attacker for Savings.
The implementation also stores a maxSupply, but mint never enforces it. That omission is not the initial authorization bug, but it increases impact by leaving issuance effectively unbounded once the attacker controls the registry pointer.
The verified BBToken implementation contains the complete exploit surface:
function mint(address _user, uint256 _amount) public {
require(_isAuthorizedAddress(msg.sender), "BBToken:: Not authorized");
_mint(_user, _amount);
}
function _isAuthorizedAddress(address _address) internal view returns (bool) {
if (registry.getContractAddress("Savings") == _address) return true;
if (registry.getContractAddress("Referral") == _address) return true;
if (registry.getContractAddress("Insurance") == _address) return true;
if (registry.getContractAddress("Income") == _address) return true;
if (registry.getContractAddress("LockedSavings") == _address) return true;
revert("BBToken: Not Registered");
}
function setRegistry(address _registry) external {
registry = Registry(_registry);
}
This code makes the exploit deterministic. mint trusts _isAuthorizedAddress, _isAuthorizedAddress trusts registry, and setRegistry lets any caller replace registry with attacker code. The authentic Registry implementation remains owner-gated:
function setContractAddress(string memory _name, address _address) external onlyOwner {
registry[_name] = _address;
registered[_address] = true;
}
But that safeguard no longer protects BEAN after the pointer is redirected away from the authentic registry.
The transaction trace shows the exact exploit sequence:
BBToken::setRegistry(0x0F7C1ED0105090Acd575C5cF4B3256B6D4559a13)
BBToken::mint(0x076530CA85f95e0a2A69ee3590144290c8B76428, 1000000000000000000000000000000000000 [1e36])
0x0F7C1ED0105090Acd575C5cF4B3256B6D4559a13::getContractAddress("Savings")
UniswapV2Router02::swapExactTokensForETHSupportingFeeOnTransferTokens(... [BEAN, WETH] ...)
UniswapV2Router02::swapExactTokensForETHSupportingFeeOnTransferTokens(... [BEAN, MID, USDC, WETH] ...)
These trace entries establish the full mechanism. First, the attacker-controlled contract becomes the token's registry. Second, the mint succeeds immediately after the fake registry answers the Savings lookup. Third, the forged BEAN is monetized in the same transaction through public liquidity, proving the exploit is permissionless and self-contained.
The exploit starts from EOA 0xc9a5643ed8e4cd68d16fe779d378c0e8e7225a54, which submits the single adversary-crafted transaction. Inside that transaction, the attacker deploys an ephemeral execution contract at 0x076530ca85f95e0a2a69ee3590144290c8b76428 and a fake registry at 0x0f7c1ed0105090acd575c5cf4b3256b6d4559a13.
The execution contract calls the BEAN proxy and overwrites the registry pointer. From that point forward, authorization queries are answered by attacker code. The next call mints 1e36 BEAN because the fake registry returns the attack contract as Savings.
The attack contract then approves the Uniswap V2 router and executes two BEAN sales. The first uses the direct BEAN/WETH pair. The second uses the multihop BEAN -> MID -> USDC -> WETH path. The trace ties both swap calls to the same transaction, and the balance diff confirms that WETH value leaves public liquidity and is redistributed to the builder and the attacker-controlled execution path.
The exploit converts unauthorized token issuance into real asset extraction from public liquidity. The collector balance diff records a net WETH loss of 5063858067635448251 wei from the pooled liquidity used by the attacker route.
{
"token_symbol": "WETH",
"amount": "5063858067635448251",
"decimal": 18
}
The sender EOA records a positive balance delta of 1818640717443581747 wei during the exploit transaction. The root-cause JSON also records 57313895498281304 wei in gas costs, leaving 1761326821945300443 wei retained profit after gas. The root cause category is therefore ATTACK, and the incident is correctly classified as ACT.
0x4019890fe5a5bd527cd3b9f7ee6d94e55b331709b703317860d028745e33a8ca0x3541499cda8ca51b24724bb8e7ce569727406e040x74463ed91bfa45bca06d59e8b383a89709842f69src/utils/Registry.sol0x7a250d5630b4cf539739df2c5dacb4c659f2488d0xdec556c2ddf5d79f56801e8a669a1eeba23af94d