Background

In May 2026, the ShapeShift FOX Colony project faced a hack when the EtherRouterCreate3 contract deployed on Arbitrum was compromised. The attacker exploited the 'arbitrary self-calling' capability within the contract's meta-transaction mechanism, along with DSAuth's automatic authorization logic on address(this), to bypass the auth modifier and replace the core routing component resolver with a malicious version. This allowed them to leverage delegatecall to drain all ERC20 assets held by the contract. The essence of this attack boils down to a complete privilege bypass caused by the semantic conflict between meta-transaction meta-language and the internal self-calling authorization pattern.

Attack overview

Vulnerability root cause

The arbitrary self-call of executeMetaTransaction: address(this).call(callData) does not filter sensitive selectors.

The EtherRouter contract itself is an upgradeable proxy architecture based on resolvers: For unknown function selectors, fallback() calls resolver.lookup(msg.sig) to find the implementation address and executes it via delegatecall. The meta-transaction function (executeMetaTransaction) routes through the old resolver 0x7490022b0e44aa65c030ac0d6728382a29458fc5 to the implementation contract 0x4e7f1e1e263678590007e89b7e129686ba7758d4 for execution. This implementation contract is not open source, and the following is based on decompiled results:

Core issue: The design intent of executeMetaTransaction is to allow users to perform certain non-sensitive operations via signatures, but it does not filter functionSignature. An attacker can use their own valid signature to make the contract call setResolver(malicious address).

Automatic authorization in DSAuth.isAuthorized: src == address(this) is allowed

EtherRouter.setResolver(address) is protected by the auth modifier, which should only allow the owner or authority to call it:

However, there is self-call auto-authorization logic in DSAuth.isAuthorized(address src, bytes4 sig):

When executeMetaTransaction triggers a self-call via address(this).call(setResolver(...)), the msg.sender visible in setResolver is the contract itself 0x5c59..., thus it is automatically allowed by DSAuth.

Individually, neither design constitutes an obvious vulnerability—self-call authorization is common in many proxy patterns, and the meta-transaction mechanism itself is reasonable. However, when both sets of logic exist simultaneously, a semantic conflict arises: the 'any self-call' capability provided by meta-transactions collides with DSAuth's 'self-call is trusted' logic, resulting in a complete permission bypass chain.

EtherRouter.fallback()'s delegatecall dynamic routing: Full control is handed over after the Resolver is hijacked.

After the resolver is replaced, the attacker only needs to call any non-existent function selector on EtherRouter, and fallback() will unconditionally delegate to the malicious implementation controlled by the attacker.

Malicious Resolver and Drain implementation: Function mapping registry without permissions + asset draining of address(this)

The attacker pre-deployed two contracts:

Malicious Resolver

0x4e321af09012e15a67756522187c05b108b7ee0a (not open source, decompiled):

Malicious Drain implementation

0x0b971e0a8ecc7d5b2465c903cf75aeaedbfc39e2 (not open source, decompiled):

Attack profit formula

Attack process

The attack process is completed in a single transaction, with all logic executed in the constructor of the temporary attack contract 0x835a701fd76b96a76ee84de037d41f059ee29f5c.

Stage one: Deploying malicious infrastructure

  1. The attacker's EOA 0xeed236afb6967f74099a0a6bf078bc6b865fbf28 initiates the transaction, creating the temporary attack contract 0x835a701fd76b96a76ee84de037d41f059ee29f5c.

  2. The attack contract calls the malicious resolver 0x4e321af09012e15a67756522187c05b108b7ee0a's set(bytes4,address), mapping the selector for drain(address,address) 0x837971e4 to the malicious drain implementation 0x0b971e0a8ecc7d5b2465c903cf75aeaedbfc39e2.

Stage two: Hijacking the Resolver through meta-transaction self-calls

  1. The attack contract calls the victim contract 0x5c59d0ec51729e40c413903be6a4612f4e2452da's executeMetaTransaction(). Since this function is not in EtherRouter's own ABI, the call goes into fallback(), routed by the old resolver to the meta-transaction implementation 0x4e7f1e....

  2. executeMetaTransaction verifies the signature via ecrecover, recovering the attacker's EOA, and the signature verification passes (the attacker used their own valid signature, not a forged one).

  3. executeMetaTransaction constructs the self-call calldata setResolver(0x4e321af...) and executes address(this).call(callData). At this point, the context is the victim contract, so msg.sender == 0x5c59.... DSAuth.isAuthorized() returns true because src == address(this), successfully replacing the resolver.

Stage three: draining assets via the hijacked Resolver

  1. The attack contract calls the victim contract's drain(USDC, 0xeed236...). This function is not present in EtherRouter's native ABI, so it enters fallback(). The hijacked resolver returns the malicious drain implementation 0x0b971e0..., and the victim contract invokes it via delegatecall. The malicious code queries USDC.balanceOf(0x5c59...) obtaining 132704591501 (i.e., 132,704.591501 USDC), then calls USDC.transfer() to directly transfer to the attacker's EOA.

  2. The attack contract calls drain(0xf929..., 0x835a701f...) again, transferring the stolen intermediate token 841086343608217839604694 units to the attack contract through the same path.

  3. The attack contract swaps the stolen intermediate token via Router 0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24 in Pair 0x5f6ce0ca... for 1.949506469643782660 WETH, with WETH directly transferred to the attacker's EOA.

Profit closure

Fund tracking

Analyzing the attacker's EOA 0xeed236afb6967f74099a0a6bf078bc6b865fbf28 via SlowMist's MistTrack:

  • Main traces:

    • Relay.link — $4,368.08, a DEX aggregator for asset swaps.

    • LI.FI — $137,073.66, cross-chain/DEX aggregator.

Funds stolen by the attacker flowed into Spark.fi Saving, and SlowMist's MistTrack will continue to monitor the fund movements of the relevant addresses.

Summary

The core lesson from this attack is that when a contract simultaneously possesses both 'any self-call for meta-transactions' and 'self-call automatic authorization' semantics, they form a complete permission bypass chain—this is not a single point code vulnerability, but an inevitable result of cross-component semantic conflict. Contract developers must clearly delineate sensitive function boundaries when designing meta-transaction or relay mechanisms, at least maintaining a list of prohibited selectors within executeMetaTransaction and using src == address(this) for unconditional self-call authorization with caution. The SlowMist security team recommends a complete external security audit before deployment.

Note: This article is a technical security analysis for educational reference only and does not constitute any investment advice. All on-chain data is sourced from public information.