Reproduced Exploit
Bando — fulfilled beneficiary balances cannot be withdrawn
1. A fulfilled service credits 1,000 BAND into the escrow's releaseable pool. The tokens sit in BandoERC20FulfillableV1. 2. beneficiaryWithdraw(uint256,address) pays that pool out only when msg.sender is the manager. The line is the one the report quotes. 3. The audited BandoFulfillmentManagerV1 im…
Chain
Other
Category
dos
Date
Nov 2024
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 45848-lack-of-an-implementation-for-withdrawing-beneficiaries-bala. Standalone Foundry PoC and full write-up: 45848-lack-of-an-implementation-for-withdrawing-beneficiaries-bala_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/dos/frozen-funds · vuln/logic/missing-check
The finding file has no
vuln/tag. Genome on the finding is frozen-funds and locked-funds.fix/use-reentrancy-guardis also present and is not this bug.
Reproduction: self-contained Foundry PoC with only
forge-std— no fork, no RPC. Full trace: output.txt.
Key info#
| Impact | HIGH — 1,000 BAND credited to a beneficiary's releaseable pool stays in the escrow. The withdraw reverts, and the manager has no withdraw to call. |
| Protocol | Bando fulfillment escrow |
| Vulnerable code | BandoERC20FulfillableV1.beneficiaryWithdraw — require(_manager == msg.sender) with no manager wrapper |
| Bug class | Missing withdraw. Funds locked after a successful fulfillment. |
| Finding | Zokyo — Bando, 2024-11-25 · #45848 |
| Report | Zokyo/2024-11-25-Bando.md |
| Source | AuditVault |
| Compiler | solc 0.8.27, cancun, via_ir |
TL;DR#
- A fulfilled service credits 1,000 BAND into the escrow's releaseable pool. The tokens sit in
BandoERC20FulfillableV1. beneficiaryWithdraw(uint256,address)pays that pool out only whenmsg.senderis the manager. The line is the one the report quotes.- The audited
BandoFulfillmentManagerV1implementsregisterFulfillmentand does not implementbeneficiaryWithdraw. - A direct withdraw reverts. A call to the missing manager selector reverts. The beneficiary's balance stays 0.
- Adding the manager withdraw (the control) pays the beneficiary the same 1,000 BAND.
The vulnerable code#
Quoted from the report, and present at bandohq/evm-fulfillment-protocol@b901030 in BandoERC20FulfillableV1.beneficiaryWithdraw. The manager file at that commit has registerFulfillment / withdrawRefund and no beneficiaryWithdraw.
function beneficiaryWithdraw(uint256 serviceID, address token) public virtual nonReentrant {
// @> VULN: only the manager may withdraw, and BandoFulfillmentManagerV1 never calls this
require(_manager == msg.sender, "Caller is not the manager");
// FIX: add manager.beneficiaryWithdraw that the beneficiary can call, which calls this
address payable beneficiary = _registryContract.beneficiaryOf(serviceID);
require(_releaseablePools[token] > 0, "There is no balance to release.");
uint256 amount = _releaseablePools[token];
_releaseablePools[token] = 0;
MockToken(token).transfer(beneficiary, amount);
}
The payout snapshots amount before clearing the pool, matching the native BandoFulfillableV1.beneficiaryWithdraw at the same commit. A separate zero-after-clear in the ERC20 sibling is not this finding and is not reproduced. The only difference between the failing run and the control is the added manager function.
Root cause#
The escrow will not pay a beneficiary unless the caller is the manager. The manager is the only contract supposed to expose that action to the beneficiary, and the audited manager does not. After registerFulfillment the tokens are in the escrow and the accounting says they are releasable, but no caller can complete the release.
Preconditions#
- A service whose beneficiary is set.
- A successful fulfillment that credits the releaseable pool and leaves the tokens in the escrow.
- The deployed manager is the pre-fix
BandoFulfillmentManagerV1(nobeneficiaryWithdraw).
Attack walkthrough#
- Mint 1,000 BAND and
registerFulfillmentthrough the manager. The escrow balance becomes 1,000 BAND and_releaseablePools[token]becomes 1,000 BAND. - Call
escrow.beneficiaryWithdraw.msg.senderis not the manager, so the require reverts. The tokens do not move. - Call
manager.beneficiaryWithdraw. The selector is not implemented. The call reverts. - Escrow balance is still 1,000 BAND. Beneficiary balance is still 0.
Diagrams#
Impact#
The beneficiary loses access to 1,000 BAND that the protocol has already marked releasable. The same gate applies to every fulfilled service whose escrow only accepts the manager and whose manager never forwards the call. The control, which only adds beneficiaryWithdraw on the manager, pays that beneficiary the full 1,000 BAND and leaves the escrow at 0.
PoC#
cd 45848-lack-of-an-implementation-for-withdrawing-beneficiaries-bala_exp
forge test -vv
test_exploit_beneficiary_balance_stuck asserts the escrow still holds 1,000 BAND, the beneficiary holds 0, the direct withdraw reverted, and the manager call reverted. test_control_added_withdraw_pays_beneficiary deploys the same escrow with the added manager withdraw and asserts the beneficiary receives 1,000 BAND.
Remediation#
Add beneficiaryWithdraw on BandoFulfillmentManagerV1 that checks the caller is the service beneficiary and then calls the escrow. That is what the later manager does, and what the control implements.
+ function beneficiaryWithdraw(uint256 serviceID, address token) external {
+ require(registry.beneficiaryOf(serviceID) == msg.sender, "Only the beneficiary can withdraw");
+ BandoERC20FulfillableV1(_escrow).beneficiaryWithdraw(serviceID, token);
+ }
Sources#
- AuditVault finding: https://github.com/Auditware/AuditVault/blob/main/findings/45848-lack-of-an-implementation-for-withdrawing-beneficiaries-bala.md
- Zokyo report (2024-11-25): https://github.com/solodit/solodit_content/blob/main/reports/Zokyo/2024-11-25-Bando.md
- Vulnerable tree: https://github.com/bandohq/evm-fulfillment-protocol/blob/b901030059506d7701096a33e9845a4687a16f9e/contracts/BandoERC20FulfillableV1.sol
- Manager without the withdraw: https://github.com/bandohq/evm-fulfillment-protocol/blob/b901030059506d7701096a33e9845a4687a16f9e/contracts/BandoFulfillmentManagerV1.sol
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 45848-lack-of-an-implementation-for-withdrawing-beneficiaries-bala_exp (evm-hack-registry mirror).
- AuditVault finding: 45848-lack-of-an-implementation-for-withdrawing-beneficiaries-bala.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.