October 20, 2026
|
Exploit Postmortem
Cover image for "The Red Village's $80.7K Loss and How Olympix Would Have Prevented It"

The Red Village's $80.7K Loss and How Olympix Would Have Prevented It

Date: September 21, 2026

Chain: Polygon

Loss: about 31.43 WETH, roughly $80.7K

Class: a public forwarding function on a router that held the right to upgrade another contract

On September 21, 2026, an attacker on Polygon sent one crafted payload through a public function on The Red Village's router, and about 13 minutes later a second transaction swept 31.43 WETH, roughly $80,700, out of the game's TRVZooKeeper vault and into the attacker's wallet. Most of it came from 299 separate player wallets that had approved the vault to spend their WETH. The function was forwardRequest(), and nothing in it checked who was calling.

The router could pass messages to the game's other contracts, and it also held the right to approve upgrades to a contract called SeasonService. Because anyone could speak through the router, anyone could ask it to replace SeasonService's code, and the replacement the attacker installed was built to move WETH.

A Game That Pays Out in WETH

The Red Village is a multiplayer dark-fantasy game built around NFT Champions, according to its DappRadar profile. The game uses WETH as its in-game currency, and tournament winnings are paid in WETH to the wallets holding the winning NFTs. That means players connected to the game routinely hold WETH, and the protocol's own contracts hold prize funds.

On-chain, the game is a registry of services. A central router, PolygonTRVRouter, resolves a service name to its proxy address and forwards a caller-supplied payload to it. TRVZooKeeper is the vault that custodies player WETH through standing approvals, and it exposes transferERC20In and transferERC20Out functions that only accounts holding those roles can call. SeasonService held both roles. SeasonService is also a UUPS proxy, an upgradeable contract whose code can be swapped for a new version by whoever passes its _authorizeUpgrade check, and the router held that role.

A Messenger Anyone Could Use

A router like this is a trusted messenger: services act on what it forwards because it's supposed to forward only what the protocol intends. forwardRequest(serviceName, serviceId, data) accepted arbitrary calldata from any caller and passed it to any registered service, so the messenger carried whatever an outsider wrote. On its own, that meant outsiders could call services through the router. Combined with the router's authority to approve SeasonService upgrades, it meant an outsider could rewrite SeasonService.

A contract's privileges belong to everyone who can make it act. Giving the router upgrade rights over SeasonService was only as safe as forwardRequest() was closed, the way handing a building's master key to the front desk only works if the front desk won't open doors for whoever walks in.

Upgrade, Then Drain

The DeFiHackLabs reproduction confirms each step against live chain state. In the setup transaction, in block 94200341, an attacker-deployed contract with no privileges called forwardRequest() and had the router relay SeasonService.upgradeTo() with a new implementation. The proxy saw the router as the caller, the router held _authorizeUpgrade, and the upgrade went through. The only role check anywhere on that path tested the router's role, never the caller's. According to the DefimonAlerts post, the new implementation contained a delegatecall backdoor that let the attacker run arbitrary code as SeasonService.

SeasonService was still an authorized service on TRVZooKeeper, so its transferERC20In and transferERC20Out roles still worked, and ZooKeeper had no way to tell that the code behind that address had changed. In the drain transaction, in block 94200868, the new code called transferERC20In to pull every player's approved WETH into the vault, 299 pulls totaling 31.35 WETH, and then called transferERC20Out to sweep the vault's whole balance, including about 0.076 WETH it already held, to the attacker's wallet at 0xa566...df3f. The total came to 31.426 WETH.

The attacker used a plain wallet throughout, with no admin key, stolen signer or multisig involved. No statement from The Red Village team, recovery or bounty appears in the public record found for this incident.

How Olympix Finds the Path From a Public Function to an Upgrade

Olympix reads a contract system the way an attacker does, as states, transitions and entry points, and it reads TRVRouter, SeasonService and TRVZooKeeper as one system because the exploit crosses all three. The states that matter are which code SeasonService runs and who can move assets through ZooKeeper, and every entry point that can change either is worth mapping, including indirect ones that pass through the router. The engine uses the design context, a router trusted by its services, to infer the invariants the system depends on but never writes down.

Olympix's static analysis surfaces the signal: a public function with no access control that forwards caller-supplied calldata from a contract holding a sensitive role, here the right to authorize upgrades. Olympix has flagged this class of missing access control before in other codebases. The signal narrows the search from every path in the system to the feasible ones an outside caller can reach, and from those to the ones that end at _authorizeUpgrade.

BugPoCer starts from the question of who is allowed to change what SeasonService runs. It runs Counterexample-Guided Invariant Refutation, working outward from the router, the service and the vault together, and the invariant it infers is that only the protocol's governing authority can change SeasonService's code. It then executes paths from every public entry point toward upgradeTo() until one reaches it with a caller who holds no role at all.

The counterexample comes back as the September 21 attack: an arbitrary address calls forwardRequest() with an upgradeTo() payload, SeasonService accepts a new implementation, and that implementation calls transferERC20In and transferERC20Out on ZooKeeper to pull approved player WETH into the vault and send it to the caller. BugPoCer returns that sequence with the minimal conditions that trigger it and a generated Forge test that replays it, produced in development instead of on mainnet. Because the engine runs automatically during development, at every merge and before release, a new role granted to the router gets tested against every public path into it.

Every Trusted Contract Is an Entry Point

The Red Village's services trusted the router, and the router trusted anyone who called it, so the trust that guarded SeasonService's upgrades reached all the way to an outside wallet. Any time a contract holds a privilege, each public function on that contract is a way to use it. To see how Olympix traces privileged roles back to every path that can reach them and returns a runnable counterexample when one is open, book a technical walkthrough.

On-Chain References

Protocol: The Red Village (TRVRouter, SeasonService, TRVZooKeeper)

Chain: Polygon

Date: September 21, 2026 (setup tx at 15:46:38 UTC)

Loss: 31.426018474517367982 WETH (about $80,700)

Attacker EOA: 0xa566...df3f

Attack contract (created in the setup tx): 0x6005...f4a8

PolygonTRVRouter (proxy, public forwardRequest): 0xc97d...f15c

SeasonService (UUPS proxy, upgraded): 0xb172...6b77

New SeasonService implementation (Upgraded event in the setup tx): 0xa382...0afa

TRVZooKeeper (WETH vault): 0x426d...74d4

AccessControl: 0x3f0B...304e

Setup and upgrade tx: 0xac06...d55d

Drain tx: 0x3730...dd35

Blocks: 94200341 (setup), 94200868 (drain)

Sources

DefimonAlerts: The Red Village exploit alert

DeFiHackLabs: The Red Village exploit PoC

DappRadar: The Red Village

More from Olympix:

No items found.

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