// 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 ISafeModule { /// Executes AS THE SAFE. `operation` 0 is CALL, which is all this needs: /// the point is that `msg.sender` at the target becomes the Safe, not that /// the Safe's storage is used. function execTransactionFromModule( address to, uint256 value, bytes calldata data, uint8 operation ) external returns (bool success); function isModuleEnabled(address module) external view returns (bool); } interface IKernelExec { function execute(bytes32 execMode, bytes calldata executionCalldata) external payable; } 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 SafeMigrateConnector — a Safe module that gives a Safe a flash loan. /// /// # The problem it exists for /// /// A SAFE CANNOT RECEIVE A FLASH-LOAN CALLBACK. The lender calls back, the /// callback is a fresh call to the Safe for a function it does not have, it /// lands on the fallback handler — and a Safe reaches its handler by CALL, so /// the handler runs in its own context and could not act as the Safe even if it /// implemented the callback. Proven in contracts/test/SafeDelegateFlash.t.sol. /// /// AND AAVE V4 REFUSES EVERY ON-BEHALF CALL — borrow and repay alike, unlike v3 /// and Morpho where repaying somebody else's debt is a gift. So the adapter /// shape, where an installed contract acts FOR the owner, cannot move a v4 /// position at all. /// /// Those two together leave a Safe with no route to its own v4 position. This /// is the route. /// /// # How it works, and why the Safe never touches the flash /// /// THE MODULE BORROWS, NOT THE SAFE. This is an ordinary contract, so Morpho's /// callback lands on it without difficulty. Inside that callback it drives the /// Safe through `execTransactionFromModule`, which executes with THE SAFE as /// `msg.sender` at the target. /// /// That is the whole trick: every venue call the Safe makes has /// `msg.sender == onBehalfOf == the Safe`, so none of them is an on-behalf call /// and v4's refusal never applies. The Safe gets a flash loan it structurally /// cannot receive, because it never receives one. /// /// # What installing it costs, stated plainly /// /// A SAFE MODULE CAN MAKE THE SAFE DO ANYTHING. That is what /// `execTransactionFromModule` is, and no module can be given less. So the /// bound has to come from this contract's own code, and it is the same bound /// the adapters use: /// /// 1. THERE IS NO `to` ANYWHERE. Every beneficiary is `m.safe` — the caller /// itself. No parameter names a destination, so there is no input that /// redirects anything. /// 2. THE CALLER MUST BE THE SAFE IT NAMES. `msg.sender == m.safe`, so /// starting a migration takes a transaction that Safe's own owners signed /// to its own threshold. One deployment serves every Safe and none of them /// can drive another. /// 3. NO CALLDATA PARAMETER. It takes a struct of addresses and amounts and /// builds every call itself. A module that forwarded arbitrary calldata /// would be a general-purpose key to the Safe wearing a specific name. /// 4. THE LENDER IS NOT A PARAMETER. It was, and the callback settles the /// flash with `approve(lender, assets)` — so a caller naming its own /// contract as the lender walked away with an allowance from this module /// over the loan token, standing after the transaction ended, over /// whatever this contract ever held. Pinned to Morpho Blue, the only /// lender anyone needs, and the only `msg.sender` the callback accepts. /// /// `isModuleEnabled` is asked of the CALLER, so a contract written to answer /// `true` passes it. That is fine and it is not the bound: rule 2 is. A fake /// Safe that passes it can only drive itself, because every call this module /// makes as a Safe goes to `m.safe`, which is `msg.sender`. /// /// The residual risk is a bug in THIS file, and it is a real one: a Safe that /// enables this is trusting these three rules to hold. Anyone who would rather /// not take that trade has the flashless path in OwnerMigrateDelegate, which /// installs nothing and asks the owner to front the debt for one transaction /// instead. contract SafeMigrateConnector { bytes32 internal constant EXEC_SINGLE = bytes32(0); uint8 internal constant CALL = 0; /// Morpho Blue. A constant rather than a constructor argument because this /// contract has no constructor arguments, so its address is a function of /// its initcode alone and is the same on every chain — see chaincfg's /// ProtocolSafeMigrateConnector. It is an Aave v4 tool and v4 is on /// Ethereum, where Morpho Blue is at this address (as on Base). Where it is /// not, there is no code here and `migrateAaveV4` refuses by name. address public constant MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; struct Migrate { /// The Safe. It is the caller, the beneficiary and the only address /// this contract will act for — all three, which is why it is one field /// and not three. address safe; /// The operating account the position is leaving. The Safe must be its /// root validator, which is what lets the Safe drive it. address account; address spoke; address collateral; address loanToken; uint256 collateralReserve; uint256 loanReserve; uint256 collateralAmount; } /// What is in flight, so the callback can prove the payload is ours. bytes32 internal _inFlight; event SafeMigrated( address indexed safe, address indexed account, address collateral, uint256 collateralAmount, address loanToken, uint256 debt ); error NotTheSafe(address caller, address safe); error ModuleNotEnabled(address safe); error NothingToMigrate(); error AlreadyInFlight(); error NotTheLender(address caller); error NoLenderOnThisChain(address lender); error PayloadNotOurs(); error DebtSurvived(uint256 left); error CollateralDidNotArrive(); error SafeCallFailed(address target); error FlashNotCovered(uint256 have, uint256 owed); /// Move an Aave v4 position out of `m.account` and into the Safe. /// /// ONE TRANSACTION, signed to the Safe's own threshold. The module flashes /// the loan token, the ACCOUNT repays its own debt and gives up its /// collateral, the SAFE re-opens the same position in its own name, and the /// new borrowing settles the flash. function migrateAaveV4(Migrate calldata m) external { if (msg.sender != m.safe) revert NotTheSafe(msg.sender, m.safe); // ASKED, NOT ASSUMED. A Safe that has not enabled this module would // fail on the first execTransactionFromModule, four frames into a flash // loan, with `GS104` and nothing to act on. if (!ISafeModule(m.safe).isModuleEnabled(address(this))) revert ModuleNotEnabled(m.safe); if (_inFlight != bytes32(0)) revert AlreadyInFlight(); if (MORPHO.code.length == 0) revert NoLenderOnThisChain(MORPHO); 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); _inFlight = keccak256(payload); IMorphoFlash(MORPHO).flashLoan(m.loanToken, owed, payload); _inFlight = bytes32(0); emit SafeMigrated( m.safe, m.account, m.collateral, m.collateralAmount, m.loanToken, owed ); } /// Morpho's callback, arriving at THIS contract rather than at the Safe. /// /// Two checks and both are load-bearing: the lender must be Morpho, and the /// payload must be the one we sent. 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 ours. function onMorphoFlashLoan(uint256 assets, bytes calldata data) external { if (msg.sender != MORPHO) revert NotTheLender(msg.sender); if (_inFlight == bytes32(0) || keccak256(data) != _inFlight) revert PayloadNotOurs(); (Migrate memory m, uint256 owed) = abi.decode(data, (Migrate, uint256)); _repayTheAccount(m, owed); uint256 got = _takeCollateral(m); _reopenForTheSafe(m, got, owed); // SETTLE. Morpho PULLS, so the allowance is what repays it. IERC20Min(m.loanToken).approve(MORPHO, assets); uint256 have = IERC20Min(m.loanToken).balanceOf(address(this)); if (have < assets) revert FlashNotCovered(have, assets); } /// 1. THE ACCOUNT REPAYS ITS OWN DEBT. /// /// Not this contract on its behalf — v4 refuses that. The flash is pushed /// into the account and the account repays, so it is both `msg.sender` and /// `onBehalfOf`. It is driven Safe -> account, because the Safe is the /// account's root validator. /// /// 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(Migrate memory m, uint256 owed) private { IERC20Min(m.loanToken).transfer(m.account, owed); _viaAccount(m, m.loanToken, abi.encodeCall(IERC20Min.approve, (m.spoke, owed))); _viaAccount( m, 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. THE COLLATERAL COMES OUT, THROUGH THE ACCOUNT AND ON TO THE SAFE. /// /// v4's `withdraw` pays `msg.sender` and its third argument is `onBehalfOf`, /// so the withdrawal lands at the ACCOUNT and has to be moved. It goes to /// the SAFE rather than here, because the Safe is the party that supplies /// it back — this contract only ever holds the flash. /// Withdraw delta only — idle collateral of the same asset stays on the account. function _takeCollateral(Migrate memory m) private returns (uint256 got) { uint256 before = IERC20Min(m.collateral).balanceOf(m.safe); uint256 atAccountBefore = IERC20Min(m.collateral).balanceOf(m.account); _viaAccount( m, m.spoke, abi.encodeCall( IAaveV4Spoke.withdraw, (m.collateralReserve, m.collateralAmount, m.account) ) ); uint256 withdrawn = IERC20Min(m.collateral).balanceOf(m.account) - atAccountBefore; _viaAccount(m, m.collateral, abi.encodeCall(IERC20Min.transfer, (m.safe, withdrawn))); got = IERC20Min(m.collateral).balanceOf(m.safe) - before; if (got == 0) revert CollateralDidNotArrive(); } /// 3, 4, 5 and 6. THE SAFE RE-OPENS THE POSITION IN ITS OWN NAME. /// /// Every one of these is the SAFE calling, with the SAFE as `onBehalfOf`, /// so not one is an on-behalf call and v4 has nothing to refuse. /// /// THE COLLATERAL ENABLE 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. /// /// The borrow pays the SAFE, so the last step hands the flash back to this /// contract to settle with. function _reopenForTheSafe(Migrate memory m, uint256 got, uint256 owed) private { _asSafe(m, m.collateral, abi.encodeCall(IERC20Min.approve, (m.spoke, got))); _asSafe(m, m.spoke, abi.encodeCall(IAaveV4Spoke.supply, (m.collateralReserve, got, m.safe))); _asSafe( m, m.spoke, abi.encodeCall(IAaveV4Spoke.setUsingAsCollateral, (m.collateralReserve, true, m.safe)) ); _asSafe(m, m.spoke, abi.encodeCall(IAaveV4Spoke.borrow, (m.loanReserve, owed, m.safe))); _asSafe( m, m.loanToken, abi.encodeCall(IERC20Min.transfer, (address(this), owed)) ); } /// One call made BY THE SAFE. CALL, never delegatecall — what is wanted is /// the Safe as `msg.sender`, not the Safe's storage. function _asSafe(Migrate memory m, address target, bytes memory data) private { if (!ISafeModule(m.safe).execTransactionFromModule(target, 0, data, CALL)) { revert SafeCallFailed(target); } } /// One call made BY THE ACCOUNT, driven by the Safe, which is its root /// validator. Two hops: the Safe calls `Kernel.execute`, and Kernel makes /// the call. function _viaAccount(Migrate memory m, address target, bytes memory data) private { _asSafe( m, m.account, abi.encodeCall( IKernelExec.execute, (EXEC_SINGLE, abi.encodePacked(target, uint256(0), data)) ) ); } }