From fbbae4b49a010ff04388f0da821e0bc65a323c09 Mon Sep 17 00:00:00 2001 From: danilo neves cruz Date: Tue, 7 Jan 2025 18:54:03 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20contracts:=20implement=20swap=20pro?= =?UTF-8?q?posals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit co-authored-by: itofarina --- .changeset/long-carpets-fold.md | 5 ++ contracts/.gas-snapshot | 140 ++++++++++++++++---------------- contracts/src/ExaPlugin.sol | 104 ++++++++++++++++-------- contracts/src/IExaAccount.sol | 13 +++ contracts/test/ExaPlugin.t.sol | 41 +++++++++- 5 files changed, 197 insertions(+), 106 deletions(-) create mode 100644 .changeset/long-carpets-fold.md diff --git a/.changeset/long-carpets-fold.md b/.changeset/long-carpets-fold.md new file mode 100644 index 00000000..4f3840a5 --- /dev/null +++ b/.changeset/long-carpets-fold.md @@ -0,0 +1,5 @@ +--- +"@exactly/plugin": patch +--- + +✨ implement swap proposals diff --git a/contracts/.gas-snapshot b/contracts/.gas-snapshot index 836cfb26..bbe53f65 100644 --- a/contracts/.gas-snapshot +++ b/contracts/.gas-snapshot @@ -1,75 +1,77 @@ -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) +ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3623750, ~: 3512005) +ExaAccountFactoryTest:test_deploy_deploysToSameAddress() (gas: 26337357) +ExaPluginTest:testFork_crossRepay_repays() (gas: 15308552) +ExaPluginTest:testFork_debitCollateral_collects() (gas: 15397454) +ExaPluginTest:testFork_swap_swaps() (gas: 12574999) +ExaPluginTest:test_borrowAtMaturity_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 409005) +ExaPluginTest:test_borrow_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 408561) +ExaPluginTest:test_collectCredit_collects() (gas: 918840) +ExaPluginTest:test_collectCredit_collects_whenHealthFactorHigherThanOne() (gas: 799420) +ExaPluginTest:test_collectCredit_collects_withEnoughSlippage() (gas: 798001) +ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 994700) +ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 1010457) +ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 362584) +ExaPluginTest:test_collectCredit_reverts_whenDisagreement() (gas: 556083) +ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 358774) +ExaPluginTest:test_collectCredit_reverts_whenHealthFactorLowerThanOne() (gas: 1000749) +ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 461516) +ExaPluginTest:test_collectCredit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 1010722) +ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 840966) +ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 354919) +ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 801434) +ExaPluginTest:test_collectDebit_collects() (gas: 651678) +ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 857729) +ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 362460) +ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 358513) +ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 854704) +ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 691008) +ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 354749) +ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 651752) +ExaPluginTest:test_collectInstallments_collects() (gas: 1336653) +ExaPluginTest:test_collectInstallments_revertsWhenNoSlippage() (gas: 1202930) +ExaPluginTest:test_collectInstallments_reverts_asNotKeeper() (gas: 363523) +ExaPluginTest:test_collectInstallments_reverts_whenExpired() (gas: 360824) +ExaPluginTest:test_collectInstallments_reverts_whenReplay() (gas: 1068433) +ExaPluginTest:test_collectInstallments_reverts_whenTimelocked() (gas: 357046) +ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1193222) +ExaPluginTest:test_crossRepay_repays() (gas: 1461904) +ExaPluginTest:test_crossRepay_repays_whenKeeper() (gas: 1458550) +ExaPluginTest:test_crossRepay_reverts_whenDisagreement() (gas: 1572071) +ExaPluginTest:test_crossRepay_reverts_whenNotKeeper() (gas: 958649) 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_exitMarket_reverts() (gas: 389152) +ExaPluginTest:test_marketWithdraw_transfersAsset_asOwner() (gas: 824132) +ExaPluginTest:test_onUninstall_uninstalls() (gas: 292368) +ExaPluginTest:test_poke() (gas: 315953) +ExaPluginTest:test_pokeETH_deposits() (gas: 377989) +ExaPluginTest:test_proposeSwap_emitsSwapProposed() (gas: 381358) +ExaPluginTest:test_propose_emitsProposed() (gas: 221350) 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_repay_partiallyRepays() (gas: 1244341) +ExaPluginTest:test_repay_partiallyRepays_whenKeeper() (gas: 1186523) +ExaPluginTest:test_repay_repays() (gas: 1162689) +ExaPluginTest:test_repay_repays_whenKeeper() (gas: 1105387) +ExaPluginTest:test_rollDebt_rolls() (gas: 1231747) +ExaPluginTest:test_rollDebt_rolls_asKeeper() (gas: 1231417) +ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40603) 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_setCollector_reverts_whenNotAdmin() (gas: 33706) +ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39796) +ExaPluginTest:test_swap_reverts_withDisagreement() (gas: 288816) +ExaPluginTest:test_swap_swaps() (gas: 268560) +ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 417650) +ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 357973) +ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 355011) +ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 626599) +ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 312619) +ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 314441) +ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 314038) +ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 314253) +ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 313687) +ExaPluginTest:test_withdraw_swapsProposed() (gas: 1597143) +ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 825765) +ExaPluginTest:test_withdraw_transfersETH() (gas: 854546) +ExaPluginTest:test_withdraw_withdrawsProposed() (gas: 816511) InstallmentsPreviewerTest:test_preview_returns() (gas: 135598) IssuerCheckerTest:test_setIssuer_emits_IssuerSet() (gas: 70861) IssuerCheckerTest:test_setIssuer_reverts_whenNotAdmin() (gas: 37272) diff --git a/contracts/src/ExaPlugin.sol b/contracts/src/ExaPlugin.sol index 62dafeb7..a8cb6e96 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,27 @@ 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 +218,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 +367,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 +404,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 +776,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..00f19685 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)); @@ -498,7 +514,30 @@ contract ExaPluginTest is ForkTest { assertEq(exa.balanceOf(receiver), amount); } - function test_withdrawWETH_transfersETH() external { + function test_withdraw_swapsProposed() external { + vm.startPrank(keeper); + account.poke(exaEXA); + account.poke(exaUSDC); + + uint256 amount = 100 ether; + uint256 minAmountOut = 490e6; + bytes memory route = + abi.encodeCall(MockSwapper.swapExactAmountIn, (address(exaEXA.asset()), amount, address(usdc), minAmountOut)); + vm.startPrank(address(account)); + account.execute( + address(account), + 0, + abi.encodeCall(IExaAccount.proposeSwap, (exaEXA, IERC20(address(usdc)), amount, minAmountOut, route)) + ); + + skip(exaPlugin.PROPOSAL_DELAY()); + + assertEq(usdc.balanceOf(address(account)), 0); + account.withdraw(); + assertGe(usdc.balanceOf(address(account)), minAmountOut); + } + + function test_withdraw_transfersETH() external { uint256 amount = 100 ether; address receiver = address(0x420); vm.prank(keeper);