F-2026-0005·missing-access-control

Unvalidated user-provided wrapped token address in pair registration enables binding legitimate tokens to malicious contracts

Fixedbridgecross-chainkey-registrygithub.com/pdxwebdev/yadakeyeventwallet
TL;DR

Token-pair registration is permissionless and accepts a user-provided wrappedToken address with no validation. An attacker can permanently bind a legitimate original token (e.g., USDC) to an attacker-controlled ERC20, drain all subsequent wraps, and block correct registration forever.

Severity
HIGH
Impact
HIGH
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
HIGH
02Section · Description

Description

Any user can register a token pair with a malicious wrapped token via registerKeyPairWithTransfer, permanently binding a legitimate original token (e.g., USDC or other not yet registered token) to an attacker-controlled ERC20. This enables complete theft of all wrapped funds for that token and permanently blocks the correct pair from ever being registered.

Root Cause

Two missing safeguards in the token pair registration logic inside registerKeyPairWithTransfer:

  • No access control on token pair registration. The newTokenPairs field in RegisterKeyPairContext is processed for any caller during any key inception or rotation. There is no onlyOwner check.
  • User-provided wrapped token address is accepted without validation. When pair.wrappedToken != address(0) (line 494), the address is stored directly into tokenPairs without verifying it was deployed by the bridge's beacon or that it enforces onlyBridge access control on mint/burn.

Vulnerable Scenario

Prerequisites:

  • The target original token (e.g., USDC) has not yet been registered as a pair on the bridge.
  • The attacker deploys a standard ERC20 with no access control on mint() and burn().
  1. Register malicious pair. The attacker generates a fresh wallet and calls registerKeyPairWithTransfer for key inception, including newTokenPairs that maps USDC to their malicious ERC20. No special privileges are required.
  2. Pair is permanently locked. Any subsequent attempt to register USDC with the correct wrapped token reverts with TokenPairExists. There is no removal or update function.
  3. Victim wraps real tokens. A legitimate user wraps USDC through the bridge during their own key rotation. The bridge transfers real USDC from the victim into itself and mints the malicious ERC20 (which has no onlyBridge restriction) to the victim's next key. The victim receives worthless tokens.
  4. Attacker mints fake tokens. The attacker freely mints any amount of the malicious ERC20 to themselves, since mint() has no access control.
  5. Attacker drains real tokens. The attacker generates another fresh wallet, does key inception with an unwrap permit for the malicious token. The bridge burns the worthless malicious tokens and releases real USDC from its reserves to the attacker. The bridge is fully drained for the affected token.
03Section · Impact

Impact

  • Fund theft. Attacker can drain 100% of the bridge's reserves for any affected token.
  • Permanent denial of service. The legitimate token pair can never be registered without a contract upgrade.
04Section · Recommendation

Recommendation

Separate token pair registration from key rotation and enforce access control:

1. Extract token pair registration into an onlyOwner function:

solidity
function registerTokenPair(
address originalToken,
string memory tokenName,
string memory tokenSymbol
) external onlyOwner {
if (tokenPairs[originalToken].wrappedToken != address(0)) revert TokenPairExists();
bytes memory initData = abi.encodeWithSelector(
WrappedToken.initialize.selector,
tokenName,
tokenSymbol,
address(this),
address(keyLogRegistry)
);
WrappedTokenProxy proxy = new WrappedTokenProxy(wrappedTokenBeacon, initData);
address wrappedToken = address(proxy);
tokenPairs[originalToken] = TokenPairData(originalToken, wrappedToken);
tokenPairs[wrappedToken] = TokenPairData(originalToken, wrappedToken);
supportedOriginalTokens.push(originalToken);
}

2. Remove newTokenPairs from RegisterKeyPairContext and registerKeyPairWithTransfer.

3. Always deploy wrapped tokens via the beacon. Never accept a user-provided wrappedToken address. This guarantees every wrapped token enforces onlyBridge on mint/burn/transfer.

Additional Recommendations

  • Add a removeTokenPair or updateTokenPair function so the owner can correct mistakes or respond to attacks.
  • Remove the wrappedToken field from the TokenPair struct passed by users, since the bridge should always deploy it.
05Section · Resolution

Resolution

YadaCoin, Confirmed.

Zealynx, Fixed.

Status
Fixed
F-2026-0005

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx