-
Notifications
You must be signed in to change notification settings - Fork 2
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
MainnetSwapSteward #3
Open
efecarranza
wants to merge
9
commits into
bgd-labs:main
Choose a base branch
from
kpk-tl:swap-steward
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9038eee
feat: mainnet swap steward
efecarranza 9a8f075
feat: update tests and add interface
efecarranza cb14d6b
chore: add docs
efecarranza 87974a7
fix: address PR feedback
efecarranza 63c4fc3
chore: remove unused error
efecarranza 920ddd5
feat: updgrade to new collector
efecarranza 161cbee
Merge remote-tracking branch 'origin/main' into swap-steward
efecarranza f251686
feat: add budget to tokens
efecarranza 602347b
feat: add budget and update tests
efecarranza File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,145 @@ | ||||
// SPDX-License-Identifier: MIT | ||||
pragma solidity ^0.8.0; | ||||
|
||||
import {IERC20} from "solidity-utils/contracts/oz-common/interfaces/IERC20.sol"; | ||||
import {OwnableWithGuardian} from "solidity-utils/contracts/access-control/OwnableWithGuardian.sol"; | ||||
import {AaveSwapper} from "aave-helpers/src/swaps/AaveSwapper.sol"; | ||||
import {ICollector, CollectorUtils as CU} from "aave-helpers/src/CollectorUtils.sol"; | ||||
import {MiscEthereum} from "aave-address-book/MiscEthereum.sol"; | ||||
import {IAggregatorInterface} from "src/finance/interfaces/IAggregatorInterface.sol"; | ||||
|
||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
import {IMainnetSwapSteward} from "src/finance/interfaces/IMainnetSwapSteward.sol"; | ||||
|
||||
/** | ||||
* @title MainnetSwapSteward | ||||
* @author luigy-lemon (Karpatkey) | ||||
* @author efecarranza (Tokenlogic) | ||||
* @notice Facilitates token swaps on behalf of the DAO Treasury using the AaveSwapper. | ||||
* Funds must be present in the AaveSwapper in order for them to be executed. | ||||
* The tokens that are to be swapped from/to are to be pre-approved via governance. | ||||
* | ||||
* -- Security Considerations | ||||
* Having previously set and validated oracles avoids mistakes that are easy to make when passing the necessary parameters to swap. | ||||
* | ||||
* -- Permissions | ||||
* The contract implements OwnableWithGuardian. | ||||
* The owner will always be the respective network Short Executor (governance). | ||||
* The guardian role will be given to a Financial Service provider of the DAO. | ||||
* | ||||
*/ | ||||
contract MainnetSwapSteward is OwnableWithGuardian, IMainnetSwapSteward { | ||||
using CU for CU.SwapInput; | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
uint256 public constant MAX_SLIPPAGE = 1000; // 10% | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
AaveSwapper public immutable SWAPPER; | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
ICollector public immutable COLLECTOR; | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
address public MILKMAN; | ||||
sakulstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
address public PRICE_CHECKER; | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
mapping(address token => bool isApproved) public swapApprovedToken; | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
mapping(address token => address oracle) public priceOracle; | ||||
|
||||
constructor(address _owner, address _guardian, address collector) { | ||||
_transferOwnership(_owner); | ||||
_updateGuardian(_guardian); | ||||
|
||||
COLLECTOR = ICollector(collector); | ||||
SWAPPER = AaveSwapper(MiscEthereum.AAVE_SWAPPER); | ||||
|
||||
// https://etherscan.io/address/0x060373D064d0168931dE2AB8DDA7410923d06E88 | ||||
_setMilkman(0x060373D064d0168931dE2AB8DDA7410923d06E88); | ||||
|
||||
// https://etherscan.io/address/0xe80a1C615F75AFF7Ed8F08c9F21f9d00982D666c | ||||
_setPriceChecker(0xe80a1C615F75AFF7Ed8F08c9F21f9d00982D666c); | ||||
sakulstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
function tokenSwap(address sellToken, uint256 amount, address buyToken, uint256 slippage) | ||||
external | ||||
onlyOwnerOrGuardian | ||||
{ | ||||
_validateSwap(sellToken, amount, buyToken, slippage); | ||||
|
||||
CU.SwapInput memory swapData = CU.SwapInput( | ||||
MILKMAN, PRICE_CHECKER, sellToken, buyToken, priceOracle[sellToken], priceOracle[buyToken], amount, slippage | ||||
); | ||||
|
||||
CU.swap(COLLECTOR, address(SWAPPER), swapData); | ||||
} | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
function setSwappableToken(address token, address priceFeedUSD) external onlyOwner { | ||||
if (priceFeedUSD == address(0)) revert MissingPriceFeed(); | ||||
sakulstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
|
||||
swapApprovedToken[token] = true; | ||||
priceOracle[token] = priceFeedUSD; | ||||
|
||||
// Validate oracle has necessary functions | ||||
if (IAggregatorInterface(priceFeedUSD).decimals() != 8) { | ||||
revert PriceFeedIncompatibility(); | ||||
} | ||||
if (IAggregatorInterface(priceFeedUSD).latestAnswer() == 0) { | ||||
revert PriceFeedIncompatibility(); | ||||
sakulstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
|
||||
emit SwapApprovedToken(token, priceFeedUSD); | ||||
sakulstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
function setPriceChecker(address newPriceChecker) external onlyOwner { | ||||
_setPriceChecker(newPriceChecker); | ||||
} | ||||
|
||||
/// @inheritdoc IMainnetSwapSteward | ||||
function setMilkman(address newMilkman) external onlyOwner { | ||||
_setMilkman(newMilkman); | ||||
} | ||||
|
||||
/// @dev Internal function to set the price checker | ||||
function _setPriceChecker(address newPriceChecker) internal { | ||||
if (newPriceChecker == address(0)) revert InvalidZeroAddress(); | ||||
address old = PRICE_CHECKER; | ||||
PRICE_CHECKER = newPriceChecker; | ||||
|
||||
emit PriceCheckerUpdated(old, newPriceChecker); | ||||
} | ||||
|
||||
/// @dev Internal function to set the Milkman instance address | ||||
function _setMilkman(address newMilkman) internal { | ||||
if (newMilkman == address(0)) revert InvalidZeroAddress(); | ||||
address old = MILKMAN; | ||||
MILKMAN = newMilkman; | ||||
|
||||
emit MilkmanAddressUpdated(old, newMilkman); | ||||
} | ||||
|
||||
/// @dev Internal function to validate a swap's parameters | ||||
function _validateSwap(address sellToken, uint256 amountIn, address buyToken, uint256 slippage) internal view { | ||||
if (amountIn == 0) revert InvalidZeroAmount(); | ||||
|
||||
if (!swapApprovedToken[sellToken] || !swapApprovedToken[buyToken]) { | ||||
revert UnrecognizedToken(); | ||||
} | ||||
|
||||
if (slippage > MAX_SLIPPAGE) revert InvalidSlippage(); | ||||
|
||||
if ( | ||||
IAggregatorInterface(priceOracle[buyToken]).latestAnswer() == 0 | ||||
|| IAggregatorInterface(priceOracle[sellToken]).latestAnswer() == 0 | ||||
) { | ||||
revert PriceFeedFailure(); | ||||
sakulstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
} | ||||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Chainlink Contracts v0.8 | ||
pragma solidity ^0.8.0; | ||
|
||
interface IAggregatorInterface { | ||
function decimals() external view returns (uint8); | ||
|
||
function latestAnswer() external view returns (int256); | ||
|
||
function latestTimestamp() external view returns (uint256); | ||
|
||
function latestRound() external view returns (uint256); | ||
|
||
function getAnswer(uint256 roundId) external view returns (int256); | ||
|
||
function getTimestamp(uint256 roundId) external view returns (uint256); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import {ICollector} from "aave-helpers/src/CollectorUtils.sol"; | ||
import {AaveSwapper} from "aave-helpers/src/swaps/AaveSwapper.sol"; | ||
|
||
interface IMainnetSwapSteward { | ||
/// @dev Slippage is too high | ||
error InvalidSlippage(); | ||
|
||
/// @dev Provided address cannot be the zero-address | ||
error InvalidZeroAddress(); | ||
|
||
/// @dev Amount cannot be zero | ||
error InvalidZeroAmount(); | ||
|
||
/// @dev Oracle cannot be the zero-address | ||
error MissingPriceFeed(); | ||
|
||
/// @dev Oracle did not return a valid value | ||
error PriceFeedFailure(); | ||
|
||
sakulstra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// @dev Oracle is returning unexpected values | ||
error PriceFeedIncompatibility(); | ||
|
||
/// @dev Token has not been previously approved for swapping | ||
error UnrecognizedToken(); | ||
|
||
/// @notice Emitted when the Milkman contract address is updated | ||
/// @param oldAddress The old Milkman instance address | ||
/// @param newAddress The new Milkman instance address | ||
event MilkmanAddressUpdated(address oldAddress, address newAddress); | ||
|
||
/// @notice Emitted when the Chainlink Price Checker contract address is updated | ||
/// @param oldAddress The old Price Checker instance address | ||
/// @param newAddress The new Price Checker instance address | ||
event PriceCheckerUpdated(address oldAddress, address newAddress); | ||
|
||
/// @notice Emitted when a token is approved for swapping with its corresponding USD oracle | ||
/// @param token The address of the token approved for swapping | ||
/// @param oracleUSD The address of the oracle providing the USD price feed for the token | ||
event SwapApprovedToken(address indexed token, address indexed oracleUSD); | ||
|
||
/// @notice Returns instance of Aave V3 Collector | ||
function COLLECTOR() external view returns (ICollector); | ||
|
||
/// @notice Returns the maximum allowed slippage for swaps (in BPS) | ||
function MAX_SLIPPAGE() external view returns (uint256); | ||
|
||
/// @notice Returns instance of the AaveSwapper contract | ||
function SWAPPER() external view returns (AaveSwapper); | ||
|
||
/// @notice Returns the address of the Milkman contract | ||
function MILKMAN() external view returns (address); | ||
|
||
/// @notice Returns address of the price checker used for swaps | ||
function PRICE_CHECKER() external view returns (address); | ||
|
||
/// @notice Returns whether token is approved to be swapped from/to | ||
/// @param token Address of the token to swap from/to | ||
function swapApprovedToken(address token) external view returns (bool); | ||
|
||
/// @notice Returns address of the Oracle to use for token swaps | ||
/// @param token Address of the token to swap | ||
function priceOracle(address token) external view returns (address); | ||
|
||
/// @notice Swaps a specified amount of a sell token for a buy token | ||
/// @param sellToken The address of the token to sell | ||
/// @param amount The amount of the sell token to swap | ||
/// @param buyToken The address of the token to buy | ||
/// @param slippage The slippage allowed in the swap | ||
function tokenSwap(address sellToken, uint256 amount, address buyToken, uint256 slippage) external; | ||
|
||
/// @notice Sets the address for the MILKMAN used in swaps | ||
/// @param to The address of MILKMAN | ||
function setMilkman(address to) external; | ||
|
||
/// @notice Sets the address for the Price checker used in swaps | ||
/// @param to The address of PRICE_CHECKER | ||
function setPriceChecker(address to) external; | ||
|
||
/// @notice Sets a token as swappable and provides its price feed address | ||
/// @param token The address of the token to set as swappable | ||
/// @param priceFeedUSD The address of the price feed for the token | ||
function setSwappableToken(address token, address priceFeedUSD) external; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better to do relative imports, otherwise remappings are hell to work with.