// SPDX-License-Identifier: MIT pragma solidity 0.8.28; interface IERC20Min { function approve(address spender, uint256 amount) external; function transfer(address to, uint256 amount) external; function balanceOf(address who) external view returns (uint256); } interface IMorphoFlash { function flashLoan(address token, uint256 assets, bytes calldata data) external; } interface IKernelExec { function execute(bytes32 execMode, bytes calldata executionCalldata) external payable; } /// Revert Lend's V3Vault. The access table is the whole reason this arm can /// exist where an adapter's cannot: /// /// repay(tokenId, amount, isShare) NO GATE — repay-on-behalf is normal /// remove(tokenId, recipient, data) tokenOwner == msg.sender, recipient FREE /// create(tokenId, recipient) no gate; pulls the NFT from msg.sender /// and `recipient` BECOMES tokenOwner /// borrow(tokenId, amount) tokenOwner == msg.sender /// /// Proven on an Arbitrum fork in contracts/test/RevertOwnerMove.t.sol. interface IRevertVault { function create(uint256 tokenId, address recipient) external; function borrow(uint256 tokenId, uint256 amount) external; function repay(uint256 tokenId, uint256 amount, bool isShare) external returns (uint256 assets, uint256 shares); function remove(uint256 tokenId, address recipient, bytes calldata data) external; function loanInfo(uint256 tokenId) external view returns (uint256 debt, uint256, uint256, uint256, uint256); } interface INpmApprove { function approve(address to, uint256 tokenId) external; } interface IAaveV4Spoke { function supply(uint256 reserveId, uint256 amount, address onBehalfOf) external; function withdraw(uint256 reserveId, uint256 amount, address onBehalfOf) external; function borrow(uint256 reserveId, uint256 amount, address onBehalfOf) external; function repay(uint256 reserveId, uint256 amount, address onBehalfOf) external; function setUsingAsCollateral(uint256 reserveId, bool useAsCollateral, address onBehalfOf) external; function getUserDebt(uint256 reserveId, address user) external view returns (uint256); } /// @title OwnerMigrateDelegate — the owner moves their own position, AS THEMSELVES. /// /// # Why this is not an adapter /// /// Every other migration arm in this repo is a contract INSTALLED ON THE /// ACCOUNT that acts on the owner's behalf, and each therefore needs the /// lending venue's permission to open a debt in a name that is not its own: /// Aave v3 has `approveDelegation`, Morpho has `setAuthorization`. /// /// AAVE V4 HAS NO SUCH GRANT THAT WORKS. Measured on an Ethereum fork: /// `setUserPositionManager(manager, true)` is a real function, it emits /// `SetUserPositionManager(user, manager, true)`, and a borrow by that manager /// `onBehalfOf` the user still reverts `Unauthorized()` — identically to a /// borrow by an address that was never granted anything. So the third-party /// route is closed on v4 and the adapter shape cannot be made to work there. /// /// # So the owner stops being a third party /// /// THIS CODE RUNS IN THE OWNER'S OWN ADDRESS. An EOA points at it with an /// EIP-7702 delegation; a smart account `delegatecall`s into it. Either way /// `address(this)` IS the owner, so every venue call this contract makes is the /// owner acting on their own position: /// /// spoke.borrow(id, amount, address(this)) // msg.sender == onBehalfOf /// /// and the question of on-behalf permission never arises. Nothing is delegated, /// nothing is authorised, and there is no grant to revoke afterwards — which /// also means no standing authority left behind, the failure mode Morpho's /// blanket `setAuthorization` carries. /// /// IT ALSO NEEDS NOTHING INSTALLED ANYWHERE. No adapter on the account, no /// module or fallback handler on the owner. That matters for the same reason /// internal/account/exit.go gives: an exit that depends on something installed /// stops working on the day somebody delists it. /// /// # What it still needs from the ACCOUNT /// /// The collateral belongs to the operating account, and only the account can /// withdraw it. The owner IS the account's root validator, so this calls /// `Kernel.execute` on the account — the same authority `cmd/api/withdraw.go` /// already uses to move balances out. No adapter is involved in that either. /// /// # The guard /// /// FIRST, NEVER IN THIS CONTRACT'S OWN CONTEXT. Called directly, this address is /// a shared singleton that owns no position and is nobody's owner, so /// `address(this)` would name the wrong party in every venue call below. /// /// SECOND, AND HERE THE TWO SHAPES GENUINELY DIFFER. A 7702 EOA carries this /// code in public, so it must be the owner calling THEMSELVES. A smart account /// cannot satisfy that: Safe's `execTransaction` delegatecalls with `msg.sender` /// preserved from ITS caller, which is whichever relayer submitted the /// transaction — and the authorisation there has already happened, because Safe /// checked the owners' signatures against its threshold before it delegatecalled /// anything. So the sender check is applied where it means something and skipped /// where the surrounding contract has already done it, told apart by the 7702 /// designation prefix `0xef01`. /// /// AND UNDERNEATH BOTH: THERE IS NO `to` ANYWHERE. Every destination is /// `address(this)`, so the worst a caller who passed the guard could do is move /// the owner's position to the owner. contract OwnerMigrateDelegate { /// Kernel's single-call execution mode. bytes32 internal constant EXEC_SINGLE = bytes32(0); /// This contract's own address, fixed at construction. IMMUTABLE, so it /// lives in the CODE — which is what makes it readable from a delegated /// context, where the storage belongs to the owner and not to this. address private immutable _self; constructor() { _self = address(this); } /// What to move on Revert. No `to`: the position ends where this code is /// running, which is the owner. struct RevertMigrate { address account; address vault; address npm; address flashLender; /// The vault's single borrow asset. USDC on every deployment this /// system reaches. address loanToken; uint256 tokenId; /// What to borrow back in the owner's name. NOT read from the old loan: /// the debt is cleared to zero to release the NFT, so the new position /// is opened at a size the owner chooses rather than at whatever the /// old one drifted to. uint256 borrowAmount; } struct V4Migrate { /// The operating account the position is leaving. address account; /// The v4 spoke, BY ADDRESS. There is no venue registry here — this is /// not an installed module and has no curated state to read. The owner /// signs the transaction that names it, which is the same authority /// that would have curated it. address spoke; /// Morpho, for the flash. address flashLender; address collateral; address loanToken; uint256 collateralReserve; uint256 loanReserve; uint256 collateralAmount; } /// In-flight state. STORAGE, not transient, because this runs in the /// owner's own account under 7702 and the callback arrives in a fresh call /// frame — there is no way to pass memory across it. /// /// CLEARED BEFORE THE FUNCTION RETURNS, always, so a half-finished /// migration cannot leave a live authorisation sitting in the owner's /// storage for the next caller. /// /// NEVER SLOT 0. Under Safe `delegatecall` slot 0 is the singleton; a /// configured Safe reverts `AlreadyInFlight` before any write, but an empty /// host (or any other layout that stores identity at slot 0) must not see /// this flag. Namespaced slot keeps flash + flashless paths host-safe. bytes32 private constant _IN_FLIGHT_SLOT = keccak256("OwnerMigrateDelegate.inFlight"); event OwnerMigrated( address indexed account, address indexed spoke, address collateral, uint256 collateralAmount, address loanToken, uint256 debt ); error NotSelf(address caller); /// Called on the implementation itself rather than through a delegation. error NotDelegated(); error NothingToMigrate(); error AlreadyInFlight(); error NotTheLender(address caller); error PayloadNotOurs(); error DebtSurvived(uint256 left); error CollateralDidNotArrive(); error FlashNotCovered(uint256 have, uint256 owed); /// The flashless path needs the debt in hand before it starts. Carries the /// token and both figures, because the remedy is "send yourself the /// difference" and the owner should not have to work out how much. error FrontingRequired(address token, uint256 needed, uint256 have); error FrontingNotReturned(uint256 before, uint256 after_); /// Move an Aave v4 position out of `account` and into this address. /// /// ONE TRANSACTION, and the owner sends it. Flash the loan token, repay the /// account's debt, take its collateral, re-open the same position here, and /// settle the flash from the new borrowing. function migrateAaveV4(V4Migrate calldata m) external { _requireOwnerContext(); if (_inFlight() != bytes32(0)) revert AlreadyInFlight(); uint256 owed = IAaveV4Spoke(m.spoke).getUserDebt(m.loanReserve, m.account); if (owed == 0 || m.collateralAmount == 0) revert NothingToMigrate(); bytes memory payload = abi.encode(m, owed); _setInFlight(keccak256(payload)); IMorphoFlash(m.flashLender).flashLoan(m.loanToken, owed, payload); _setInFlight(bytes32(0)); emit OwnerMigrated( m.account, m.spoke, m.collateral, m.collateralAmount, m.loanToken, owed ); } /// Morpho's callback, arriving at the owner's own address. /// /// TWO CHECKS AND BOTH ARE LOAD-BEARING. The lender must be the contract we /// just called — otherwise anybody could invoke this with a payload of their /// choosing — and the payload must be the one we sent, which is what /// `_inFlight` records. Neither alone is enough: the first admits a /// forged payload from the real lender during an unrelated flash, the /// second admits a forged lender replaying our own payload. function onMorphoFlashLoan(uint256 assets, bytes calldata data) external { bytes32 flight = _inFlight(); if (flight == bytes32(0) || keccak256(data) != flight) revert PayloadNotOurs(); // WHICH MIGRATION IS IN FLIGHT, told by the payload's own length rather // than by a tag. Both structs are fixed-size with no dynamic members, // so each encodes to exactly one length and the two differ — and the // hash check above already proved this payload is one WE built, so the // length is not attacker-chosen. if (data.length == 8 * 32) { (RevertMigrate memory rm, uint256 flash) = abi.decode(data, (RevertMigrate, uint256)); if (msg.sender != rm.flashLender) revert NotTheLender(msg.sender); _onRevertFlash(rm, flash); return; } (V4Migrate memory m, uint256 owed) = abi.decode(data, (V4Migrate, uint256)); if (msg.sender != m.flashLender) revert NotTheLender(msg.sender); _repayTheAccount(m); uint256 got = _takeCollateral(m); _reopenHere(m, got, owed); // SETTLE. Morpho PULLS, so the allowance is what repays it. IERC20Min(m.loanToken).approve(m.flashLender, assets); uint256 have = IERC20Min(m.loanToken).balanceOf(address(this)); if (have < assets) revert FlashNotCovered(have, assets); } /// 1. REPAY THE ACCOUNT'S DEBT — AND THE ACCOUNT MUST DO IT ITSELF. /// /// v4 GATES REPAY-ON-BEHALF TOO, which is the assumption this contract was /// first written on and it is wrong. On Aave v3 and on Morpho, repaying /// somebody else's debt is permissionless — a gift nobody needs permission /// to give. On a v4 spoke it reverts `Unauthorized()`, exactly as borrowing /// on behalf does. Measured: the flash arrived, the approval landed, and /// `repay(8, owed, account)` from this address reverted. /// /// SO NOTHING HERE IS AN ON-BEHALF CALL ANY MORE. The flash is pushed INTO /// the account and the account repays its own debt through /// `Kernel.execute`, which makes it both `msg.sender` and `onBehalfOf`. /// Combined with the owner supplying and borrowing for themselves, the /// whole sequence contains no call where those two differ — which is why it /// needs no grant from anyone on any venue. /// /// AN EXACT FIGURE, never a max-means-all sentinel: Aave refuses that on /// behalf of another account, which is the bug the v3 adapter's fork test /// found. function _repayTheAccount(V4Migrate memory m) private { uint256 owed = IAaveV4Spoke(m.spoke).getUserDebt(m.loanReserve, m.account); if (owed == 0) revert NothingToMigrate(); IERC20Min(m.loanToken).transfer(m.account, owed); _viaAccount(m.account, m.loanToken, abi.encodeCall(IERC20Min.approve, (m.spoke, owed))); _viaAccount( m.account, m.spoke, abi.encodeCall(IAaveV4Spoke.repay, (m.loanReserve, owed, m.account)) ); uint256 left = IAaveV4Spoke(m.spoke).getUserDebt(m.loanReserve, m.account); if (left != 0) revert DebtSurvived(left); } /// 2. TAKE THE COLLATERAL, THROUGH THE ACCOUNT. /// /// v4's `withdraw` pays `msg.sender` and its third argument is `onBehalfOf`, /// so the withdrawal has to be made BY the account — the tokens land there — /// and then moved here. Both calls go through `Kernel.execute`, which the /// owner may make because the owner is the root validator. /// /// MEASURED, not assumed: `got` is the withdraw delta, not the account's /// full token balance — idle collateral of the same asset stays put. function _takeCollateral(V4Migrate memory m) private returns (uint256 got) { uint256 before = IERC20Min(m.collateral).balanceOf(address(this)); uint256 atAccountBefore = IERC20Min(m.collateral).balanceOf(m.account); _viaAccount( m.account, m.spoke, abi.encodeCall( IAaveV4Spoke.withdraw, (m.collateralReserve, m.collateralAmount, m.account) ) ); uint256 withdrawn = IERC20Min(m.collateral).balanceOf(m.account) - atAccountBefore; _viaAccount( m.account, m.collateral, abi.encodeCall(IERC20Min.transfer, (address(this), withdrawn)) ); got = IERC20Min(m.collateral).balanceOf(address(this)) - before; if (got == 0) revert CollateralDidNotArrive(); } /// 3, 4 and 5. RE-OPEN IT HERE — supply, SWITCH ON, borrow. /// /// Every one of these names `address(this)` as the beneficiary, and this /// address is the caller, so none of them is an on-behalf call and none /// needs a grant. That is the whole point of the delegate shape. /// /// THE MIDDLE CALL IS NOT OPTIONAL. A v4 supply is a deposit until /// `setUsingAsCollateral`, so borrowing without it opens a debt against /// collateral the protocol does not count. function _reopenHere(V4Migrate memory m, uint256 got, uint256 owed) private { IERC20Min(m.collateral).approve(m.spoke, got); IAaveV4Spoke(m.spoke).supply(m.collateralReserve, got, address(this)); IAaveV4Spoke(m.spoke).setUsingAsCollateral(m.collateralReserve, true, address(this)); IAaveV4Spoke(m.spoke).borrow(m.loanReserve, owed, address(this)); } /// Move an Aave v4 position out of `account` and into this address, WITHOUT /// A FLASH LOAN. /// /// # Why this exists beside migrateAaveV4 /// /// A SAFE CANNOT TAKE A FLASH LOAN. Proven in /// contracts/test/SafeDelegateFlash.t.sol against a real Safe v1.4.1: the /// lender calls BACK, the callback is a fresh external call to the Safe for /// a function it does not have, and it lands on the fallback handler — which /// Safe reaches by `call`, in the HANDLER's own context, so it could not act /// as the Safe even if it implemented the callback. The delegatecall frame /// that started the flash is still on the stack and can receive nothing. /// /// The three ways out are a Safe module (permanent `execTransaction`- /// equivalent power), a fallback handler (cannot act as the Safe anyway), /// or removing the callback. THIS REMOVES THE CALLBACK. /// /// # What it costs instead /// /// THE OWNER FRONTS THE DEBT, for the length of one transaction. The /// sequence repays the account's debt from this address's own balance and /// ends by borrowing the same amount back in this address's name, so the /// balance returns before the transaction ends — but it has to be there /// when it starts. `FrontingRequired` says so before anything moves rather /// than failing partway. /// /// That is the whole difference. Every other property is the flash /// version's: no on-behalf call anywhere, so no grant on any venue, and /// nothing installed on either side. function migrateAaveV4Flashless(V4Migrate calldata m) external { _requireOwnerContext(); uint256 owed = IAaveV4Spoke(m.spoke).getUserDebt(m.loanReserve, m.account); if (owed == 0 || m.collateralAmount == 0) revert NothingToMigrate(); uint256 have = IERC20Min(m.loanToken).balanceOf(address(this)); if (have < owed) revert FrontingRequired(m.loanToken, owed, have); _repayTheAccount(m); uint256 got = _takeCollateral(m); _reopenHere(m, got, owed); // THE FRONTED MONEY IS BACK. The borrow paid it, so ending below where // this started would mean the sequence leaked — checked rather than // reasoned about, because the whole case for this shape is that the // owner is made whole inside one transaction. uint256 after_ = IERC20Min(m.loanToken).balanceOf(address(this)); if (after_ < have) revert FrontingNotReturned(have, after_); emit OwnerMigrated( m.account, m.spoke, m.collateral, m.collateralAmount, m.loanToken, owed ); } /// THIS CODE IS RUNNING AS SOMEBODY'S OWNER, and that somebody authorised it. /// /// Both halves matter. Without the first, a direct call to the singleton /// would run the whole sequence with `address(this)` naming a contract that /// owns nothing. Without the second, anyone could drive a 7702 owner's /// migration at a moment of their choosing. function _requireOwnerContext() private view { if (address(this) == _self) revert NotDelegated(); // 7702 designation: 0xef01 || 00 || address. Deployed contract code can // never begin 0xef, so this tells the two apart without asking either // party to declare which it is. AgentRegistry uses the same test. bytes memory code = address(this).code; bool delegatedEOA = code.length >= 2 && code[0] == 0xef && code[1] == 0x01; if (delegatedEOA && msg.sender != address(this)) revert NotSelf(msg.sender); } /// Move a Revert Lend position out of `account` and into this address. /// /// # Why this one cannot be an adapter, and it is a different reason again /// /// Aave v4 refuses every on-behalf call. Revert refuses none of them by /// name — it gates on `tokenOwner`, and the last leg needs /// `tokenOwner == msg.sender`. An installed adapter can be handed the NFT /// and can even register the loan to the owner, and then cannot borrow /// against it, because it is not the owner. Here it is. /// /// # The NFT goes through THIS address, never to the owner separately /// /// It arrives by `safeTransferFrom`, so a holder must implement /// `onERC721Received` — which is why this contract does. Since this code /// runs AS the owner, that one implementation covers a 7702 EOA and a smart /// account alike, and the NFT is never sent anywhere that might refuse it. /// /// THE DEBT IS CLEARED, NOT MOVED. Revert releases collateral only on a /// zero-debt loan, so the sequence repays in full and re-borrows. That is /// why `borrowAmount` is a parameter: the owner chooses the size of the /// position they end up with, rather than inheriting a figure the old loan /// happened to drift to. function migrateRevert(RevertMigrate calldata m) external { _requireOwnerContext(); if (_inFlight() != bytes32(0)) revert AlreadyInFlight(); (uint256 debt,,,,) = IRevertVault(m.vault).loanInfo(m.tokenId); if (debt == 0) revert NothingToMigrate(); // HEADROOM ON THE FLASH, because the repay below is by ASSETS and the // debt accrues per block. The remainder is returned by the repay itself // — Revert takes what is owed and no more — so an over-sized flash // costs nothing but is what makes the repay total rather than nearly. uint256 flash = debt + (debt / 100) + 1; bytes memory payload = abi.encode(m, flash); _setInFlight(keccak256(payload)); IMorphoFlash(m.flashLender).flashLoan(m.loanToken, flash, payload); _setInFlight(bytes32(0)); emit OwnerMigrated(m.account, m.vault, m.npm, m.tokenId, m.loanToken, debt); } function _inFlight() private view returns (bytes32 v) { bytes32 slot = _IN_FLIGHT_SLOT; assembly { v := sload(slot) } } function _setInFlight(bytes32 v) private { bytes32 slot = _IN_FLIGHT_SLOT; assembly { sstore(slot, v) } } /// The Revert sequence, inside Morpho's callback. function _onRevertFlash(RevertMigrate memory m, uint256 assets) private { // 1. CLEAR THE DEBT. Ungated, so this address may do it directly — // unlike Aave v4, where even a repay must be made by the account. IERC20Min(m.loanToken).approve(m.vault, assets); IRevertVault(m.vault).repay(m.tokenId, assets, false); (uint256 left,,,,) = IRevertVault(m.vault).loanInfo(m.tokenId); if (left != 0) revert DebtSurvived(left); // 2. THE ACCOUNT HANDS THE NFT OVER. `remove` is gated on tokenOwner, // which is the ACCOUNT, so it goes through Kernel.execute — and its // recipient is free, so it comes here. _viaAccount( m.account, m.vault, abi.encodeCall(IRevertVault.remove, (m.tokenId, address(this), "")) ); // 3. RE-DEPOSIT IT IN THE OWNER'S OWN NAME. `create` pulls the NFT from // whoever calls it and makes the RECIPIENT the tokenOwner, so this // single call is what transfers ownership of the loan. INpmApprove(m.npm).approve(m.vault, m.tokenId); IRevertVault(m.vault).create(m.tokenId, address(this)); // 4. AND BORROW AS THE OWNER. tokenOwner is now this address, which is // the leg no adapter could reach. if (m.borrowAmount != 0) IRevertVault(m.vault).borrow(m.tokenId, m.borrowAmount); // AND THE DIFFERENCE IS THE OWNER'S TO PAY, which is the honest // economics of this arm rather than a limitation of it. // // The old debt is CLEARED — Revert releases collateral only on a // zero-debt loan — and a new one is opened at whatever size the owner // asked for. Ending with LESS debt than you started with means paying // the difference, and even ending with the "same" nominal figure means // paying the interest that accrued in between. Both come out of the // owner's own balance, which is where this code is running. // // So the shortfall is named rather than left as a bare flash failure: // FrontingRequired says which token and how much, and the commonest // case is a few wei of accrued interest. IERC20Min(m.loanToken).approve(m.flashLender, assets); uint256 have = IERC20Min(m.loanToken).balanceOf(address(this)); if (have < assets) revert FrontingRequired(m.loanToken, assets, have); } /// The NFT passes through this address in step 2, so it has to say it can /// hold one. Without this the vault's `remove` reverts "transfer to non /// ERC721Receiver implementer" and the whole migration with it. function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { return this.onERC721Received.selector; } /// One call made BY the account, authorised by this address being its root /// validator. Kernel packs a single execution as target ++ value ++ data. function _viaAccount(address account, address target, bytes memory data) private { IKernelExec(account).execute( EXEC_SINGLE, abi.encodePacked(target, uint256(0), data) ); } }