diff --git a/.gitmodules b/.gitmodules index ae9d5ae5..acde3796 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/aave-address-book"] path = lib/aave-address-book url = https://github.com/bgd-labs/aave-address-book +[submodule "lib/composable-cow"] + path = lib/composable-cow + url = https://github.com/cowprotocol/composable-cow diff --git a/lib/composable-cow b/lib/composable-cow new file mode 160000 index 00000000..c86013be --- /dev/null +++ b/lib/composable-cow @@ -0,0 +1 @@ +Subproject commit c86013be3d563ede7e0354371f7e90f1818d6d13 diff --git a/scripts/AaveSwapperDeployment.s.sol b/scripts/AaveSwapperDeployment.s.sol index 98806116..2f7ad3c1 100644 --- a/scripts/AaveSwapperDeployment.s.sol +++ b/scripts/AaveSwapperDeployment.s.sol @@ -12,8 +12,8 @@ contract DeployAaveSwapper is Script { function run() external { vm.startBroadcast(); - address aaveSwapper = address(new AaveSwapper()); - ITransparentProxyFactory(MiscEthereum.TRANSPARENT_PROXY_FACTORY).create( + address aaveSwapper = address(new AaveSwapper(0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74)); + TransparentProxyFactory(MiscEthereum.TRANSPARENT_PROXY_FACTORY).create( aaveSwapper, ProxyAdmin(MiscEthereum.PROXY_ADMIN), abi.encodeWithSelector(AaveSwapper.initialize.selector) diff --git a/src/swaps/AaveSwapper.sol b/src/swaps/AaveSwapper.sol index eae26aca..0b4b073b 100644 --- a/src/swaps/AaveSwapper.sol +++ b/src/swaps/AaveSwapper.sol @@ -10,23 +10,30 @@ import {OwnableWithGuardian} from 'solidity-utils/contracts/access-control/Ownab import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; import {AaveGovernanceV2} from 'aave-address-book/AaveGovernanceV2.sol'; +import {ComposableCoW} from 'composable-cow/ComposableCoW.sol'; +import {ERC1271Forwarder} from 'composable-cow/ERC1271Forwarder.sol'; +import {IConditionalOrder} from 'composable-cow/interfaces/IConditionalOrder.sol'; import {IAaveSwapper} from './interfaces/IAaveSwapper.sol'; import {IPriceChecker} from './interfaces/IExpectedOutCalculator.sol'; import {IMilkman} from './interfaces/IMilkman.sol'; +import {IAggregatorV3Interface} from './interfaces/IAggregatorV3Interface.sol'; -/** - * @title AaveSwapper - * @author efecarranza.eth - * @notice Helper contract to swap assets using milkman - */ -contract AaveSwapper is IAaveSwapper, Initializable, OwnableWithGuardian, Rescuable { + + +/// @title AaveSwapper +/// @author efecarranza.eth +/// @notice Helper contract to swap assets using milkman +contract AaveSwapper is Initializable, OwnableWithGuardian, Rescuable, ERC1271Forwarder { using SafeERC20 for IERC20; /// @inheritdoc IAaveSwapper address public constant BAL80WETH20 = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; - /// @inheritdoc IAaveSwapper + constructor(address _composableCoW) ERC1271Forwarder(ComposableCoW(_composableCoW)) {} + + /// @notice Initializes the contract. + /// Reverts if already initialized function initialize() external initializer { _transferOwnership(AaveGovernanceV2.SHORT_EXECUTOR); _updateGuardian(0xA519a7cE7B24333055781133B13532AEabfAC81b); @@ -44,12 +51,6 @@ contract AaveSwapper is IAaveSwapper, Initializable, OwnableWithGuardian, Rescua uint256 amount, uint256 slippage ) external onlyOwner { - if (fromToken == address(0) || toToken == address(0)) revert Invalid0xAddress(); - if (recipient == address(0)) revert InvalidRecipient(); - if (amount == 0) revert InvalidAmount(); - - IERC20(fromToken).forceApprove(milkman, amount); - bytes memory data = _getPriceCheckerAndData(toToken, fromOracle, toOracle, slippage); IMilkman(milkman).requestSwapExactTokensForTokens( @@ -74,6 +75,82 @@ contract AaveSwapper is IAaveSwapper, Initializable, OwnableWithGuardian, Rescua ); } + /// @notice Function to swap one token for another with a limit price + /// @param milkman Address of the Milkman contract to submit the order + /// @param priceChecker Address of the price checker to validate order + /// @param fromToken Address of the token to swap from + /// @param toToken Address of the token to swap to + /// @param recipient Address of the account receiving the swapped funds + /// @param amount The amount of fromToken to swap + /// @param amountOut The limit price of the toToken (minimium amount to receive) + /// @dev For amountOut, use the token's atoms for decimals (ie: 6 for USDC, 18 for DAI) + function limitSwap( + address milkman, + address priceChecker, + address fromToken, + address toToken, + address recipient, + uint256 amount, + uint256 amountOut + ) external onlyOwner { + _swap(milkman, priceChecker, fromToken, toToken, recipient, amount, abi.encode(amountOut)); + + emit LimitSwapRequested(milkman, fromToken, toToken, amount, recipient, amountOut); + } + + /// @notice Function to swap one token for another at a time-weighted-average-price + /// @param handler Address of the COW Protocol contract handling TWAP swaps + /// @param relayer Address of the GvP2 Order contract + /// @param fromToken Address of the token to swap + /// @param toToken Address of the token to receive + /// @param recipient Address that will receive toToken + /// @param sellAmount The amount of tokens to sell per TWAP swap + /// @param minPartLimit Minimum amount of toToken to receive per TWAP swap + /// @param startTime Timestamp of when TWAP orders start + /// @param numParts Number of TWAP swaps to take place (each for sellAmount) + /// @param partDuration How long each TWAP takes (ie: hourly, weekly, etc) + /// @param span The timeframe the orders can take place in + function twapSwap( + address handler, + address relayer, + address fromToken, + address toToken, + address recipient, + uint256 sellAmount, + uint256 minPartLimit, + uint256 startTime, + uint256 numParts, + uint256 partDuration, + uint256 span + ) external onlyOwner { + if (fromToken == address(0) || toToken == address(0)) revert Invalid0xAddress(); + if (recipient == address(0)) revert InvalidRecipient(); + if (sellAmount == 0 || numParts == 0) revert InvalidAmount(); + + TWAPData memory twapData = TWAPData( + IERC20(fromToken), + IERC20(toToken), + recipient, + sellAmount, + minPartLimit, + startTime, + numParts, + partDuration, + span, + bytes32(0) + ); + IConditionalOrder.ConditionalOrderParams memory params = IConditionalOrder + .ConditionalOrderParams( + IConditionalOrder(handler), + 'AaveSwapper-TWAP-Swap', + abi.encode(twapData) + ); + composableCoW.create(params, true); + + IERC20(fromToken).forceApprove(relayer, sellAmount * numParts); + emit TWAPSwapRequested(handler, fromToken, toToken, recipient, sellAmount * numParts); + } + /// @inheritdoc IAaveSwapper function cancelSwap( address tradeMilkman, @@ -88,22 +165,81 @@ contract AaveSwapper is IAaveSwapper, Initializable, OwnableWithGuardian, Rescua ) external onlyOwnerOrGuardian { bytes memory data = _getPriceCheckerAndData(toToken, fromOracle, toOracle, slippage); - IMilkman(tradeMilkman).cancelSwap( + _cancelSwap(tradeMilkman, priceChecker, fromToken, toToken, recipient, amount, data); + } + + /// @notice Function to cancel an existing limit swap + /// @param tradeMilkman Address of the Milkman contract created upon order submission + /// @param priceChecker Address of the price checker to validate order + /// @param fromToken Address of the token to swap from + /// @param toToken Address of the token to swap to + /// @param recipient Address of the account receiving the swapped funds + /// @param amount The amount of fromToken to swap + /// @param amountOut The limit price of the toToken (minimium amount to receive) + function cancelLimitSwap( + address tradeMilkman, + address priceChecker, + address fromToken, + address toToken, + address recipient, + uint256 amount, + uint256 amountOut + ) external onlyOwnerOrGuardian { + _cancelSwap( + tradeMilkman, + priceChecker, + fromToken, + toToken, + recipient, amount, + abi.encode(amountOut) + ); + } + + /// @notice Function to cancel a pending time-weighted-average-price swap + /// @param handler Address of the COW Protocol contract handling TWAP swaps + /// @param fromToken Address of the token to swap + /// @param toToken Address of the token to receive + /// @param recipient Address that will receive toToken + /// @param sellAmount The amount of tokens to sell per TWAP swap + /// @param minPartLimit Minimum amount of toToken to receive per TWAP swap + /// @param startTime Timestamp of when TWAP orders start + /// @param numParts Number of TWAP swaps to take place (each for sellAmount) + /// @param partDuration How long each TWAP takes (ie: hourly, weekly, etc) + /// @param span The timeframe the orders can take place in + function cancelTwapSwap( + address handler, + address fromToken, + address toToken, + address recipient, + uint256 sellAmount, + uint256 minPartLimit, + uint256 startTime, + uint256 numParts, + uint256 partDuration, + uint256 span + ) external onlyOwnerOrGuardian { + TWAPData memory twapData = TWAPData( IERC20(fromToken), IERC20(toToken), recipient, - bytes32(0), - priceChecker, - data - ); - - IERC20(fromToken).safeTransfer( - address(AaveV3Ethereum.COLLECTOR), - IERC20(fromToken).balanceOf(address(this)) + sellAmount, + minPartLimit, + startTime, + numParts, + partDuration, + span, + bytes32(0) ); - - emit SwapCanceled(fromToken, toToken, amount); + IConditionalOrder.ConditionalOrderParams memory params = IConditionalOrder + .ConditionalOrderParams( + IConditionalOrder(handler), + 'AaveSwapper-TWAP-Swap', + abi.encode(twapData) + ); + bytes32 hashedOrder = composableCoW.hash(params); + composableCoW.remove(hashedOrder); + emit TWAPSwapCanceled(fromToken, toToken, sellAmount * numParts); } /// @inheritdoc IAaveSwapper @@ -133,20 +269,84 @@ contract AaveSwapper is IAaveSwapper, Initializable, OwnableWithGuardian, Rescua return owner(); } - /// @inheritdoc IRescuableBase - function maxRescue( - address - ) public pure override(RescuableBase, IRescuableBase) returns (uint256) { - return type(uint256).max; + /// @notice Internal function that handles swaps + /// @param milkman Address of the Milkman contract to submit the order + /// @param priceChecker Address of the price checker to validate order + /// @param fromToken Address of the token to swap from + /// @param toToken Address of the token to swap to + /// @param recipient Address of the account receiving the swapped funds + /// @param amount The amount of fromToken to swap + /// @param priceCheckerData abi-encoded data for price checker + function _swap( + address milkman, + address priceChecker, + address fromToken, + address toToken, + address recipient, + uint256 amount, + bytes memory priceCheckerData + ) internal { + if (fromToken == address(0) || toToken == address(0)) revert Invalid0xAddress(); + if (recipient == address(0)) revert InvalidRecipient(); + if (amount == 0) revert InvalidAmount(); + + IERC20(fromToken).forceApprove(milkman, amount); + + IMilkman(milkman).requestSwapExactTokensForTokens( + amount, + IERC20(fromToken), + IERC20(toToken), + recipient, + priceChecker, + priceCheckerData + ); + } + + /// @notice Internal function that handles swap cancellations + /// @param tradeMilkman Address of the Milkman contract to submit the order + /// @param priceChecker Address of the price checker to validate order + /// @param fromToken Address of the token to swap from + /// @param toToken Address of the token to swap to + /// @param recipient Address of the account receiving the swapped funds + /// @param amount The amount of fromToken to swap + /// @param priceCheckerData abi-encoded data for price checker + function _cancelSwap( + address tradeMilkman, + address priceChecker, + address fromToken, + address toToken, + address recipient, + uint256 amount, + bytes memory priceCheckerData + ) internal { + IMilkman(tradeMilkman).cancelSwap( + amount, + IERC20(fromToken), + IERC20(toToken), + recipient, + priceChecker, + priceCheckerData + ); + + IERC20(fromToken).safeTransfer( + address(AaveV3Ethereum.COLLECTOR), + IERC20(fromToken).balanceOf(address(this)) + ); + + emit SwapCanceled(fromToken, toToken, amount); } - /// @dev Internal function to encode swap data + /// @notice Helper function to abi-encode data for price checker + /// @param toToken Address of the token to swap to + /// @param fromOracle Address of the oracle to check fromToken price + /// @param toOracle Address of the oracle to check toToken price + /// @param slippage The allowed slippage compared to the oracle price (in BPS) function _getPriceCheckerAndData( address toToken, address fromOracle, address toOracle, uint256 slippage - ) internal pure returns (bytes memory) { + ) internal view returns (bytes memory) { if (toToken == BAL80WETH20) { return abi.encode(slippage, ''); } else { @@ -154,12 +354,16 @@ contract AaveSwapper is IAaveSwapper, Initializable, OwnableWithGuardian, Rescua } } - /// @dev Internal function to encode data for price checker + /// @notice Helper function to abi-encode Chainlink oracle data + /// @param fromOracle Address of the oracle to check fromToken price + /// @param toOracle Address of the oracle to check toToken price function _getChainlinkCheckerData( address fromOracle, address toOracle - ) internal pure returns (bytes memory) { + ) internal view returns (bytes memory) { if (fromOracle == address(0) || toOracle == address(0)) revert OracleNotSet(); + if (!(IAggregatorV3Interface(fromOracle).decimals() > 0)) revert InvalidOracle(); + if (!(IAggregatorV3Interface(toOracle).decimals() > 0)) revert InvalidOracle(); address[] memory paths = new address[](2); paths[0] = fromOracle; diff --git a/src/swaps/MiniSwapper.sol b/src/swaps/MiniSwapper.sol new file mode 100644 index 00000000..52ae09b1 --- /dev/null +++ b/src/swaps/MiniSwapper.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; +import {Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; +import {OwnableWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol'; +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; +import {ComposableCoW} from 'composable-cow/ComposableCoW.sol'; +import {ERC1271Forwarder} from 'composable-cow/ERC1271Forwarder.sol'; +import {IConditionalOrder} from 'composable-cow/interfaces/IConditionalOrder.sol'; + +struct TWAPData { + IERC20 sellToken; + IERC20 buyToken; + address receiver; + uint256 partSellAmount; // amount of sellToken to sell in each part + uint256 minPartLimit; // max price to pay for a unit of buyToken denominated in sellToken + uint256 t0; + uint256 n; + uint256 t; + uint256 span; + bytes32 appData; +} + +/// @title AaveSwapper +/// @author efecarranza.eth +/// @notice Helper contract to swap assets using milkman +contract MiniSwapper is OwnableWithGuardian, Rescuable, ERC1271Forwarder { + using SafeERC20 for IERC20; + + event TWAPSwapCanceled(address indexed fromToken, address indexed toToken, uint256 amount); + event TWAPSwapRequested( + address handler, + address indexed fromToken, + address indexed toToken, + address recipient, + uint256 totalAmount + ); + + /// @notice Provided address is zero address + error Invalid0xAddress(); + + /// @notice Amount needs to be greater than zero + error InvalidAmount(); + + /// @notice Recipient cannot be the zero address + error InvalidRecipient(); + + constructor(address _composableCoW) ERC1271Forwarder(ComposableCoW(_composableCoW)) {} + + function twapSwap( + address handler, + address relayer, + address fromToken, + address toToken, + address recipient, + uint256 sellAmount, + uint256 minPartLimit, + uint256 startTime, + uint256 numParts, + uint256 partDuration, + uint256 span + ) external onlyOwner { + if (fromToken == address(0) || toToken == address(0)) revert Invalid0xAddress(); + if (recipient == address(0)) revert InvalidRecipient(); + if (sellAmount == 0 || numParts == 0) revert InvalidAmount(); + + TWAPData memory twapData = TWAPData( + IERC20(fromToken), + IERC20(toToken), + recipient, + sellAmount, + minPartLimit, + startTime, + numParts, + partDuration, + span, + bytes32(0) + ); + IConditionalOrder.ConditionalOrderParams memory params = IConditionalOrder + .ConditionalOrderParams( + IConditionalOrder(handler), + 'AaveSwapper-TWAP-Swap', + abi.encode(twapData) + ); + composableCoW.create(params, true); + + IERC20(fromToken).forceApprove(relayer, sellAmount * numParts); + emit TWAPSwapRequested(handler, fromToken, toToken, recipient, sellAmount * numParts); + } + + function cancelTwapSwap( + address handler, + address fromToken, + address toToken, + address recipient, + uint256 sellAmount, + uint256 minPartLimit, + uint256 startTime, + uint256 numParts, + uint256 partDuration, + uint256 span + ) external onlyOwnerOrGuardian { + TWAPData memory twapData = TWAPData( + IERC20(fromToken), + IERC20(toToken), + recipient, + sellAmount, + minPartLimit, + startTime, + numParts, + partDuration, + span, + bytes32(0) + ); + IConditionalOrder.ConditionalOrderParams memory params = IConditionalOrder + .ConditionalOrderParams( + IConditionalOrder(handler), + 'AaveSwapper-TWAP-Swap', + abi.encode(twapData) + ); + bytes32 hashedOrder = composableCoW.hash(params); + composableCoW.remove(hashedOrder); + emit TWAPSwapCanceled(fromToken, toToken, sellAmount * numParts); + } + + /// @inheritdoc Rescuable + function whoCanRescue() public view override returns (address) { + return owner(); + } +} diff --git a/src/swaps/README.md b/src/swaps/README.md index aa25b5a0..d1316273 100644 --- a/src/swaps/README.md +++ b/src/swaps/README.md @@ -70,6 +70,64 @@ base-USD (ie: V3 oracles and not V2 oracles). AaveSwapper supports base-ETH swap For example USDC/ETH to AAVE/ETH or USDC/USD to AAVE/USD. It does not support USDC/ETH to AAVE/USD swaps and this can lead to bad trades because of price differences. +``` +function limitSwap( + address milkman, + address priceChecker, + address fromToken, + address toToken, + address recipient, + uint256 amount, + uint256 amountOut + ) external onlyOwner +``` + +Limit orders are used when wanting a specific price and not minding leaving an order open. When dealing with DAO swaps, knowing the price 5 days in advance is hard, and maybe relying on oracles is not what the DAO wants, especially dealing with low-liquidty tokens, or big orders that might move the market a lot but not the Oracle reference price. + +The `amountOut` here is in the token that is to be RECEIVED. For example, let's say we want to swap 1 wETH for USDC, and the price is $2,000, and we want to get that or better, we would specify the swap in terms of the USDC to be received, in this case, 2,000 USDC. The `amountOut` is measured in the smallest atom of the currency. For tokens with 18 decimals, this would be quotedin wei. For 6 decimals, it would be in 0.000001 increments. + +For example, if swapping 1 wETH for DAI, at a price of 2,000, then the `amountOut` needs to be `2000000000000000000000`. If swapping 1 wETH for USDC, the `amountOut` needs to be `2000000000` instead. + +Swap fees/gas costs need to be taken into account, especially for smaller orders. For orders in the hundreds of thousands, or millions, this is just going to be a very tiny amount in percentage terms so it won't really matter much, but for a small order, it might. Take for example, the 1 wETH for USDC swap described above. If the limit order is at 2,000 and gas costs are $50, the trade will not settle until price trades at 2,050 because that needs to be taken from the value of the swap. Alternatively, the limit could be made 1,950 because the DAO wants the trade to settle once it hits 2,000 and not have to worry about it. + +Limit orders are best suited for stable-to-stable swaps, especially bigger orders as the gas costs are going to be a tiny franction and swaps are likely to occurr rather easily. + +``` + function twapSwap( + address handler, + address relayer, + address fromToken, + address toToken, + address recipient, + uint256 sellAmount, + uint256 minPartLimit, + uint256 startTime, + uint256 numParts, + uint256 partDuration, + uint256 span + ) external onlyOwner { +``` + +TWAP (or time-weighted average price) orders are used when wanting to average a certain price for a swap. For example, let's say the DAO wants to acquire TokenX but the DAO wants to do periodical purchases in order to get an average price and not worry about fluctuations. With TWAP orders, the DAO could for example purchase a certain amount of TokenX every Monday, every hour, or every first of the month. + +The `handler` is the address of the COW Swap contract that can take TWAP orders, the address can be found [here](https://github.com/cowprotocol/composable-cow?tab=readme-ov-file#deployed-contracts) under TWAP. The `relayer` address is the COW Swap contract that handles moving of tokens, more can be read [here](https://docs.cow.fi/cow-protocol/reference/contracts/core/vault-relayer). + +`fromToken` is the token the user wants to sell, and `toToken` is the token that is to be acquired. `recipient` is the address that will receive the tokens, which is likely to be the Aave V3 Ethereum Collector. + +For the TWAP specific orders, the parameters and their explanation are as follows: + +`sellAmount` is the amount of tokens of `fromToken` to be sold each time. For example, let's say the DAO wants to sell 100,000 units of DAI every week for one month, then `sellAmount` would be 25,000, as there will be 4 swaps total. + +`minPartLimit` is the minimum amount the DAO is willing to accept per order. For example, following the above example, and with WETH trading at 2,000, which would yield 50 WETH (or 12.5 per each of the four orders), the minimum the DAO is willing to take is 12 (or 10, or anything). + +`startTime` is when the orders can first take effect, in unix epoch seconds. For example, the DAO wants the orders to take place on Mondays, and the proposal is to be executed on a Sunday, one can specify the `startTime` as block.timestamp + 1 day to ensure it's on Monday. + +`numParts` is the number of swaps to take place. In the example referenced above, this would be 4, to do a weekly swap for a month. + +`partDuration` is how long to wait until the next order. Again, using the example above, this would be the uint256 representation of 1 week. If the DAO wanted daily buys, this would be 1 day in uint256. + +`span` is to allow some extra customization on time of day, or days of the week the swaps will take place. Using the daily purchases example, the day has 86400 seconds, if the DAO only wanted to swap during the first half of the day, `span` would be set to 43200. The value 0 means the order can take place anytime during the interval (anytime during the day, week, month, etc). + ``` function cancelSwap( address tradeMilkman, @@ -85,10 +143,44 @@ function cancelSwap( ``` This methods cancels a pending trade. Trades should take just a couple of minutes to be picked up and traded, so if something's not right, the user -should move fast to cancel. +should assume something's off. Most likely, this function will be called when a swap is not getting executed because slippage might be too tight and there's no match for it. +``` +function cancelLimitSwap( + address tradeMilkman, + address priceChecker, + address fromToken, + address toToken, + address recipient, + uint256 amount, + uint256 amountOut + ) external onlyOwnerOrGuardian +``` + +This methods cancels a pending limit trade. Trades should take just a couple of minutes to be picked up and traded, so if something's not right, the user +should think that something might be off. + +For limit orders, keep in mind the price might be at the the limit price, but having to account for the cost of the swap might need the price to move a bit further. This should not matter for big swaps but it could be a thing in smaller swaps (especially around test swaps for validation). + +``` +function cancelTwapSwap( + address handler, + address fromToken, + address toToken, + address recipient, + uint256 sellAmount, + uint256 minPartLimit, + uint256 startTime, + uint256 numParts, + uint256 partDuration, + uint256 span + ) external onlyOwnerOrGuardian +``` + +This method cancels a pending TWAP swap. Portions that have already happened will not be reimbursed, but any subsequent ones will be cancelled. + ``` function getExpectedOut( @@ -128,9 +220,10 @@ Deposits funds held on AaveCurator into Aave V2/Aave V3 on behalf of the Collect ### Deployed Address -Please check out https://github.com/charlesndalton/milkman for updates on addresses. +Please check out https://github.com/charlesndalton/milkman for updates on addresses under the DEPLOYMENTS.md section. Milkman: [`0x11C76AD590ABDFFCD980afEC9ad951B160F02797`](https://etherscan.io/address/0x11C76AD590ABDFFCD980afEC9ad951B160F02797) Chainlink Price Checker: [`0xe80a1C615F75AFF7Ed8F08c9F21f9d00982D666c`](https://etherscan.io/address/0xe80a1C615F75AFF7Ed8F08c9F21f9d00982D666c) B80BAL20WETH Price Checker: [`0xBeA6AAC5bDCe0206A9f909d80a467C93A7D6Da7c`](https://etherscan.io/address/0xBeA6AAC5bDCe0206A9f909d80a467C93A7D6Da7c) -Mainnet: [`0x`]() +Limit Order Price Checker: [`0xcfb9Bc9d2FA5D3Dd831304A0AE53C76ed5c64802`](https://etherscan.io/address/0xcfb9Bc9d2FA5D3Dd831304A0AE53C76ed5c64802) +Mainnet: [`0x3ea64b1C0194524b48F9118462C8E9cd61a243c7`](https://etherscan.io/address/0x3ea64b1C0194524b48F9118462C8E9cd61a243c7) diff --git a/src/swaps/Tests.s.sol b/src/swaps/Tests.s.sol new file mode 100644 index 00000000..224674bb --- /dev/null +++ b/src/swaps/Tests.s.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {Script} from 'forge-std/Script.sol'; +import {MiscEthereum} from 'aave-address-book/MiscEthereum.sol'; +import {GPv2Order} from "cowprotocol/libraries/GPv2Order.sol"; + +import {AaveSwapper} from 'src/swaps/AaveSwapper.sol'; +import {MiniSwapper} from 'src/swaps/MiniSwapper.sol'; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; +import {IConditionalOrder} from 'composable-cow/interfaces/IConditionalOrder.sol'; + +interface IComposableCoW { + function getTradeableOrderWithSignature( + address owner, + IConditionalOrder.ConditionalOrderParams calldata params, + bytes calldata offchainInput, + bytes32[] calldata proof + ) external view returns (GPv2Order.Data memory order, bytes memory signature); +} + +struct TWAPData { + IERC20 sellToken; + IERC20 buyToken; + address receiver; + uint256 partSellAmount; // amount of sellToken to sell in each part + uint256 minPartLimit; // max price to pay for a unit of buyToken denominated in sellToken + uint256 t0; + uint256 n; + uint256 t; + uint256 span; + bytes32 appData; +} + +contract TestTWAPSwap is Script { + using SafeERC20 for IERC20; + + address public constant fromToken = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; // USDC + address public constant toToken = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // wETH + address public constant recipient = 0x3765A685a401622C060E5D700D9ad89413363a91; // me + uint256 public constant amount = 120e6; + uint256 public constant numParts = 6; + + address public constant COMPOSABLE_COW = 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74; + address public constant TWAP_HANDLER = 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5; + address public constant COW_RELAYER = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; + + function run() external { + vm.startBroadcast(); + + // MiniSwapper(0x15135198E254259899e472C4D2Aa566fEC59077D).emergencyTokenTransfer(fromToken, recipient, amount); + + MiniSwapper swapper = new MiniSwapper(COMPOSABLE_COW); + IERC20(fromToken).transfer(address(swapper), 120e6); + + swapper.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + fromToken, // 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + toToken, // 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 + recipient, // 0x3765A685a401622C060E5D700D9ad89413363a91 + amount / numParts, // 120e6 / 6 + 0.005 ether, + block.timestamp, + 6, + 1 hours, + 0 + ); + + vm.stopBroadcast(); + + // TWAPData memory twapData = TWAPData( + // IERC20(fromToken), + // IERC20(toToken), + // recipient, + // amount / numParts, + // 0.005 ether, + // 1704456647, + // numParts, + // 1 hours, + // 0, + // '' + // ); + // IConditionalOrder.ConditionalOrderParams memory params = IConditionalOrder + // .ConditionalOrderParams( + // IConditionalOrder(TWAP_HANDLER), + // 'AaveSwapper-TWAP-Swap', + // abi.encode(twapData) + // ); + + // bytes32[] memory proof = new bytes32[](0); + // (GPv2Order.Data memory order, bytes memory sig) = IComposableCoW(COMPOSABLE_COW).getTradeableOrderWithSignature( + // 0x15135198E254259899e472C4D2Aa566fEC59077D, + // params, + // "", + // proof + // ); + + // bytes32 domainSeparator = 0xc078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943; + // bytes32 orderDigest = GPv2Order.hash(order, domainSeparator); + + // MiniSwapper(0x15135198E254259899e472C4D2Aa566fEC59077D).isValidSignature(orderDigest, sig); + } +} diff --git a/src/swaps/interfaces/IAaveSwapper.sol b/src/swaps/interfaces/IAaveSwapper.sol index e3577f5e..c13b6c91 100644 --- a/src/swaps/interfaces/IAaveSwapper.sol +++ b/src/swaps/interfaces/IAaveSwapper.sol @@ -2,13 +2,46 @@ pragma solidity ^0.8.0; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; + interface IAaveSwapper { + struct TWAPData { + IERC20 sellToken; + IERC20 buyToken; + address receiver; + uint256 partSellAmount; // amount of sellToken to sell in each part + uint256 minPartLimit; // max price to pay for a unit of buyToken denominated in sellToken + uint256 t0; + uint256 n; + uint256 t; + uint256 span; + bytes32 appData; + } + + event LimitSwapRequested( + address milkman, + address indexed fromToken, + address indexed toToken, + uint256 amount, + address indexed recipient, + uint256 minAmountOut + ); + /// @dev Emitted when a swap is canceled /// @param fromToken The token to swap from /// @param toToken The token to swap to /// @param amount Amount of fromToken to swap event SwapCanceled(address indexed fromToken, address indexed toToken, uint256 amount); + event TWAPSwapCanceled(address indexed fromToken, address indexed toToken, uint256 amount); + event TWAPSwapRequested( + address handler, + address indexed fromToken, + address indexed toToken, + address recipient, + uint256 totalAmount + ); + /// @dev Emitted when a swap is submitted to Cow Swap /// @param milkman Address of Milkman (Cow Swap) contract /// @param fromToken Address of the token to swap from diff --git a/src/swaps/interfaces/IAggregatorV3Interface.sol b/src/swaps/interfaces/IAggregatorV3Interface.sol new file mode 100644 index 00000000..b455e646 --- /dev/null +++ b/src/swaps/interfaces/IAggregatorV3Interface.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IAggregatorV3Interface { + function decimals() external view returns (uint8); +} diff --git a/tests/swaps/AaveSwapperTest.t.sol b/tests/swaps/AaveSwapperTest.t.sol index 8eb760dc..4b652807 100644 --- a/tests/swaps/AaveSwapperTest.t.sol +++ b/tests/swaps/AaveSwapperTest.t.sol @@ -11,11 +11,28 @@ import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; import {AaveSwapper} from 'src/swaps/AaveSwapper.sol'; import {IAaveSwapper} from 'src/swaps/interfaces/IAaveSwapper.sol'; +import {IAggregatorV3Interface} from '../../src/swaps/interfaces/IAggregatorV3Interface.sol'; + +interface IComposableCoW { + function singleOrders(address user, bytes32 hash) external returns (bool); +} + +contract MockOracle { + fallback() external {} // Nothing Happens +} contract AaveSwapperTest is Test { event DepositedIntoV2(address indexed token, uint256 amount); event DepositedIntoV3(address indexed token, uint256 amount); event GuardianUpdated(address oldGuardian, address newGuardian); + event LimitSwapRequested( + address milkman, + address indexed fromToken, + address indexed toToken, + uint256 amount, + address indexed recipient, + uint256 minAmountOut + ); event SwapCanceled(address indexed fromToken, address indexed toToken, uint256 amount); event SwapRequested( address milkman, @@ -27,12 +44,25 @@ contract AaveSwapperTest is Test { address indexed recipient, uint256 slippage ); - event TokenUpdated(address indexed token, bool allowed); + event TWAPSwapCanceled(address indexed fromToken, address indexed toToken, uint256 amount); + event TWAPSwapRequested( + address handler, + address indexed fromToken, + address indexed toToken, + address recipient, + uint256 totalAmount + ); address public constant BAL80WETH20 = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; address public constant BPT_PRICE_CHECKER = 0xBeA6AAC5bDCe0206A9f909d80a467C93A7D6Da7c; address public constant CHAINLINK_PRICE_CHECKER = 0xe80a1C615F75AFF7Ed8F08c9F21f9d00982D666c; + address public constant LIMIT_ORDER_PRICE_CHECKER = 0xcfb9Bc9d2FA5D3Dd831304A0AE53C76ed5c64802; address public constant MILKMAN = 0x060373D064d0168931dE2AB8DDA7410923d06E88; + address public constant BAD_ORACLE = 0x05225Cd708bCa9253789C1374e4337a019e99D56; + + address public constant COMPOSABLE_COW = 0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74; + address public constant TWAP_HANDLER = 0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5; + address public constant COW_RELAYER = 0xC92E8bdf79f0507f65a392b0ab4667716BFE0110; AaveSwapper public swaps; @@ -40,7 +70,7 @@ contract AaveSwapperTest is Test { vm.createSelectFork(vm.rpcUrl('mainnet'), 21185924); vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); - swaps = new AaveSwapper(); + swaps = new AaveSwapper(COMPOSABLE_COW); vm.stopPrank(); } } @@ -126,10 +156,10 @@ contract AaveSwapperSwap is AaveSwapperTest { swaps.swap( MILKMAN, CHAINLINK_PRICE_CHECKER, - AaveV2EthereumAssets.WETH_UNDERLYING, - AaveV2EthereumAssets.AAVE_UNDERLYING, - address(0), - AaveV2EthereumAssets.AAVE_ORACLE, + AaveV3EthereumAssets.WETH_UNDERLYING, + AaveV3EthereumAssets.AAVE_UNDERLYING, + AaveV3EthereumAssets.WETH_ORACLE, + AaveV3EthereumAssets.AAVE_ORACLE, address(AaveV2Ethereum.COLLECTOR), 0, 200 @@ -144,9 +174,9 @@ contract AaveSwapperSwap is AaveSwapperTest { MILKMAN, CHAINLINK_PRICE_CHECKER, address(0), - AaveV2EthereumAssets.AAVE_UNDERLYING, - address(0), - AaveV2EthereumAssets.AAVE_ORACLE, + AaveV3EthereumAssets.AAVE_UNDERLYING, + AaveV3EthereumAssets.WETH_ORACLE, + AaveV3EthereumAssets.AAVE_ORACLE, address(AaveV2Ethereum.COLLECTOR), 1_000e18, 200 @@ -160,10 +190,10 @@ contract AaveSwapperSwap is AaveSwapperTest { swaps.swap( MILKMAN, CHAINLINK_PRICE_CHECKER, - AaveV2EthereumAssets.WETH_UNDERLYING, - address(0), + AaveV3EthereumAssets.WETH_UNDERLYING, address(0), - AaveV2EthereumAssets.AAVE_ORACLE, + AaveV3EthereumAssets.WETH_ORACLE, + AaveV3EthereumAssets.AAVE_ORACLE, address(AaveV2Ethereum.COLLECTOR), 1_000e18, 200 @@ -188,6 +218,115 @@ contract AaveSwapperSwap is AaveSwapperTest { vm.stopPrank(); } + function test_revertsIf_fromOracleNotSet() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(AaveSwapper.OracleNotSet.selector); + swaps.swap( + MILKMAN, + CHAINLINK_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + address(0), + AaveV2EthereumAssets.USDC_ORACLE, + address(AaveV3Ethereum.COLLECTOR), + 1_000e18, + 200 + ); + vm.stopPrank(); + } + + function test_revertsIf_toOracleNotSet() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(AaveSwapper.OracleNotSet.selector); + swaps.swap( + MILKMAN, + CHAINLINK_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + AaveV2EthereumAssets.AAVE_ORACLE, + address(0), + address(AaveV3Ethereum.COLLECTOR), + 1_000e18, + 200 + ); + vm.stopPrank(); + } + + function test_revertsIf_fromOracleIsInvalidNoDecimalsFunction() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(); + swaps.swap( + MILKMAN, + CHAINLINK_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + BAD_ORACLE, + AaveV2EthereumAssets.USDC_ORACLE, + address(AaveV3Ethereum.COLLECTOR), + 1_000e18, + 200 + ); + vm.stopPrank(); + } + + function test_revertsIf_toOracleIsInvalidNoDecimalsFunction() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(); + swaps.swap( + MILKMAN, + CHAINLINK_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + AaveV2EthereumAssets.AAVE_ORACLE, + BAD_ORACLE, + address(AaveV3Ethereum.COLLECTOR), + 1_000e18, + 200 + ); + vm.stopPrank(); + } + + function test_revertsIf_fromOracleIsInvalid() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(AaveSwapper.InvalidOracle.selector); + vm.mockCall( + BAD_ORACLE, + abi.encodeWithSelector(IAggregatorV3Interface.decimals.selector), + abi.encode(uint8(0)) + ); + swaps.swap( + MILKMAN, + CHAINLINK_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + BAD_ORACLE, + AaveV2EthereumAssets.USDC_ORACLE, + address(AaveV3Ethereum.COLLECTOR), + 1_000e18, + 200 + ); + vm.stopPrank(); + } + + function test_revertsIf_fromOracleIsInvalidWithFallbackFunction() public { + address badOracle = address(new MockOracle()); + + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(); + swaps.swap( + MILKMAN, + CHAINLINK_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + badOracle, + AaveV2EthereumAssets.USDC_ORACLE, + address(AaveV3Ethereum.COLLECTOR), + 1_000e18, + 200 + ); + vm.stopPrank(); + } + function test_successful() public { deal(AaveV2EthereumAssets.AAVE_UNDERLYING, address(swaps), 1_000e18); vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); @@ -461,3 +600,426 @@ contract GetExpectedOut is AaveSwapperTest { assertEq(expected / 1e18, 27); // WETH and BAL are 18 decimals } } + +contract LimitSwap is AaveSwapperTest { + function setUp() public override { + vm.createSelectFork(vm.rpcUrl('mainnet'), 18815161); + + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + swaps = new AaveSwapper(COMPOSABLE_COW); + vm.stopPrank(); + } + + function test_revertsIf_invalidCaller() public { + uint256 amount = 1_000e18; + vm.expectRevert('Ownable: caller is not the owner'); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.WETH_UNDERLYING, + AaveV2EthereumAssets.AAVE_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + amount, + 1_000e18 + ); + } + + function test_revertsIf_amountIsZero() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(AaveSwapper.InvalidAmount.selector); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.WETH_UNDERLYING, + AaveV2EthereumAssets.AAVE_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 0, + 1_000e18 + ); + vm.stopPrank(); + } + + function test_revertsIf_fromTokenIsZeroAddress() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(AaveSwapper.Invalid0xAddress.selector); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + address(0), + AaveV3EthereumAssets.AAVE_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 1_000e18 + ); + vm.stopPrank(); + } + + function test_revertsIf_toTokenIsZeroAddress() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(AaveSwapper.Invalid0xAddress.selector); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(0), + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 1_000e18 + ); + vm.stopPrank(); + } + + function test_revertsIf_invalidRecipient() public { + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + vm.expectRevert(AaveSwapper.InvalidRecipient.selector); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + address(0), + 1_000e18, + 1_000e18 + ); + vm.stopPrank(); + } + + function test_successful() public { + deal(AaveV2EthereumAssets.AAVE_UNDERLYING, address(swaps), 1_000e18); + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + + vm.expectEmit(true, true, true, true); + emit LimitSwapRequested( + MILKMAN, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + 1_000e18, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18 + ); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 1_000e18 + ); + vm.stopPrank(); + } +} + +contract CancelLimitSwap is AaveSwapperTest { + function setUp() public override { + vm.createSelectFork(vm.rpcUrl('mainnet'), 18815161); + + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + swaps = new AaveSwapper(COMPOSABLE_COW); + vm.stopPrank(); + } + + function test_revertsIf_invalidCaller() public { + uint256 amount = 1_000e18; + vm.expectRevert('ONLY_BY_OWNER_OR_GUARDIAN'); + swaps.cancelLimitSwap( + makeAddr('milkman-instance'), + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.WETH_UNDERLYING, + AaveV2EthereumAssets.AAVE_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + amount, + amount + ); + } + + function test_revertsIf_noMatchingTrade() public { + deal(AaveV2EthereumAssets.AAVE_UNDERLYING, address(swaps), 1_000e18); + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 1_000e18 + ); + + vm.expectRevert(); + swaps.cancelLimitSwap( + makeAddr('not-milkman-instance'), + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 1_000e18 + ); + vm.stopPrank(); + } + + function test_successful() public { + deal(AaveV2EthereumAssets.AAVE_UNDERLYING, address(swaps), 1_000e18); + vm.startPrank(AaveGovernanceV2.SHORT_EXECUTOR); + + vm.expectEmit(true, true, true, true); + emit LimitSwapRequested( + MILKMAN, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + 1_000e18, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18 + ); + swaps.limitSwap( + MILKMAN, + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 1_000e18 + ); + + vm.expectEmit(); + emit SwapCanceled( + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + 1_000e18 + ); + swaps.cancelLimitSwap( + 0x524c7Dfc9fEd2C68fAcBfA2aBF8aD58fd6fdb408, // Address generated by tests + LIMIT_ORDER_PRICE_CHECKER, + AaveV2EthereumAssets.AAVE_UNDERLYING, + AaveV2EthereumAssets.USDC_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 1_000e18 + ); + vm.stopPrank(); + } +} + +contract TWAPSwap is AaveSwapperTest { + function setUp() public override { + vm.createSelectFork(vm.rpcUrl('mainnet'), 18928427); + + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + swaps = new AaveSwapper(COMPOSABLE_COW); + vm.stopPrank(); + } + + function test_revertsIf_invalidCaller() public { + uint256 amount = 1_000e18; + vm.expectRevert('Ownable: caller is not the owner'); + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + amount, + 0.001 ether, + block.timestamp, + 5, + 1 days, + 0 + ); + } + + function test_revertsIf_fromTokenAddressIsZeroAddress() public { + vm.expectRevert(AaveSwapper.Invalid0xAddress.selector); + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + address(0), + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 0.001 ether, + block.timestamp, + 5, + 1 days, + 0 + ); + } + + function test_revertsIf_toTokenAddressIsZeroAddress() public { + vm.expectRevert(AaveSwapper.Invalid0xAddress.selector); + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + AaveV3EthereumAssets.USDC_UNDERLYING, + address(0), + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 0.001 ether, + block.timestamp, + 5, + 1 days, + 0 + ); + } + + function test_revertsIf_recipientIsAddressZero() public { + vm.expectRevert(AaveSwapper.InvalidRecipient.selector); + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(0), + 1_000e18, + 0.001 ether, + block.timestamp, + 5, + 1 days, + 0 + ); + } + + function test_revertsIf_amountIsZero() public { + vm.expectRevert(AaveSwapper.InvalidAmount.selector); + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV3Ethereum.COLLECTOR), + 0, + 0.001 ether, + block.timestamp, + 5, + 1 days, + 0 + ); + } + + function test_revertsIf_numberOfPartsIsZero() public { + vm.expectRevert(AaveSwapper.InvalidAmount.selector); + vm.prank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV3Ethereum.COLLECTOR), + 1_000e18, + 0.001 ether, + block.timestamp, + 0, + 1 days, + 0 + ); + } + + function test_successful() public { + uint256 amount = 1_000e6; + uint256 numParts = 5; + deal(AaveV3EthereumAssets.USDC_UNDERLYING, address(swaps), 100_000e6); + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + vm.expectEmit(true, true, true, true, address(swaps)); + emit TWAPSwapRequested( + TWAP_HANDLER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + amount * numParts + ); + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + amount, + 0.001 ether, + block.timestamp, + numParts, + 1 days, + 0 + ); + vm.stopPrank(); + } +} + +contract CancelTWAPSwap is AaveSwapperTest { + function setUp() public override { + vm.createSelectFork(vm.rpcUrl('mainnet'), 18928427); + + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + swaps = new AaveSwapper(COMPOSABLE_COW); + vm.stopPrank(); + } + + function test_revertsIf_invalidCaller() public { + vm.expectRevert('ONLY_BY_OWNER_OR_GUARDIAN'); + swaps.cancelTwapSwap( + TWAP_HANDLER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + 1_000e18, + 0.001 ether, + block.timestamp, + 5, + 1 days, + 0 + ); + } + + function test_successful() public { + uint256 amount = 1_000e6; + uint256 numParts = 5; + vm.startPrank(GovernanceV3Ethereum.EXECUTOR_LVL_1); + + swaps.twapSwap( + TWAP_HANDLER, + COW_RELAYER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + amount, + 0.001 ether, + block.timestamp, + numParts, + 1 days, + 0 + ); + + // Creating this order yields the following order hash: + // 0xb4a52ecc4f17d0df0e10d5dfb8604b3bc238ae7ad2c74019b730b23b82a31539 + bool orderExists = IComposableCoW(COMPOSABLE_COW).singleOrders(address(swaps), 0xb4a52ecc4f17d0df0e10d5dfb8604b3bc238ae7ad2c74019b730b23b82a31539); + + assertTrue(orderExists, "Order does not exist"); + + vm.expectEmit(true, true, true, true, address(swaps)); + emit TWAPSwapCanceled( + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + amount * numParts + ); + + swaps.cancelTwapSwap( + TWAP_HANDLER, + AaveV3EthereumAssets.USDC_UNDERLYING, + AaveV3EthereumAssets.WETH_UNDERLYING, + address(AaveV2Ethereum.COLLECTOR), + amount, + 0.001 ether, + block.timestamp, + numParts, + 1 days, + 0 + ); + + orderExists = IComposableCoW(COMPOSABLE_COW).singleOrders(address(swaps), 0xb4a52ecc4f17d0df0e10d5dfb8604b3bc238ae7ad2c74019b730b23b82a31539); + assertFalse(orderExists, "Order exists after removing"); + vm.stopPrank(); + } +}