# IMDSTR security audit

**Snapshot:** 2026-09-25 09:13–09:25 UTC. **Method:** verified explorer source plus read-only `eth_call`, `eth_getStorageAt`, and narrowly scoped log queries; no transactions. Ethereum block sampled: 26,053,500; Robinhood block sampled: 72,111,468. Addresses in this report link to the primary explorer record: [Ethereum token/proxy](https://etherscan.io/address/0x0000198C940D8cD70Cb9ACeC5E3af8216ac57d2F#code), [Ethereum implementation](https://etherscan.io/address/0x16d3f65b708883df042d98e1c7a49b32a33e2a14#code), [Ethereum hook](https://etherscan.io/address/0xA16026A28aA581AA96713d20C608Da7F8db86444#code), [Ethereum launcher](https://etherscan.io/address/0x1216eDc56A93CC8f7bE67E1cBaD264AB2803FCff#code), [Ethereum adapter](https://etherscan.io/address/0x6BDca0523530D4B1Cc2e7e6043ce4c5adfC56C51#code), [Ethereum timelock](https://etherscan.io/address/0xBd3ed9F4AbD9946cA6F59C8F13A3EbebDE1EA29D#code), [Robinhood token](https://robin.etherscan.io/address/0x0000198C940D8cD70Cb9ACeC5E3af8216ac57d2F#code), [Robinhood hook](https://robin.etherscan.io/address/0x50c1927c08340b1BbC3fD3C448eEe9aaa937e444#code), [Robinhood launcher](https://robin.etherscan.io/address/0xfdA3fB00662011287AFc6d0c958AC73D8e7Cae62#code), and [Robinhood timelock](https://robin.etherscan.io/address/0x16D3f65B708883DF042d98e1c7a49B32A33E2A14#code).

## Plain-language summary

IMDSTR is not trustless. Ordinary holders cannot mint more tokens, take the NFT treasury, avoid the pool fee, or redirect bridge messages under the code reviewed. At the snapshot, the Ethereum bridge held **454,972,103.105216 IMDSTR** against **448,751,994.674948030475952932 IMDSTR** issued on Robinhood—an apparent surplus of **6,220,108.430267969524047068**, consistent with burns, not an uncovered remote supply. This is a near-time comparison, not an atomic cross-chain proof.

The decisive risk is one key: `0x35da…a059` is the only proposer, executor, and canceller on both 48-hour timelocks. After the delay it can upgrade the Ethereum token to arbitrary code, change transfer exemptions, change the bridge peer and LayerZero security configuration, redirect fee recipients and buyback routes, and change the NFT relist multiple. A stolen or malicious deployer key can therefore dilute or confiscate holders through an upgrade or create unbacked Robinhood IMDSTR through hostile bridge configuration. The delay gives observers time to react, but there is no multisignature quorum.

The same EOA acts instantly as the seat operator. Its instant authority is narrow in the reviewed implementation: register agents, approve/revoke only the exact ERC-1271 worker-authorization digests for NFTs the strategy still owns, and nothing that directly transfers IMDSTR, ETH, or NFTs. A delayed upgrade could widen that power.

Trading fees are enforced by token transfer restrictions around the configured v4 pools. At present the Ethereum global distributor is zero and the tested Robinhood launcher is not a distributor. Direct calls to PoolManager do not skip callbacks. Governance can, after 48 hours, add distributors or upgrade the token and thereby permit fee-free transfers and alternate liquidity venues. The fee is 10% on sells and mature buys and can only be lowered to 1% in the current hook; it cannot be raised without replacing code. Buy fees begin at 99% and fall by one percentage point per minute to the configured floor.

No critical permissionless exploit was reproduced. The most material code-level weakness is that public buyback swaps accept zero minimum output, making fee-funded purchases sandwichable. Everything below distinguishes observed facts from inference.

## Findings table

| ID | Severity | Title | Location |
|---|---|---|---|
| F-01 | high | One EOA is the complete governance quorum on both chains | `TimelockController` constructor/roles; both deployments |
| F-02 | high | Delayed UUPS upgrade can replace all Ethereum token and treasury rules | `BaseStrategy.sol: _authorizeUpgrade`; `IMDSeatStrategy` proxy |
| F-03 | high | Delayed LayerZero reconfiguration can create unbacked Robinhood supply | `OAppCore.sol: setPeer, setDelegate`; `OAppOptionsType3.sol: setEnforcedOptions` |
| F-04 | medium | Fee-funded buybacks have no slippage floor and are sandwichable | `SingleNFTStrategyLauncher.sol:352–374`; `RobinhoodLauncher.sol:275–301, 332–347` |
| F-05 | low | Governance can create fee-free transfer paths through distributors | `BaseStrategy.sol:2334–2350`; `IMDSTRRobinhood.sol:40–43` |
| F-06 | low | Several security-sensitive configuration changes lack dedicated events | `BaseStrategy.sol:updateHookAddress`; hooks:`updateFeeAddress`; OFT inherited inspector/delegate configuration |
| F-07 | info | Six shared decimals discard bridge dust below 10^12 local units | inherited `OFTCore.sharedDecimals`, `_removeDust` |

## Detailed findings

### F-01 — High — one-key governance

**Real-world impact:** compromise or misuse of one EOA can, after 48 hours, take complete control of token, bridge, and fee policy on both chains.

**VERIFIED.** `getMinDelay()` returned `172800` on both timelocks. `hasRole(PROPOSER_ROLE|EXECUTOR_ROLE|CANCELLER_ROLE, 0x35da…a059)` returned `true` on both; `hasRole(EXECUTOR_ROLE, address(0))` returned `false`. Each timelock retained its own default-admin role. Ethereum logs from deployment block 26,049,886 through 26,053,500 contained only the four role grants and minimum-delay event—no `CallScheduled`, `CallExecuted`, or `Cancelled`, so no Ethereum operation was pending at the snapshot. The analogous Robinhood range scan was refused by public RPC with `-32005 network is busy`; pending Robinhood operations are therefore **unanswered**, not assumed absent.

**Exploit scenario (inference):** theft of the EOA schedules an upgrade and malicious peer/config change, waits 48 hours, then executes it. Because the same key can cancel and execute, no independent actor can veto it.

**Expected / observed:** expected resilient governance to require independent approvals; observed a 1-of-1 operational quorum. **Recommendation:** put proposer/executor/canceller behind a threshold multisig with independent signers; retain the timelock; add monitoring and an independent, narrowly scoped emergency canceller.

### F-02 — High — unrestricted delayed upgrade

**Real-world impact:** governance can introduce arbitrary minting, confiscation, treasury withdrawal, signature authority, or transfer bypass logic.

**VERIFIED.** EIP-1967 implementation-slot read at the proxy returned `0x16d3f65b708883df042d98e1c7a49b32a33e2a14`. `_authorizeUpgrade` is `onlyOwner`; `owner()` returned the Ethereum timelock. The implementation is initializer-locked, but that does not constrain future UUPS implementations.

**Exploit scenario (inference):** the sole governance key schedules `upgradeToAndCall` to code that mints IMDSTR or transfers treasury NFTs/ETH, then executes after 48 hours. **Expected / observed:** immutable holder invariants would survive administration; observed that they are upgrade-dependent. **Recommendation:** publish upgrade bytecode and storage-layout diffs before scheduling; require a multisig; consider a permanently frozen implementation once mature.

### F-03 — High — mutable bridge trust root

**Real-world impact:** hostile peer or verification configuration can mint Robinhood IMDSTR without corresponding adapter custody.

**VERIFIED.** Peers were reciprocal: Ethereum EID 30416 mapped to the Robinhood token, and Robinhood EID 30101 mapped to the Ethereum adapter. Both endpoint delegates were the respective timelocks. Owner-only `setPeer`, `setDelegate`, and `setEnforcedOptions` are inherited; endpoint delegates can set send/receive libraries and configs. Current libraries and DVNs are recorded under “Supply integrity.”

**Exploit scenario (inference):** governance sets the Robinhood peer to an attacker OApp and weakens its receive library/DVNs; authenticated messages from that peer invoke `_credit`, which mints. No rate limiter caps the damage. **Expected / observed:** remote minting remains cryptographically tied to adapter locks; observed that the same 1-of-1 governance can replace that trust configuration after 48 hours. **Recommendation:** multisig governance, mandatory two-DVN minimum enforced operationally, on-chain supply/custody alarms, peer-change alerts, and a daily mint/bridge rate limit or circuit breaker.

### F-04 — Medium — zero-output-protection buybacks

**Real-world impact:** searchers can extract part of the NFT-sale and fee revenue intended for burns by sandwiching predictable public swaps.

**VERIFIED.** Ethereum `_buyAndBurnIMD` calls `swapExactTokensForTokens(amountIn, 0, …)`; Robinhood `_buy` uses a market-wide price limit and no caller-provided minimum output. `processTokenTwap` and `processBuybacks` are public and their bucket sizes/state are public.

**Exploit scenario (reasoned, not executed):** an attacker observes a keeper call, buys first to worsen execution, lets the zero-minimum buyback clear, then sells back. The call succeeds with materially less burn output rather than reverting. **Expected / observed:** expected a TWAP/oracle-derived minimum; observed `amountOutMin = 0`. **Recommendation:** enforce a manipulation-resistant TWAP deviation bound and non-zero minimum output; randomize or auction execution only as a supplement. No live sandwich was attempted under the rules of engagement.

### F-05 — Low — distributor fee bypass

**Real-world impact:** governance can enable fee-free transfers and support a hookless secondary pool, reducing fee revenue and the NFT/burn flows.

**VERIFIED.** Ethereum `setDistributor` is owner-only and `setGlobalDistributor` is factory-only; the launcher owner can change the latter. Robinhood `setDistributor` is owner-only. Ethereum `globalDistributor()` was zero; the launcher agreed. Robinhood `isDistributor(RobinhoodLauncher)` returned false.

**Exploit scenario (inference):** governance allowlists a router or alternate pool address; transfers involving it bypass the canonical PoolManager allowance and therefore the hook fee. **Expected / observed:** expected all economic trading to pay the fee; observed an explicit delayed exemption mechanism. **Recommendation:** restrict distributors to immutable bridge/system addresses, emit and monitor changes, and document that the fee is policy rather than an ERC-20-wide invariant.

### F-06 — Low — incomplete configuration event coverage

**Real-world impact:** monitoring can miss or detect late a hook/fee-address/security-setting change, shortening the practical response window.

**VERIFIED.** `updateHookAddress` and hook `updateFeeAddress` write security-sensitive addresses without dedicated events in the verified source; inherited endpoint configuration emits at lower layers but is not consistently represented by application-specific events. **Recommendation:** emit old/new values for every privileged setting and alert on both application and endpoint events.

### F-07 — Info — shared-decimal dust

**Real-world impact:** bridge requests are rounded down to multiples of 10^12 wei (0.000001 IMDSTR); dust remains with the sender and is not lost.

**VERIFIED.** `sharedDecimals()` returned 6 on both sides while the ERC-20 uses 18 local decimals. The inherited debit path applies `_removeDust`. **Recommendation:** display the rounded amount in UIs and retain `minAmountLD`; no defect was found.

## Architecture and privileges

### Ethereum

- **IMDSeatStrategy proxy/token** — `IMDSeatStrategy → NFTStrategy → BaseStrategy → ERC20/UUPSUpgradeable/Ownable/ReentrancyGuard`. The EIP-1967 proxy points to `0x16d3…2a14`; owner is timelock `0xBd3e…A29D`. Public economic methods are `buyTargetNFT`, `sellTargetNFT`, burn, and TWAP processing. Owner-only: UUPS upgrade, `updateHookAddress`, `setDistributor`, `setSeatOperator`, `sweepToken`, ownership transfer/renunciation. Factory-only (the launcher): name/symbol, relist multiplier, global distributor. Seat operator or owner: `registerAgent`, `authorizeWorker`, revoke authorization.
- **NFTStrategyHook** — non-upgradeable `BaseHook + ReentrancyGuard`; only PoolManager reaches callbacks. Launcher-owner gated: update fee address and lower fee. Fee address or launcher owner can irreversibly renounce the 3% address share. Current fee address is deployer EOA; `defaultFee=1000` bps; minimum is 100 bps.
- **SingleNFTStrategyLauncher** — `Ownable + ReentrancyGuard`; owner is Ethereum timelock. Owner-only: one-shot launch/reserve bridge (already deployed state was not exhaustively reconstructed), name/symbol, multiplier, global distributor, $IMD pool, ownership. Public `processTokenTwap` earns 0.5%. The BurnExecutor destination is constant, but the selected $IMD pool is mutable.
- **IMDSTRAdapter** — `OFTAdapter + Ownable`; locks the Ethereum token and burns 1% in either direction. Owner is Ethereum timelock. Inherited owner powers: peer, delegate, enforced options, message inspector, ownership. Its endpoint delegate (the same timelock) controls LayerZero libraries/config.
- **Timelock** — 48 hours; self-administered; deployer EOA alone has proposer, executor, canceller. Role administration itself must be performed by the timelock.

### Robinhood

- **IMDSTRRobinhood** — non-proxy `OFT + ERC20 + Ownable`; mints only in inherited OFT `_credit`, burns on debit and public `burn`. Owner is Robinhood timelock. Owner-only: one-shot hook, distributors, peer, delegate, enforced options, message inspector, ownership. Hook is `0x50c1…e444`.
- **RobinhoodStrategyHook** — non-upgradeable equivalent fee hook. Fee address is deployer and fee is 1000 bps. Launcher-owner can only lower to 100 bps, update/renounce fee address under the same rules. Callback entry is PoolManager-only.
- **RobinhoodLauncher** — owner is Robinhood timelock. Owner-only: launch, Across fee (capped at 3%), $IMD pool, $IMD burn route, ownership. Public bridging/buyback/burn keeper methods. Across, Ethereum hook recipient, WETH endpoints, and token are constructor-bound.
- **Timelock** — distinct contract at the same address as the Ethereum implementation; do not conflate them. Same 48-hour and sole-EOA roles.

PoolManagers are `0x000000000004444c5dc75cB358380D2e3dE08A90` (Ethereum) and `0x8366a39CC670B4001A1121B8F6A443A643e40951` (Robinhood). They are external Uniswap infrastructure, not owned by these timelocks.

## Fund flows

**Ethereum trade:** exact-input swap → hook takes `abs(output-or-input delta) × fee / 10,000`; token-denominated fee is internally swapped to ETH → 87% calls strategy `addFees`, 10% reaches launcher for public $IMD buy-and-burn, remainder (rounding included) goes to fee address. The owner cannot increase the current hook fee; buys during the post-launch ramp may nevertheless be as high as 99%, declining 100 bps/minute. The one launch-time seeding buy is explicitly fee-free.

**NFT pot restriction (VERIFIED):** `buyTargetNFT` is public and can call any marketplace target except the NFT collection itself, but value cannot exceed `currentFees` or the block-based `getMaxPriceForBuy`; success requires the strategy’s collection balance to increase exactly by one and ownership of the caller-specified ID to become the strategy. Thus pot ETH can be paid to any counterparty only in a transaction that delivers exactly one identity.md NFT. It cannot buy another collection under current code. The relist price is actual ETH cost × multiplier / 1000 (currently 1500 = 1.5×). `sellTargetNFT` requires `msg.value == listed price`, verifies continuing ownership, transfers only then, and deletes the listing. There is no current path to take or buy it below that recorded price; upgrade governance can change this.

Sale ETH accrues to `ethToTwap` and buys/burns IMDSTR. The Ethereum $IMD swap sends output to the hard-coded BurnExecutor, but governance can choose a hostile or illiquid pool and zero minimum output permits bad execution. Therefore “only reaches BurnExecutor” is true for output destination under current code, not an assurance of fair price or immutable future behavior.

**Robinhood trade:** hook sends 3% to the deployer fee address and 97% to the launcher. The launcher divides that 97% as 77/97 pot, 10/97 $IMD buy/burn, and 10/97 IMDSTR buy/burn (integer remainder stays with pot). Once the pot reaches its fixed threshold it calls the fixed Across SpokePool with fixed recipient `ethereumHook`; owner may set relayer haircut only from 0–3%. On failure the amount returns to the pot bucket. Under reviewed code the Robinhood pot share has no alternate destination; Across/relayer and cross-chain liveness remain external trust assumptions.

## Supply integrity and LayerZero

Near-time observations were:

| Quantity | Raw units | Tokens |
|---|---:|---:|
| Ethereum total supply | 995395797284343550000000000 | 995,395,797.28434355 |
| Ethereum adapter balance | 454972103105216000000000000 | 454,972,103.105216 |
| Robinhood total supply | 448751994674948030475952932 | 448,751,994.674948030475952932 |
| custody surplus over remote supply | 6220108430267969524047068 | 6,220,108.430267969524047068 |

This shows **no observed unbacked Robinhood supply**. The surplus is consistent with Robinhood buyback/user burns and bridge fee mechanics: remote burns need not release Ethereum custody unless performed through the bridge. It is not proof over all history, and calls were not atomic across chains. Messages in flight can temporarily alter the comparison.

Both sides use 6 shared decimals and reciprocal peers. Enforced option for message type 1 was `0x000301001101000000000000000000000000000249f0` (150,000 receive gas); type 2 was empty. Ethereum used send library `0xbB2E…dCe1`, receive library `0xc02A…24C2`; Robinhood used `0xC391…2de7`, receive `0xe184…1043`. Receive-library grace flag returned false on each.

Decoded ULN configs (ABI `UlnConfig`) showed two required DVNs, no optional DVNs: Ethereum→Robinhood required `0x589d…236b` and `0xa59b…0ba5` with 15 confirmations; Robinhood→Ethereum required `0x0ffe…fdf8` and `0xd01a…5b12` with 20 confirmations. Receive direction used the same pair with 20 confirmations on Ethereum and 15 on Robinhood. Executor configs were 10,000 max-message-size with executors `0x1732…3059` (Ethereum send) and `0x4208…0a0b` (Robinhood send).

`setPeer` alone is insufficient if honest DVNs reject attacker packets, but the same timelock is endpoint delegate and can change the libraries/DVNs. That combined power is F-03. No compose payload is used by this deployment (`composeMsg` is empty in launch reserve flow), but inherited OFT compose support remains part of the library surface.

## Hook and pool analysis

The address flags and `getHookPermissions` enable before-initialize, after-add-liquidity, after-swap, and after-swap-return-delta. They do **not** enable before-swap-return-delta, so the classic v4 “claim the entire input before swap” NoOp rug vector is absent. `BaseHook` applies `onlyPoolManager` to external callbacks. The hook’s internal sell-to-ETH swap re-enters PoolManager while the hook’s reentrancy guard is active; its nested callback completes accounting, and no permissionless inconsistent-state extraction was reproduced.

Exact-output swaps revert. Fee arithmetic rounds down (`amount × bps / 10,000`); sub-10,000/base-fee-unit fragments may pay zero, but splitting is constrained by gas and pool rounding and no profitable exploit was demonstrated. Distribution rounding remainder goes to the 3% address on Ethereum and pot on Robinhood.

With no distributor exemption, ordinary transfers revert unless mint/burn or a PoolManager transfer consumes transient allowance granted by the configured hook. Another pool using the same PoolManager cannot manufacture that allowance because its callback is not the configured hook; a canonical-pool direct PoolManager swap still invokes the hook. An allowlisted distributor can move tokens freely and can seed/use other venues, hence F-05.

## Owner, deployer, and seat-operator risk

| Actor/action | Holder impact | Timing |
|---|---|---|
| Either timelock: transfer/renounce ownership | changes or can freeze administration | 48h |
| Ethereum timelock: UUPS upgrade | arbitrary token/treasury behavior, including mint/confiscation | 48h |
| Ethereum timelock: hook address, distributors/global distributor | disable fee operation or enable fee bypass | 48h |
| Ethereum timelock via launcher: multiplier 1.1×–10×, name/symbol, $IMD pool | changes NFT economics/metadata; can degrade burns | 48h |
| Ethereum timelock: seat operator; sweep non-collection/non-IMDSTR ERC-20 | controls seat rewards and registration; cannot sweep pot/NFT/token under current code | 48h |
| Both timelocks/OFT delegates: peer, libraries, DVNs, options, inspector | can halt bridge or create hostile mint path | 48h |
| Robinhood timelock: distributors/hook (hook once), Across fee, buyback pool, $IMD burn route | fee bypass, degraded/rerouted burns; Across fee capped 3% | 48h |
| Deployer as timelock proposer/executor/canceller | initiates, completes, or cancels all of the above; no co-signer | action then 48h |
| Deployer as hook fee address | receives 3%; may instantly renounce it to buyback, but cannot redirect it alone | instant only for renunciation |
| Deployer as seat operator | register agent; approve/revoke scoped worker digest | instant |

ERC-1271 is materially scoped: `authorizeWorker` constructs the digest itself, requires wallet=this strategy, unexpired authorization, and current ownership of the specified identity.md NFT. `isValidSignature` ignores supplied signature bytes but returns magic only for the pre-approved digest while ownership continues. The operator cannot approve arbitrary permit/order hashes through this function. There is no nonce consumption in the strategy; expiry/nonce semantics rely on the external IMD verifier, while revocation and loss of NFT ownership invalidate locally.

## Code review and comparison with TokenWorks

The fork preserves the original NFT acquisition postconditions, exact-price relisting, block-rising buy cap, transfer restriction, transient PoolManager allowance, and UUPS shape. Observed IMD-specific differences are: `IMDSeatStrategy` seat registration/ERC-1271/sweep/burn; configurable global distributor rather than TokenWorks’ hard-coded handler; 87/10/3 fee split; fee can only be lowered; permanent fee-share renunciation; $IMD BurnExecutor routing; Robinhood reserve/OFT/Across branch; and a 1% adapter bridge burn. The original implementation and hook were reviewed from [verified implementation](https://etherscan.io/address/0x31e9De22a08bafA9a8E292087bE9dace2E6442a2#code) and [verified hook](https://etherscan.io/address/0x8fb66C6E0f3cbb25001e0f1C0352Cc888cFF6444#code); this was a source comparison, not a claim that upstream is audited or safe.

Standard-pitfall conclusions:

- Reentrancy guards protect NFT buy/sell, public buybacks, and bridged-fee deposit. External marketplace calls occur before accounting but strict before/after NFT ownership and balance checks constrain the result. No reentrancy exploit was reproduced.
- NFT transfers use `transferFrom`, not safe transfer; this avoids receiver callback reentrancy but buyers that are incompatible contracts can strand their purchase by their own call. The strategy’s acquisition postcondition is balance `+1`, so batch delivery or receiving a different ID fails atomically.
- ERC-20 approval is standard; reserve bridging approves the adapter for an exact one-shot amount. Permit2 approvals used for liquidity are broad and rely on immutable system addresses.
- OFT debit removes dust and honors `minAmountLD`; no rate limit exists. There is no custom compose-message handler in application code.
- `buyTargetNFT` permits an arbitrary marketplace target. This does not let value leave without exactly one target-collection NFT arriving, but it intentionally does not validate market price beyond the block-based cap. Keepers/users can choose which listed identity NFT is bought.
- Missing events are F-06. No unchecked downcast, supply-expanding public mint, fee-raising function, or owner NFT-withdraw function was found in the reviewed implementation.

## Reproduction record and limitations

All finding evidence was observed at 2026-09-25 UTC. Reproduce state with `cast call <address> '<signature>' [args] --rpc-url <public RPC>`; exact calls and observed values are stated in each section. The implementation used `cast storage <proxy> 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`. Role IDs were `keccak256("PROPOSER_ROLE") = 0xb09a…9cc1`, executor `0xd8aa…9e63`, canceller `0xfd64…f783`. Peer inputs were EIDs 30416 and 30101. LayerZero config was read with `getSendLibrary`, `getReceiveLibrary`, and library `getConfig(eid,oapp,type)` (types 1 and 2). Source-location evidence is reproducible at the linked explorer `#code` pages; line numbers refer to the named verified source file rather than the explorer’s combined-page numbering.

Public RPCs: Flashbots answered `eth_blockNumber` but rejected `eth_call` as not whitelisted, so Ethereum calls rotated to `ethereum-rpc.publicnode.com`. Robinhood reads used `rpc.ordofi.network`; its full timelock log query returned `-32005`, and `robinhood-rpc.publicnode.com` refused the archive range without a token. Consequently Robinhood pending operations, a complete historical bridge-message reconciliation, and an atomic two-chain supply snapshot remain **unanswered**. No claim in the findings depends on assuming those unavailable results.

This report is a bounded source/state review, not formal verification. No transaction, exploit, signature, payment, load test, or volume fuzzing was performed.
