Docs

How a move actually works.

What a move is

A position on Morpho Blue is two numbers in one market: the collateral you have pledged and the shares of debt you owe. The market is defined by five immutable parameters — the token it lends, the token it lends against, the oracle it prices that token with, the rate model it charges by, and the fraction of the collateral it will lend up to. Nothing about a position can be changed except by adding to it or taking from it.

So "move my loan from Apple to Nvidia" is not an operation Morpho has. It is four of them, in one order, and the order matters: repay, withdraw, swap, supply, borrow. The obstacle is the first one — repaying needs dollars, and you spent the dollars you borrowed.

A move borrows them for the length of one transaction. Morpho Blue will lend anyone any token it is holding, with no fee, on the condition that it comes back before the call ends — a flash loan. Kedge takes the debt out of Morpho, repays your loan with it, does the four steps and pays it back out of the new loan. From outside, one transaction went past and your loan is against a different stock.

The six calls

The whole thing is a list handed to Bundler3, Morpho's own batching contract, which runs each entry in order through GeneralAdapter1. The adapter acts for the transaction's initiator — you — and for nobody else; there is no address passed in from this site that it will act on.

  1. morphoFlashLoan(USDG, F) — F is the debt plus a small cushion. Everything below runs inside its callback, and Bundler3 will only re-enter with the exact list that was hashed into the call, so nothing can be substituted mid-flight.
  2. morphoRepay(from, shares = all of them) — by shares, never by an amount of dollars. A debt grows every second; a repayment quoted in dollars a minute earlier leaves a few units owed, and a position with dust debt and no collateral is a position anyone may liquidate.
  3. morphoWithdrawCollateral(from, everything) — allowed now, because the position owes nothing.
  4. UniversalRouter.execute(...) — one call with two hops in it: the old stock to dollars, then those dollars to the new stock. The second hop spends the router's whole balance of dollars, because what the first hop will produce is not known when the transaction is built. The only floor is the last command, a SWEEP that moves the new stock out and reverts if there is less of it than you agreed to accept.
  5. morphoSupplyCollateral(to, everything) then morphoBorrow(to, F) — pledged for you, and the same dollars borrowed back against it.
  6. The flash loan is repaid out of that borrow, and one call after the callback repays the cushion into the new loan. What you owe afterwards is what you owed before, to within the interest of the seconds in between.

The permission, and how it is given back

Two of those calls act on your position rather than for anyone: withdrawing collateral and borrowing. Morpho requires an authorisation for that, and most apps ask you to grant one and leave it standing for ever.

Kedge's default is to sign two messages instead — a grant and a revocation, over consecutive nonces — and put both inside the move. The first call of the bundle grants the permission; the last takes it back. After the transaction isAuthorized(you, adapter) is false again, which the fork suite asserts after every move it makes. If you would rather grant it as an ordinary transaction and keep it, the app will do that too.

Where the numbers come from

The trading cost

Both hops are priced by simulating the pools: js/sim.js is a port of Uniswap's own arithmetic — TickMath, SqrtPriceMath and SwapMath.computeSwapStep — in BigInt, with the same roundings, walking the tick data actually read from the chain. v3 and v4 use the same maths; the only difference that matters is that a v4 pool can carry a protocol fee on top of its LP fee, which is folded in exactly as Pool.swap folds it.

The cost quoted is measured against those same pools at their mid price — no fee, no size — so it contains both pool fees and the price your own order moves, and nothing else. It cannot be negative.

What the oracles think

Morpho values collateral from an oracle, not from a pool. The two disagree here, by different amounts for different stocks, so the value of your position in Morpho's eyes can rise or fall across a move for reasons that have nothing to do with what you paid. That is reported on its own line and never folded into the cost. Adding them together would make many moves on this chain look free.

The rate you will pay

Every market here uses Morpho's Adaptive Curve IRM, which prices borrowing off how full the market is. These markets are small — a few thousand dollars of spare capacity is normal — so a loan arriving is not a rounding error. js/irm.js is a line-by-line port of the deployed AdaptiveCurveIrm, ExpLib, MathLib and SharesMathLib, and it takes the market state as an argument, so it can be asked what a market will charge in a state that has not happened yet. The scan checks it against the contract on every market listed, and they must agree to the unit.

Estimated, and exact

Reading the tick data around one pool's price is about thirty calls. A board comparing fourteen destinations cannot do that for all of them and still be usable, so the board prices each pool as though the liquidity at its current tick ran on unchanged, and says so. That ranking picks the five pools per hop worth reading properly; those are then read and quoted exactly, and that is what a move is sent on. The app will not send a move that is still priced from an estimate.

What Kedge refuses

  • A destination market with less left to lend than your debt — your loan has to be borrowable again on arrival, or the transaction cannot complete.
  • A move that would land above 90% of the destination's liquidation threshold.
  • A market whose oracle disagrees with a Uniswap pool for the same stock by more than 25%, or that has no pool to check against at all. These are listed as suspect and never offered as a destination.
  • A hop that would walk past the tick data read for it. The honest answer there is "Kedge does not know how deep this is", which is not the same as "it is shallow".
  • Any market that does not lend USDG, or whose rate model is not the Adaptive Curve IRM the port was written from, or whose id does not re-derive by hashing its own parameters.

What it costs to use

Nothing. Kedge takes no fee, has no token, and deploys no contract. The costs of a move are the two pool fees, the price impact, and gas. Morpho's flash loan is free.

What has been checked

  • The whole move, on a fork of this chain: 35 of 35 checks (tools/test-move.mjs). A position opened through the real js/bundles.js, moved through the real js/move.js and js/route.js, then read back out of Morpho.
  • The collateral promised is the collateral received, to the wei. The local simulator and the router agreed exactly.
  • The debt is unchanged but for the interest of the seconds in between, and the flash loan's cushion is repaid rather than left as a larger loan.
  • A rate predicted for a state that had not happened matched the rate the market charged once the loan was in it, 0.008 bps apart.
  • Nothing left behind: no balance in the adapter, Bundler3 or the router; no allowance standing; no authorisation standing.
  • The refusals refuse, including a bundle sent with no authorisation and a minimum the pools could not meet — both reverted, as they must.
  • The rate model agrees with the deployed contract on every market listed, at the same block (tools/scan.mjs).
  • This page's app, in a real browser, against a fork: 13 of 13 (tools/e2e.mjs). A stub wallet opens a position and moves it by clicking what a reader clicks; the harness first proves the page is reading the same fork it writes to, then checks every claim against Morpho rather than against the screen.
  • Every address, read back off the chain — code present and the exact function selectors this site calls, with a control that looks for a selector that cannot exist (tools/check-addresses.mjs, 14 of 14).
  • Text contrast on every page, at desktop and at 390 px, with a deliberately failing control that has to be caught (tools/contrast.mjs).

Not built: a sabotage sweep — deliberately breaking each part of the move to confirm the suite notices — and a click-through of this site driven against a fork. They are named here because a gap nobody mentions reads as a clean bill.

The machine-readable version of all of this is /js/proof.json, built by tools/proof.mjs from the files the suites wrote themselves. It also carries a SHA-256 of each pure module, so a score can be checked against the code it scored.

Addresses

Everything Kedge touches was deployed by somebody else, and all of it is public.