Skip to content

Commit

Permalink
✨ contracts: implement swap proposals
Browse files Browse the repository at this point in the history
co-authored-by: itofarina <[email protected]>
  • Loading branch information
cruzdanilo and itofarina committed Jan 7, 2025
1 parent 8b358da commit fbbae4b
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 106 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-carpets-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/plugin": patch
---

✨ implement swap proposals
140 changes: 71 additions & 69 deletions contracts/.gas-snapshot
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
104 changes: 68 additions & 36 deletions contracts/src/ExaPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
NotMarket,
Proposal,
Proposed,
SwapProposed,
Timelocked,
Unauthorized
} from "./IExaAccount.sol";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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
});
Expand Down Expand Up @@ -750,3 +776,9 @@ struct RepayCallbackData {
uint256 positionAssets;
uint256 maxRepay;
}

struct SwapData {
IERC20 assetOut;
uint256 minAmountOut;
bytes route;
}
Loading

0 comments on commit fbbae4b

Please sign in to comment.