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

MainnetSwapSteward #3

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[profile.default]
src = "src"
out = "out"
test = "tests"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Expand Down
Empty file removed src/finance/.empty
Empty file.
148 changes: 148 additions & 0 deletions src/finance/MainnetSwapSteward.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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";
Copy link
Contributor

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.


Copy link
Contributor

Choose a reason for hiding this comment

The 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;

/// @inheritdoc IMainnetSwapSteward
address public priceChecker;

/// @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,
address _swapper,
address _milkman,
address _priceChecker
) {
_transferOwnership(_owner);
_updateGuardian(_guardian);
_setMilkman(_milkman);
_setPriceChecker(_priceChecker);

COLLECTOR = ICollector(_collector);
SWAPPER = AaveSwapper(_swapper);
}

/// @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, priceChecker, 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 InvalidZeroAddress();

swapApprovedToken[token] = true;
priceOracle[token] = priceFeedUSD;

// Validate oracle has necessary functions
if (IAggregatorInterface(priceFeedUSD).decimals() != 8) {
revert PriceFeedIncompatibleDecimals();
}
if (IAggregatorInterface(priceFeedUSD).latestAnswer() == 0) {
revert PriceFeedInvalidAnswer();
}

emit ApprovedToken(token, priceFeedUSD);
}

/// @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 = priceChecker;
priceChecker = 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();
}
}
}
17 changes: 17 additions & 0 deletions src/finance/interfaces/IAggregatorInterface.sol
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);
}
86 changes: 86 additions & 0 deletions src/finance/interfaces/IMainnetSwapSteward.sol
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 did not return a valid value
error PriceFeedFailure();

/// @dev Oracle does not have correct number of decimals
error PriceFeedIncompatibleDecimals();

/// @dev Oracle is returning unexpected value
error PriceFeedInvalidAnswer();

/// @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 ApprovedToken(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 priceChecker() 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;
}
Loading