From 321d80d930dad49fc14994c3de9830650a105d93 Mon Sep 17 00:00:00 2001 From: itofarina Date: Tue, 31 Dec 2024 14:29:27 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20propose=20swap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit co-authored-by: danilo neves cruz --- contracts/.gas-snapshot | 181 +++++++++++++++++---------------- contracts/foundry.toml | 2 +- contracts/src/ExaPlugin.sol | 106 ++++++++++++------- contracts/src/IExaAccount.sol | 13 +++ contracts/test/ExaPlugin.t.sol | 16 +++ 5 files changed, 191 insertions(+), 127 deletions(-) diff --git a/contracts/.gas-snapshot b/contracts/.gas-snapshot index 836cfb26..6f590dff 100644 --- a/contracts/.gas-snapshot +++ b/contracts/.gas-snapshot @@ -1,90 +1,91 @@ -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) -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) -InstallmentsPreviewerTest:test_preview_returns() (gas: 135598) -IssuerCheckerTest:test_setIssuer_emits_IssuerSet() (gas: 70861) -IssuerCheckerTest:test_setIssuer_reverts_whenNotAdmin() (gas: 37272) -IssuerCheckerTest:test_setIssuer_reverts_whenZeroAddress() (gas: 35607) -IssuerCheckerTest:test_setOperationExpiry_emits_OperationExpirySet() (gas: 46864) -IssuerCheckerTest:test_setOperationExpiry_reverts_whenNotAdmin() (gas: 38985) -IssuerCheckerTest:test_setOperationExpiry_reverts_whenZeroValue() (gas: 37451) -IssuerCheckerTest:test_setPrevIssuerWindow_emits_PrevIssuerWindowSet() (gas: 46944) -IssuerCheckerTest:test_setPrevIssuerWindow_reverts_whenNotAdmin() (gas: 39074) -MockSwapperTest:test_swapExactAmountIn_swaps() (gas: 220542) -MockSwapperTest:test_swapExactAmountOut_swaps() (gas: 220669) -RefunderTest:test_refund_refunds() (gas: 242406) -RefunderTest:test_refund_reverts_whenExpired() (gas: 69541) -RefunderTest:test_refund_reverts_whenNotKeeper() (gas: 56909) -RefunderTest:test_refund_reverts_whenReplay() (gas: 279286) -RefunderTest:test_refund_reverts_whenTimelocked() (gas: 63276) -RefunderTest:test_refund_reverts_whenWrongSignature() (gas: 71153) -RefunderTest:test_refund_toleratesTimeDrift() (gas: 242492) \ No newline at end of file +ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3642465, ~: 3492463) +ExaAccountFactoryTest:test_deploy_deploysToSameAddress() (gas: 26484452) +ExaPluginTest:testFork_crossRepay_repays() (gas: 15361869) +ExaPluginTest:testFork_debitCollateral_collects() (gas: 15450602) +ExaPluginTest:testFork_swap_swaps() (gas: 12628497) +ExaPluginTest:test_borrowAtMaturity_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 406793) +ExaPluginTest:test_borrow_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 406280) +ExaPluginTest:test_collectCredit_collects() (gas: 916148) +ExaPluginTest:test_collectCredit_collects_whenHealthFactorHigherThanOne() (gas: 796747) +ExaPluginTest:test_collectCredit_collects_withEnoughSlippage() (gas: 795361) +ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 991831) +ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 1006901) +ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 360638) +ExaPluginTest:test_collectCredit_reverts_whenDisagreement() (gas: 553496) +ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 356711) +ExaPluginTest:test_collectCredit_reverts_whenHealthFactorLowerThanOne() (gas: 997138) +ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 459294) +ExaPluginTest:test_collectCredit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 1007139) +ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 837991) +ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 352867) +ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 798750) +ExaPluginTest:test_collectDebit_collects() (gas: 648853) +ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 854046) +ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 360471) +ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 356417) +ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 850960) +ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 687870) +ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 352642) +ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 648883) +ExaPluginTest:test_collectInstallments_collects() (gas: 1333286) +ExaPluginTest:test_collectInstallments_revertsWhenNoSlippage() (gas: 1199635) +ExaPluginTest:test_collectInstallments_reverts_asNotKeeper() (gas: 361534) +ExaPluginTest:test_collectInstallments_reverts_whenExpired() (gas: 358761) +ExaPluginTest:test_collectInstallments_reverts_whenReplay() (gas: 1064964) +ExaPluginTest:test_collectInstallments_reverts_whenTimelocked() (gas: 354994) +ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1189922) +ExaPluginTest:test_crossRepay_repays() (gas: 1456955) +ExaPluginTest:test_crossRepay_repays_whenKeeper() (gas: 1453846) +ExaPluginTest:test_crossRepay_reverts_whenDisagreement() (gas: 1567951) +ExaPluginTest:test_crossRepay_reverts_whenNotKeeper() (gas: 955475) +ExaPluginTest:test_debitCollateral_collects() (gas: 770907) +ExaPluginTest:test_exitMarket_reverts() (gas: 387047) +ExaPluginTest:test_marketWithdraw_transfersAsset_asOwner() (gas: 820781) +ExaPluginTest:test_onUninstall_uninstalls() (gas: 290394) +ExaPluginTest:test_poke() (gas: 314155) +ExaPluginTest:test_pokeETH_deposits() (gas: 375819) +ExaPluginTest:test_proposeSwap_emitsSwapProposed() (gas: 380499) +ExaPluginTest:test_propose_emitsProposed() (gas: 220403) +ExaPluginTest:test_refund_refunds() (gas: 360972) +ExaPluginTest:test_repay_partiallyRepays() (gas: 1239253) +ExaPluginTest:test_repay_partiallyRepays_whenKeeper() (gas: 1182029) +ExaPluginTest:test_repay_repays() (gas: 1157853) +ExaPluginTest:test_repay_repays_whenKeeper() (gas: 1101146) +ExaPluginTest:test_rollDebt_rolls() (gas: 1226954) +ExaPluginTest:test_rollDebt_rolls_asKeeper() (gas: 1226624) +ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40455) +ExaPluginTest:test_setCollector_reverts_whenAddressZero() (gas: 32143) +ExaPluginTest:test_setCollector_reverts_whenNotAdmin() (gas: 33628) +ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39757) +ExaPluginTest:test_swap_reverts_withDisagreement() (gas: 287681) +ExaPluginTest:test_swap_swaps() (gas: 267084) +ExaPluginTest:test_withdrawWETH_transfersETH() (gas: 850674) +ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 415373) +ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 355851) +ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 353022) +ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 623404) +ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 311212) +ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 312772) +ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 312600) +ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 312836) +ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 312279) +ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 822239) +ExaPluginTest:test_withdraw_withdrawsProposed() (gas: 813008) +InstallmentsPreviewerTest:test_preview_returns() (gas: 134970) +IssuerCheckerTest:test_setIssuer_emits_IssuerSet() (gas: 70595) +IssuerCheckerTest:test_setIssuer_reverts_whenNotAdmin() (gas: 37087) +IssuerCheckerTest:test_setIssuer_reverts_whenZeroAddress() (gas: 35466) +IssuerCheckerTest:test_setOperationExpiry_emits_OperationExpirySet() (gas: 46653) +IssuerCheckerTest:test_setOperationExpiry_reverts_whenNotAdmin() (gas: 38890) +IssuerCheckerTest:test_setOperationExpiry_reverts_whenZeroValue() (gas: 37333) +IssuerCheckerTest:test_setPrevIssuerWindow_emits_PrevIssuerWindowSet() (gas: 46687) +IssuerCheckerTest:test_setPrevIssuerWindow_reverts_whenNotAdmin() (gas: 38889) +MockSwapperTest:test_swapExactAmountIn_swaps() (gas: 220282) +MockSwapperTest:test_swapExactAmountOut_swaps() (gas: 220424) +RefunderTest:test_refund_refunds() (gas: 242095) +RefunderTest:test_refund_reverts_whenExpired() (gas: 69299) +RefunderTest:test_refund_reverts_whenNotKeeper() (gas: 56701) +RefunderTest:test_refund_reverts_whenReplay() (gas: 278779) +RefunderTest:test_refund_reverts_whenTimelocked() (gas: 63103) +RefunderTest:test_refund_reverts_whenWrongSignature() (gas: 70935) +RefunderTest:test_refund_toleratesTimeDrift() (gas: 242181) \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 23faf119..c48b5f64 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 = 449 deny_warnings = true verbosity = 3 isolate = true diff --git a/contracts/src/ExaPlugin.sol b/contracts/src/ExaPlugin.sol index 62dafeb7..7c0d6190 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,29 @@ 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) + function proposeSwap(IMarket market, IERC20 assetOut, uint256 amount, 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, assetOut, amount, minAmountOut, route, block.timestamp + PROPOSAL_DELAY + ); + } + + 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 +220,19 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount { address market = address(proposal.market); if (market == address(0)) revert NoProposal(); - if (market != address(EXA_WETH)) { - _executeFromSender(market, 0, abi.encodeCall(IERC4626.withdraw, (proposal.amount, proposal.receiver, msg.sender))); - return; - } uint256 amount = proposal.amount; - _executeFromSender(market, 0, abi.encodeCall(IERC4626.withdraw, (amount, address(this), msg.sender))); - WETH.withdraw(amount); - proposal.receiver.safeTransferETH(amount); + if (market != address(EXA_WETH) || proposal.swapData.length != 0) { + _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); + } } function collectCollateral( @@ -346,21 +369,22 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount { /// @inheritdoc BasePlugin function pluginManifest() external pure override returns (PluginManifest memory manifest) { - manifest.executionFunctions = new bytes4[](14); + manifest.executionFunctions = new bytes4[](15); manifest.executionFunctions[0] = this.propose.selector; - manifest.executionFunctions[1] = this.swap.selector; - manifest.executionFunctions[2] = this.crossRepay.selector; - manifest.executionFunctions[3] = this.repay.selector; - manifest.executionFunctions[4] = this.rollDebt.selector; - manifest.executionFunctions[5] = this.withdraw.selector; - manifest.executionFunctions[6] = this.collectCollateral.selector; - manifest.executionFunctions[7] = bytes4(keccak256("collectCredit(uint256,uint256,uint256,bytes)")); - manifest.executionFunctions[8] = bytes4(keccak256("collectCredit(uint256,uint256,uint256,uint256,bytes)")); - manifest.executionFunctions[9] = this.collectDebit.selector; - manifest.executionFunctions[10] = this.collectInstallments.selector; - manifest.executionFunctions[11] = this.poke.selector; - manifest.executionFunctions[12] = this.pokeETH.selector; - manifest.executionFunctions[13] = this.receiveFlashLoan.selector; + manifest.executionFunctions[1] = this.proposeSwap.selector; + manifest.executionFunctions[2] = this.swap.selector; + manifest.executionFunctions[3] = this.crossRepay.selector; + manifest.executionFunctions[4] = this.repay.selector; + manifest.executionFunctions[5] = this.rollDebt.selector; + manifest.executionFunctions[6] = this.withdraw.selector; + manifest.executionFunctions[7] = this.collectCollateral.selector; + manifest.executionFunctions[8] = bytes4(keccak256("collectCredit(uint256,uint256,uint256,bytes)")); + manifest.executionFunctions[9] = bytes4(keccak256("collectCredit(uint256,uint256,uint256,uint256,bytes)")); + manifest.executionFunctions[10] = this.collectDebit.selector; + manifest.executionFunctions[11] = this.collectInstallments.selector; + manifest.executionFunctions[12] = this.poke.selector; + manifest.executionFunctions[13] = this.pokeETH.selector; + manifest.executionFunctions[14] = this.receiveFlashLoan.selector; ManifestFunction memory selfRuntimeValidationFunction = ManifestFunction({ functionType: ManifestAssociatedFunctionType.SELF, @@ -382,60 +406,64 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount { functionId: uint8(FunctionId.RUNTIME_VALIDATION_BALANCER), dependencyIndex: 0 }); - manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](14); + manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](15); manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({ executionSelector: IExaAccount.propose.selector, associatedFunction: selfRuntimeValidationFunction }); manifest.runtimeValidationFunctions[1] = ManifestAssociatedFunction({ - executionSelector: IExaAccount.swap.selector, + executionSelector: IExaAccount.proposeSwap.selector, associatedFunction: selfRuntimeValidationFunction }); manifest.runtimeValidationFunctions[2] = ManifestAssociatedFunction({ + executionSelector: IExaAccount.swap.selector, + associatedFunction: selfRuntimeValidationFunction + }); + manifest.runtimeValidationFunctions[3] = ManifestAssociatedFunction({ executionSelector: IExaAccount.crossRepay.selector, associatedFunction: keeperOrSelfRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[3] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[4] = ManifestAssociatedFunction({ executionSelector: IExaAccount.repay.selector, associatedFunction: keeperOrSelfRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[4] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[5] = ManifestAssociatedFunction({ executionSelector: IExaAccount.rollDebt.selector, associatedFunction: keeperOrSelfRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[5] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[6] = ManifestAssociatedFunction({ executionSelector: IExaAccount.withdraw.selector, associatedFunction: keeperOrSelfRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[6] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[7] = ManifestAssociatedFunction({ executionSelector: IExaAccount.collectCollateral.selector, associatedFunction: keeperRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[7] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[8] = ManifestAssociatedFunction({ executionSelector: bytes4(keccak256("collectCredit(uint256,uint256,uint256,bytes)")), associatedFunction: keeperRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[8] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[9] = ManifestAssociatedFunction({ executionSelector: bytes4(keccak256("collectCredit(uint256,uint256,uint256,uint256,bytes)")), associatedFunction: keeperRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[9] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[10] = ManifestAssociatedFunction({ executionSelector: IExaAccount.collectDebit.selector, associatedFunction: keeperRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[10] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[11] = ManifestAssociatedFunction({ executionSelector: IExaAccount.collectInstallments.selector, associatedFunction: keeperRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[11] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[12] = ManifestAssociatedFunction({ executionSelector: IExaAccount.poke.selector, associatedFunction: keeperRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[12] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[13] = ManifestAssociatedFunction({ executionSelector: IExaAccount.pokeETH.selector, associatedFunction: keeperRuntimeValidationFunction }); - manifest.runtimeValidationFunctions[13] = ManifestAssociatedFunction({ + manifest.runtimeValidationFunctions[14] = ManifestAssociatedFunction({ executionSelector: this.receiveFlashLoan.selector, associatedFunction: balancerRuntimeValidationFunction }); @@ -750,3 +778,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..31641d4f 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, IERC20 assetOut, uint256 amount, 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,16 @@ event Proposed( address indexed account, IMarket indexed market, address indexed receiver, uint256 amount, uint256 unlock ); +event SwapProposed( + address indexed account, + IMarket indexed market, + IERC20 indexed assetOut, + uint256 amount, + uint256 minAmountOut, + bytes route, + uint256 unlock +); + struct FixedPool { uint256 borrowed; uint256 supplied; @@ -87,6 +99,7 @@ struct Proposal { IMarket market; address receiver; uint256 timestamp; + bytes swapData; } error BorrowLimitExceeded(); diff --git a/contracts/test/ExaPlugin.t.sol b/contracts/test/ExaPlugin.t.sol index b4731e8d..8ce35c90 100644 --- a/contracts/test/ExaPlugin.t.sol +++ b/contracts/test/ExaPlugin.t.sol @@ -39,6 +39,7 @@ import { InsufficientLiquidity, NoProposal, Proposed, + SwapProposed, Timelocked, Unauthorized } from "../src/IExaAccount.sol"; @@ -175,6 +176,21 @@ contract ExaPluginTest is ForkTest { account.execute(address(account), 0, abi.encodeCall(IExaAccount.propose, (exaEXA, amount, receiver))); } + function test_proposeSwap_emitsSwapProposed() external { + uint256 amount = 1; + bytes memory route = bytes("route"); + + vm.startPrank(owner); + + vm.expectEmit(true, true, true, true, address(exaPlugin)); + emit SwapProposed( + address(account), exaEXA, IERC20(address(usdc)), amount, 1, route, block.timestamp + exaPlugin.PROPOSAL_DELAY() + ); + account.execute( + address(account), 0, abi.encodeCall(IExaAccount.proposeSwap, (exaEXA, IERC20(address(usdc)), amount, 1, route)) + ); + } + function test_swap_swaps() external { uint256 prevUSDC = usdc.balanceOf(address(account)); uint256 prevEXA = exa.balanceOf(address(account));