Reproduced Exploit
Itos: `reentrantSettle` self-transfer bypasses payment
Chain
Other
Category
untagged
Date
Jan 1970
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: Itos-security-review_2025-05-24. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.
Vulnerability classes: vuln/reentrancy · vuln/theft
Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable
reentrantSettlefunction of RFTLib is reproduced verbatim (marked@>) with faithful minimal doubles; local deploy, no fork.
Root cause#
RFTLib.reentrantSettle tracks a cumulative transact.delta[token] across nested (reentrant) calls and validates it against the contract's own balance change at the end of the outer call. It never rejects payer == address(this), so a malicious IRFTPayer can re-enter during its tokenRequestCB and trigger a second settle with a negative amount whose safeTransfer becomes a self-transfer — leaving the real balance unchanged while zeroing the tracked delta. The vulnerable lines, reproduced verbatim from the finding:
function reentrantSettle(
@> address payer, // @audit payer is set to requester
address[] memory tokens,
int256[] memory balanceChanges,
bytes memory data
) internal returns (bytes memory cbData) {
...
// Handle and track all balance changes.
int256 change = balanceChanges[i];
if (change < 0) {
@> TransferHelper.safeTransfer(token, payer, uint256(-change)); // @audit if payer == address(this), this is a self-transfer
}
// If we want tokens we transfer from when it is not an RFTPayer. Otherwise we wait to request at the end.
if (change > 0 && !isRFTPayer) {
TransferHelper.safeTransferFrom(token, payer, address(this), uint256(change));
}
// Handle bookkeeping.
@> transact.delta[token] += change; // @audit delta is reduced because change is negative
}
...
}
}
The final validation only asserts finalBalance >= startBalance + delta. Because the callback nets delta back to zero via a balance-neutral self-transfer, the expected balance collapses to the untouched starting balance and the check passes even though the payer transferred nothing in.
Why it's exploitable here#
Following the finding's scenario, with the hub holding 1000e18 of honest depositors' reserves and the attacker holding 0 tokens:
- The attacker calls
deposit(attacker, +100e18).reentrantSettlesnapshotsstartBalance = 1000e18, setsdelta = +100e18, and because the attacker is anIRFTPayerit defers the inbound pull totokenRequestCB. - In the callback the attacker re-enters with
payout(hub, 100e18)→reentrantSettle(hub, -100e18). The negative branch runssafeTransfer(token, hub, 100e18)— a hub-to-hub self-transfer, so the real balance is unchanged, butdelta += -100e18nets it to0. - The outer call validates:
expectedBalance = 1000e18 + 0 = 1000e18, andfinalBalance = 1000e18, so the check passes. The hub credits the attacker100e18of redeemable shares. - The attacker redeems
100e18from the hub reserves. It paid0and walked away with100e18, draining honest depositors' reserves from1000e18down to900e18.
Attack path#
Marked-line walkthrough (Playground)#
The EVM Playground pins each step to the exact executed source line in 0x671d353a…:
- L67 — Expected balance math helper: The signed-add helper computes the hub's expected end balance as start balance plus the tracked delta, the value the final payment check compares against.
- L131 — Start-balance snapshot store: This mapping snapshots each token's starting balance so the routine can later diff the hub's real balance against the expected net change.
- L172 — Settle-entry length guard: On entry the routine only verifies the token and balance-change arrays match, with no guard against the payer being the hub itself.
- L209 — Inbound pull deferred to callback: Root cause: for an IRFTPayer the pull is skipped here, deferring payment to the attacker's callback, which self-transfers to net the tracked delta to zero.
- L210 — Honest pull-payment path skipped: An ordinary payer's deposit would be pulled straight in here, but the IRFTPayer branch skips this transfer so the hub collects no tokens.
- L241 — Bookkeeping cleared after fooled check: Once the fooled balance check passes, the start-balance and delta bookkeeping are deleted as if the deposit had been fully paid.
- L280 — ERC20 approve boilerplate: Setup: the ERC20 token double's standard approve function, plain boilerplate that plays no role in the payment-bypass itself.
PoC#
Registry (Foundry, local deploy — verbatim vulnerable source + harm-asserting test):
cd 58706-h-01-payers-exploit-reentrantsettle-to-bypass-payments-with_exp && forge test -vvv
The browser Playground replays the same synthetic opcode-for-opcode and measures the harm: the attacker deposits +100e18 it never pays, its callback self-transfers to zero the tracked delta, and it redeems 100e18 of honest reserves for free. Both gates are green (registry forge test PASS + Playground _verify-poc VERDICT: PASS).
Sources & further analysis#
Reproductions & code
- No executable Forge reproduction is claimed; the historical source/toolchain was unavailable for this finding.
- AuditVault finding: Itos-security-review_2025-05-24.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Itos:
reentrantSettleself-transfer bypasses payment". - Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
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.