myXRP
Claude Opus

Contract audit · 컨트랙트 감사

Claude Opus

claude-opus-5-thinking-high

3 October 2026 · MyXRPBridge on Flare

Contract: MyXRPBridge Network: Flare Mainnet, C-chain (EVM chain id 14, confirmed via net_version) Address: 0x14b9DD90edaAff6527c1b168550B3FAA15b9F6AF Underlying asset: FXRP 0xAd552A648C74D49E10027AB8a618A3ad4901c5bE Compiler (per verified metadata): v0.8.28+commit.7893614a, optimizer enabled, 200 runs, EVM target paris, metadata hash ipfs Runtime bytecode size: 2,917 bytes State read at: block 71,252,013 (0x43f382d), 3 October 2026, ~21:37 UTC RPC used: https://flare-api.flare.network/ext/C/rpc Explorer used: https://flare-explorer.flare.network (Blockscout instance for Flare mainnet)


1. Engagement

This is a single-contract review of a sealed, non-custodial 1:1 wrapper. The deliverable is a public technical opinion on whether the deployed code does what its source says, what powers exist over user funds, and what residual risk a holder of myXRP carries.

The review was conducted against the deployed bytecode and live chain state, not only against a source listing. Every claim below that concerns the live contract was produced by an eth_call, eth_getCode, eth_getStorageAt, log query, or explorer verification record executed during the engagement. Where a fact could not be established on chain, this report says so explicitly rather than inferring it.

No legal, regulatory, operational, or corporate matter is in scope. This report makes no statement about any licence, registration, company number, office, or counterparty, because none of those can be verified from chain data. No statement is made about the economics of holding myXRP, the value of FXRP, or any off-chain arrangement.

Scope boundary on identity. This audit covers the single address 0x14b9DD90edaAff6527c1b168550B3FAA15b9F6AF and nothing else. It is not an opinion on any other deployment, any front end, any other contract that may share the name myXRP, or on which contract any particular website or interface points at. A reader who wants to rely on this report must check that the address they are interacting with is byte-for-byte the address above.


2. Scope

In scope

ItemDetail
Source under reviewSingle file, MyXRPBridge.sol, 124 lines as verified on chain
Deployed runtime2,917 bytes at the address above
Creation transaction0xee361ab34728ed5a817ff9db70b67573ff02f029ae47a8b7e0242e9bec4485e5
Constructor argumentsfxrp = 0xAd552A648C74D49E10027AB8a618A3ad4901c5bE, maxAssets_ = 10000000000000
External dependencyFXRP token contract, reviewed only to the depth needed to state the trust assumption it imposes

Out of scope


3. Method

  1. Bytecode retrieval. eth_getCode at latest returned 2,917 bytes; keccak256 of the runtime is 0x56ab269123202ad1d7915d7ed85ac8c6bd2fcd67146cf7bef775342b7d7c4d19.
  2. Verification cross-check. The explorer's stored deployed_bytecode for this address was fetched and compared byte-for-byte against the live eth_getCode result. They are identical. The explorer reports is_verified: true, is_fully_verified: true, is_partially_verified: false, is_changed_bytecode: false, proxy_type: null.
  3. Source comparison. The verified on-chain source was downloaded and diffed against the source supplied for review. After stripping comments and normalising whitespace, both reduce to 96 identical statement lines. The only differences are NatSpec/comment text. The code under review is the code that is deployed.
  4. Disassembly. The runtime was disassembled (1,697 instructions over the 2,864-byte executable region; the trailing 53 bytes are the CBOR metadata block declaring solc 0.8.28). The dispatch table was enumerated by walking PUSH-aware, and every PUSH4 immediate was resolved.
  5. Revert-site reconciliation. Three error selectors did not appear as literal four-byte strings in the runtime. Disassembly resolved this: the optimizer emits them pre-divided, as PUSH4 x; PUSH1 0xe3; SHL, i.e. x << 227. 0x1e9acf17 << 3 = 0xf4d678b8 (InsufficientBalance), 0x12171d83 << 3 = 0x90b8ec18 (TransferFailed), 0x1bb5f5b3 << 3 = 0xddafad98 (NotOpen). All nine declared errors are present, at the expected multiplicity (two InsufficientBalance sites, two TransferFailed, two ZeroAmount, two Reentered, one each of the rest). No undeclared revert path exists other than the standard Panic(uint256) handler.
  6. Opcode census over the executable region, PUSH-aware.
  7. Live state reads of every public getter, plus raw storage slots 0–4 and the EIP-1967 and EIP-1822 proxy slots.
  8. Negative testing by eth_call: seventeen probes against invalid input, absent privileged functions, empty calldata, and an unknown selector, each matched against the expected revert selector.
  9. Positive simulation. Full redemption was simulated from the live holder of the entire supply, at latest, to prove the exit path is open and funded.
  10. Historical reconstruction. All contract logs and all token transfers since deployment were enumerated. The dataset is small enough to be exhaustive rather than sampled.

Method limitation, stated plainly. A local solc 0.8.28 recompilation from source to compare artefacts apart from the explorer was not performed; no compiler was available in the review environment and the review did not install one. Source-to-bytecode equivalence therefore rests on (a) the explorer's full-match verification record and (b) the disassembly in step 4–5, which confirmed that the deployed dispatch table, immutables, storage layout, external-call sites, event topics, and complete error catalogue correspond one-to-one with the reviewed source and contain nothing additional. These two lines of evidence agree. A reader who requires a third should recompile with the published settings (0.8.28, optimizer on, 200 runs, EVM paris, metadata ipfs).


4. Recorded on-chain facts

All values read at block 71,252,013 unless noted.

PropertyValue readNotes
name()"myXRP"ABI-encoded dynamic string, offset 32, length 5
symbol()"myXRP"Identical to name()
decimals()6Matches FXRP's 6
underlying()0xAd552A648C74D49E10027AB8a618A3ad4901c5bEEquals the stated official FXRP address
maxAssets()1000000000000010,000,000.000000 FXRP
totalSupply()2500000025.000000 myXRP
room()99999750000009,999,975.000000
FXRP balanceOf(bridge)2500000025.000000 FXRP
OPENS_AT()17907804002026-09-30 15:00:00 UTC = 2026-10-01 00:00:00 Asia/Seoul
Native FLR balance0Contract is non-payable
Verified sourceYes, full matchsolc 0.8.28, single file, no libraries, no additional sources
owner() / admin() / implementation() / proxiableUUID() / paused() / fee() / oracle()All revertNo such functions; selectors absent from the runtime
EIP-1967 implementation / admin / beacon slots0x0All three empty
EIP-1822 proxiableUUID slot0x0Empty
Storage slot 025000000totalSupply
Storage slots 1, 20x0Mapping base slots, correctly empty
Storage slot 30x0locked — reentrancy flag is at rest, not stuck
Storage slot 40x0Unused; no hidden state

Immutables. underlying and maxAssets are immutable, so they live in code, not storage. This is confirmed in the runtime: the FXRP address and 0x09184e72a000 (10e12) appear as code constants spliced at the eight immutable reference offsets recorded in the creation bytecode. OPENS_AT likewise appears as the literal PUSH4 0x6abd23f0 (= 1,790,780,400) at both its read sites. None of these three values can ever change.

Deployment. Created in block 70,992,363 at Unix time 1790780404, i.e. 2026-09-30 15:00:04 UTC — four seconds after OPENS_AT. Deployer: EOA 0x998718199D8aF65ed3a1cb03cfEc2faF7c301eFb, nonce 0, gas used 685,201, status success. That account's transaction list on this explorer contains exactly one transaction: this deployment. The deployer holds no role in the contract, because the contract has no roles.

Complete history. Three state-changing user transactions exist, all on 2026-09-30:

Time (UTC)ActionAmountTransaction
20:06:06deposit → mint25.0000000x90e45c9ebe46e5e7e5f4f9b5e49ad39cf2df47e6736574d5e941e1022e3b669e
20:20:10withdraw → burn25.0000000x77389a139de45485535256f2bc1c41c05ccce8a3ac7fe68dfea929911fa698f1
20:21:01deposit → mint25.0000000xfaac95e3812ec784df113609f0ce9b5c1de7c4bf7365d75c6f288b767f48036b

Each is matched by an FXRP transfer in the same transaction, in the same direction, for the identical amount. Six logs total (three Transfer, one Deposit, one Withdraw, one Deposit). One holder, 0x38A22D748582a98695489400fc46734848b37CAC, holding the whole 25.000000 supply. The round trip in the middle of that sequence is useful evidence: it demonstrates that redemption worked in production, not only in simulation.

Live redemption simulation. withdraw(25000000) called from the holder at latest returns successfully (0x). withdraw(25000001) reverts InsufficientBalance(). The exit is open and exactly funded.


5. System overview

The contract is a sealed 1:1 wrapper, not a vault, not a yield instrument, and not a cross-chain messaging bridge. There is no bridge in the message-passing sense: no relayer, no validator set, no signature verification, no light client. The word "bridge" here describes a token-for-token wrapper on a single chain.

Mechanically:

There is no interest, no index, no rebase, no share price, no exchange rate, no oracle, no price feed, no FTSO read, no fee, no treasury, no operator withdrawal, no allowlist, no blocklist. The redemption rate is fixed at 1:1 by construction, in code, permanently.


6. Trust model

This is the most important section for a holder, so it is stated without hedging.

6.1 What you do not have to trust

There is no privileged party. This was tested, not assumed:

Conclusion: no one — including the deployer — can mint myXRP without depositing FXRP, move another holder's balance, extract FXRP from the contract, pause it, upgrade it, or change its parameters. The contract is immutable and ownerless. This is a strong property and it is the main thing this audit confirms.

6.2 What you do have to trust

The wrapper's solvency is only as good as the asset it wraps. FXRP is not immutable:

These are properties of the Flare FAssets system under its own governance. They are out of audit scope, but they are in the trust model, and the bridge has no defence against them: it cannot pause, cannot migrate, cannot be upgraded, and holds no reserve. A holder of myXRP is a holder of FXRP with one extra immutable contract in between.


7. Function-by-function behaviour

constructor(address fxrp, uint256 maxAssets_)

Rejects a zero token address (ZeroAddress) and a zero cap (ZeroAmount), then writes both to immutables. No owner is recorded. No tokens are minted. The constructor is the only moment at which any parameter of this system was settable, and it has passed. Constructor arguments decode to the official FXRP address and 10000000000000; the explorer's decoded arguments agree.

room() → uint256

maxAssets - totalSupply, floored at zero by an explicit >= guard rather than relying on checked arithmetic. Returns 9999975000000. Pure view, no external call.

deposit(uint256 amount) — nonReentrant

Ordered: time gate → zero gate → cap gate → external pull → mint.

The external call precedes the state write, so this function is not strict checks-effects-interactions. It is nonetheless safe here, for two separate reasons, both verified: the function is reentrancy-locked, and totalSupply += amount / balanceOf[msg.sender] += amount compile to an SLOAD at that point rather than reuse of a stale cached value, so no lost update is possible even if a hypothetical callback mutated those slots. See I-03.

withdraw(uint256 amount) — nonReentrant

Ordered: zero gate → balance gate → liquidity gate → burn → external push.

approve(address spender, uint256 amount)

Unconditional absolute set, emits Approval, returns true. approve(address(0), 1) is permitted and returns true — harmless, since the zero address cannot transact, but noted as I-09.

transfer(address to, uint256 amount)

Delegates to _transfer(msg.sender, to, amount), returns true. No external call, therefore no reentrancy surface, therefore correctly not locked.

transferFrom(address from, address to, uint256 amount)

Reads the allowance, reverts InsufficientAllowance if short (confirmed live), decrements unless the allowance is type(uint256).max. The infinite-allowance exemption is the conventional gas optimisation and is not a defect. Returns true.

_transfer(address from, address to, uint256 amount) — internal


8. Token standard conformance and gaps versus ERC-20

The contract is a correct but deliberately minimal ERC-20. The following are gaps, not bugs, and integrators should know them.

AspectStatus
name, symbol, decimalsPresent, as constants. ABI encoding verified live.
totalSupply, balanceOf, transfer, transferFrom, approve, allowanceAll present with correct signatures and bool returns.
Transfer, Approval eventsPresent; topics verified in bytecode and in live logs.
Failure signallingReverts with custom errors instead of returning false. Permitted by ERC-20, but integrators that branch on a false return will see a reverted transaction instead.
transfer to address(0)Rejected. Many ERC-20s allow it. Intentional here.
Zero-value transferPermitted, emits Transfer, as the standard requires.
EIP-2612 permitAbsent. No gasless approvals, no nonces, no DOMAIN_SEPARATOR.
increaseAllowance / decreaseAllowanceAbsent.
ERC-165 supportsInterfaceAbsent.
ERC-4626Not implemented and should not be described as such. There is no asset(), convertToShares, previewDeposit, maxDeposit, or deposit(uint256,address). The accessor is underlying(). It is a 1:1 wrapper, not a tokenised vault.
Hooks / callbacksNone. No _beforeTokenTransfer, no ERC-777 behaviour, no receiver callback. This is a security positive.
Checkpointing / snapshotsAbsent. Unlike FXRP itself, myXRP has no balanceOfAt. Governance or airdrop integrations relying on historical balances must index events.

The ABI published on the explorer has 29 entries and matches this exactly: one constructor, nine errors, four events, fifteen functions. No additional entry exists.


9. Detailed risk analysis

9.1 Arithmetic

Solidity 0.8.28 checked arithmetic throughout, with one audited unchecked block in _transfer whose preconditions are both established above. There is no division, no multiplication, no exponentiation, and no scaling anywhere in the contract. Because myXRP and FXRP both use 6 decimals and the rate is fixed at 1:1, there is no rounding, no dust, no precision loss, and no rounding-direction question. This eliminates an entire class of findings that dominate vault audits — share inflation, first-depositor attacks, donation-driven price manipulation, and rounding-in-favour-of-the-wrong-party — none of which apply to this contract, because there is no share price to manipulate.

One arithmetic observation, confirmed live: deposit(2^256 - 1) reverts with Panic(0x11) (arithmetic overflow) from totalSupply + amount, rather than with CapExceeded(). The outcome is safe — it fails closed — but the error surfaced is a compiler panic rather than the intended domain error. See I-04.

9.2 Reentrancy

9.3 Access control

None exists, by design, and this was verified three ways (selector search, live calls, storage inspection) in §6.1. There is no role to misconfigure, no two-step ownership handover to get wrong, no timelock to bypass, and no multisig to compromise. The review found no access-control defect because the contract has no access control to defect.

9.4 Upgradeability

None. No proxy pattern, no DELEGATECALL, no empty slot reserved for an implementation, no SELFDESTRUCT, no metamorphic-contract vector (CREATE2 count zero, and the contract was deployed by a plain CREATE from a nonce-0 EOA, not by a factory — contractFactory is empty in the explorer's creation record). The deployed code at this address is final. Consequently no bug found in future can ever be patched at this address; the only remedy would be a new deployment and a voluntary migration by holders.

9.5 Pause and circuit breaker

None. Deposits cannot be halted, and neither can redemptions. The absence of a pause is a two-sided trade-off that should be stated honestly rather than sold as a pure positive: nobody can freeze your redemption, and equally nobody can stop deposits if FXRP itself is ever found to be compromised. Given that the contract holds no discretionary power over user funds and that redemption is the only exit, the absence of a pause is the defensible choice. See L-02.

9.6 Oracle

None. No price feed, no FTSO consumption, no TWAP, no Chainlink or FDC reference, no block.number-based pricing. The exchange rate is the integer identity. Oracle manipulation is not applicable to this contract. The only consensus-dependent value read is block.timestamp, discussed in §9.9.

9.7 Fees

None. deposit and withdraw take no protocol fee, no mint fee, no exit fee, no spread. There is no fee recipient address and no function to set one. The amount deposited is the amount credited; the amount burned is the amount returned. Verified on chain: the 25.000000 FXRP deposit credited exactly 25.000000 myXRP, and the 25.000000 redemption returned exactly 25.000000 FXRP.

9.8 Solvency invariant

The invariant that matters is:

FXRP.balanceOf(bridge) ≥ totalSupply

Current state: 25000000 ≥ 25000000. Exactly met, with zero surplus and zero deficit.

The invariant is maintained by construction:

The invariant can be broken only from outside the contract, by the FXRP asset manager burning the bridge's FXRP, or by a future FXRP implementation introducing a transfer fee. In a deficit, withdraw reverts InsufficientLiquidity for anyone whose redemption exceeds the remaining balance, and redemption becomes first-come-first-served. The contract contains no pro-rata haircut mechanism — it is strictly first-come, and that should be understood. There is no way for the contract to socialise a shortfall, and no way for it to be topped up except by a voluntary FXRP donation from anyone.

9.9 Cap

maxAssets is an immutable 10000000000000 units (10,000,000.000000). The check is totalSupply + amount > maxAssets.

Two points integrators consistently get wrong, both verified against the live history:

  1. The cap limits outstanding supply, not cumulative issuance. When the 25.000000 was redeemed on 30 September, room() returned to 9,999,975.000000 after the subsequent re-deposit, not 9,999,950.000000. Lifetime issuance through this contract is unbounded; concurrent issuance is capped.
  2. The cap counts totalSupply, not the FXRP balance. Donated FXRP consumes no room.

A boundary check was run live: a deposit of exactly room() passes the cap gate. A deposit of maxAssets + 1 reverts CapExceeded. The boundary is inclusive and correct, with no off-by-one.

9.10 Open time

OPENS_AT = 1790780400 is a compile-time constant, present twice in the runtime as a literal. It is not settable, not extendable, and not bypassable. It equals 2026-09-30 15:00:00 UTC, which is 2026-10-01 00:00:00 in Asia/Seoul (UTC+9).

Design notes: the gate is applied only to deposit. withdraw, transfer, transferFrom, and approve are ungated. This is correct and not an omission — before OPENS_AT no supply could exist, so gating the other functions would have had no effect. block.timestamp is validator-influenceable by a small margin, which in principle could have advanced the open by seconds; the contract was deployed four seconds after the gate had already passed, so the gate has never been load-bearing on chain and timestamp tolerance is moot.

9.11 Constructor immutables

Both are correct and both are permanent. underlying matches the official FXRP address given for review, character for character, as read from the live contract. maxAssets matches the decoded constructor argument. Because they are immutable rather than storage, they cannot be altered even by a storage-collision bug, and they cost no SLOAD to read. The zero-address and zero-cap guards in the constructor prevented the two deployment mistakes that would have bricked the contract.

9.12 Event completeness

Four events declared, four present in the bytecode (LOG3 ×4 for Transfer and Approval, LOG2 ×2 for Deposit and Withdraw), and all four observed in live logs.

Every state transition emits: mint emits Transfer(0 → user) and Deposit; burn emits Transfer(user → 0) and Withdraw; transfers emit Transfer; approvals emit Approval. The dual emission on mint and burn is deliberate and useful — standard ERC-20 indexers see the supply change, and purpose-built indexers get an unambiguous bridge event. Supply can therefore be reconstructed completely from logs alone. No silent state change exists anywhere in the contract.

Minor completeness notes, non-defects: the value field in Deposit/Withdraw is not indexed (correct — amounts should not be topics); there is no deployment event; and the allowance decrement in transferFrom emits no Approval, which is conventional ERC-20 behaviour.

9.13 Error catalogue

All nine declared errors were resolved in the bytecode and reconciled against their source sites. Seven of nine were triggered live.

ErrorSelectorSitesLive-triggered
ZeroAddress()0xd92e233d1 runtime (+ constructor)Yes — transfer(0x0, 1)
ZeroAmount()0x1f2a20052Yes — deposit(0), withdraw(0)
CapExceeded()0xa4875a491Yes — deposit(cap+1)
NotOpen()0xddafad981No — gate already passed; confirmed by disassembly
InsufficientBalance()0xf4d678b82Yes — withdraw and transfer with no balance
InsufficientAllowance()0x13be252b1Yes — transferFrom with no allowance
InsufficientLiquidity()0xbb55fd271No — contract is exactly solvent; confirmed by disassembly
TransferFailed()0x90b8ec182No — FXRP reverts rather than returning false; confirmed by disassembly
Reentered()0xb5dfd9e52No — requires a callback the underlying does not have

Plus the standard Panic(uint256) handler (0x4e487b71), reached once, as described in §9.1. The runtime contains no Error(string) handler (0x08c379a0 absent), i.e. the contract itself never emits a string revert reason; any string reason a caller sees has bubbled up from FXRP.

9.14 External call order

FunctionOrderAssessment
depositchecks → interaction → effects → eventsNot strict CEI. Safe: locked, single trusted callee, and the += operations re-read storage. See I-03.
withdrawchecks → effects → interaction → eventStrict CEI. Correct.
transfer / transferFrom / approveno interactionN/A

Return values of both CALLs are checked. No low-level call is used, no raw assembly, no returndata juggling, no try/catch that could swallow a failure. The contract does not use SafeERC20; this is acceptable because underlying is a single, fixed, ABI-conformant token that returns bool, and because a non-conformant return would cause an ABI-decode revert — failing closed, never silently succeeding.

9.15 Fee-on-transfer and rebasing assumptions

The contract credits the requested amount, not the observed balance delta. It therefore assumes the underlying is neither fee-on-transfer nor rebasing. As of this review that assumption holds and was verified both statically (no transferFee anywhere in the 64 verified FAsset source files; no blocklist) and empirically (25.000000 FXRP sent, 25.000000 received, zero loss). FAsset is a checkpointed but non-rebasing token: balances change only through mint, burn, and transfer.

The residual exposure is that FXRP is UUPS-upgradeable while this bridge is not. See M-01 and M-02.

9.16 Approval race

Standard ERC-20 approval front-running applies in full: approve sets an absolute value, so a spender watching the mempool can spend the old allowance and then the new one. There is no permit, no increaseAllowance/decreaseAllowance, and no approve-must-be-zero-first guard. This is normal and well understood; it is listed because an audit of this shape should not omit it. Mitigation is behavioural: set to zero, then set the new value. See L-04.

9.17 Zero address

Handled at every point it matters: the constructor rejects a zero underlying; _transfer rejects a zero destination, which is what protects the solvency invariant; deposit and withdraw operate on msg.sender and so cannot involve the zero address. approve(address(0), x) is permitted and inert (I-09). The zero address appears in logs only as the mint/burn counterparty, which is correct convention.

9.18 The locked flag

locked is uint256 private at storage slot 3, confirmed by both the derived layout and the disassembly (PUSH1 0x01; PUSH1 0x03; SSTORE immediately before the NotOpen check in deposit). It is 0 at rest in live state, so the lock is not stuck and the contract is not bricked.

Observations:

9.19 What an attacker can and cannot do

Cannot:

Can:

9.20 Denial of service and gas

No loops, no dynamic arrays, no unbounded iteration, no storage growth per user beyond a single mapping slot, no external call in a loop, and no gas-dependent control flow. Gas cost per operation is constant. The contract cannot be gas-griefed and cannot grow into an unusable state.


10. Findings

Severity reflects impact on user funds combined with likelihood, following ordinary audit convention.

IDSeverityTitleStatus
—CriticalNo critical finding. The review found none.—
—HighNo high finding. The review found none.—
M-01MediumDeposit credits the requested amount, not the observed balance deltaOpen — accepted by design, cannot be remediated at this address
M-02MediumSolvency and liquidity depend entirely on an upgradeable external token with mint, burn, and transfer-pause powersOpen — inherent, acknowledged
L-01LowNo rescue path for donated or mistakenly sent tokensOpen — accepted by design
L-02LowNo pause or circuit breaker, and the contract is immutableOpen — accepted by design
L-03LowThe cap limits outstanding supply, not cumulative issuance, and may be misreadOpen — documentation
L-04LowStandard ERC-20 approval race; no permit or increaseAllowanceOpen — accepted, standard
I-01InformationalNo EIP-2612 permit, no increaseAllowance/decreaseAllowanceAcknowledged
I-02InformationalFailures revert with custom errors rather than returning falseAcknowledged
I-03Informationaldeposit performs its external call before the state writeAcknowledged — safe as deployed
I-04Informationaldeposit with an enormous amount reverts Panic(0x11) rather than CapExceededAcknowledged
I-05InformationalReentrancy lock uses storage, not transient storage (EVM target paris)Acknowledged
I-06Informationalname() and symbol() return the identical stringAcknowledged
I-07InformationalNo ERC-165, no checkpointing, no snapshot supportAcknowledged
I-08InformationalEntire supply is held by a single addressObservation only
I-09Informationalapprove(address(0), x) is permittedAcknowledged — inert
I-10InformationalNot ERC-4626 and should not be described as a vaultAcknowledged

M-01 — Deposit credits the requested amount, not the observed balance delta

Impact: Medium. Likelihood: Low today. Status: Open; unfixable at this address.

deposit writes balanceOf[msg.sender] += amount using the caller's requested amount rather than measuring FXRP.balanceOf(address(this)) before and after the pull. If the underlying ever takes a fee on transfer, the contract credits more myXRP than FXRP it actually received, and the solvency invariant moves into deficit by the fee. Because redemption is strictly first-come, the shortfall would land entirely on the last redeemers.

This does not affect the contract as deployed: the current FAsset implementation has no transfer fee, which was confirmed both by searching all 64 verified source files and by observing a real 25.000000 deposit arrive in full. The exposure is forward-looking, and it is material only because FXRP is upgradeable while this bridge is not.

Recommended remediation, for any future deployment: measure the balance delta — snapshot underlying.balanceOf(address(this)) before and after transferFrom, and credit the difference. This is the standard hardening and it costs one additional STATICCALL. It cannot be applied to this address.

M-02 — Solvency and liquidity depend on an upgradeable external token

Impact: Medium to High if realised. Likelihood: Low. Status: Open; inherent to the design.

Verified on chain: FXRP is an EIP-1967 proxy with upgradeTo/upgradeToAndCall; its FAsset implementation exposes mint(address,uint256) and burn(address,uint256) restricted to asset manager 0x2a3fe068cd92178554cabcf7c95adf49b4b0b6a8, with burn taking an arbitrary owner; and FAsset transfers revert when the asset manager's emergencyPauseLevel() reaches FULL_AND_TRANSFER. emergencyPauseLevel() currently returns 0.

Consequences for this bridge, which has no pause, no upgrade, no migration, and no reserve:

This is not a defect in the reviewed code. It is the trust assumption a holder accepts, and it is stated here so that it is not mistaken for an absence of risk. No remediation is available within the reviewed contract; the mitigation is disclosure.

L-01 — No rescue path for stranded tokens

FXRP sent to the contract other than through deposit, and any other ERC-20 sent to it at all, is permanently locked: there is no sweep, rescue, or operatorWithdraw, and FXRP can leave only against a myXRP burn. Native FLR cannot be sent at all, the contract being non-payable (live balance 0). This is the direct cost of having no privileged party, and it is the right trade: a rescue function is also a drain function. Surplus FXRP can only over-collateralise the outstanding supply, never under-collateralise it. Accepted by design.

L-02 — No pause, no upgrade, no emergency control

Nothing can halt deposits — not the deployer, not anyone. If a defect were found in this contract, or if FXRP itself were compromised, the only response available is social: tell holders to redeem and stop depositing, and deploy a new contract. Conversely, nothing can halt redemptions either. For a contract with no discretionary power over user funds, this asymmetry favours the holder, and the review regards the choice as defensible. Accepted by design, recorded so that readers do not assume an emergency control exists.

L-03 — The cap is on outstanding supply, not lifetime issuance

maxAssets bounds concurrent totalSupply, not cumulative minting. The live history demonstrates this directly: 25.000000 minted, burned, and minted again, with room() returning to 9,999,975.000000 rather than 9,999,950.000000. Lifetime throughput is unbounded. Anyone quoting maxAssets should describe it as a ceiling on tokens outstanding at one time. Separately, maxAssets is not a statement about tokens already issued; at the time of reading, issuance stands at 25.000000 against a ceiling of 10,000,000.000000. Documentation only.

L-04 — Approval race

The standard ERC-20 front-running window on approve. No permit, no incremental allowance helpers. Mitigation is to zero an allowance before resetting it. Accepted, standard.

Informational items


11. Residual risk

Even on the finding that the contract itself is sound, a holder of myXRP retains the following risk, none of which this audit can remove:

  1. Underlying-asset risk. myXRP is a claim on FXRP held by an immutable contract. Everything that can happen to FXRP — its upgradeability, its asset manager's mint and burn authority, its emergency pause, its collateralisation and agent model, its own redemption path to XRP — passes straight through. This is the dominant residual risk and it lives outside the reviewed code.
  2. Immutability risk. No defect found later can be patched here. Immutability removes administrator risk and adds permanence risk.
  3. First-come redemption. Should the FXRP backing ever fall short, redemption is strictly first-come-first-served. There is no pro-rata haircut, no insurance mechanism in the contract, and no top-up function other than voluntary donation by anyone.
  4. No emergency stop. Deposits cannot be halted, in any circumstance.
  5. Address-confusion risk. The contract has no namespace protection, and name() and symbol() are not unique identifiers. Verify the address, not the ticker. This audit covers one address only.
  6. Front-end and approval risk. The contract is safe; the interface a user reaches it through is not in scope. Granting an unlimited FXRP allowance to any contract carries its own exposure.
  7. Verification-chain risk. Source-to-bytecode equivalence rests on the explorer's full-match record plus the disassembly described in §3, not on a local recompilation. Both lines of evidence agree, and a third is available to anyone who recompiles with the published settings.
  8. Unexercised code paths. NotOpen, InsufficientLiquidity, TransferFailed, and Reentered have never fired on chain. Their correctness was established by disassembly rather than by live execution, which is a weaker form of evidence, though the paths are short and each was read instruction by instruction.

12. Conclusion

MyXRPBridge at 0x14b9DD90edaAff6527c1b168550B3FAA15b9F6AF on Flare mainnet is a 2,917-byte, fully verified, non-upgradeable, ownerless 1:1 wrapper that holds official FXRP and issues an equal number of myXRP units at the same six decimals, and this review found no critical and no high-severity issue; it confirmed on chain that the contract has no owner, admin, pauser, upgrader, minter, oracle, or fee path, that no proxy or upgrade slot is set and no storage slot holds an address, that its runtime contains no DELEGATECALL, CREATE, CREATE2, or SELFDESTRUCT, that its only three external calls all target the one immutable FXRP address, that both mutating functions share a single reentrancy lock which is at rest, that the arithmetic contains no division or scaling and no inflation or self-transfer defect, that redemption is ungated by time or cap and simulates successfully for the entire outstanding supply at the current block, and that the solvency invariant FXRP.balanceOf(bridge) ≥ totalSupply currently holds exactly, at 25.000000 against 25.000000, within an immutable ceiling of 10,000,000.000000 and a deposit gate of 1 October 2026 00:00 Asia/Seoul that is fixed in bytecode; the two medium findings are not defects in this code but consequences of its deliberate minimalism — the contract credits the requested amount rather than the observed balance delta, and its solvency and liquidity depend wholly on FXRP, an upgradeable token whose asset manager can mint, burn, and pause transfers — so the correct summary for a reader is that holding myXRP is as safe as holding FXRP plus one immutable, ownerless, fee-free contract that cannot be changed by anyone, including its deployer, and that all remaining material risk lies in the underlying asset and in the permanence of a contract that can never be patched.


Prepared from live Flare mainnet state at block 71,252,013 on 3 October 2026. This report is a technical opinion on one contract's code and observable state. It is not a guarantee against loss, not an endorsement, not financial advice, and not a statement about any legal, regulatory, corporate, or operational matter — none of which can be verified from chain data and none of which were examined.