# $IMD (BridgedFP) Ethereum security review — investigation record

**Observation window:** 2026-09-25 07:18:29–07:23:10 UTC  
**Target:** Ethereum `0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7`  
**Review type:** read-only verified-source review and public RPC/explorer calls; no transaction was signed or broadcast.

## Plain-language answer

No public function was found that lets an ordinary account mint, freeze, seize, blacklist, pause, or upgrade your IMD. IMD is a non-proxy LayerZero V2 OFT: an outbound transfer burns IMD and an authenticated inbound LayerZero message mints it. At the observation time, the Base adapter held **3,876,680.529188 FP**, while the three observed remote supplies (Ethereum, Arbitrum, Robinhood Chain) summed to **3,876,565.566118 tokens**. Custody therefore exceeded observed issued supply by **114.963070 FP**; the snapshot did not show an unbacked deficit.

The principal holder risk is the **single owner EOA** `0x047F...54B7`. It is both contract owner and LayerZero endpoint delegate on Ethereum and Base. With no timelock, it can change trusted peers and LayerZero libraries/DVN settings. A malicious or stolen key could configure a route that accepts messages not backed by FP on Base, minting unbacked IMD, or could disable/misconfigure routes and stop bridging. It can also install a message inspector that blocks outbound transfers, change token name/symbol, and transfer or renounce ownership. It cannot directly edit balances or allowances through any function in this implementation.

**Verdict:** no confirmed unprivileged critical/high/medium vulnerability was identified. One material, verified **informational trust risk** and two verified protocol-behavior observations are recorded below. “Informational” does not mean economically harmless: compromise of the owner/delegate key could affect the full circulating supply.

## Scope, sources, and evidence standard

Reviewed verified Solidity returned by:

- `GET https://eth.blockscout.com/api/v2/smart-contracts/0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7` at 2026-09-25 07:18:29 UTC (BridgedFP plus 39 submitted dependencies; compiler `v0.8.26+commit.8a97fa7a`, Cancun, optimization disabled, non-proxy).
- `GET https://base.blockscout.com/api/v2/smart-contracts/0xab152db8aac047b6757ffcf495ffe88d7712690a` at 2026-09-25 07:20:18 UTC (FrenPetOFTAdapter plus dependencies; compiler 0.8.26, non-proxy).
- LayerZero’s official deployment page, `https://docs.layerzero.network/v2/deployments/chains/ethereum`, for identification of Endpoint V2, SendUln302, ReceiveUln302, and Executor addresses. On-chain return data, not the documentation, is the authority for the live OApp configuration.

RPC URLs used exactly as shown in the evidence: `https://ethereum-rpc.publicnode.com`, `https://base-rpc.publicnode.com`, `https://arbitrum-one-rpc.publicnode.com`, and `https://rpc.mainnet.chain.robinhood.com`. Calls were spaced by at least one second. The investigation calls succeeded. A final repeat of the already-observed Base historical call at 07:25:54 UTC received HTTP 403, `Archive requests require a personal token`; therefore a reader may need a different archive RPC to reproduce that pinned block later. No required datum was missing because the identical pinned call had succeeded at 07:20:01 UTC.

Cross-chain systems have no single shared block number. “Same block” can only mean one pinned block per chain. The principal comparison used Ethereum block **26052926**, timestamp **2026-09-25 07:18:47 UTC**, hash `0xa54e...7243`, and Base block **51765727**, timestamp **2026-09-25 07:20:01 UTC**, hash `0xbfad...6815` (74 seconds apart). Arbitrum and Robinhood supply calls were latest-state observations at 07:21:35 UTC, so the aggregate is an attributable near-simultaneous snapshot, not an atomic invariant proof.

Terminology in this report:

- **VERIFIED** means observed in verified source or returned by the exact read call.
- **INFERENCE** means a conclusion from verified facts.
- **SUSPECTED/UNANSWERED** means the evidence was insufficient; it is not promoted to a finding.

## Architecture and live authority map

### Inheritance and token flow

`BridgedFP → OFT → OFTCore → OApp (OAppSender + OAppReceiver)`, with `OFT` also inheriting OpenZeppelin `ERC20`; OAppCore inherits OpenZeppelin `Ownable`. The target adds only mutable `name`/`symbol` storage and three metadata setters (`src/BridgedFP.sol:8-80`). There is no proxy or initializer.

- Outbound `send`: `_debit` burns the rounded amount (`lib/devtools/packages/oft-evm/contracts/OFT.sol:56-69`), then Endpoint V2 sends the message (`OFTCore.sol:198-221`). A later revert reverts the burn atomically.
- Inbound `lzReceive`: only the immutable endpoint may call and the `(srcEid,sender)` must equal the configured peer (`OAppReceiver.sol:121-135`); `_credit` mints (`OFT.sol:78-87`).
- Compose: mint occurs first, and compose payload registration is sent to the trusted endpoint for execution in a separate transaction (`OFTCore.sol:266-296`). The OFT itself does not call an arbitrary recipient hook during mint.

### Live identities and peers (VERIFIED)

At 07:18:58 UTC, `eth_call` via the Ethereum RPC returned:

| Getter / input | Observed |
|---|---|
| `owner()` | `0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7` |
| `endpoint()` | `0x1a44076050125825900e736c501f859c50fE728c` |
| Endpoint `delegates(IMD)` | same `0x047F...54B7` |
| `msgInspector()` | zero address |
| `sharedDecimals()` / `decimalConversionRate()` | `6` / `1e12` |

`eth_getCode(0x047F...54B7, latest)` at 07:23:10 UTC returned `0x`: the controller was an EOA, not a contract wallet.

Peer discovery was event-complete from deployment block 23,501,863 using:

`GET https://eth.blockscout.com/api?module=logs&action=getLogs&fromBlock=23501863&toBlock=latest&address=0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7&topic0=0x238399d427b947898edb290f5ff0f9109849b1c3ba196a42e35f00c50a54b98b`

at 07:21:10 UTC. It returned exactly three `PeerSet` logs. Current `peers(eid)` getters confirmed:

| EID | Network | Peer |
|---:|---|---|
| 30184 | Base | `0xab152db8aac047b6757ffcf495ffe88d7712690a` |
| 30110 | Arbitrum | `0x374080d830b66a3e4c8ecbca846e922a2039a024` |
| 30416 | Robinhood Chain | `0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127` |

The mapping is not enumerable, so event-history completeness assumes Blockscout returned all matching logs as stated. No other current nonzero EID can be proven by enumerating storage alone.

### Ethereum LayerZero route configuration (VERIFIED)

At 07:19:36–07:22:32 UTC, calls to Endpoint `0x1a44...728c` showed all three paths use default:

- send library `0xbB2Ea70C9E858123480642Cf96acbcCE1372dCe1` (SendUln302);
- receive library `0xc02Ab410f0734EFa3F14628780e6e695156024C2` (ReceiveUln302), `isDefault=true`;
- executor config: maximum message size `10,000`, executor `0x173272739Bd7Aa6e4e214714048a9fE699453059`;
- receive-library timeout: zero address, expiry 0 (observed for Base path).

Decoded `getConfig(IMD,library,eid,2)` ULN tuples are `(confirmations, requiredDVNCount, optionalDVNCount, optionalDVNThreshold, requiredDVNs, optionalDVNs)`:

| Path | Direction/config | Confirmations | Required DVNs | Optional |
|---|---|---:|---|---|
| Base 30184 | Ethereum send | 15 | 4: `0x380275805876fF19055EA900CDb2B46a94ecF20D`, `0x589dEDbD617e0CBcB916A9223F4D1300c294236b`, `0xa4Fe5A5b9a846458A70Cd0748228aeD3BF65c2Cd`, `0xa59BA433ac34D2927232918Ef5B2eaAfcF130BA5` | none |
| Base 30184 | Ethereum receive | 10 | same 4 | none |
| Arbitrum 30110 | Ethereum send | 15 | same 4 | none |
| Arbitrum 30110 | Ethereum receive | 20 | same 4 | none |
| Robinhood 30416 | Ethereum send | 15 | 2: `0x589d...236b`, `0xa59B...0BA5` | none |
| Robinhood 30416 | Ethereum receive | 5 | same 2 | none |

Exact reproducible call template (replace EID/library with table values):

`cast call 0x1a44076050125825900e736c501f859c50fE728c 'getConfig(address,address,uint32,uint32)(bytes)' 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 <library> <eid> 2 --rpc-url https://ethereum-rpc.publicnode.com`

Expected: ABI-encoded effective ULN config. Observed: configs decoded above; no optional DVNs. Raw Base receive began `0x...0020...000a...0004...`, encoding 10 confirmations and 4 required DVNs.

For every peer, `enforcedOptions(eid,1)` returned `0x0003010011010000000000000000000000000000ea60` (type-3 receive gas option, value `0xea60` = 60,000 gas); `enforcedOptions(eid,2)` returned empty `0x`. Base-to-Ethereum was symmetrical for message types at 07:22:45 UTC. Empty type-2 options are discussed under code-review observations.

On Base, the adapter owner and Endpoint delegate were also `0x047F...54B7`; Ethereum peer was the target. Base→Ethereum used default SendUln302 `0xB532...Dda2` and ReceiveUln302 `0xc70A...4bAf`. Send ULN required 10 confirmations and four DVNs (`0x5548...4b47`, `0x9e05...ac25`, `0xa7b5...0d99`, `0xcd37...36f6`); receive required 15 and the same four.

## Every privileged action affecting holders

All are **instant; none is timelocked**.

| Actor now | Function | Source location | Holder impact |
|---|---|---|---|
| Owner EOA | `setPeer(eid,peer)` | `OAppCore.sol:43-58` | Add/replace/remove trusted mint/burn route. Removal or wrong peer halts that path; attacker-controlled peer plus an accepted LayerZero path can authorize unbacked mint messages. |
| Owner EOA | `setDelegate(delegate)` | `OAppCore.sol:80-82` | Gives a new address Endpoint configuration power. Does not automatically revoke ownership. |
| Endpoint delegate EOA | Endpoint `setSendLibrary`, `setReceiveLibrary`, `setReceiveLibraryTimeout`, `setConfig` | verified `IMessageLibManager.sol:140-160`; implementation is the LayerZero Endpoint, not IMD source | Can change message libraries, confirmation/DVN requirements, executor configuration, or acceptance grace period; may halt bridging or weaken message authentication. |
| Owner EOA | `setMsgInspector(address)` | `OFTCore.sol:96-99` | A reverting inspector can censor all outbound sends inspected in `_buildMsgAndOptions` (`:231-252`); zero disables it. It does not affect ordinary ERC-20 transfers. |
| Owner EOA | `setEnforcedOptions(array)` | `OAppOptionsType3.sol:28-48` | Can alter mandatory execution options/gas; bad settings can make sends/delivery impractical. |
| Owner EOA | `updateName`, `updateSymbol`, `updateNameAndSymbol` | `src/BridgedFP.sol:45-80` | Changes wallet/explorer-facing metadata, creating spoofing/confusion risk; balances are unchanged. |
| Owner EOA | `transferOwnership(newOwner)` | `Ownable.sol:84-89` | Immediate one-step handover; typo/malicious recipient obtains all IMD owner functions. Endpoint delegate remains independently configured until changed. |
| Owner EOA | `renounceOwnership()` | `Ownable.sol:76-78` | Permanently disables IMD `onlyOwner` functions; does **not** by itself clear the Endpoint delegate, so the EOA may retain endpoint configuration power. |

Negative capability result (VERIFIED source): there is no owner mint, burn-from-holder, pause, blacklist, confiscation, fee, tax, snapshot, permit, or upgrade function. Owner cannot directly spend an allowance or transfer a holder balance. The only mint path is `_credit`, reached from authenticated `lzReceive`.

## Supply integrity analysis

### Snapshot (VERIFIED)

Exact calls and observed results:

1. At 07:18:58 UTC, Ethereum block 26052926:  
   `cast call 0xD34a...263B7 'totalSupply()(uint256)' --block 26052926 --rpc-url https://ethereum-rpc.publicnode.com`  
   observed `3846342970991000000000000` = **3,846,342.970991 IMD**; expected an ERC-20 supply integer.
2. At 07:20:01 UTC, Base block 51765727:  
   `cast call 0xFF0C...E105 'balanceOf(address)(uint256)' 0xab152...690a --block 51765727 --rpc-url https://base-rpc.publicnode.com`  
   observed `3876680529188000000000000` = **3,876,680.529188 FP**; expected adapter FP custody.
3. At 07:21:35 UTC, Arbitrum latest block 508692960:  
   `cast call 0x3740...a024 'totalSupply()(uint256)' --rpc-url https://arbitrum-one-rpc.publicnode.com`  
   observed **9 FP-equivalent tokens**.
4. At 07:21:35 UTC, Robinhood latest block 72044544:  
   `cast call 0x5f7b...7127 'totalSupply()(uint256)' --rpc-url https://rpc.mainnet.chain.robinhood.com`  
   observed `30213595127000000000000` = **30,213.595127 tokens**.

Arithmetic: `3,876,680.529188 locked − (3,846,342.970991 + 9 + 30,213.595127 issued) = 114.963070 FP surplus`.

**VERIFIED conclusion:** the snapshots do not show more observed remote supply than Base collateral.  
**UNANSWERED:** the exact cause of the 114.963070 surplus was not established; plausible categories include in-flight messages and adapter-held unissued amounts. Proving it requires a cross-chain message-level reconciliation beyond these asynchronous snapshots.  
**Important:** comparing Ethereum alone to Base would incorrectly label 30,337.558197 FP as unexplained; two additional current Ethereum peers account for nearly all of it.

### Can Ethereum IMD be minted without FP locked on Base?

- **Ordinary unprivileged caller: no path found.** `lzReceive` rejects a non-endpoint caller, and then rejects a non-peer sender. At 07:23:10 UTC this exact read-only simulation:  
  `cast call 0xD34a...263B7 'lzReceive((uint32,bytes32,uint64),bytes32,bytes,address,bytes)' '(30184,0x000000000000000000000000ab152db8aac047b6757ffcf495ffe88d7712690a,1)' 0x00...00 0x 0x000000000000000000000000000000000000dEaD 0x --from 0x000000000000000000000000000000000000dEaD --rpc-url https://ethereum-rpc.publicnode.com`  
  reverted `OnlyEndpoint(0x...dEaD)`, as expected.
- **Peer change alone: insufficient but dangerous.** `setPeer` only changes the OApp-level sender check; a packet must still be verified/delivered by Endpoint V2. Thus “set peer to attacker” does not directly mint. Combined with control of the configured source peer and/or endpoint security configuration, it can make attacker-originated messages acceptable.
- **Owner/delegate route: yes, conditionally.** The same EOA can install a peer and, as Endpoint delegate, change libraries/DVN configuration. A read-only owner simulation of `setPeer` returned `0x` (success), and a simulation of Endpoint `setSendLibrary(IMD,30184,currentLibrary)` also returned `0x`; no timelock intervened. An arbitrary address attempting `setPeer` reverted `OwnableUnauthorizedAccount(0x...dEaD)`. Therefore a compromised/malicious key has the prerequisites to weaken the authentication route and arrange mint messages not tied to Base locking. This is a trust-risk scenario, not a demonstrated present unbacked mint.
- **LayerZero/DVN/library compromise:** if the required verification set or selected library falsely verifies a packet, Endpoint can deliver it and IMD will mint. Current Base inbound configuration requires all four listed DVNs and 10 confirmations, which is defense in depth, but this is an external trust boundary rather than a code-enforced proof of Base custody.

## Findings

### F-01 — INFO — Single EOA has immediate control of the bridge trust root

**Real-world impact:** compromise or malicious use of one EOA can halt bridging or reconfigure peers/libraries/DVNs so that unbacked IMD can be minted, potentially impairing the full supply.

- **Status:** VERIFIED authority and absence of delay; the theft/unbacked-mint scenario is conditional and was not executed.
- **Location:** `OAppCore.sol:43-58,80-82`; `OFTCore.sol:96-99`; `OAppOptionsType3.sol:28-48`; Endpoint interface `IMessageLibManager.sol:140-160`; one-step ownership at `Ownable.sol:76-89`.
- **Description:** one EOA is simultaneously IMD owner, Base-adapter owner, and Endpoint delegate on both chains. No multisig/timelock boundary exists in these contracts. The role can alter both application authentication (peers) and transport authentication (libraries/DVNs).
- **Exploit/failure scenario:** attacker steals the EOA key; attacker configures an attacker-controlled peer and a verification path the attacker controls; attacker sends an OFT payload for 1,000,000 tokens without depositing FP; Endpoint delivers it; `_lzReceive` passes the new peer check and `_credit` mints 1,000,000 IMD. Alternatively, setting peer to zero or a reverting inspector immediately stops relevant bridge use. The exact attack packet was deliberately not sent and is therefore a reasoned conditional scenario, not a reproduced exploit.
- **Reproduction:** at 07:23:10 UTC, the non-owner `setPeer` simulation from `0x...dEaD` reverted `OwnableUnauthorizedAccount`; the same call from `0x047F...54B7` using the current peer returned `0x`. Endpoint `setSendLibrary` from that EOA returned `0x`. `eth_getCode(owner)` returned `0x`. Expected: only owner/delegate succeeds; observed: exactly that, with no delayed queue.
- **Recommendation:** move both OApp ownership and Endpoint delegation on every mesh chain to a carefully governed multisig; add a timelock for peer/library/DVN changes with monitoring and an emergency process; use two-step ownership; ensure ownership transfer and Endpoint delegate rotation are performed together. Consider immutable or delayed minimum DVN thresholds if the operational model permits it.

This is intentionally classified as a trust assumption, not an access-control bypass: unprivileged access checks worked.

### F-02 — INFO — Shared-decimal rounding creates sub-micro-token non-bridging dust

**Real-world impact:** users can leave less than 0.000001 token behind per send; this is bounded, remains with the sender, and is not extractable by another account.

- **Status:** VERIFIED code and live constants.
- **Location:** `OFTCore.sol:79-87,343-387`; `OFT.sol:56-69`.
- **Description:** local decimals are 18, shared decimals are 6, so `decimalConversionRate = 10^12`. `_removeDust` rounds down before burn.
- **Concrete scenario:** input `1,000,000,000,001` wei (0.000001000001 token) becomes `1,000,000,000,000` wei; `1` wei remains in the sender’s balance. If `minAmountLD` is `1,000,000,000,001`, the call reverts; if it is at most the rounded value, only the rounded value is burned/minted.
- **Reproduction:** at 07:18:58 UTC, `sharedDecimals()` returned `6` and `decimalConversionRate()` returned `1000000000000`. Expected under standard OFT behavior: floor to a shared-decimal quantum; observed code implements it.
- **Recommendation:** UI/SDK should quote `amountSentLD`, set `minAmountLD` deliberately, and disclose the 0.000001-token quantum. No contract fix is required for the standard OFT model.

### F-03 — INFO — No on-chain bridge rate limit or supply cap

**Real-world impact:** a failure of peer/DVN/library authentication can mint up to the OFT message-format limit without a per-period circuit breaker; legitimate large sends are likewise unrestricted by IMD-specific limits.

- **Status:** VERIFIED absence in complete verified source; the dependency-failure consequence is an INFERENCE.
- **Location:** `OFTCore.sol:108-132,175-221,375-408`; `OFT.sol:56-87`.
- **Description:** the default implementation reports total supply as the quote maximum but does not enforce that value as a cap, and no daily/per-message rate limit exists. Amounts are encoded as `uint64` shared-decimal units, giving a format ceiling of 18,446,744,073,709.551615 tokens at six shared decimals—far above current supply.
- **Failure scenario:** after a transport/authentication compromise, an accepted message can mint a material amount in one delivery; there is no local rolling cap to contain it. This does not create the authentication compromise.
- **Reproduction:** full verified source contains no rate-limit/cap state or guard; `quoteOFT` labels max as unused (`OFTCore.sol:108-132`). Expected if defense-in-depth limits were intended: a checked cap/window; observed: none.
- **Recommendation:** consider a destination mint rate limiter and global supply/custody invariant monitor with an emergency response. This changes availability and operations, so size limits to normal maximum flows and coordinate them across all peers.

## Standard OFT pitfall review (verified or explicitly bounded)

| Topic | Result |
|---|---|
| Mint authorization | No public/direct mint. Endpoint-only plus peer check precedes `_credit`; direct attack simulation reverted. Conditional admin/transport risk is F-01. |
| Shared decimals/dust | Six shared decimals and 1e12 conversion rate; safe floor-before-burn behavior, bounded dust (F-02). `_toSD` narrowing is safe only after division provided values remain within the documented `uint64` OFT ceiling; no current-supply issue observed. |
| Rate limits | None (F-03). This is defense-in-depth absence, not an unprivileged exploit by itself. |
| Compose handling | Credit occurs before `endpoint.sendCompose`; arbitrary recipient compose executes separately. No same-call untrusted recipient reentrancy found. Type-2 enforced options are empty, so sender-supplied options determine compose execution resources. A compose can be delayed/fail while minted tokens remain credited; this is intended asynchronous behavior, but integrations must not assume atomic compose completion. |
| Reentrancy | Outbound state effect is burn before trusted Endpoint call; revert is atomic. Inbound caller is immutable Endpoint and compose is queued. No exploitable reentrancy path found. |
| Approvals | IMD `send` burns `msg.sender` directly and `approvalRequired()` is false; allowances are irrelevant to bridging. Ordinary ERC-20 `approve` replacement has the well-known allowance-change race; use increase/decrease or zero-first UX where appropriate. No IMD-specific approval bug found. |
| Events | Metadata setters emit old/new events; peer, options, inspector, ownership, ERC-20, OFT send/receive and Endpoint delegate/library/config operations emit in inherited/protocol code. No material missing-event issue found. |
| Upgrade/initialization | Non-proxy constructor deployment; no initializer or upgrade entry point. |
| Pause/freeze/confiscation | Absent. A malicious inspector can censor outbound bridge sends but cannot stop ERC-20 transfers or seize balances. |
| Fee-on-transfer/rebasing adapter concerns | Base FP behavior was not exhaustively audited in this assignment. The adapter uses the supplied standard OFTAdapter/SafeERC20 dependency; current locked balance was measured directly. This remains contextual scope, not a finding against BridgedFP. |

## Facts, inferences, and unanswered questions

**Verified facts:** source/compiler/non-proxy status; all values and configs above; access-control reverts; three historical peer events and current peer getter values; owner is an EOA; supply snapshot has a 114.963070 FP surplus; no direct privileged balance editing function exists.

**Inferences:** a stolen owner/delegate key can combine peer and Endpoint configuration powers to create an unbacked-mint route; transport compromise can do likewise; lack of a limiter increases blast radius. These follow from reachable privileged functions but were not—and under the rules could not be—executed on mainnet.

**Unanswered/limitations:**

1. The 114.963070 FP surplus was not reconciled message-by-message. Cross-chain in-flight state can change between non-atomic snapshots.
2. Peer discovery depends on explorer log completeness because Solidity mappings cannot enumerate keys.
3. DVN addresses and threshold/confirmation values were read and decoded, but the organizational/operator independence and off-chain security of each DVN were not audited.
4. Arbitrum and Robinhood peer source code, their full endpoint settings, the FP token internals, and LayerZero protocol implementation internals were contextual rather than full code-audit scope.
5. No transactions, signatures, state mutations, volume fuzzing, or fork execution were performed. Read-only `eth_call` success proves current authorization/reachability, not future behavior.

## Overall severity count

| Critical | High | Medium | Low | Info |
|---:|---:|---:|---:|---:|
| 0 | 0 | 0 | 0 | 3 |
