Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aave Swapper - TWAP Orders #225

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/composable-cow
Submodule composable-cow added at c86013
4 changes: 2 additions & 2 deletions scripts/AaveSwapperDeployment.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
270 changes: 237 additions & 33 deletions src/swaps/AaveSwapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(
Expand All @@ -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,
Comment on lines +114 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@efecarranza does it make sense to make these params as immutable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @brotherlymite when building the original AaveSwapper, feedback was provided that we should be allowed to pass the specific price checker, so that's why I went with that route for this even though they are not a price checker, but kind of the same concept. We could have it as a contract variable that can be updated as well in case of changes to the cow swap relayer or handler, though I doubt those values are likely to change.

@eboado If my memory serves me well, I think you and I had discussed this approach. Wanted to see if you had any thoughts around it. Thank you

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,
Expand All @@ -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
Expand Down Expand Up @@ -133,33 +269,101 @@ 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 {
return abi.encode(slippage, _getChainlinkCheckerData(fromOracle, toOracle));
}
}

/// @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;
Expand Down
Loading