Drips Network: When Giving Became Receiving
Drips Network Exploit
On July 14, 2026, a legacy DaiDripsHub deployment on Ethereum was exploited for approximately 24,883 DAI. The attacker did not bypass access control or manipulate an oracle. Instead, a crafted uint128 amount became negative when converted to int128, reversing an intended deposit into a reserve withdrawal.
Overview
Attacker: 0x84dA7a5e2315Eb798f04B75554AeB15047269CCE
Attack Contract: 0x00c64B5a926ba1fceC30EfaD88C344c619F54F12
Vulnerable Hub Proxy: 0x73043143e0a6418cc45d82d4505b096b802fd365
Implementation at the Attack Block: 0x8d321e80487356c846f34456d31ce761776ef697
DAI Reserve: 0xf9bbb2df44cfe46e501cf91c99b2f8fef9d9d44a
Exploit TX: 0xc38a6e2259a85ced94238a0b0a49697992f2a6b8140c28f3fd2343d3d8434130
Exploit Analysis
The vulnerable contract exposes give(address receiver, uint128 amt). Its intended behavior is simple: the caller gives amt DAI to a receiver, and the receiver can collect it immediately.
The public function passes the caller and the unsigned amount into _give().
The Unsafe Signed Conversion
Inside _give(), the contract increases the receiver’s collectable amount and then calls _transfer() with -int128(amt).
The negative sign is meant to describe direction. A negative value means funds should move from the user into the hub. A positive value means funds should move from the reserve to the user.
That convention becomes dangerous because amt is an untrusted uint128. The contract converts it to int128 without checking that it is no greater than type(int128).max.
How One Number Reversed the Transfer
The reserve held exactly 24,882.995421947667857715 DAI. In DAI’s smallest unit, let that balance be B:
B = 24,882,995,421,947,667,857,715
The attacker supplied:
A = 2^128 - B
A = 340,282,366,920,938,438,580,379,185,484,100,353,741
A is a valid uint128, but it is larger than the maximum positive int128. When explicitly converted, the same 128-bit pattern is interpreted as a signed value:
int128(A) = A - 2^128 = -B
The contract then negates it:
-int128(A) = B
Instead of passing a negative deposit amount into _transfer(), the contract passes the positive reserve balance.
The Withdrawal Branch
_transfer() interprets positive and negative values as opposite operations.
For a positive amount, the hub calls erc20Reserve.withdraw() and then sends the withdrawn DAI to user. For a negative amount, it pulls DAI from the user and deposits it into the reserve.
The crafted conversion therefore selected the positive branch and withdrew the full recorded reserve balance.
The reserve itself correctly restricts withdrawals to its configured hub and checks its internal balance. Those protections did not help because the vulnerable hub was the authorized caller and requested exactly the available balance.
Attack Flow
The exploit completed in one transaction:
The attacker contract called the hub’s
give()function withA = 2^128 - B._give()recorded the huge unsigned amount as collectable for the chosen receiver.int128(A)became-B, and the leading negative sign changed it back to positiveB._transfer()treated positiveBas a withdrawal and pulled the entire DAI balance from the reserve.The hub sent the DAI to the attack contract, which forwarded it to the attacker.
The exploit transaction confirms this flow. The hub emitted the crafted A value, while the ERC-20 transfers show 24,882.995421947667857715 DAI moving from the reserve to the hub, then to the attack contract, and finally to the attacker.
The Root Cause
The root cause was an unchecked unsigned-to-signed conversion used to control transfer direction.
Solidity 0.8 checked arithmetic does not reject this explicit conversion. Converting a uint128 above type(int128).max to int128 preserves the 128-bit pattern and interprets its high bit as the sign bit.
The vulnerable assumption was that -int128(amt) must always be negative because amt was unsigned. That is only true when:
amt <= type(int128).max
No such validation existed. As a result, user-controlled input could cross the signed boundary and turn the intended deposit signal into a withdrawal signal.
Conclusion
When converting unsigned values to signed values, always validate that the input fits in the positive range of the destination type before casting. Checked arithmetic does not make explicit type conversions safe by itself.
Transfer direction should also be represented explicitly instead of being inferred from the sign of a user-controlled value. Separate deposit and withdrawal paths, or a checked conversion helper, would have prevented this bug.
Furthermore, conducting a security audit is strongly recommended for all projects, including smart contracts, backends, wallets, and dapps.





