Review · Flare C-Chain · 3 October 2026, 19:35 London

myXRP Flare Bridge Security Review Report

This letter is a code review of one deployed runtime. The desk read MyXRPBridge the way a stranger would: which calls move official FXRP, which calls mint or burn the receipt named myXRP, and whether any path mints a balance without a matching pull, retargets the FXRP address, or lets a privileged key take assets the receipt still claims. After the review, those three paths are absent.

myXRP at this address is a sealed 1:1 receipt. It is not native XRP, and it is not a rebasing share. name() and symbol() are the constant myXRP. Decimals are 6, the same width as the FXRP unit, so one base unit on the receipt is one base unit of the asset pulled. There is no operator key on the contract. Withdraw can pay only the FXRP the contract still holds. That limit is stated below as a closed note, not as an open finding.

Result. Closed. Critical 0 · high 0 · medium 0 · low 0 · informational 6, all closed. Nothing in this letter requires a code change before use. The reading is the bytecode at 19:35 London on 3 October 2026.

Artefacts

ItemValue read for this letter
ContractMyXRPBridge at 0x14b9DD90edaAff6527c1b168550B3FAA15b9F6AF
ChainFlare C-Chain, creation block 70,992,363
Creation30 September 2026, 16:00:04 London (15:00:04 UTC), transaction 0xee361ab34728ed5a817ff9db70b67573ff02f029ae47a8b7e0242e9bec4485e5. The bytecode did not exist before that transaction.
Constructor(fxrp, maxAssets_) only. fxrp is 0xAd552A648C74D49E10027AB8a618A3ad4901c5bE. maxAssets_ is 10000000000000, which is 10,000,000.000000 FXRP at 6 decimals.
Compilersolc v0.8.28+commit.7893614a, optimizer on, 200 runs, EVM paris. Explorer status: fully verified. The contract is not marked as a proxy.
Source published30 September 2026, 23:11 London (22:11 UTC), on the Flare explorer. That file is what this letter diffs.
Public deskmyxrp.fi, the deposit panel, add-to-wallet, /explorer-verify/, and /token.json

Out of scope: the cabinet ledger that credits XRP from a public snapshot, holder key custody, and the internal economics of the FAsset system. This letter also does not re-review the FXRP implementation. It reviews the bridge that calls that address, and it states the assumption that call relies on.

What the runtime stores

The receipt is an ERC-20-shaped balance map. Balances are stored directly. There is no index. Storage that changes after deployment is totalSupply, balanceOf, allowance, and a private reentrancy word locked. Everything that chooses the asset or the ceiling is fixed in the creation transaction.

maxAssets() read for this letter is 10,000,000.000000 FXRP. That figure is the ceiling. It is not a count of myXRP already minted. Minted supply is whatever totalSupply() returns, and deposit refuses a mint that would pass the ceiling.

deposit

deposit(uint256 amount) is nonReentrant. The order is deliberate, and it is the opposite of a push.

  1. If block.timestamp < OPENS_AT, revert NotOpen. Withdraw does not perform this check. Redemption is not on the same clock as the first mint.
  2. A zero amount reverts ZeroAmount.
  3. totalSupply + amount > maxAssets reverts CapExceeded. The addition is checked arithmetic (solc 0.8). A sum that overflows the word reverts before a wrapped comparison can succeed.
  4. underlying.transferFrom(msg.sender, address(this), amount) must return true. A false return reverts TransferFailed, and the mint below is never reached. A revert inside the token reverts the whole frame.
  5. Only then does the contract add amount to totalSupply and to balanceOf[msg.sender], and emit Transfer from the zero address plus Deposit.

There is no recipient argument. The receipt is minted to the caller who paid the FXRP. There is no second mint function, no mint for an arbitrary account, and no path that increases totalSupply except this one.

The external call sits before the credit, inside the reentrancy word. locked is set to 1 on entry and cleared on exit. If the FXRP call tried to re-enter deposit or withdraw, the second entry reverts Reentered. The receipt itself has no receiver hook, so a transfer of myXRP does not call back into this contract.

The pull-then-mint order is what stops a token that returns true and moves nothing from being ignored. It also means the letter’s 1:1 claim assumes the asset at the immutable address moves the full amount or reverts. A fee-on-transfer token would let the mint record amount while the contract received less. The address under review is official FXRP, fixed in the constructor. This letter does not treat that address as a fee-on-transfer token. See RH-N6.

withdraw

withdraw(uint256 amount) is nonReentrant. It is not gated by OPENS_AT. The order is checks, then effects, then the external push.

  1. A zero amount reverts ZeroAmount.
  2. balanceOf[msg.sender] must cover amount, or the call reverts InsufficientBalance. The function does not read allowance. A spender approved on the receipt cannot redeem. The holder redeems, or the holder first transfers the receipt and the new holder redeems. See RH-N4.
  3. underlying.balanceOf(address(this)) must cover amount, or the call reverts InsufficientLiquidity. This check happens before any burn is stored. If the contract’s FXRP balance is short, the receipt balance is left as it was.
  4. The contract then writes balanceOf[msg.sender] -= amount and totalSupply -= amount, and emits Transfer to the zero address.
  5. underlying.transfer(msg.sender, amount) must return true. A false return reverts TransferFailed. Because the revert unwinds the frame, the burn in step 4 is not kept. See RH-N2.
  6. Only a successful push emits Withdraw.

There is no to argument. FXRP goes to the caller who burned the receipt. There is no sweep, no skim, and no function that transfers underlying to any other account. FXRP that arrives by a plain transfer, rather than by deposit, has no recovery path. See RH-N3.

The receipt as a token

transfer and transferFrom move the claim. They do not move FXRP. Burning happens only in withdraw. _transfer reverts ZeroAddress when the recipient is the zero address, so a holder cannot burn by sending to zero and leave the FXRP behind.

approve overwrites the spender’s allowance and emits Approval. transferFrom spends that allowance, except when the stored allowance is type(uint256).max: that value is treated as infinite and is not decreased. The classic allowance-overwrite race of ERC-20 applies. It is recorded as RH-N5. It is not a path that mints myXRP or moves FXRP by itself.

Inside _transfer, the balance update is unchecked after a checked comparison balanceOf[from] >= amount. The subtraction cannot underflow. The addition to the recipient could wrap only if two balances summed past 2256−1. They cannot. Every balance is a slice of totalSupply, and deposit refuses to let totalSupply pass maxAssets, which is 1013 base units. The unchecked add is safe under that cap. It is not a finding.

The contract does not implement permit, votes, or a transfer hook. Integrators that assume 18 decimals will mis-scale the receipt. Both the bridge and official FXRP use 6.

room, cash, and the ceiling

room() returns maxAssets - totalSupply, or zero when supply is already at the ceiling. It is minting headroom. It is not underlying.balanceOf(address(this)).

A direct transfer of FXRP into the contract raises the cash balance and does not raise totalSupply, so it does not shrink room(). It also does not change anyone’s receipt. Because minting is 1:1 with the amount pulled, a donation does not dilute existing holders. There is no share price and no assets * supply / totalAssets rounding, so a donation cannot inflate a first depositor’s claim. The public desk’s room line is this headroom, in myXRP units, on this contract.

The invariant the letter signs, under the FXRP assumption in RH-N6, is: after every successful deposit or withdraw, the contract’s FXRP balance is at least totalSupply. A donation makes cash strictly greater than supply. A shortfall makes withdraw revert InsufficientLiquidity for the amount the cash cannot cover, and it does not delete the receipt.

Properties

PropertyWhere it is enforcedReading
One myXRP minted requires one FXRP pulleddepositHolds, if the immutable asset moves the full amount
One myXRP burned pays one FXRP, or the transaction revertswithdrawHolds
Supply cannot pass the ceilingCapExceededHolds. Ceiling is immutable
The FXRP address cannot be replacedunderlying immutableHolds. No setter
The open instant cannot be movedOPENS_AT constantHolds. NotOpen before it
Redemption is not closed by that instantwithdrawHolds. No read of OPENS_AT
No silent mintdispatchHolds. One mint site
No admin pulldispatchHolds. FXRP leaves only inside withdraw, to msg.sender
Allowance is not a redemption rightwithdrawHolds. See RH-N4
Re-entry during the token calllockedReverts Reentered
Donation does not dilute holders1:1 mint, no indexHolds
Proxy, delegatecall, selfdestruct, initializesource and explorerAbsent. See RH-N1

Public desk

The desk walked myxrp.fi as a stranger: connect, read the panel, open explorer-verify, fetch token.json. The pages bind this bridge address. The name on the page is myXRP. The room line follows room() on this contract, which is headroom under maxAssets, not the raw FXRP cash balance.

Method

The review is a manual reading of the verified source against the live runtime, plus the property list above. The Flare explorer reports the source fully verified for this bytecode, compiler and optimizer as in the table, and does not mark the address as a proxy. The desk searched that source for a second minter, an owner, a pause, a supply setter, delegatecall, selfdestruct, tx.origin authorization, and a function that sends underlying anywhere except the withdrawing caller. None are present.

This letter is not a machine-checked proof, and it is not a review of the FXRP implementation behind 0xAd552A648C74D49E10027AB8a618A3ad4901c5bE. Upgrade or pause of that asset sits with FAssets, outside this runtime.

Notes

Six notes. All informational. All closed. A note here records a property or an external assumption. It does not ask for a patch.

RH-N1 · no upgrade slot on this runtime

Severity: Informational · Status: Closed · Path: runtime dispatch

The source has no proxy dispatcher, no delegatecall, no selfdestruct, and no initialize. The explorer does not mark the address as a proxy, and the verified runtime matches the published source. The source file was published at 23:11 London on 30 September 2026. No patch.

RH-N2 · a failed push does not keep the burn

Severity: Informational · Status: Closed · Path: MyXRPBridge.withdraw

State is written before underlying.transfer: the caller’s balance and totalSupply drop, then the token is pushed. If that call returns false, TransferFailed reverts the transaction, so the burn is not kept. If the token reverts, the frame reverts the same way. The liquidity check also sits before the burn, so a short cash balance never stores a reduced receipt. No patch. The order is the control.

RH-N3 · FXRP sent outside deposit has no sweep

Severity: Informational · Status: Closed · Path: underlying.transfer call sites

The only instruction that sends FXRP is inside withdraw, and it sends amount to msg.sender after burning that same amount of the caller’s receipt. A plain ERC-20 transfer of FXRP to the bridge address increases the cash balance and does not mint a receipt. Nothing in the runtime can forward that surplus to a recovery address, because nothing in the runtime has an admin. Holders are not diluted: the extra cash is not a share-price input. The surplus simply stays. No patch. The absence of a sweep is the same fact as the absence of an operator key.

RH-N4 · allowance does not redeem

Severity: Informational · Status: Closed · Path: withdraw versus transferFrom

withdraw debits balanceOf[msg.sender] and never reads allowance. An approved spender can move the receipt with transferFrom. After that transfer, the new holder is the account that can redeem. A router that expects “allowance implies withdraw” will not find that behaviour here. No patch. Integrators should redeem from the holding account.

RH-N5 · allowance overwrite

Severity: Informational · Status: Closed · Path: approve

approve replaces the previous allowance in one write. That is the ordinary ERC-20 race between an old spender and a new approval. type(uint256).max is not decreased on transferFrom, so an infinite approval stays infinite until a later approve replaces it. The race cannot mint myXRP and cannot pull FXRP. No patch.

RH-N6 · the asset called is outside this runtime

Severity: Informational · Status: Closed · Path: underlying

Official FXRP at 0xAd552A648C74D49E10027AB8a618A3ad4901c5bE is the immutable callee. This bridge cannot pause it, replace it, or upgrade it. If that token reverts or returns false, deposit and withdraw revert, and RH-N2 keeps the receipt on a failed push. The 1:1 credit assumes that token moves the full amount. The bridge does not take a fee of its own. Review of the FAsset implementation is outside this letter. No patch on MyXRPBridge.

Verdict

The review is closed. No critical, high, medium, or low finding is open. The six notes above stay closed. This letter reads the bytecode of 19:35 London on 3 October 2026. The source file on the Flare explorer was published at 23:11 London on 30 September 2026. The creation transaction is 30 September 2026, 16:00:04 London. This letter stays with the published bridge.

Write [email protected] if you need a copy of this letter on paper.

← Back to Audit