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 8, 2025
1 parent 7191552 commit 03e5bbb
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 108 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
141 changes: 72 additions & 69 deletions contracts/.gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,75 +1,78 @@
ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3591780, ~: 3505774)
ExaAccountFactoryTest:test_deploy_deploysToSameAddress() (gas: 25415344)
ExaPluginTest:testFork_collectCollateral_collects() (gas: 14899874)
ExaPluginTest:testFork_crossRepay_repays() (gas: 14811014)
ExaPluginTest:testFork_swap_swaps() (gas: 12080960)
ExaPluginTest:test_borrowAtMaturity_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 408873)
ExaPluginTest:test_borrow_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 408407)
ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3623221, ~: 3510368)
ExaAccountFactoryTest:test_deploy_deploysToSameAddress() (gas: 26260190)
ExaPluginTest:testFork_collectCollateral_collects() (gas: 15359734)
ExaPluginTest:testFork_crossRepay_repays() (gas: 15270723)
ExaPluginTest:testFork_swap_swaps() (gas: 12537192)
ExaPluginTest:test_borrowAtMaturity_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 409027)
ExaPluginTest:test_borrow_reverts_withUnauthorized_whenReceiverNotCollector() (gas: 408539)
ExaPluginTest:test_collectCollateral_collects() (gas: 774831)
ExaPluginTest:test_collectCredit_collects() (gas: 918642)
ExaPluginTest:test_collectCredit_collects_whenHealthFactorHigherThanOne() (gas: 799222)
ExaPluginTest:test_collectCredit_collects_withEnoughSlippage() (gas: 797913)
ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 994502)
ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 1007595)
ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 362561)
ExaPluginTest:test_collectCredit_reverts_whenDisagreement() (gas: 555951)
ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 358575)
ExaPluginTest:test_collectCredit_reverts_whenHealthFactorLowerThanOne() (gas: 997887)
ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 461273)
ExaPluginTest:test_collectCredit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 1007860)
ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 840768)
ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 354765)
ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 801236)
ExaPluginTest:test_collectDebit_collects() (gas: 651480)
ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 854867)
ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 362328)
ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 358336)
ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 851820)
ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 690766)
ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 354595)
ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 651554)
ExaPluginTest:test_collectInstallments_collects() (gas: 1336411)
ExaPluginTest:test_collectInstallments_revertsWhenNoSlippage() (gas: 1202754)
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: 356914)
ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1193002)
ExaPluginTest:test_crossRepay_repays() (gas: 1462355)
ExaPluginTest:test_crossRepay_repays_whenKeeper() (gas: 1459024)
ExaPluginTest:test_crossRepay_reverts_whenDisagreement() (gas: 1577338)
ExaPluginTest:test_crossRepay_reverts_whenNotKeeper() (gas: 958947)
ExaPluginTest:test_exitMarket_reverts() (gas: 389020)
ExaPluginTest:test_marketWithdraw_transfersAsset_asOwner() (gas: 818728)
ExaPluginTest:test_onUninstall_uninstalls() (gas: 284118)
ExaPluginTest:test_poke() (gas: 315821)
ExaPluginTest:test_pokeETH_deposits() (gas: 377813)
ExaPluginTest:test_propose_emitsProposed() (gas: 218650)
ExaPluginTest:test_collectCredit_collects() (gas: 918840)
ExaPluginTest:test_collectCredit_collects_whenHealthFactorHigherThanOne() (gas: 799508)
ExaPluginTest:test_collectCredit_collects_withEnoughSlippage() (gas: 798089)
ExaPluginTest:test_collectCredit_collects_withPrevIssuerSignature() (gas: 994722)
ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 1010413)
ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 362606)
ExaPluginTest:test_collectCredit_reverts_whenDisagreement() (gas: 556083)
ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 358752)
ExaPluginTest:test_collectCredit_reverts_whenHealthFactorLowerThanOne() (gas: 1000771)
ExaPluginTest:test_collectCredit_reverts_whenPrevSignatureNotValidAnymore() (gas: 461427)
ExaPluginTest:test_collectCredit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 1010678)
ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 840988)
ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 354897)
ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 801412)
ExaPluginTest:test_collectDebit_collects() (gas: 651656)
ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 857640)
ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 362438)
ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 358535)
ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 854682)
ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 690986)
ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 354749)
ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 651708)
ExaPluginTest:test_collectInstallments_collects() (gas: 1336631)
ExaPluginTest:test_collectInstallments_revertsWhenNoSlippage() (gas: 1202886)
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: 357134)
ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1193222)
ExaPluginTest:test_crossRepay_repays() (gas: 1462554)
ExaPluginTest:test_crossRepay_repays_whenKeeper() (gas: 1459201)
ExaPluginTest:test_crossRepay_reverts_whenDisagreement() (gas: 1577471)
ExaPluginTest:test_crossRepay_reverts_whenNotKeeper() (gas: 959145)
ExaPluginTest:test_exitMarket_reverts() (gas: 389174)
ExaPluginTest:test_marketWithdraw_transfersAsset_asOwner() (gas: 824078)
ExaPluginTest:test_onUninstall_uninstalls() (gas: 292280)
ExaPluginTest:test_poke() (gas: 315909)
ExaPluginTest:test_pokeETH_deposits() (gas: 377989)
ExaPluginTest:test_proposeSwap_emitsSwapProposed() (gas: 381314)
ExaPluginTest:test_propose_emitsProposed() (gas: 221372)
ExaPluginTest:test_refund_refunds() (gas: 361573)
ExaPluginTest:test_repay_partiallyRepays() (gas: 1244077)
ExaPluginTest:test_repay_partiallyRepays_whenKeeper() (gas: 1186237)
ExaPluginTest:test_repay_repays() (gas: 1162403)
ExaPluginTest:test_repay_repays_whenKeeper() (gas: 1105101)
ExaPluginTest:test_rollDebt_rolls() (gas: 1231461)
ExaPluginTest:test_rollDebt_rolls_asKeeper() (gas: 1231175)
ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40494)
ExaPluginTest:test_repay_partiallyRepays() (gas: 1244341)
ExaPluginTest:test_repay_partiallyRepays_whenKeeper() (gas: 1186501)
ExaPluginTest:test_repay_repays() (gas: 1162667)
ExaPluginTest:test_repay_repays_whenKeeper() (gas: 1105409)
ExaPluginTest:test_rollDebt_rolls() (gas: 1231747)
ExaPluginTest:test_rollDebt_rolls_asKeeper() (gas: 1231439)
ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40581)
ExaPluginTest:test_setCollector_reverts_whenAddressZero() (gas: 32267)
ExaPluginTest:test_setCollector_reverts_whenNotAdmin() (gas: 33728)
ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39884)
ExaPluginTest:test_swap_reverts_withDisagreement() (gas: 289355)
ExaPluginTest:test_swap_swaps() (gas: 269053)
ExaPluginTest:test_withdrawWETH_transfersETH() (gas: 849248)
ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 414897)
ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 357819)
ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 354879)
ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 621112)
ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 307194)
ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 308974)
ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 308776)
ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 308946)
ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 308354)
ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 819923)
ExaPluginTest:test_withdraw_withdrawsProposed() (gas: 810691)
ExaPluginTest:test_setCollector_reverts_whenNotAdmin() (gas: 33795)
ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39796)
ExaPluginTest:test_swap_reverts_withDisagreement() (gas: 289510)
ExaPluginTest:test_swap_swaps() (gas: 269163)
ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 417717)
ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 357995)
ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 354989)
ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 626506)
ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 312479)
ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 314198)
ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 314038)
ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 314275)
ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 313572)
ExaPluginTest:test_withdraw_swapsProposed() (gas: 1622407)
ExaPluginTest:test_withdraw_swapsWETH() (gas: 1643094)
ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 825680)
ExaPluginTest:test_withdraw_transfersETH() (gas: 854714)
ExaPluginTest:test_withdraw_withdrawsProposed() (gas: 816448)
InstallmentsPreviewerTest:test_preview_returns() (gas: 135598)
IssuerCheckerTest:test_setIssuer_emits_IssuerSet() (gas: 70861)
IssuerCheckerTest:test_setIssuer_reverts_whenNotAdmin() (gas: 37272)
Expand Down
112 changes: 74 additions & 38 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 @@ -198,17 +214,26 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount {

function withdraw() external {
Proposal storage proposal = proposals[msg.sender];
address market = address(proposal.market);
if (market == address(0)) revert NoProposal();
IMarket market = proposal.market;
if (address(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);
address receiver = proposal.receiver;
_executeFromSender(
address(market),
0,
abi.encodeCall(
IERC4626.withdraw,
(amount, market == EXA_WETH && proposal.swapData.length == 0 ? address(this) : receiver, msg.sender)
)
);
if (proposal.swapData.length != 0) {
SwapData memory data = abi.decode(proposal.swapData, (SwapData));
swap(IERC20(market.asset()), data.assetOut, amount, data.minAmountOut, data.route);
} else if (market == EXA_WETH) {
WETH.withdraw(amount);
receiver.safeTransferETH(amount);
}
}

function collectCollateral(
Expand Down Expand Up @@ -345,21 +370,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 @@ -381,60 +407,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 @@ -747,3 +777,9 @@ struct RepayCallbackData {
uint256 positionAssets;
uint256 maxRepay;
}

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

0 comments on commit 03e5bbb

Please sign in to comment.