diff --git a/contracts/.gas-snapshot b/contracts/.gas-snapshot index 836cfb26..2401fdd6 100644 --- a/contracts/.gas-snapshot +++ b/contracts/.gas-snapshot @@ -1,75 +1,75 @@ -ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3622088, ~: 3517377) -ExaAccountFactoryTest:test_deploy_deploysToSameAddress() (gas: 25497599) -ExaPluginTest:testFork_crossRepay_repays() (gas: 14850557) -ExaPluginTest:testFork_debitCollateral_collects() (gas: 14939416) -ExaPluginTest:testFork_swap_swaps() (gas: 12120371) -ExaPluginTest:test_borrowAtMaturity_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 408851) -ExaPluginTest:test_borrow_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 408407) -ExaPluginTest:test_collectCredit_collects() (gas: 918642) -ExaPluginTest:test_collectCredit_collects_whenHealthFactorHigherThanOne() (gas: 799244) -ExaPluginTest:test_collectCredit_collects_withEnoughSlippage() (gas: 797913) -ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 994480) -ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 1007617) -ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 362539) -ExaPluginTest:test_collectCredit_reverts_whenDisagreement() (gas: 555951) -ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 358597) -ExaPluginTest:test_collectCredit_reverts_whenHealthFactorLowerThanOne() (gas: 997865) -ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 461362) -ExaPluginTest:test_collectCredit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 1007771) -ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 840746) -ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 354765) -ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 801258) -ExaPluginTest:test_collectDebit_collects() (gas: 651480) -ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 854845) -ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 362350) -ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 358314) -ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 851733) -ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 690788) -ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 354595) -ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 651576) -ExaPluginTest:test_collectInstallments_collects() (gas: 1336411) -ExaPluginTest:test_collectInstallments_revertsWhenNoSlippage() (gas: 1202776) -ExaPluginTest:test_collectInstallments_reverts_asNotKeeper() (gas: 363391) -ExaPluginTest:test_collectInstallments_reverts_whenExpired() (gas: 360647) -ExaPluginTest:test_collectInstallments_reverts_whenReplay() (gas: 1068213) -ExaPluginTest:test_collectInstallments_reverts_whenTimelocked() (gas: 356936) -ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1193002) -ExaPluginTest:test_crossRepay_repays() (gas: 1461705) -ExaPluginTest:test_crossRepay_repays_whenKeeper() (gas: 1458484) -ExaPluginTest:test_crossRepay_reverts_whenDisagreement() (gas: 1571938) -ExaPluginTest:test_crossRepay_reverts_whenNotKeeper() (gas: 958451) -ExaPluginTest:test_debitCollateral_collects() (gas: 774260) -ExaPluginTest:test_exitMarket_reverts() (gas: 388998) -ExaPluginTest:test_marketWithdraw_transfersAsset_asOwner() (gas: 818760) -ExaPluginTest:test_onUninstall_uninstalls() (gas: 284188) -ExaPluginTest:test_poke() (gas: 315843) -ExaPluginTest:test_pokeETH_deposits() (gas: 377813) -ExaPluginTest:test_propose_emitsProposed() (gas: 218628) +ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3558282, ~: 3424192) +ExaAccountFactoryTest:test_deploy_deploysToSameAddress() (gas: 26255734) +ExaPluginTest:testFork_crossRepay_repays() (gas: 15238277) +ExaPluginTest:testFork_debitCollateral_collects() (gas: 15327356) +ExaPluginTest:testFork_swap_swaps() (gas: 12508201) +ExaPluginTest:test_borrowAtMaturity_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 409049) +ExaPluginTest:test_borrow_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 408605) +ExaPluginTest:test_collectCredit_collects() (gas: 918972) +ExaPluginTest:test_collectCredit_collects_whenHealthFactorHigherThanOne() (gas: 799574) +ExaPluginTest:test_collectCredit_collects_withEnoughSlippage() (gas: 798243) +ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 994810) +ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 1010699) +ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 362825) +ExaPluginTest:test_collectCredit_reverts_whenDisagreement() (gas: 556237) +ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 358906) +ExaPluginTest:test_collectCredit_reverts_whenHealthFactorLowerThanOne() (gas: 1000947) +ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 461648) +ExaPluginTest:test_collectCredit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 1010853) +ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 841164) +ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 355051) +ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 801588) +ExaPluginTest:test_collectDebit_collects() (gas: 651810) +ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 857927) +ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 362636) +ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 358623) +ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 854815) +ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 691206) +ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 354881) +ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 651906) +ExaPluginTest:test_collectInstallments_collects() (gas: 1336785) +ExaPluginTest:test_collectInstallments_revertsWhenNoSlippage() (gas: 1203106) +ExaPluginTest:test_collectInstallments_reverts_asNotKeeper() (gas: 363677) +ExaPluginTest:test_collectInstallments_reverts_whenExpired() (gas: 360956) +ExaPluginTest:test_collectInstallments_reverts_whenReplay() (gas: 1068675) +ExaPluginTest:test_collectInstallments_reverts_whenTimelocked() (gas: 357222) +ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1193376) +ExaPluginTest:test_crossRepay_repays() (gas: 1462145) +ExaPluginTest:test_crossRepay_repays_whenKeeper() (gas: 1458924) +ExaPluginTest:test_crossRepay_reverts_whenDisagreement() (gas: 1572378) +ExaPluginTest:test_crossRepay_reverts_whenNotKeeper() (gas: 958869) +ExaPluginTest:test_debitCollateral_collects() (gas: 774590) +ExaPluginTest:test_exitMarket_reverts() (gas: 389196) +ExaPluginTest:test_marketWithdraw_transfersAsset_asOwner() (gas: 824331) +ExaPluginTest:test_onUninstall_uninstalls() (gas: 284175) +ExaPluginTest:test_poke() (gas: 316041) +ExaPluginTest:test_pokeETH_deposits() (gas: 378077) +ExaPluginTest:test_propose_emitsProposed() (gas: 221380) ExaPluginTest:test_refund_refunds() (gas: 361573) -ExaPluginTest:test_repay_partiallyRepays() (gas: 1244077) -ExaPluginTest:test_repay_partiallyRepays_whenKeeper() (gas: 1186237) -ExaPluginTest:test_repay_repays() (gas: 1162425) -ExaPluginTest:test_repay_repays_whenKeeper() (gas: 1105079) -ExaPluginTest:test_rollDebt_rolls() (gas: 1231461) -ExaPluginTest:test_rollDebt_rolls_asKeeper() (gas: 1231153) -ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40494) -ExaPluginTest:test_setCollector_reverts_whenAddressZero() (gas: 32289) -ExaPluginTest:test_setCollector_reverts_whenNotAdmin() (gas: 33750) -ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39884) -ExaPluginTest:test_swap_reverts_withDisagreement() (gas: 288750) -ExaPluginTest:test_swap_swaps() (gas: 268450) -ExaPluginTest:test_withdrawWETH_transfersETH() (gas: 849226) -ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 414919) -ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 357797) -ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 354879) -ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 621183) -ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 307334) -ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 309092) -ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 308754) -ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 308924) -ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 308358) -ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 820063) -ExaPluginTest:test_withdraw_withdrawsProposed() (gas: 810809) +ExaPluginTest:test_repay_partiallyRepays() (gas: 1244517) +ExaPluginTest:test_repay_partiallyRepays_whenKeeper() (gas: 1186677) +ExaPluginTest:test_repay_repays() (gas: 1162865) +ExaPluginTest:test_repay_repays_whenKeeper() (gas: 1105519) +ExaPluginTest:test_rollDebt_rolls() (gas: 1231945) +ExaPluginTest:test_rollDebt_rolls_asKeeper() (gas: 1231637) +ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40472) +ExaPluginTest:test_setCollector_reverts_whenAddressZero() (gas: 32267) +ExaPluginTest:test_setCollector_reverts_whenNotAdmin() (gas: 33751) +ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39862) +ExaPluginTest:test_swap_reverts_withDisagreement() (gas: 288926) +ExaPluginTest:test_swap_swaps() (gas: 268670) +ExaPluginTest:test_withdrawWETH_transfersETH() (gas: 854611) +ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 417738) +ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 358083) +ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 355165) +ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 626754) +ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 312707) +ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 314550) +ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 314127) +ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 314297) +ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 313731) +ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 826029) +ExaPluginTest:test_withdraw_withdrawsProposed() (gas: 816775) InstallmentsPreviewerTest:test_preview_returns() (gas: 135598) IssuerCheckerTest:test_setIssuer_emits_IssuerSet() (gas: 70861) IssuerCheckerTest:test_setIssuer_reverts_whenNotAdmin() (gas: 37272) diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 23faf119..10487f1f 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -2,7 +2,7 @@ solc_version = "0.8.28" evm_version = "cancun" optimizer = true -optimizer_runs = 200 +optimizer_runs = 649 deny_warnings = true verbosity = 3 isolate = true diff --git a/contracts/src/ExaPlugin.sol b/contracts/src/ExaPlugin.sol index 62dafeb7..669c6699 100644 --- a/contracts/src/ExaPlugin.sol +++ b/contracts/src/ExaPlugin.sol @@ -38,6 +38,7 @@ import { NotMarket, Proposal, Proposed, + SwapProposed, Timelocked, Unauthorized } from "./IExaAccount.sol"; @@ -112,12 +113,30 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount { function propose(IMarket market, uint256 amount, address receiver) external { _checkMarket(market); - proposals[msg.sender] = Proposal({ amount: amount, market: market, timestamp: block.timestamp, receiver: receiver }); + proposals[msg.sender] = + Proposal({ amount: amount, market: market, timestamp: block.timestamp, receiver: receiver, swapData: "" }); emit Proposed(msg.sender, market, receiver, amount, block.timestamp + PROPOSAL_DELAY); } - function swap(IERC20 assetIn, IERC20 assetOut, uint256 maxAmountIn, uint256 minAmountOut, bytes memory route) + // TODO add to manifest, only self. or merge with propose + function proposeSwap(IMarket market, uint256 amount, IERC20 assetOut, uint256 minAmountOut, bytes memory route) external + { + _checkMarket(market); + proposals[msg.sender] = Proposal({ + amount: amount, + market: market, + timestamp: block.timestamp, + receiver: msg.sender, + swapData: abi.encode(SwapData({ assetOut: assetOut, minAmountOut: minAmountOut, route: route })) + }); + emit SwapProposed( + msg.sender, market, msg.sender, amount, block.timestamp + PROPOSAL_DELAY, assetOut, minAmountOut, route + ); + } + + function swap(IERC20 assetIn, IERC20 assetOut, uint256 maxAmountIn, uint256 minAmountOut, bytes memory route) + public returns (uint256 amountIn, uint256 amountOut) { uint256 balanceIn = assetIn.balanceOf(msg.sender); @@ -202,14 +221,19 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount { address market = address(proposal.market); if (market == address(0)) revert NoProposal(); + uint256 amount = proposal.amount; if (market != address(EXA_WETH)) { - _executeFromSender(market, 0, abi.encodeCall(IERC4626.withdraw, (proposal.amount, proposal.receiver, msg.sender))); - return; + _executeFromSender(market, 0, abi.encodeCall(IERC4626.withdraw, (amount, proposal.receiver, msg.sender))); + } else { + _executeFromSender(market, 0, abi.encodeCall(IERC4626.withdraw, (amount, address(this), msg.sender))); + WETH.withdraw(amount); + proposal.receiver.safeTransferETH(amount); + } + + if (proposal.swapData.length != 0) { + SwapData memory data = abi.decode(proposal.swapData, (SwapData)); + swap(IERC20(proposal.market.asset()), data.assetOut, amount, data.minAmountOut, data.route); } - uint256 amount = proposal.amount; - _executeFromSender(market, 0, abi.encodeCall(IERC4626.withdraw, (amount, address(this), msg.sender))); - WETH.withdraw(amount); - proposal.receiver.safeTransferETH(amount); } function collectCollateral( @@ -750,3 +774,9 @@ struct RepayCallbackData { uint256 positionAssets; uint256 maxRepay; } + +struct SwapData { + IERC20 assetOut; + uint256 minAmountOut; + bytes route; +} diff --git a/contracts/src/IExaAccount.sol b/contracts/src/IExaAccount.sol index 62e315e4..9db046b2 100644 --- a/contracts/src/IExaAccount.sol +++ b/contracts/src/IExaAccount.sol @@ -6,6 +6,8 @@ import { IERC4626 } from "openzeppelin-contracts/contracts/interfaces/IERC4626.s interface IExaAccount { function propose(IMarket market, uint256 amount, address receiver) external; + function proposeSwap(IMarket market, uint256 amount, IERC20 assetOut, uint256 minAmountOut, bytes memory route) + external; function swap(IERC20 assetIn, IERC20 assetOut, uint256 maxAmountIn, uint256 minAmountOut, bytes memory route) external returns (uint256 amountIn, uint256 amountOut); @@ -62,6 +64,17 @@ event Proposed( address indexed account, IMarket indexed market, address indexed receiver, uint256 amount, uint256 unlock ); +event SwapProposed( + address indexed account, + IMarket indexed market, + address indexed receiver, + uint256 amount, + uint256 unlock, + IERC20 assetOut, + uint256 minAmountOut, + bytes route +); + struct FixedPool { uint256 borrowed; uint256 supplied; @@ -87,6 +100,7 @@ struct Proposal { IMarket market; address receiver; uint256 timestamp; + bytes swapData; } error BorrowLimitExceeded();