# IMDSTR cross-chain security investigation record

**Scope/snapshot.** Read-only work performed 2026-09-25 08:57–09:08 UTC. Ethereum chain ID 1: block **26,053,418**, `https://ethereum-rpc.publicnode.com`. Robinhood chain ID 4663: block **72,101,715**, `https://rpc.mainnet.chain.robinhood.com`. Requests were paced above one second and stayed below 300. The first Ethereum provider (`https://rpc.flashbots.net/fast`) accepted chain/block reads but rejected historical `eth_call` with HTTP 403, `rpc method is not whitelisted`; it was replaced once. No transactions, signatures, payments, load tests, or private-data access occurred.

Labels: **VERIFIED** means observed in verified source or RPC; **SUSPECTED** means code reasoning only; **UNANSWERED** means evidence was unavailable. Exact reproduction is in §9.

## Plain-language answer

**VERIFIED.** Ordinary holders cannot mint IMDSTR, freely transfer around its designated pool, take a treasury NFT below its recorded price, or directly spend the NFT pot on a non-identity.md asset. Ethereum IMDSTR is nevertheless upgradeable. One deployer EOA is the only proposer, executor, and canceller on both 48-hour timelocks. After the delay it can replace the Ethereum token logic; change exemptions, hook, bridge peer, libraries and DVNs; redirect infrastructure; change NFT resale multiple; and replace the seat operator. The same EOA is the current seat operator and 3% fee recipient. It can immediately operate Identity.MD seats and permanently renounce its fee share. This is a delayed single-key system, not multisig governance.

The bridge was not under-collateralized at the snapshots: Ethereum held more tokens than Robinhood supply. It held **15,395,558.020534969524047068 IMDSTR extra**, contradicting a source comment that the figures always equal. That surplus needs event-history reconciliation; it is not itself proof of theft or unbacked minting.

## 1. Architecture and privileges

### Ethereum

**VERIFIED — token/strategy.** `IMDSeatStrategy -> NFTStrategy -> BaseStrategy -> Initializable, UUPSUpgradeable, Ownable, ReentrancyGuard, ERC20`. RPC returned owner `0xBd3ed9F4AbD9946cA6F59C8F13A3EbebDE1EA29D`, implementation `0x16D3f65B708883DF042d98E1C7a49B32A33E2A14`, collection `0x0000eC93127BAA929E58E97dd0095A2BFb38ec1D`, hook `0xA16026A28aA581AA96713d20C608Da7F8db86444`, factory `0x1216eDc56A93CC8f7bE67E1cBaD264AB2803FCff`, router `0x00000000000044a361Ae3cAc094c9D1b14Eece97`, PoolManager `0x000000000004444c5dc75cB358380D2e3dE08A90`, and seat operator/deployer `0x35da9c0303507ddf708e87f2568eddf12c47a059`. `getImplementation()` confirms the UUPS slot. [Verified source](https://eth.blockscout.com/address/0x16d3f65b708883df042d98e1c7a49b32a33e2a14?tab=contract).

| Contract | Privileged functions | Caller now | Delay |
|---|---|---|---|
| IMDSTR | `upgradeToAndCall`, ownership, `updateHookAddress`, `setDistributor`, `setSeatOperator`, `sweepToken` | ETH timelock | 48h |
| IMDSTR | name/symbol, `setPriceMultiplier`, `setGlobalDistributor` | launcher (timelock-owned) | 48h |
| IMDSTR | `authorizeWorker`, revoke, `registerAgent` | seat operator or owner | instant for operator |
| IMDSTR | `increaseTransferAllowance`, `addFees` | configured hook | callback only |
| Launcher | name/symbol/multiplier/distributor, `updateImdPool`, one-time `bridgeReserve`, ownership | ETH timelock | 48h |
| Hook | `updateFeeAddress`, `lowerFee` | launcher owner/timelock | 48h |
| Hook | `renounceFeeAddress` | fee EOA or timelock | instant for EOA |
| Adapter/OApp | peer, delegate, options, inspector, preCrime, ownership; Endpoint library/DVN config | timelock owner/delegate | 48h |

Live `globalDistributor=0`. `isDistributor` was false for hook, launcher and deployer, true for adapter. The full mapping is not enumerable. Multiplier 1500 (1.5x), buy increment 0.01 ETH, TWAP increment 1 ETH/delay 1 block, `currentFees=1.118737278364655259 ETH`, `availableFunds=getMaxPriceForBuy=1.02 ETH`.

**VERIFIED — hook.** `BaseHook, ReentrancyGuard`, not Ownable; authority is dynamically the launcher owner. Fee recipient was deployer, default 1000 bps, minimum 100 bps, not renounced. `lowerFee` requires a strict decrease, so current code cannot raise it. Enabled flags: beforeInitialize, afterAddLiquidity, afterSwap, afterSwapReturnDelta. [Verified source](https://eth.blockscout.com/address/0xA16026A28aA581AA96713d20C608Da7F8db86444?tab=contract), `NFTStrategyHook.sol:151–178,243–261`.

**VERIFIED — launcher.** `Ownable, ReentrancyGuard`, owned by timelock; wired to listed strategy, implementation, hook. Robinhood reserve 300m and `reserveBridged=true`. IMD pool fee 10000, spacing 200, no hook. [Verified source](https://eth.blockscout.com/address/0x1216eDc56A93CC8f7bE67E1cBaD264AB2803FCff?tab=contract).

**VERIFIED — adapter.** `OFTAdapter/OApp`, owner and Endpoint delegate are the timelock. Shared decimals 6, conversion `10^12`; inspector/preCrime zero. Peer at Robinhood EID 30416 is Robinhood IMDSTR. Enforced type-1 options `0x000301001101000000000000000000000000000249f0`; type 2 empty. [Verified source](https://eth.blockscout.com/address/0x6BDca0523530D4B1Cc2e7e6043ce4c5adfC56C51?tab=contract).

**VERIFIED — timelock.** `getMinDelay=172800`. The deployer alone was true among tested accounts for PROPOSER, EXECUTOR, CANCELLER; zero address and timelock were false. Tested accounts were false for TIMELOCK_ADMIN_ROLE; its admin is DEFAULT_ADMIN_ROLE, consistent with self-administration after bootstrap. One key can schedule, wait, execute or cancel, but tested roles do not bypass time.

### Robinhood

**VERIFIED.** At the same address, `IMDSTRRobinhood` is a non-proxy `OFT`, not Ethereum's implementation. Owner `0x16D3…2A14`, Endpoint `0x6F475642a6e85809B1c36Fa62763669b1b48DD5B`, 6 shared/18 local decimals, conversion `10^12`, inspector/preCrime zero. Owner-only: one-time `setHook`, `setDistributor`, peer/delegate/options/inspector/preCrime, ownership. Public `burn` only burns caller funds. [Verified source](https://robin.etherscan.io/address/0x0000198C940D8cD70Cb9ACeC5E3af8216ac57d2F#code), `src/robinhood/IMDSTRRobinhood.sol:1–77`.

**VERIFIED.** Robinhood hook fee recipient/deployer, fee 1000, minimum 100, not renounced; PoolManager `0x8366a39CC670B4001A1121B8F6A443A643e40951`; permissions match Ethereum. [Source](https://robin.etherscan.io/address/0x50c1927c08340b1BbC3fD3C448eEe9aaa937e444#code). Launcher owner is Robinhood timelock and hook is listed target; live IMD pool fee/spacing 10000/100. [Source](https://robin.etherscan.io/address/0xfdA3fB00662011287AFc6d0c958AC73D8e7Cae62#code). Owner controls pool and bridge configuration; permissionless processors use fixed state.

**VERIFIED — timelock.** Delay 172800; same deployer alone has proposer/executor/canceller among tested accounts. Endpoint delegate is the timelock. **UNANSWERED:** Timelock operations are not enumerable; no candidate IDs were supplied and full CallScheduled history was not scanned, so no assertion is made about pending operations.

## 2. Fund flows

**VERIFIED — Ethereum fee.** Hook `NFTStrategyHook.sol:203–218` floor-rounds 87% to `strategy.addFees`, 10% to launcher, remainder (nominal 3%, including dust) to feeAddress. Token fees are internally swapped to ETH (`:313–407`). Launcher `processTokenTwap` spends up to 1 ETH, rewards caller 0.5%, swaps the rest with minimum output zero, and sends output directly to fixed BurnExecutor (`SingleNFTStrategyLauncher.sol:270–295,350–374`). Owner may change pool coordinates, not compiled recipient without new code.

**VERIFIED — NFT pot.** Anyone can call `buyTargetNFT`, but `NFTStrategy.sol:149–206` caps value by `currentFees` and `getMaxPriceForBuy`, rejects collection as call target, requires collection balance +1 and ownership of exact expected identity.md token. Target/calldata are arbitrary, but ETH cannot leave without those postconditions; actual balance reduction is charged. Maximum rises 0.01 ETH per Ethereum block (`BaseStrategy.sol:285–294`).

**VERIFIED — resale.** `NFTStrategy.sol:195–235` stores `cost*multiplier/1000`; buyer must pay exact stored price and ownership is rechecked. `sweepToken` rejects collection and IMDSTR. Factory can prospectively choose 1.1x–10x but cannot modify an existing `nftForSale`. **SUSPECTED:** a delayed upgrade can remove these limits.

**VERIFIED source / UNANSWERED history — Robinhood pot.** Robinhood hook/launcher route the pot share through `_bridgeToEthereum`; Ethereum hook unwraps all received WETH and sends its entire ETH balance to its single configured strategy (`NFTStrategyHook.sol:181–197`). No end-to-end bridge transaction was reconstructed, so exclusivity is source-supported, not historically proven.

## 3. Supply integrity and LayerZero

**VERIFIED snapshot.** Robinhood `totalSupply=439576545084681030475952932`; Ethereum adapter balance `454972103105216000000000000`; surplus lock `15395558020534969524047068` units. This is over-collateralization, not current unbacked supply, but differs from the adapter comment that locked balance “always equals” remote supply (`IMDSTRAdapter.sol:13–18`). Blocks are near-contemporaneous, not atomic; in-flight messages/history were not enumerated.

**VERIFIED.** Adapter `:29–67` removes `10^12` dust, charges 1% on debit (gross lock then fee burn), and on credit burns 1% locked tokens then transfers net. Robinhood OFT mint/burn is Endpoint-authenticated. No compose option or custom compose handler is configured.

Peers: Ethereum EID 30416 -> Robinhood token; Robinhood EID 30101 -> Ethereum adapter. Owners can change peers; Endpoint delegates (timelocks) can change libraries/DVNs. Thus a stolen deployer key can schedule malicious peer/security changes, wait 48h, then make the new trusted path mint without Ethereum lock. Current configuration:

| Path | Library | Confirms | Required DVNs | Optional |
|---|---|---:|---|---|
| ETH send | `0xbB2Ea70C9E858123480642Cf96acbcCE1372dCe1` | 15 | `0x589d…236b`, `0xa59b…0ba5` | none |
| ETH receive | `0xc02Ab410f0734EFa3F14628780e6e695156024C2` | 20 | same | none |
| RH send | `0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7` | 20 | `0x0ffe…fdf8`, `0xd01a…5b12` | none |
| RH receive | `0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043` | 15 | same | none |

## 4. Hook and pool

**VERIFIED.** Hook only allows launcher pool initialization (`NFTStrategyHook.sol:264–280`). Tokens reject ordinary transfers except mint/burn, distributor involvement, or PoolManager transfers covered by transient hook allowance (`BaseStrategy.sol:393 onward`; Robinhood token `:49–77`). Another pool/router or direct PoolManager call cannot move tokens unless a distributor is one side or authorized hook grants allowance in that transaction. Owner-allowlisted distributor OTC movement is fee-free by design.

Exact-output swaps revert. Fee is floor `abs(amount)*fee/10000`; tiny swaps may pay zero. Current hook cannot raise fee. Buy fee starts at 99% and falls 100 bps/minute to default. BaseHook validates PoolManager caller. `afterSwap` takes fee, may perform nested swap, then settles; external payouts and pot deposits are guarded, as are NFT/TWAP operations. **SUSPECTED:** nested-swap/arbitrary-marketplace paths merit fork invariants; no reproducible accounting or reentrancy failure was observed and high-volume fuzzing was prohibited.

## 5. Holder-impact powers

**VERIFIED.** Deployer can instantly authorize/revoke workers, register agents, receive 3%, and as fee recipient permanently renounce that share to IMD buy-and-burn. Updating the fee address itself requires the timelock. It can propose/execute/cancel every delayed action on both chains.

After 48h it can upgrade Ethereum IMDSTR to arbitrary compatible code (thus mint/confiscate/change pot, NFT, signature and transfer behavior); change hook/distributors/seat operator/metadata; sweep non-collection/non-IMDSTR ERC20s; lower fee; redirect fee recipient; alter relist multiplier; repoint zero-min IMD pool; change LayerZero peers/options/inspector/preCrime/libraries/DVNs; change Robinhood distributors/launcher bridge configuration; and transfer/renounce ownership.

**VERIFIED ERC-1271.** `IMDSeatStrategy.sol:143–217` constructs a chain/collection-bound EIP-712 digest, requires wallet=this, future expiry and current NFT ownership, then records only that digest. Signature bytes are ignored, but magic is returned only for an approved digest while NFT remains held. Operator cannot approve arbitrary permit/order hashes through present code, but can pair arbitrary devices for treasury NFTs. Expiry is not rechecked in `isValidSignature`; comments rely on IMD enforcement.

## 6. Code-review observations

**VERIFIED fork additions:** burn-to-zero allowance, mutable global distributor override, seat/agent/ERC-1271, ERC20 sweep, 1% bridge burn, fee-free opening buy, Robinhood reserve, 87/10/3 split, bridged-fee deposit, fixed IMD BurnExecutor. A full semantic diff against TokenWorks original was not completed; these are explicit `IDMD fork` sections/additions.

- Shared-decimal dust: 18 local/6 shared; below `10^12` removed. Caller min amount protects net quote.
- No application rate limit. Security rests on peer and Endpoint stack.
- No custom compose handler/type-2 option.
- SafeERC20 is used by adapter; one-time reserve approves its balance.
- Fee math floors; split remainder reaches fee address. Tiny fee/bridge amounts may round to zero. No profitable loop demonstrated.
- Missing dedicated events: hook `updateFeeAddress` (`:151–158`); BaseStrategy hook/name/symbol/globalDistributor setters (`:218–279`).
- Marketplace call is arbitrary but guarded with strict NFT postconditions.
- Both buy-and-burn routes use `amountOutMin=0`, creating MEV/value-loss exposure.

## 7. Findings

### F-01 — Single EOA controls both delayed governance domains

**High — one key compromise can, after 48h, enable confiscation, dilution, fee diversion or unbacked remote minting.** **VERIFIED**, timelock role state; `BaseStrategy.sol:204–223`; inherited OApp controls. Scenario: stolen key schedules malicious UUPS or peer/DVN change, waits 172800s, executes. Recommendation: threshold multisig, separate proposer/canceller, and monitoring of schedules/peer/library/config. Reproduce R1–R4 below; expected resilient governance would require independent approvals.

### F-02 — Lock and remote supply do not match documented equality

**Low — unexplained surplus collateral weakens solvency monitoring and may hide stranded funds/accounting errors.** **VERIFIED**, `IMDSTRAdapter.sol:13–18,29–67`. Observed values differ by 15.396m; expected by comment is equality plus explicitly quantified in-flight GUIDs. Recommendation: reconcile OFTSent/OFTReceived/BridgeFeeBurned history and publish an in-flight-aware invariant. Reproduce R5–R6. This is not observed insolvency.

### F-03 — Zero-minimum-output buy-and-burn is MEV-exposed

**Medium — sandwiches can reduce tokens burned for the same treasury ETH.** **VERIFIED code condition**, `SingleNFTStrategyLauncher.sol:350–374`, `BaseStrategy.sol:368–390`. Public processing routes to swaps with min output 0. Recommendation: manipulation-resistant TWAP minimum, impact cap, protected keeper execution. Reproduce by opening linked source and following `processTokenTwap`; no exploit transaction was sent.

### F-04 — Incomplete configuration event coverage

**Low — log-only monitoring may miss material changes.** **VERIFIED**, hook `updateFeeAddress:151–158`; BaseStrategy setters `:218–279`. Assignment succeeds without dedicated emit. Recommendation: indexed old/new events and meanwhile decode timelock calldata.

### F-05 — ERC-1271 expiry not checked at validation time

**Info — a consumer that omits its own expiry check can accept an expired approved digest.** **VERIFIED**, `IMDSeatStrategy.sol:146–157,208–217`. Authorization checks expiry once but stores only tokenId+1; validation checks ownership only. Recommendation: store/check expiry on-chain or formally test every consumer. This is integration risk, not a demonstrated current backend bypass.

## 8. Uncertainty/unanswered

- Pending timelock operations and complete distributor membership were not enumerated.
- Robinhood Blockscout API returned Cloudflare HTML; verified source came from Etherscan HTML. Guessed launcher getters that reverted were excluded.
- No LayerZero/Across history scan, so surplus and Robinhood-pot history remain unreconciled.
- No fork transaction or volume fuzzing. Further reentrancy/accounting conclusions are SUSPECTED.
- DVN addresses/config are verified; organizational independence is not.

## 9. Reproduction ledger

Form: `cast call <address> '<signature>' <inputs> --block <block> --rpc-url <URL>`, timestamp window above.

| ID | Exact target/call | Result |
|---|---|---|
| R1 | each timelock `getMinDelay()` | `172800` |
| R2 | each timelock `hasRole(keccak256("PROPOSER_ROLE"),deployer)`; EXECUTOR/CANCELLER likewise | all true |
| R3 | same roles with zero and timelock | all false |
| R4 | admin role `0x5f58e3a2…6ca5` for tested addresses | false |
| R5 | ETH token `balanceOf(adapter)` | `454972103105216000000000000` |
| R6 | RH token `totalSupply()` | `439576545084681030475952932` |
| R7 | ETH token `getImplementation()`, `owner()` | implementation and ETH timelock above |
| R8 | ETH adapter `peers(30416)` | bytes32 RH token |
| R9 | RH token `peers(30101)` | bytes32 ETH adapter |
| R10 | both sides `sharedDecimals()`, `decimalConversionRate()` | 6, `1000000000000` |
| R11 | each Endpoint `delegates(oapp)` | respective timelock |
| R12 | Endpoint `getSendLibrary(oapp,eid)`, `getReceiveLibrary(oapp,eid)` | §3 libraries |
| R13 | each library `getConfig(eid,oapp,2)` | ULN tuple in §3 |
| R14 | ETH hook fee/min/address/renounced | 1000, 100, deployer, false |
| R15 | both hooks `getHookPermissions()` | `(true,false,false,true,false,false,false,true,false,false,false,true,false,false)` |
| R16 | ETH token `isDistributor` for hook/adapter/launcher/deployer | false/true/false/false |
| R17 | ETH collection/hook/factory/poolManager/seatOperator getters | TARGET addresses; deployer operator |
| R18 | launcher reserveBridged/reserve | true / 300m tokens |

Role hashes: proposer `0xb09aa5ae…9cc1`, executor `0xd8aa0f31…9e63`, canceller `0xfd643c72…f783`. Ethereum sources were fetched from `https://eth.blockscout.com/api/v2/smart-contracts/<address>` and linked explorer pages. Robinhood EID 30416 was cross-checked using [Paxos network documentation](https://docs.paxos.com/guides/stablecoin/usdg/mainnet); live peers/config came from RPC.
