
Date: July 14, 2026
Chain: Ethereum
Loss: 24,882.99 DAI, about $24,883
Class: unchecked unsigned-to-signed integer cast that reversed a transfer
At 09:16:35 UTC on July 14, 2026, an attacker called a function named give() on an old Drips Network contract and asked to give away an amount of DAI that was 39 digits long. Nobody holds that much DAI, and nobody needed to, because the number was built so that the contract would read it backwards.
The give() call ended with the contract paying out instead of collecting. All 24,882.995421947667857715 DAI in the legacy DaiReserve, every last unit of it, went to the attacker in a single transaction. The contract converted an unsigned amount into a signed one without checking that it fit, and the sign of that number was the only thing deciding which way the money moved.
Drips is an Ethereum protocol for funding open source software. Its documentation describes three ways funds can flow between users: streaming, which moves funds over time at a fixed per-second rate, splitting, which forwards a fixed fraction of whatever a user receives, and giving, which transfers an amount from one user to another immediately.
Giving is the simplest of the three, and the legacy DaiDripsHub exposed it as give(address receiver, uint128 amt). A caller names a receiver and an amount, the hub records that amount as collectable for the receiver, and the caller's DAI moves into the DaiReserve, the contract that actually holds the funds. That legacy deployment was still live and still holding DAI in July 2026, behind a proxy at 0x7304...d365.
According to Verichains' analysis, give() hands off to an internal _give() function, which records the amount for the receiver and then calls _transfer() with the value -int128(amt). _transfer() reads the sign of what it gets. A positive amount means "pay the user", so the hub withdraws DAI from the reserve and sends it out. A negative amount means "collect from the user", so the hub pulls DAI from the caller and deposits it into the reserve. For an ordinary gift, int128(amt) is positive, its negation is negative, and the reserve takes DAI in.
The weak point is the cast. amt arrives as a uint128, an unsigned whole number that can never be negative and can go up to about 3.4 times 10 to the 38th. int128 is its signed cousin, with half that range on the positive side, because the top bit is spent on the sign. When Solidity converts a uint128 that's bigger than int128's maximum, it keeps the same 128 bits and simply reads them as a negative number. Solidity 0.8's checked arithmetic doesn't cover explicit conversions like this, so no error is raised.
That makes the teaching moment here simple: when a number's sign decides which way funds move, that number can never come straight from a caller without a range check, because a cast will happily turn a huge positive into a negative. A car odometer that rolls past its last digit back to zero gives the same effect, where going far enough forward lands you behind where you started.
Verichains lays out the arithmetic. The reserve held exactly B = 24,882,995,421,947,667,857,715 in DAI's smallest unit. The attacker passed A = 2^128 minus B, which is 340,282,366,920,938,438,580,379,185,484,100,353,741. A is a valid uint128, but it's larger than int128's maximum, so int128(A) comes out as minus B. The hub then negated it, producing plus B, and passed that positive number to _transfer(), which treated it as a withdrawal of the reserve's entire balance.
The attacker's wallet, 0x84dA...9CCE, sent the transaction to its attack contract, 0x00c6...4F12, in block 25529927. The Etherscan record shows three DAI transfers of 24,882.995421947667857715 each: from the DaiReserve to the DripsHub proxy, from the proxy to the attack contract, and from the attack contract to the attacker's wallet. Verichains notes that the attacker didn't bypass access control or manipulate an oracle. give() was public, and the only thing the attack needed was a number calculated against the reserve's balance at that block.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and takes in the context around the code to infer the rules the system relies on without stating them. In DaiDripsHub, the state that matters is the DaiReserve's balance, and give() is an open entry point that changes it. The engine maps the paths through that entry point and ranks them by signals, patterns in the code that mark a path as dangerous. The signal here is a narrowing cast from an unsigned to a signed type on a caller-supplied value with no bounds check, feeding a sign that decides the direction of a transfer. Olympix's static analysis surfaces that pattern, and Olympix has flagged this class before in other codebases.
The rule give() depends on fits in one sentence: a gift can move DAI from the caller into the reserve, and it can never move DAI out of the reserve to the caller. BugPoCer tests rules like that one with Counterexample-Guided Invariant Refutation, starting from what the whole system has to guarantee and working outward to the inputs that could violate it. Here it treats the DaiReserve's balance as the thing to protect and drives values through the flagged cast until that balance moves the wrong way. Any amount above int128's maximum reverses the direction, and an amount sized against the reserve's balance turns the reversal into a full drain.
The output is a counterexample with the concrete call, the minimal condition that triggers it (an amount above type(int128).max) and a generated Forge test that replays it. Set against the reserve balance, that counterexample is the July 14 transaction, produced in development instead of on mainnet. The engine runs automatically during development, at every merge and before release, so a legacy contract like this one gets the same standing check as the code a team shipped last week.
The DaiDripsHub that lost these funds was a legacy deployment that was still live and still holding real DAI. Verichains' fix is short, validating that an input fits the positive range of the destination type before casting and representing transfer direction explicitly instead of inferring it from a sign, and it's the kind of rule a tool can enforce on every contract a team still has live. If you'd like to see the counterexamples Olympix produces for your own contracts, old and new, book a technical walkthrough.
Protocol: Drips Network (legacy DaiDripsHub)
Chain: Ethereum
Date: July 14, 2026, 09:16:35 UTC
Loss: 24,882.995421947667857715 DAI (about $24,883)
Attacker EOA: 0x84dA...9CCE
Attack contract: 0x00c6...4F12
DaiDripsHub proxy: 0x7304...d365
Implementation at attack block: 0x8d32...f697
DaiReserve (drained): 0xf9bb...d44a
Exploit tx: 0xc38a...4130
Block: 25529927
Verichains: Drips Network, When Giving Became Receiving