Exploit Analysis | ShapeShift FOX Colony Authorization Trust Chain Flaw
Background
In May 2026, the EtherRouterCreate3 contract deployed by the ShapeShift FOX Colony project on Arbitrum was exploited. The attacker abused the “arbitrary self-call” capability within the contract’s meta-transaction mechanism, combined with DSAuth’s automatic authorization logic for address(this), to bypass the auth modifier and replace the contract’s core routing component, resolver, with a malicious version. The attacker then used delegatecall to drain all ERC20 assets held by the contract. The essence of this attack was a complete privilege bypass caused by a “semantic conflict between meta-transaction primitives and internal self-call authorization patterns.”
Attack Overview
Root Cause Analysis
Arbitrary Self-Call in executeMetaTransaction: address(this).call(callData) Does Not Filter Sensitive Selectors
The EtherRouter contract itself is an upgradeable proxy architecture based on a resolver. For unknown function selectors, fallback() calls resolver.lookup(msg.sig) to locate the implementation address and then executes it through delegatecall. The meta-transaction feature (executeMetaTransaction) was routed by the old resolver 0x7490022b0e44aa65c030ac0d6728382a29458fc5 to implementation contract 0x4e7f1e1e263678590007e89b7e129686ba7758d4. The implementation contract is not open source; the following is based on decompiled results:
function executeMetaTransaction(
address userAddress,
bytes memory functionSignature,
bytes32 sigR,
bytes32 sigS,
uint8 sigV
) public returns (bytes memory) {
// Construct message using nonce, address(this), chainid, functionSignature
// Recover signer address via ecrecover and verify == userAddress
require(recoveredAddress == userAddress);
// nonce++
_executeMetaTransaction[userAddress]++;
// Construct calldata
bytes memory callData = abi.encodePacked(
functionSignature,
0x2bcc191e283bfba76a1369ec8ba06566f33010645097c104c312753e04935e8,
userAddress
);
// ⚠️ Vulnerability: arbitrary self-call to address(this) after signature verification,
// without blocking sensitive selectors such as
// setResolver(address), setOwner(address), setAuthority(address)
(bool success, bytes memory returnData) = address(this).call(callData);
require(success);
emit MetaTransactionExecuted(userAddress, msg.sender,
functionSignature);
return returnData;
}The core issue is that executeMetaTransaction was designed to allow users to execute certain non-sensitive operations through signatures, but it performs no filtering on functionSignature. An attacker can use their own valid signature to make the contract call setResolver(maliciousAddress) on itself.
Automatic Authorization in DSAuth.isAuthorized: src == address(this) Is Automatically Allowed
EtherRouter.setResolver(address) is protected by the auth modifier and should only be callable by the owner or authority:
Resolver public resolver;
function setResolver(address _resolver) public auth {
resolver = Resolver(_resolver);
}However, DSAuth.isAuthorized(address src, bytes4 sig) contains automatic authorization logic for self-calls:
function isAuthorized(address src, bytes4 sig) internal view returns (bool) {
if (src == address(this)) {
return true; // ⚠️ Vulnerability: unconditional authorization for self-calls
} else if (src == owner) {
return true;
} else if (authority == DSAuthority(0)) {
return false;
} else {
return authority.canCall(src, this, sig);
}
}When executeMetaTransaction triggers a self-call through address(this).call(setResolver(...)), the msg.sender observed by setResolver becomes the contract itself 0x5c59..., and DSAuth automatically authorizes it.
Viewed independently, neither design appears to be an obvious vulnerability — self-call authorization is common in many proxy patterns, and meta-transaction mechanisms themselves are legitimate. However, when both logics coexist, a semantic conflict emerges: the “arbitrary self-call” capability provided by meta-transactions directly collides with DSAuth’s “self-call equals trusted” logic, together forming a complete privilege bypass chain.
Dynamic Routing via delegatecall in EtherRouter.fallback(): Full Control Transferred After Resolver Hijacking
fallback() external payable {
address target = resolver.lookup(msg.sig);
require(target != address(0));
// ⚠️ Vulnerability: unconditional delegatecall to resolver-returned address.
// Once resolver is replaced, any unknown selector can be routed
// to attacker-controlled implementations.
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), target, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}After the resolver is replaced, the attacker only needs to invoke any nonexistent function selector on EtherRouter, and fallback() will unconditionally delegate execution to the attacker-controlled implementation.
Malicious Resolver and Drain Implementation: Permissionless Function Mapping Registry + address(this) Asset Drain
The attacker pre-deployed two contracts:
Malicious Resolver
0x4e321af09012e15a67756522187c05b108b7ee0a (not open source, decompiled):
contract FunctionPointerRegistry {
mapping(bytes4 => address) private _lookup;
function lookup(bytes4 sig) public returns (address) {
return _lookup[sig];
}
// ⚠️ Vulnerability: no access control at all.
// Anyone can register arbitrary selector → implementation mappings.
function set(bytes4 functionSig, address implementation) public {
_lookup[functionSig] = implementation;
}
}Malicious Drain Implementation
0x0b971e0a8ecc7d5b2465c903cf75aeaedbfc39e2 (not open source, decompiled):
function drain(address token, address recipient) public {
// ⚠️ Since this function is executed via delegatecall
// from the victim contract,
// address(this) is actually the victim contract address 0x5c59...
uint256 balance = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(recipient, balance);
}Attack Profit Formula
Attacker EOA signs →
executeMetaTransaction self-calls setResolver(malicious resolver)
→ DSAuth self-call bypass
→ resolver hijacked
→ call EtherRouter.drain(token, attacker)
→ fallback()
→ malicious resolver.lookup(0x837971e4)
→ malicious drain implementation
→ delegatecall
→ address(this) inside drain == victim contract
→ IERC20(token).balanceOf(victim contract)
→ IERC20(token).transfer(attacker, balance)
= attacker obtains all ERC20 assets held by the victim contractAttack Flow
The entire exploit was completed in a single transaction, with all logic executed within the constructor of the temporary attack contract 0x835a701fd76b96a76ee84de037d41f059ee29f5c.
Phase 1: Deploy Malicious Infrastructure
The attacker EOA 0xeed236afb6967f74099a0a6bf078bc6b865fbf28 initiated the transaction and deployed the temporary attack contract 0x835a701fd76b96a76ee84de037d41f059ee29f5c.
The attack contract then called set(bytes4,address) on the malicious resolver 0x4e321af09012e15a67756522187c05b108b7ee0a, mapping the selector 0x837971e4 for drain(address,address) to the malicious drain implementation 0x0b971e0a8ecc7d5b2465c903cf75aeaedbfc39e2.
Phase 2: Hijack the Resolver Through Meta-Transaction Self-Call
The attack contract called executeMetaTransaction() on the victim contract 0x5c59d0ec51729e40c413903be6a4612f4e2452da. Since this function was not part of EtherRouter’s native ABI, the call entered fallback() and was routed by the old resolver to the meta-transaction implementation 0x4e7f1e....
executeMetaTransaction verified the signature through ecrecover, recovering the attacker EOA. The signature verification passed successfully because the attacker used their own valid signature rather than forging someone else’s.
executeMetaTransaction then constructed self-call calldata for setResolver(0x4e321af...) and executed address(this).call(callData). Since the execution context was the victim contract, msg.sender == 0x5c59.... DSAuth.isAuthorized() returned true because src == address(this), and the resolver was successfully replaced.
Phase 3: Drain Assets Through the Hijacked Resolver
The attack contract called drain(USDC, 0xeed236...) on the victim contract. Since this function was not part of EtherRouter’s native ABI, the call entered fallback(). The hijacked resolver returned the malicious drain implementation 0x0b971e0..., and the victim contract executed it via delegatecall.
The malicious code queried USDC.balanceOf(0x5c59...) and obtained 132704591501 (i.e., 132,704.591501 USDC), then directly transferred the funds to the attacker EOA through USDC.transfer().
The attack contract then called drain(0xf929..., 0x835a701f...), following the same path to transfer 841086343608217839604694 units of stolen intermediary tokens into the attack contract.
The attack contract then used Router 0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24 to swap the stolen intermediary tokens for 1.949506469643782660 WETH in Pair 0x5f6ce0ca..., with the WETH sent directly to the attacker EOA.
Profit Summary
Fund Tracing
Using SlowMist MistTrack, we conducted address profiling and counterparty analysis on attacker EOA 0xeed236afb6967f74099a0a6bf078bc6b865fbf28:
- Gas Source: The attacker’s initial gas funding originated from TornadoCash (
0x12d66f87a04a9e220743712ce6d9bb1b5616b8fc). - Malicious Label: MistTrack has labeled this address as “ShapeShift Exploiter”.
- Key Traces:
- Relay.link — $4,368.08, DEX aggregator used for asset swapping.
- Tornado.Cash — $218.09, initial gas withdrawn from mixer.
- LI.FI — $137,073.66, cross-chain / DEX aggregator.
The stolen funds were transferred into Spark.fi Saving and also showed interactions with Tornado.Cash, increasing the difficulty of subsequent tracing efforts. SlowMist MistTrack will continue monitoring the fund movements associated with the relevant addresses.
Conclusion
The core lesson of this attack is that when a contract simultaneously supports both “arbitrary self-calls through meta-transactions” and “automatic authorization for self-calls,” the two semantics together form a complete privilege bypass chain. This is not a single-point code vulnerability, but rather an inevitable consequence of semantic conflicts across components.
When designing meta-transaction or relay mechanisms, smart contract developers must clearly define the boundaries of sensitive functions, at minimum maintaining a denylist of prohibited selectors within executeMetaTransaction, and should use unconditional self-call authorization such as src == address(this) with extreme caution.
The SlowMist security team recommends that projects undergo comprehensive external security audits before deployment.
About SlowMist
SlowMist is a threat intelligence firm focused on blockchain security, established in January 2018. The firm was started by a team with over ten years of network security experience to become a global force. Our goal is to make the blockchain ecosystem as secure as possible for everyone. We are now a renowned international blockchain security firm that has worked on various well-known projects such as HashKey Exchange, OSL, MEEX, BGE, BTCBOX, Bitget, BHEX.SG, OKX, Binance, HTX, Amber Group, Crypto.com, etc.
SlowMist offers a variety of services that include but are not limited to security audits, threat information, defense deployment, security consultants, and other security-related services. We also offer AML (Anti-money laundering) software, MistEye (Security Monitoring), SlowMist Hacked (Crypto hack archives), FireWall.x (Smart contract firewall) and other SaaS products. We have partnerships with domestic and international firms such as Akamai, BitDefender, RC², TianJi Partners, IPIP, etc. Our extensive work in cryptocurrency crime investigations has been cited by international organizations and government bodies, including the United Nations Security Council and the United Nations Office on Drugs and Crime.
By delivering a comprehensive security solution customized to individual projects, we can identify risks and prevent them from occurring. Our team was able to find and publish several high-risk blockchain security flaws. By doing so, we could spread awareness and raise the security standards in the blockchain ecosystem.
