October 20, 2026
|
Exploit Postmortem
Cover image for "Unistreet Launchpad's $17.8K Loss and How Olympix Would Have Prevented It"

Unistreet Launchpad's $17.8K Loss and How Olympix Would Have Prevented It

Date: August 6, 2026

Chain: Ethereum

Loss: 17,743.91 USDC and 0.0072 WETH, about $17.8K, plus the launch tokens in the same positions

Class: caller-supplied calldata forwarded with the factory's own authority

Unistreet pitches itself as a launchpad for memestocks on Ethereum, where anyone can launch a token, pair it with a stock or a base asset, and open trading on Uniswap V4 in a single transaction. On August 2, 2026, at 15:37:59 UTC, the team deployed a new version of the contract that does the launching, LaunchpadFactoryAuto. To keep launched liquidity in place, the factory held the Uniswap V4 position NFT for every launch it created.

Four days later, at 00:27:47 UTC on August 6, one transaction launched a throwaway token, took control of every position the factory held, and burned seven of them, sweeping 17,743.91 USDC and 0.0072 WETH, about $17.8K, to the attacker along with the launched tokens in those pools. It was possible because launch() accepted two blobs of instructions from whoever called it and passed them to Uniswap's position manager with the factory's own identity attached.

A Factory That Holds the Keys

Uniswap V4 represents each liquidity position as an NFT managed by the PositionManager contract, and whoever owns or is approved for that NFT can remove the liquidity and take the tokens. LaunchpadFactoryAuto kept those NFTs for every launch, recorded each one in positionIdOf, and exposed a permissionless harvest() that collected trading fees from a position and split them between the protocol, the caller and the token's holders. Unistreet described the arrangement afterward in its own words: the factory held the liquidity NFTs for launched tokens.

That made the factory the single owner of every launch's liquidity, and anything the factory said to the PositionManager carried the authority of all of those positions at once.

Two Parameters Nobody Checked

The verified LaunchpadFactoryAuto source on Etherscan shows how launch() sets up a new pool. It clones a token, approves the PositionManager through Permit2 to spend the seed amount, and then builds a two-item list of calls out of the function's own arguments, initCalldata and modifyCalldata, which it sends to the PositionManager's multicall(). The intent is that the first item initializes the new pool and the second adds its liquidity, but the function never decodes or checks either one. Any caller, for any token, could put any PositionManager instruction in those slots, and the PositionManager would run it as a request from the factory.

Uniswap's PositionManager handles a multicall by running each item against itself with the original caller preserved, so to the PositionManager, every instruction inside looked like the factory speaking. According to the DeFiHackLabs analysis of the attack, the attacker's modifyCalldata was a setApprovalForAll call naming the attacker's contract as an operator. The factory, through its own launch() function, granted a stranger control of every position NFT it owned, including the ones belonging to other people's launches.

The lesson is the one Unistreet itself drew. A contract that holds assets for many users can't forward caller-written instructions to the contract that controls those assets, because the receiving contract only sees who sent the call, and that's the custodian. Building the instructions inside the function, from validated inputs, is what keeps a launch call from becoming a call about everyone else's money. A valet who hands the car keys to anyone who fills out a parking ticket has the same problem, however carefully the garage is locked.

Seven Positions in One Constructor

The attacker's wallet, 0xc94e23C58b9b2998eDB7ABC8F99393FEaD985076, sent a single contract creation transaction, and the whole attack ran inside the constructor of the contract it deployed, 0xc7d8c70f4349AcC55409800C8768e801b7556B77. That contract called launch() for a token named CPOC, and the factory minted the token's 1,000,000 supply and passed the batch of instructions to the PositionManager, injected approval included. The CPOC supply went to the attack contract.

With operator rights in place, the contract burned seven of the factory's positions in turn and took both sides of each pool. Blockscout's record of the transaction shows the NFTs with IDs 360162, 364347, 360385, 363137, 363145, 363678 and 364293 going to the zero address, followed in each case by the Uniswap V4 PoolManager releasing the pool's tokens to the attack contract. The largest pool, a launch token called UNISTREET paired with USDC, released 17,685.82 USDC. A second USDC pool released 58.09 USDC, two pools paired with WETH released 0.0037 and 0.0035 WETH, and the remaining three held only launch tokens. Across the seven positions, the contract also collected the launched tokens themselves, among them PIGCON, UNISTR, LESLIE, POTATO, UNICORN and CATSTREET.

Before the transaction ended, the attack contract forwarded everything to the attacker's wallet: 17,743.91 USDC, 0.0072 WETH, the CPOC supply and the launch tokens from all seven positions. Holders of those launch tokens were left with tokens whose pools had no liquidity behind them.

Swapped, Bridged and Mixed

Four minutes after the exploit, at 00:31 UTC, the attacker approved USDC for Relay's approval proxy and moved it through Relay. Over the next day and a half, the wallet sent seven deposits of 1 ETH each to the Tornado Cash router, the first at 07:25 UTC on August 6 and the last at 07:39 UTC on August 7, with more Relay transactions in between.

Unistreet posted a full statement on X shortly afterward. It said the launch factory "had a critical flaw," explained that it forwarded caller-supplied data to Uniswap's position manager while holding the liquidity NFTs, linked the exploit transaction, and took responsibility. The team disabled the vulnerable factory and deployed a replacement, LaunchpadFactorySafe, which builds every position manager instruction internally, never accepts caller-supplied calldata, and locks each launch's liquidity in its own vault that can only collect fees. According to the statement, the new contracts pass a test that replays the original attack and confirms it's rejected.

How Olympix Traces Whose Authority a Call Carries

Olympix reads a contract the way an attacker would, as states, transitions and entry points, and takes in the context around the code, including the external protocols it calls, to infer the rules the system depends on but never writes down. In LaunchpadFactoryAuto, the state that matters is who controls the position NFTs the factory holds, and launch() is a permissionless entry point that reaches the PositionManager with the factory as the caller. The engine maps every path from external entry points to calls made with the factory's authority and uses signals to decide which ones to attack. Olympix's static analysis surfaces the signal here: raw bytes from a public function's arguments placed straight into an external call to a contract where the factory holds privileged assets, with no decoding, selector check or allowlist in between.

The invariant is simple to state: only the factory's own logic should be able to change who controls the positions it holds. From every path into the PositionManager, the engine narrows to the feasible ones a stranger can reach, and then to the ones that can alter approvals or remove liquidity on positions the caller didn't create.

BugPoCer runs Counterexample-Guided Invariant Refutation, working outward from the factory and the PositionManager together and executing those paths until one breaks the rule. The counterexample it returns is the August 6 transaction in miniature: a fresh address calls launch() with an ordinary pool initialization as initCalldata and a setApprovalForAll naming itself as modifyCalldata, becomes an operator over every factory-held position, and burns one to take its tokens. It comes with the minimal conditions that trigger it and a generated Forge test that replays it.

Olympix has flagged this class before in other codebases, where a contract relayed caller-controlled calls with its own privileges. Because the engine runs automatically in development, at every merge and before release, this finding would have appeared when launch() first accepted those two parameters, before the factory went live on August 2.

Custody Changes What a Call Means

A launchpad that holds every pool's liquidity in one contract turns each of that contract's outgoing calls into a statement about all of its users' money, and launch() let any caller write part of that statement. Unistreet's replacement factory closes the gap by generating its own instructions, and the same rule can be tested against a contract before it ever holds a position. To see Olympix trace which of your contracts' external calls carry privileges a caller can steer, book a technical walkthrough.

On-Chain References

Protocol: Unistreet launchpad (LaunchpadFactoryAuto)

Chain: Ethereum

Date: August 6, 2026, 00:27:47 UTC

Loss: 17,743.91 USDC and 0.0072 WETH (about $17.8K), plus launch tokens from seven positions

Attacker EOA: 0xc94e...5076

Attack contract: 0xc7d8...6B77

Vulnerable contract (LaunchpadFactoryAuto): 0xFB60...D825

Factory deployment tx: 0x02bc...6c8c

Uniswap V4 PositionManager: 0xbD21...ee9e

Exploit tx: 0x9583...6d14

Block: 25692311

Sources

Unistreet: Security Incident and Migration

DeFiHackLabs: UnistreetLaunchpad Proof of Concept

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.