October 18, 2026
The $2M GIWA Impersonation: Why “Verified” Chains Can Still Be Fake
On September 27, 2026, approximately 766 ETH worth roughly $2.08 million was drained from a bridge contract on Ethereum. The immediate…
By martin yeung
6 min read
On September 27, 2026, approximately 766 ETH worth roughly $2.08 million was drained from a bridge contract on Ethereum. The immediate reaction from the affected DEX, DYORSwap, was precise: this was not a smart contract exploit. No reentrancy was triggered. No integer overflow was exploited. No private key was compromised. Instead, the attackers built an entire counterfeit Layer 2 network, assigned it the chain ID that the market expected for GIWA, deployed a bridge contract on Ethereum that accepted deposits for that counterfeit chain, and then replaced the bridge's controlling logic after enough ETH had accumulated. The code that users interacted with did exactly what it was written to do. The failure lived one layer above the Solidity: in how a multi-chain DEX decided what counted as "real."
This matters because the industry keeps treating bridge security as a smart contract problem. GIWA proves that the more dangerous attack surface is the verification model, the set of assumptions a protocol uses to decide which external systems deserve trust. When that model reduces network identity to a single integer that anyone can copy, the strongest audit in the world will not save the funds.
What actually happened, step by step
- A mainnet that did not exist. GIWA, an Ethereum L2 associated with the Korean exchange Upbit, had not launched a mainnet. Only a Sepolia testnet was public, carrying chain ID 91342. Third-party trackers, however, had circulated 9134 as the expected mainnet chain ID.
- A counterfeit chain was stood up. Two fresh wallets, funded through a swap service, deployed a working EVM-compatible network that responded with chain ID 9134. It included an RPC endpoint, a block explorer surface, and a bridge contract on Ethereum. The operators even posted rollup-style data batches to Ethereum every few minutes using the blob format, which made the network look operationally like a real L2.
- The bridge contract mimicked a standard deposit locator. The contract was written to behave like a legitimate deposit contract, accepting ETH on Ethereum for crediting on the counterfeit chain. Users who believed they were early on the real GIWA mainnet sent ETH to this contract, expecting the ETH to back their balances on the L2.
- DYORSwap integrated the network. The DEX treated the counterfeit chain as GIWA's mainnet and added support for it. Its initial verification concluded that the network was legitimate because the chain ID matched the expected value, 9134.
- Deposits accumulated for roughly 13 hours. In total, 1,333 addresses sent about 767.65 ETH to the bridge. Most deposits arrived within a 13-hour window.
- The operators changed the bridge's "portal code" and withdrew. After enough ETH had accumulated, the operators replaced the bridge's controlling code and moved 766.25 ETH in a single transaction. From there, funds were routed toward Tornado Cash.
- No on-chain hack occurred on GIWA itself. GIWA confirmed that it had not launched a mainnet and that no mainnet RPC existed to leak. The loss was the result of a counterfeit network being accepted as real by an integration layer.
The real coding and software security issue: identity was reduced to an identifier
The root cause is not a missing require in a token transfer. It is a category error in the verification model. The system treated a chain ID as proof of network identity. In EVM-based systems, a chain ID is a number a network chooses for itself. There is no central registry that assigns it, and no technical mechanism prevents anyone from standing up a new EVM network and setting the same number. Chain ID exists primarily to prevent cross-chain transaction replay. It answers the question "which network is this transaction intended for?" It does not answer the question "who operates this network and should we trust their bridge?"
DYORSwap's integration flow effectively asked the first question and treated the answer as if it were the second. That is the software security failure. It is the same class of mistake that appears in bridge contracts when a message proof is validated without binding the source chain identity, except here the consequence was not a forged message but a forged network.
The second software layer that made the drain possible was the bridge contract's control model. The contract accepted deposits before the destination chain had any independent verification. More importantly, the operators retained the ability to alter the bridge's controlling logic. In a production bridge, the entity that can change the withdrawal logic is the entity that controls the funds. If that entity is the same party that deployed the bridge and populated the counterfeit chain, the bridge is not a bridge. It is a custody contract with a branding layer.
The vulnerable pattern, expressed in Solidity
The following is a simplified representation of the verification logic that fails when chain ID is treated as identity. It is not the exact DYORSwap codebase, but it captures the structural defect: an integration gate that checks a spoofable integer and nothing else.
solidity
// VULNERABLE: chain ID treated as proof of network identity
contract NetworkVerifier {
uint256 public expectedChainId;
function isTrustedNetwork(
uint256 suppliedChainId,
address rpcOperator
) external view returns (bool) {
return suppliedChainId == expectedChainId;
}
}// VULNERABLE: chain ID treated as proof of network identity
contract NetworkVerifier {
uint256 public expectedChainId;
function isTrustedNetwork(
uint256 suppliedChainId,
address rpcOperator
) external view returns (bool) {
return suppliedChainId == expectedChainId;
}
}The flaw is not that the check is missing. The flaw is that the check is insufficient. Any deployer can pass suppliedChainId = 9134. The rpcOperator argument is accepted but never used. The function returns true for a counterfeit network and a real network alike, as long as the integer matches.
The fix: anchor verification to something an attacker cannot mint
A production integration should never rely on a self-declared identifier. It should anchor trust to a value that is either published through an authenticated channel controlled by the official project or verified on-chain against an immutable or governance-controlled address. The code below shows a pattern that combines an on-chain registry with a confirmation delay.
solidity
// FIXED: verification anchored to a governance-controlled on-chain registry
contract NetworkRegistry {
struct NetworkRecord {
address officialBridge;
address governance;
uint64 registeredAt;
bool active;
}
mapping(uint256 => NetworkRecord) public networks;
uint256 public constant CONFIRMATION_DELAY = 7 days;
function registerNetwork(
uint256 chainId,
address officialBridge,
address governance
) external {
require(networks[chainId].registeredAt == 0, "already registered");
require(officialBridge != address(0), "invalid bridge");
networks[chainId] = NetworkRecord({
officialBridge: officialBridge,
governance: governance,
registeredAt: uint64(block.timestamp),
active: false
});
}
function activateNetwork(uint256 chainId) external {
NetworkRecord storage record = networks[chainId];
require(record.registeredAt != 0, "unknown network");
require(!record.active, "already active");
require(
block.timestamp >= record.registeredAt + CONFIRMATION_DELAY,
"confirmation delay not elapsed"
);
record.active = true;
}
function isTrustedBridge(
uint256 chainId,
address bridge
) external view returns (bool) {
NetworkRecord memory record = networks[chainId];
return record.active && record.officialBridge == bridge;
}
}// FIXED: verification anchored to a governance-controlled on-chain registry
contract NetworkRegistry {
struct NetworkRecord {
address officialBridge;
address governance;
uint64 registeredAt;
bool active;
}
mapping(uint256 => NetworkRecord) public networks;
uint256 public constant CONFIRMATION_DELAY = 7 days;
function registerNetwork(
uint256 chainId,
address officialBridge,
address governance
) external {
require(networks[chainId].registeredAt == 0, "already registered");
require(officialBridge != address(0), "invalid bridge");
networks[chainId] = NetworkRecord({
officialBridge: officialBridge,
governance: governance,
registeredAt: uint64(block.timestamp),
active: false
});
}
function activateNetwork(uint256 chainId) external {
NetworkRecord storage record = networks[chainId];
require(record.registeredAt != 0, "unknown network");
require(!record.active, "already active");
require(
block.timestamp >= record.registeredAt + CONFIRMATION_DELAY,
"confirmation delay not elapsed"
);
record.active = true;
}
function isTrustedBridge(
uint256 chainId,
address bridge
) external view returns (bool) {
NetworkRecord memory record = networks[chainId];
return record.active && record.officialBridge == bridge;
}
}This changes the security property from "the integer matches" to "the bridge address matches a record that was registered through an authenticated governance process and survived a delay." A counterfeiter can still deploy a network with chain ID 9134. The counterfeiter cannot make that network's bridge address appear in a registry controlled by the legitimate project.
The bridge contract itself needs a separate fix. If the operators can change the withdrawal logic after deposits arrive, the bridge is not safe regardless of how the network was verified. A production bridge should use a timelock on any change to withdrawal logic and should separate the deposit acceptance period from the period in which withdrawal logic can be altered.
solidity
// FIXED: withdrawal logic changes are timelocked and cannot affect pending deposits
contract TimelockedBridge {
address public pendingImplementation;
uint256 public implementationUnlockTime;
uint256 public constant TIMELOCK = 14 days;
mapping(address => uint256) public deposits;
function proposeImplementation(address newImplementation) external onlyGovernance {
pendingImplementation = newImplementation;
implementationUnlockTime = block.timestamp + TIMELOCK;
}
function finalizeImplementation() external onlyGovernance {
require(
block.timestamp >= implementationUnlockTime,
"timelock not elapsed"
);
// existing deposits remain withdrawable under the old logic
// until users have had a full exit window
}
function deposit() external payable {
deposits[msg.sender] += msg.value;
}
}// FIXED: withdrawal logic changes are timelocked and cannot affect pending deposits
contract TimelockedBridge {
address public pendingImplementation;
uint256 public implementationUnlockTime;
uint256 public constant TIMELOCK = 14 days;
mapping(address => uint256) public deposits;
function proposeImplementation(address newImplementation) external onlyGovernance {
pendingImplementation = newImplementation;
implementationUnlockTime = block.timestamp + TIMELOCK;
}
function finalizeImplementation() external onlyGovernance {
require(
block.timestamp >= implementationUnlockTime,
"timelock not elapsed"
);
// existing deposits remain withdrawable under the old logic
// until users have had a full exit window
}
function deposit() external payable {
deposits[msg.sender] += msg.value;
}
}The point of the timelock is not to make the bridge immutable. Bridges need upgrade paths. The point is to make sure that a logic change cannot be used as a surprise withdrawal switch. If users can see the proposed change for 14 days before it takes effect, they can exit. If the operators can change the code and drain in the same block, the bridge is a honeypot with a UI.
What the software security community should take from this
- Chain ID is a namespace, not an identity document. It prevents replay. It does not prove legitimacy. Any verification flow that stops at chain ID is incomplete by construction.
- The most dangerous vulnerabilities are often in the integration layer, not the contract layer. The Solidity was not broken. The assumption about what Solidity was connecting to was broken.
- A bridge that can change its withdrawal logic without a delay is not a bridge. It is an escrow contract controlled by whoever deployed it. The presence of a UI and an RPC endpoint does not change that.
- Verification must anchor to a value the attacker cannot mint. An official bridge address, a governance-controlled registry entry, or a signed attestation from the legitimate project are anchors. A chain ID is not.
- Counterfeit infrastructure is now cheap to produce. Building a working EVM network with an RPC, a bridge, and rollup-style data posting no longer requires deep protocol engineering. The defensive bar has to rise accordingly.
The GIWA incident will be remembered as a $2 million loss, but the more useful memory is the structural lesson. The industry has spent years hardening smart contracts against reentrancy and arithmetic errors. The next class of losses will come from systems that were never taught to ask who they were talking to.