# $IMD (BridgedFP) security audit

**Target:** Ethereum mainnet `0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7`  
**Review date:** 2026-09-25 UTC  
**Evidence block:** Ethereum `26,052,971` and Base `51,765,960`, both timestamped `2026-09-25T07:27:47Z`  
**Review type:** read-only verified-source review and public-RPC `eth_call`; no transactions, signatures, fuzzing, or state changes

## Plain-language summary

IMD is a normal transferable ERC-20 whose special feature is a LayerZero bridge. On Ethereum, tokens are burned when sent away and minted only when the LayerZero Endpoint delivers a message from a configured peer. The reviewed contract is not a proxy. Its owner cannot call a direct mint, take tokens from a holder, freeze an address, pause transfers, change balances, or upgrade the code.

The important caveat is administrative trust. One ordinary externally owned account (EOA), `0x047F...54B7`, is both the IMD owner and its LayerZero delegate. It can instantly replace trusted peer contracts and reconfigure the message libraries/DVN security stack. A malicious or stolen owner key could therefore arrange authenticated messages that mint IMD without locking FP, or disrupt bridging. There is no contract-level delay, multisig, or holder veto. This is the principal risk to holders.

At the paired evidence timestamp, Ethereum had **3,846,342.970991 IMD** and the Base adapter held **3,876,680.529188 FP**. Thus Base collateral exceeded Ethereum IMD by **30,337.558197 FP**, rather than falling short. This is not a proof of exact global backing because the Base adapter also serves other configured peers; a complete invariant is `Base FP locked = aggregate outstanding supply across every destination, adjusted for in-flight messages`, not `Base FP locked = Ethereum IMD alone`.

No unprivileged critical, high, medium, or low code vulnerability was confirmed. The two high-severity entries below are deliberately labeled **privileged trust risks**, not access-control bypasses. One informational entry records expected LayerZero shared-decimal rounding.

## Verified findings

| ID | Severity | Title | Where |
|---|---|---|---|
| IMD-01 | high (privileged trust risk) | Owner can replace a trusted peer and thereby authorize unbacked mint messages | `OAppCore.sol:43-56`; `OAppReceiver.sol:121-134`; `OFT.sol:78-91` |
| IMD-02 | high (privileged trust risk) | The same owner EOA is the LayerZero delegate and can weaken or replace the verification configuration immediately | `OAppCore.sol:80-82`; LayerZero Endpoint configuration surface |
| IMD-03 | info | Sends are rounded down to six shared decimals; sub-micro-token dust remains with the sender | `OFTCore.sol:343-385` |

Severity meanings used here: **high** means compromise of the stated privileged key can create materially unbacked supply or break bridge availability; **info** means documented behavior with no demonstrated asset loss when callers use the returned quote and slippage bound.

## Architecture and live configuration

### Code and inheritance

The verified compiler is Solidity `0.8.26`, without optimizer. [`BridgedFP.sol`](https://eth.blockscout.com/address/0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7?tab=contract_source_code) inherits LayerZero `OFT`, which combines `OFTCore`, `OApp` sender/receiver logic, pre-crime simulation/options helpers, OpenZeppelin `ERC20`, and OpenZeppelin `Ownable`. It is directly deployed, not a proxy. Custom lines 9-80 add mutable name/symbol storage and three owner-only metadata setters; they do not add mint or burn entry points.

Inbound execution is `Endpoint -> OAppReceiver.lzReceive -> OFTCore._lzReceive -> OFT._credit -> ERC20._mint`. `lzReceive` checks both `msg.sender == endpoint` and `origin.sender == peers[origin.srcEid]`. Outbound `send` rounds the amount to shared precision, burns it, and calls the Endpoint; any later revert rolls the whole transaction back. Composed calls are queued with `endpoint.sendCompose` only after crediting, and execute separately.

### Observed roles and path configuration

All values below are **VERIFIED** with historical `eth_call` at Ethereum block `26,052,971` unless noted.

- Owner: `0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7`.
- The owner had code `0x`, confirming an EOA at that block.
- Endpoint: `0x1a44076050125825900e736c501f859c50fE728c`.
- Endpoint delegate for IMD: the same owner EOA.
- `msgInspector`: zero; `preCrime`: zero.
- Local decimals: 18; shared decimals: 6; conversion rate: `10^12` local units.
- Current peers discovered from all `PeerSet` logs and checked through `peers(eid)`:

| LayerZero EID | Peer |
|---:|---|
| 30110 | `0x374080d830b66A3e4c8EcbCa846e922A2039A024` |
| 30184 (Base) | `0xab152db8aaC047b6757fFCF495FfE88d7712690A` |
| 30416 | `0x5f7Bb59365Ce557C26DBcAa4eE9D39A4B95B7127` |

For all three routes, the effective send library was `0xbB2Ea70C9E858123480642Cf96acbcCE1372dCe1` and receive library was `0xc02Ab410f0734EFa3F14628780e6e695156024C2`; the `isDefault` return was `true`, so these were inherited Endpoint defaults rather than explicit per-OApp library overrides. No receive-library timeout was set for the Base path (`lib=0`, `expiry=0`).

The send executor configuration for all routes was `maxMessageSize=10,000`, executor `0x173272739Bd7Aa6e4e214714048a9fE699453059`. The decoded ULN configurations were:

| EID | Send confirmations / required DVNs | Receive confirmations / required DVNs | Optional DVNs |
|---:|---|---|---|
| 30110 | 15 / 4-of-4 | 20 / 4-of-4 | none |
| 30184 | 15 / 4-of-4 | 10 / 4-of-4 | none |
| 30416 | 15 / 2-of-2 | 5 / 2-of-2 | none |

The four-DVN set was `0x380275805876Ff19055EA900CDb2B46a94ecF20D`, `0x589dEDbD617e0CBcB916A9223F4d1300c294236b`, `0xa4fE5A5B9A846458a70Cd0748228aED3bF65c2cd`, and `0xa59BA433ac34D2927232918Ef5B2eaAfcF130BA5`. The two-DVN set was the second and fourth addresses. Explorer labels identify these respectively as Horizen Labs, LayerZero, Canary, and Nethermind, but labels are descriptive evidence, not a security guarantee. The [LayerZero configuration documentation](https://docs.layerzero.network/v2/developers/evm/configuration/dvn-executor-config) explains the ULN fields and delegate-controlled configuration.

`enforcedOptions(eid, SEND=1)` was `0x0003010011010000000000000000000000000000ea60` on all routes, a type-3 receive-gas option of 60,000. `SEND_AND_CALL=2` was empty on all routes. This does not prevent a caller from supplying the extra compose execution options needed for a composed transfer.

### Every privileged IMD function

There is one application role: `owner`. The Endpoint separately recognizes the delegate, currently the same address.

| Function | Caller now | Holder impact | Delay |
|---|---|---|---|
| `setPeer(eid, peer)` | owner | Add/replace/remove an authenticated remote; can enable unbacked minting or stop a route | instant |
| `setDelegate(delegate)` | owner | Move Endpoint configuration authority, including to a different or zero address | instant |
| Endpoint `setSendLibrary`, `setReceiveLibrary`, `setReceiveLibraryTimeout`, `setConfig` | delegate | Change availability and message-verification assumptions; insecure configuration can permit forged mint messages | instant, except an optional receive-library grace period chosen by the delegate |
| `setEnforcedOptions(params)` | owner | Raise execution requirements/costs or make a route difficult to use; cannot directly alter balances | instant |
| `setMsgInspector(inspector)` | owner | A reverting inspector can block new outbound sends; a successful inspector does not mint or seize | instant |
| `setPreCrime(preCrime)` | owner | Changes simulation helper only; production `lzReceive` does not consult it | instant |
| `updateName`, `updateSymbol`, `updateNameAndSymbol` | owner | Change wallet/explorer display metadata; balances and token address remain unchanged | instant |
| `transferOwnership(newOwner)` | owner | Transfers all application-level powers in one step; no acceptance step | instant |
| `renounceOwnership()` | owner | Permanently disables application owner functions, while the existing Endpoint delegate remains configured | instant |

The Base adapter separately reported the same owner and delegate, the FP token `0xFF0C...E105`, and reciprocal Ethereum peer at EID 30101. Compromise of the one EOA therefore spans both sides of the bridge.

## Supply integrity

**VERIFIED:** IMD has no public or owner-only direct mint. `OFT._credit` is the only mint path in the reviewed implementation. It is internal and reached in production through `lzReceive`, which requires the canonical Endpoint and the configured peer. Ordinary transfers preserve supply; outbound bridge sends burn the rounded amount.

**VERIFIED paired snapshot:** using `totalSupply()` on Ethereum block `26,052,971` returned `3,846,342,970,991,000,000,000,000` wei, while `FP.balanceOf(adapter)` on Base block `51,765,960` returned `3,876,680,529,188,000,000,000,000` wei. Both block timestamps were exactly `2026-09-25T07:27:47Z`. The difference was `30,337,558,197,000,000,000,000` wei, or 30,337.558197 tokens, in favor of locked collateral.

**INFERENCE:** the excess is consistent with supply on the adapter's other destinations and/or in-flight messages. Because the Base adapter has peers at EIDs 30110, 30367, 30101, and 30416, Ethereum supply alone should not equal the adapter balance.

**UNANSWERED:** this review did not obtain synchronized total supplies and in-flight packet state for every remote network. Therefore it does not claim a proof of global one-to-one backing. It does establish that the requested Ethereum-vs-Base snapshot showed no collateral shortfall.

## Detailed findings

### IMD-01 — Owner can replace a trusted peer and thereby authorize unbacked mint messages

**Severity: high (privileged trust risk).** Theft or malicious use of the owner key can create arbitrary IMD through a newly trusted remote without corresponding FP locked, diluting holders and breaking redemption backing.

**Status:** VERIFIED authority and execution path; the malicious use case is a clearly marked threat scenario, not an observed exploit.

**Location:** `lib/devtools/packages/oapp-evm/contracts/oapp/OAppCore.sol:43-56` (`setPeer`); `OAppReceiver.sol:121-134` (`lzReceive` checks the mutable peer); `OFT.sol:78-91` (`_credit` mints). Custom contract inheritance begins at `src/BridgedFP.sol:8`.

**Description:** `setPeer` is correctly protected by `onlyOwner`, so this is not an access-control bypass. However, LayerZero authentication proves that a message came from the configured peer; it cannot prove that the peer locked FP. The owner can replace the Base peer with an owner-controlled OApp. Messages legitimately emitted by that new peer then pass Endpoint verification and cause `_credit` to mint on Ethereum. There is no rate limit or supply cap in `BridgedFP`, so repeated messages can amplify the loss (subject to the OFT's `uint64` shared-decimal amount per message).

**Exploit scenario:** an attacker steals the single owner EOA, calls `setPeer(30184, attackerPeer)`, then has `attackerPeer` send a normal OFT-format message claiming an amount without transferring FP into the canonical Base adapter. Once the configured LayerZero stack delivers it, IMD mints to the chosen recipient. Existing IMD is diluted and the forged portion has no FP claim in the adapter.

**Reproduction (read-only):** at `2026-09-25T07:31:36Z`, URL `https://ethereum-rpc.publicnode.com`, block `26052971`:

```text
eth_call from 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7
to 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7
setPeer(30184, 0x000000000000000000000000000000000000000000000000000000000000dEaD)
observed: 0x (successful simulation; no state persisted)

same call from 0x0000000000000000000000000000000000000001
observed: revert OwnableUnauthorizedAccount(0x000...001)
```

Expected secure-design result under the present ownership model: only the owner succeeds; that is exactly what occurred. Expected defense-in-depth result for holder protection: a peer replacement should not become effective immediately under one key.

**Recommendation:** put ownership behind a well-secured multisig and a public timelock; use a two-step peer-change process with a meaningful delay, monitoring, and an emergency cancel path. Optionally enforce a per-period inbound mint cap and pause/circuit-breaker that is independent from the peer-setting key. Preserve an escape path for stalled routes, but do not give one EOA immediate unrestricted authority.

### IMD-02 — The same owner EOA can immediately replace or weaken the LayerZero security stack

**Severity: high (privileged trust risk).** A compromised delegate can configure a security stack it controls and forge messages from an otherwise unchanged peer, causing unbacked minting or bridge denial of service.

**Status:** VERIFIED delegate identity, configuration, and setter authorization; malicious reconfiguration is a threat scenario, not an observed event.

**Location:** `lib/devtools/packages/oapp-evm/contracts/oapp/OAppCore.sol:80-82` (`setDelegate`) and LayerZero EndpointV2 `setSendLibrary`, `setReceiveLibrary`, `setReceiveLibraryTimeout`, and `setConfig`. The Endpoint interface is included in the verified source bundle.

**Description:** the owner EOA is also the Endpoint delegate. Current routes use recognized libraries and strong required-DVN sets (4-of-4 on EIDs 30110/30184; 2-of-2 on EID 30416), which is positive. Nevertheless, the delegate can change those settings immediately. A hostile delegate can select permissive registered configuration, attacker-controlled DVN contracts where accepted, or otherwise reduce verification assumptions; it can also select incompatible libraries/configuration and halt delivery. Because `_lzReceive` trusts Endpoint-delivered origin data, a verification-stack compromise can forge an existing peer and reach the mint path even without changing `peers`.

**Reproduction (read-only):** at `2026-09-25T07:31:36Z`, URL `https://ethereum-rpc.publicnode.com`, block `26052971`:

```text
Endpoint.delegates(IMD)
observed: 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7

eth_call from that delegate to Endpoint
setSendLibrary(IMD, 30184, 0xbB2Ea70C9E858123480642Cf96acbcCE1372dCe1)
observed: 0x (successful simulation; no state persisted)

same call from 0x0000000000000000000000000000000000000001
observed: revert LZ_Unauthorized
```

Configuration was read with `getSendLibrary(IMD,eid)`, `getReceiveLibrary(IMD,eid)`, and `library.getConfig(eid,IMD,{1,2})`; the decoded observed values are in the architecture table. Expected result under LayerZero's delegate model: the delegate succeeds and an outsider fails; observed behavior matched.

**Recommendation:** assign both owner and delegate to a multisig plus timelock, ideally with operational separation or independent signers; alert on every `DelegateSet`, library, timeout, and config event. Commit to minimum DVN diversity/threshold policy off-chain and, if the threat model warrants it, enforce configuration constraints through a controller contract rather than a raw EOA. Add inbound rate limits so one configuration failure cannot mint unbounded supply before response.

### IMD-03 — Sends are rounded down to six shared decimals

**Severity: info.** At most `0.000000999999999999` IMD per send is left in the sender's wallet; it is not transferred to another party or destroyed.

**Status:** VERIFIED expected OFT behavior.

**Location:** `lib/devtools/packages/oft-evm/contracts/OFTCore.sol:343-385` (`_removeDust`, `_toSD`, `_debitView`).

**Description:** local precision is 18 decimals and shared precision is 6, so the conversion rate is `10^12`. `_removeDust` floors the requested amount to a multiple of `10^12`; the floored amount is the amount burned and received remotely. The remainder stays in the sender's Ethereum balance. `minAmountLD` lets callers reject an unfavorable result. This is not exploitable rounding in the reviewed default OFT.

**Reproduction (read-only):** at `2026-09-25T07:33:04Z`, URL `https://ethereum-rpc.publicnode.com`, block `26052971`, call:

```text
quoteOFT(SendParam({
  dstEid: 30184,
  to: 0x000000000000000000000000047f606fd5b2baa5f5c6c4ab8958e45cb6b054b7,
  amountLD: 1000000000001,
  minAmountLD: 0,
  extraOptions: 0x,
  composeMsg: 0x,
  oftCmd: 0x
}))
observed OFTReceipt: amountSentLD=1000000000000, amountReceivedLD=1000000000000
```

Expected: floor `1,000,000,000,001` to `1,000,000,000,000`, leaving one wei with the sender. Observed matched.

**Recommendation:** no contract fix is required. Interfaces should always show the quoted received amount, set `minAmountLD` from that quote, and explain that precision below six decimals remains on the source chain.

## Other code-review conclusions

- **Rate limits:** none are implemented. This is not independently exploitable by an unprivileged caller, but it magnifies IMD-01/02 because a trusted-message failure has no application-level velocity cap.
- **Compose messages:** no arbitrary target is called inside IMD's receive execution. Credit occurs first, then the trusted Endpoint queues a separate compose payload for the recipient. No reproducible reentrancy loss was found.
- **Reentrancy:** outbound burn precedes the Endpoint call, and a revert is atomic. Inbound mint precedes `sendCompose`; reentry into `lzReceive` still requires the Endpoint and a trusted peer. No exploitable path was confirmed.
- **Approvals:** inherited OpenZeppelin ERC-20 behavior is standard. The classic replacement-allowance ordering risk applies to users changing a nonzero allowance directly to another nonzero value, but it is not specific to IMD and no contract-held allowance or custom approval bypass was found. OFT sends burn the caller's own balance and report `approvalRequired() == false`.
- **Rate/supply bounds:** each message encodes a `uint64` amount at six shared decimals. The resulting per-message ceiling is about 18.446 trillion tokens, not an economically meaningful protection for the present supply, and messages can repeat.
- **Events:** custom metadata changes emit `NameUpdated`/`SymbolUpdated`; peer, inspector, pre-crime, enforced-option, ownership, delegate, and Endpoint configuration paths emit their respective events. No material missing-event issue was confirmed.
- **Metadata:** the owner can rename/re-symbol the asset but cannot change the immutable token address. This can confuse interfaces but does not alter balances.
- **Zero recipient:** inbound credit maps a zero recipient to `0x000000000000000000000000000000000000dEaD`; funds are effectively burned rather than causing the whole receive to fail. This is standard in the bundled OFT implementation and requires the sender to choose a zero recipient; no theft from another user was demonstrated.

## Dropped findings and rejected leads

The requested `artifacts/findings.md` from step 1 was absent from the supplied working tree (only the two pinned methodology inputs existed), so there were no enumerated step-1 findings to re-run one by one. The following independently considered leads were dropped:

| Lead | Reason dropped |
|---|---|
| Public or owner-only direct mint | No external/public mint exists; production mint is internal `_credit` behind Endpoint and peer checks. |
| Proxy/upgrade backdoor | Address is a direct verified deployment and no upgrade entry point or delegatecall proxy was found. |
| Owner can seize, blacklist, pause, or burn a holder's balance | No such function exists. Owner risk is through bridge configuration and metadata, not direct balance control. |
| Ethereum supply is undercollateralized because it differs from Base adapter balance | Rejected: locked FP exceeded Ethereum IMD at the synchronized timestamp, and the adapter serves multiple destination peers. Equality to Ethereum alone is the wrong invariant. |
| Unprivileged `lzReceive` spoofing | Rejected: a non-Endpoint caller fails `OnlyEndpoint`, and Endpoint delivery also checks the configured peer. |
| Compose-message reentrancy drains IMD | Rejected: no arbitrary recipient call occurs in IMD's receive frame; compose execution is queued at the Endpoint after minting. No end-to-end loss trace was found. |
| Dust is burned or stolen | Rejected: only the rounded amount is debited; the remainder stays with the sender. Retained as informational UX behavior, IMD-03. |
| Missing events conceal privileged changes | Rejected: the reviewed setters and Endpoint configuration paths emit events. |
| Approval behavior permits the owner to spend holders' IMD | Rejected: standard ERC-20 allowance requires holder authorization; the owner has no bypass. |
| `setPreCrime` lets its target mint through simulation | Rejected: `lzReceiveAndRevert` always reverts and `lzReceiveSimulate` is restricted to `address(this)`; state changes are rolled back. |

## Evidence, reproducibility, and limitations

Primary source pages: [Ethereum IMD verified source](https://eth.blockscout.com/address/0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7?tab=contract_source_code), [Base adapter verified source](https://base.blockscout.com/address/0xab152db8aac047b6757ffcf495ffe88d7712690a?tab=contract_source_code), [FP token on Base](https://base.blockscout.com/token/0xFF0C532FDB8Cd566Ae169C1CB157ff2Bdc83E105), and [LayerZero EndpointV2 on Ethereum](https://etherscan.io/address/0x1a44076050125825900e736c501f859c50fE728c#code). Historical peer-setting transactions are attributable through the `PeerSet` logs on those explorer pages.

To reproduce the snapshot with Foundry `cast`:

```bash
# Ethereum, block 26052971
cast call 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 'owner()(address)' --block 26052971 --rpc-url https://ethereum-rpc.publicnode.com
cast call 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 'totalSupply()(uint256)' --block 26052971 --rpc-url https://ethereum-rpc.publicnode.com
cast call 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 'peers(uint32)(bytes32)' 30184 --block 26052971 --rpc-url https://ethereum-rpc.publicnode.com
cast call 0x1a44076050125825900e736c501f859c50fE728c 'delegates(address)(address)' 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 --block 26052971 --rpc-url https://ethereum-rpc.publicnode.com
cast call 0x1a44076050125825900e736c501f859c50fE728c 'getSendLibrary(address,uint32)(address)' 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 30184 --block 26052971 --rpc-url https://ethereum-rpc.publicnode.com

# Base, block 51765960 (same timestamp)
cast call 0xFF0C532FDB8Cd566Ae169C1CB157ff2Bdc83E105 'balanceOf(address)(uint256)' 0xab152db8aac047b6757ffcf495ffe88d7712690a --block 51765960 --rpc-url https://base-rpc.publicnode.com
```

For ULN details, call `getConfig(eid, IMD, 2)` on the returned send/receive library and ABI-decode as `(uint64 confirmations,uint8 requiredDVNCount,uint8 optionalDVNCount,uint8 optionalDVNThreshold,address[] requiredDVNs,address[] optionalDVNs)`. Config type 1 on the send library decodes as `(uint32 maxMessageSize,address executor)`.

All RPC requests succeeded; no rate-limited data is omitted. Requests were kept below one per second on average and far below the 300-request ceiling. The assessment reviewed the verified target and inherited dependencies plus the Base adapter context; it did not audit the internal security of the Endpoint, message libraries, four DVNs, FP token, or every other remote peer. Facts above are labeled VERIFIED, attack consequences that follow from those facts are labeled scenarios/inferences, and the incomplete global-supply reconciliation is explicitly UNANSWERED.
