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
| Item | Value read for this letter |
|---|---|
| Contract | MyXRPBridge at 0x14b9DD90edaAff6527c1b168550B3FAA15b9F6AF |
| Chain | Flare C-Chain, creation block 70,992,363 |
| Creation | 30 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. |
| Compiler | solc v0.8.28+commit.7893614a, optimizer on, 200 runs, EVM paris. Explorer status: fully verified. The contract is not marked as a proxy. |
| Source published | 30 September 2026, 23:11 London (22:11 UTC), on the Flare explorer. That file is what this letter diffs. |
| Public desk | myxrp.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.
name,symbol, anddecimalsare constants. No setter writes them.OPENS_ATis the constant1790780400. That instant is 30 September 2026, 15:00:00 UTC, which is 1 October 2026, 00:00:00 in Seoul.depositreads it. No function writes it.underlyingandmaxAssetsareimmutable. The constructor revertsZeroAddressif the asset is the zero address, andZeroAmountif the ceiling is zero. After the constructor returns, neither value has a writer.- The address that sent the creation transaction keeps no role. The source has no
owner, no access-control list, and no function gated bymsg.senderexcept the ordinary token holder and spender checks.
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.
- If
block.timestamp < OPENS_AT, revertNotOpen. Withdraw does not perform this check. Redemption is not on the same clock as the first mint. - A zero amount reverts
ZeroAmount. totalSupply + amount > maxAssetsrevertsCapExceeded. The addition is checked arithmetic (solc 0.8). A sum that overflows the word reverts before a wrapped comparison can succeed.underlying.transferFrom(msg.sender, address(this), amount)must return true. A false return revertsTransferFailed, and the mint below is never reached. A revert inside the token reverts the whole frame.- Only then does the contract add
amounttototalSupplyand tobalanceOf[msg.sender], and emitTransferfrom the zero address plusDeposit.
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.
- A zero amount reverts
ZeroAmount. balanceOf[msg.sender]must coveramount, or the call revertsInsufficientBalance. The function does not readallowance. 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.underlying.balanceOf(address(this))must coveramount, or the call revertsInsufficientLiquidity. This check happens before any burn is stored. If the contract’s FXRP balance is short, the receipt balance is left as it was.- The contract then writes
balanceOf[msg.sender] -= amountandtotalSupply -= amount, and emitsTransferto the zero address. underlying.transfer(msg.sender, amount)must return true. A false return revertsTransferFailed. Because the revert unwinds the frame, the burn in step 4 is not kept. See RH-N2.- 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
| Property | Where it is enforced | Reading |
|---|---|---|
| One myXRP minted requires one FXRP pulled | deposit | Holds, if the immutable asset moves the full amount |
| One myXRP burned pays one FXRP, or the transaction reverts | withdraw | Holds |
| Supply cannot pass the ceiling | CapExceeded | Holds. Ceiling is immutable |
| The FXRP address cannot be replaced | underlying immutable | Holds. No setter |
| The open instant cannot be moved | OPENS_AT constant | Holds. NotOpen before it |
| Redemption is not closed by that instant | withdraw | Holds. No read of OPENS_AT |
| No silent mint | dispatch | Holds. One mint site |
| No admin pull | dispatch | Holds. FXRP leaves only inside withdraw, to msg.sender |
| Allowance is not a redemption right | withdraw | Holds. See RH-N4 |
| Re-entry during the token call | locked | Reverts Reentered |
| Donation does not dilute holders | 1:1 mint, no index | Holds |
Proxy, delegatecall, selfdestruct, initialize | source and explorer | Absent. 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.