Merge pull request #20 from propeller-heads/feature/angle-adapter
Angle Adapter
This commit is contained in:
719
evm/src/angle/AngleAdapter.sol
Normal file
719
evm/src/angle/AngleAdapter.sol
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
||||||
|
import {IERC20Metadata} from
|
||||||
|
"openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
||||||
|
|
||||||
|
/// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy
|
||||||
|
uint256 constant RESERVE_LIMIT_FACTOR = 10;
|
||||||
|
uint256 constant STANDARD_TOKEN_DECIMALS = 10 ** 18;
|
||||||
|
|
||||||
|
/// @title AngleAdapter
|
||||||
|
/// @dev Information about prices: When swapping collateral to agEUR, the trade
|
||||||
|
/// price will not decrease(amountOut). Instead, when swapping agEUR to
|
||||||
|
/// collateral, it will, because agEUR is minted, and this mechanism is used to
|
||||||
|
/// stabilize the agEUR price.
|
||||||
|
contract AngleAdapter is ISwapAdapter {
|
||||||
|
ITransmuter immutable transmuter;
|
||||||
|
|
||||||
|
constructor(ITransmuter _transmuter) {
|
||||||
|
transmuter = _transmuter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
/**
|
||||||
|
* @dev It is not possible to reproduce the swap in a view mode (like
|
||||||
|
* Bancor, Uniswap v2, etc..) as the swap produce a change of storage in
|
||||||
|
* the Angle protocol, that impacts the price post trade. Due to the
|
||||||
|
* architecture of Angle, it's not possible to calculate the storage
|
||||||
|
* modifications of Angle inside the adapter.
|
||||||
|
*/
|
||||||
|
function price(bytes32, IERC20, IERC20, uint256[] memory)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (Fraction[] memory)
|
||||||
|
{
|
||||||
|
revert NotImplemented("AngleAdapter.price");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
/**
|
||||||
|
* @dev The price post trade is indipendent by the amount, since is the
|
||||||
|
* price with minimal fees with 0 slippage. In Angle there is no price with
|
||||||
|
* 0 slippage, so we use the PRECISE_UNIT (10^18, that is a small value) as
|
||||||
|
* input amount to have a slippage ---> 0.
|
||||||
|
*/
|
||||||
|
function swap(
|
||||||
|
bytes32,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
OrderSide side,
|
||||||
|
uint256 specifiedAmount
|
||||||
|
) external returns (Trade memory trade) {
|
||||||
|
if (specifiedAmount == 0) {
|
||||||
|
return trade;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 gasBefore = gasleft();
|
||||||
|
if (side == OrderSide.Sell) {
|
||||||
|
trade.calculatedAmount = sell(sellToken, buyToken, specifiedAmount);
|
||||||
|
} else {
|
||||||
|
trade.calculatedAmount = buy(sellToken, buyToken, specifiedAmount);
|
||||||
|
}
|
||||||
|
trade.gasUsed = gasBefore - gasleft();
|
||||||
|
uint8 decimals = side == OrderSide.Sell
|
||||||
|
? IERC20Metadata(address(sellToken)).decimals()
|
||||||
|
: IERC20Metadata(address(buyToken)).decimals();
|
||||||
|
trade.price =
|
||||||
|
getPriceAt(address(sellToken), address(buyToken), side, decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
/// @dev Mint may have no limits, but we underestimate them to make sure,
|
||||||
|
/// with the same amount of sellToken. We use the quoteIn (incl. fee),
|
||||||
|
/// because calculating fee requires a part of the implementation of the
|
||||||
|
/// Angle Diamond Storage, and therefore redundant functions and excessive
|
||||||
|
/// contract size, with an high complexity. In addition, we underestimate to
|
||||||
|
/// RESERVE_LIMIT_FACTOR to ensure swaps with OrderSide.Buy won't fail
|
||||||
|
/// anyway.
|
||||||
|
function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
override
|
||||||
|
returns (uint256[] memory limits)
|
||||||
|
{
|
||||||
|
limits = new uint256[](2);
|
||||||
|
address sellTokenAddress = address(sellToken);
|
||||||
|
address buyTokenAddress = address(buyToken);
|
||||||
|
address transmuterAddress = address(transmuter);
|
||||||
|
|
||||||
|
if (buyTokenAddress == transmuter.agToken()) {
|
||||||
|
// mint(buy agToken)
|
||||||
|
Collateral memory collatInfo =
|
||||||
|
transmuter.getCollateralInfo(sellTokenAddress);
|
||||||
|
if (collatInfo.isManaged > 0) {
|
||||||
|
limits[0] =
|
||||||
|
LibManager.maxAvailable(collatInfo.managerData.config);
|
||||||
|
} else {
|
||||||
|
limits[0] = sellToken.balanceOf(transmuterAddress);
|
||||||
|
}
|
||||||
|
limits[1] =
|
||||||
|
transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress);
|
||||||
|
limits[1] = limits[1] / RESERVE_LIMIT_FACTOR;
|
||||||
|
limits[0] = limits[0] / RESERVE_LIMIT_FACTOR;
|
||||||
|
} else {
|
||||||
|
// burn(sell agToken)
|
||||||
|
Collateral memory collatInfo =
|
||||||
|
transmuter.getCollateralInfo(buyTokenAddress);
|
||||||
|
if (collatInfo.isManaged > 0) {
|
||||||
|
limits[1] =
|
||||||
|
LibManager.maxAvailable(collatInfo.managerData.config);
|
||||||
|
} else {
|
||||||
|
limits[1] = buyToken.balanceOf(transmuterAddress);
|
||||||
|
}
|
||||||
|
limits[0] =
|
||||||
|
transmuter.quoteIn(limits[1], buyTokenAddress, sellTokenAddress);
|
||||||
|
limits[1] = limits[1] / RESERVE_LIMIT_FACTOR;
|
||||||
|
limits[0] = limits[0] / RESERVE_LIMIT_FACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function getCapabilities(bytes32, IERC20, IERC20)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (Capability[] memory capabilities)
|
||||||
|
{
|
||||||
|
capabilities = new Capability[](2);
|
||||||
|
capabilities[0] = Capability.SellOrder;
|
||||||
|
capabilities[1] = Capability.BuyOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
/// @dev Since Angle has no pool IDs but supports 3 tokens(agToken and the
|
||||||
|
/// collaterals), we return all the available collaterals and the
|
||||||
|
/// agToken(agEUR)
|
||||||
|
function getTokens(bytes32)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
override
|
||||||
|
returns (IERC20[] memory tokens)
|
||||||
|
{
|
||||||
|
address[] memory collateralsAddresses = transmuter.getCollateralList();
|
||||||
|
tokens = new IERC20[](collateralsAddresses.length + 1);
|
||||||
|
for (uint256 i = 0; i < collateralsAddresses.length; i++) {
|
||||||
|
tokens[i] = IERC20(collateralsAddresses[i]);
|
||||||
|
}
|
||||||
|
tokens[collateralsAddresses.length] = IERC20(transmuter.agToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPoolIds(uint256, uint256)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (bytes32[] memory)
|
||||||
|
{
|
||||||
|
revert NotImplemented("AngleAdapter.getPoolIds");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calculates pool prices for specified amounts
|
||||||
|
/// @param tokenIn The token being sold
|
||||||
|
/// @param tokenOut The token being bought
|
||||||
|
/// @param side Order side
|
||||||
|
/// @param decimals Decimals of the sell token
|
||||||
|
/// @return The price as a fraction corresponding to the provided amount.
|
||||||
|
function getPriceAt(
|
||||||
|
address tokenIn,
|
||||||
|
address tokenOut,
|
||||||
|
OrderSide side,
|
||||||
|
uint8 decimals
|
||||||
|
) internal view returns (Fraction memory) {
|
||||||
|
uint256 amountOut;
|
||||||
|
uint256 amountIn;
|
||||||
|
if (side == OrderSide.Sell) {
|
||||||
|
amountIn = 10 ** decimals;
|
||||||
|
amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut);
|
||||||
|
} else {
|
||||||
|
amountOut = 10 ** decimals;
|
||||||
|
amountIn = transmuter.quoteOut(amountOut, tokenIn, tokenOut);
|
||||||
|
}
|
||||||
|
return Fraction(amountOut, amountIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Executes a sell order on the contract.
|
||||||
|
/// @param sellToken The token being sold.
|
||||||
|
/// @param buyToken The token being bought.
|
||||||
|
/// @param amount The amount to be traded.
|
||||||
|
/// @return calculatedAmount The amount of tokens received.
|
||||||
|
function sell(IERC20 sellToken, IERC20 buyToken, uint256 amount)
|
||||||
|
internal
|
||||||
|
returns (uint256 calculatedAmount)
|
||||||
|
{
|
||||||
|
address sellTokenAddress = address(sellToken);
|
||||||
|
address buyTokenAddress = address(buyToken);
|
||||||
|
|
||||||
|
sellToken.transferFrom(msg.sender, address(this), amount);
|
||||||
|
sellToken.approve(address(transmuter), amount);
|
||||||
|
calculatedAmount = transmuter.swapExactInput(
|
||||||
|
amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Executes a buy order on the contract.
|
||||||
|
/// @param sellToken The token being sold.
|
||||||
|
/// @param buyToken The token being bought.
|
||||||
|
/// @param amountOut The amount of buyToken to receive.
|
||||||
|
/// @return calculatedAmount The amount of tokens received.
|
||||||
|
function buy(IERC20 sellToken, IERC20 buyToken, uint256 amountOut)
|
||||||
|
internal
|
||||||
|
returns (uint256 calculatedAmount)
|
||||||
|
{
|
||||||
|
address sellTokenAddress = address(sellToken);
|
||||||
|
address buyTokenAddress = address(buyToken);
|
||||||
|
calculatedAmount =
|
||||||
|
transmuter.quoteOut(amountOut, sellTokenAddress, buyTokenAddress);
|
||||||
|
|
||||||
|
sellToken.transferFrom(msg.sender, address(this), calculatedAmount);
|
||||||
|
sellToken.approve(address(transmuter), calculatedAmount);
|
||||||
|
transmuter.swapExactOutput(
|
||||||
|
amountOut,
|
||||||
|
type(uint256).max,
|
||||||
|
sellTokenAddress,
|
||||||
|
buyTokenAddress,
|
||||||
|
msg.sender,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAgToken is IERC20 {
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
MINTER ROLE ONLY FUNCTIONS
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/// @notice Lets a whitelisted contract mint agTokens
|
||||||
|
/// @param account Address to mint to
|
||||||
|
/// @param amount Amount to mint
|
||||||
|
function mint(address account, uint256 amount) external;
|
||||||
|
|
||||||
|
/// @notice Burns `amount` tokens from a `burner` address after being asked
|
||||||
|
/// to by `sender`
|
||||||
|
/// @param amount Amount of tokens to burn
|
||||||
|
/// @param burner Address to burn from
|
||||||
|
/// @param sender Address which requested the burn from `burner`
|
||||||
|
/// @dev This method is to be called by a contract with the minter right
|
||||||
|
/// after being requested
|
||||||
|
/// to do so by a `sender` address willing to burn tokens from another
|
||||||
|
/// `burner` address
|
||||||
|
/// @dev The method checks the allowance between the `sender` and the
|
||||||
|
/// `burner`
|
||||||
|
function burnFrom(uint256 amount, address burner, address sender)
|
||||||
|
external;
|
||||||
|
|
||||||
|
/// @notice Burns `amount` tokens from a `burner` address
|
||||||
|
/// @param amount Amount of tokens to burn
|
||||||
|
/// @param burner Address to burn from
|
||||||
|
/// @dev This method is to be called by a contract with a minter right on
|
||||||
|
/// the AgToken after being
|
||||||
|
/// requested to do so by an address willing to burn tokens from its address
|
||||||
|
function burnSelf(uint256 amount, address burner) external;
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
TREASURY ONLY FUNCTIONS
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/// @notice Adds a minter in the contract
|
||||||
|
/// @param minter Minter address to add
|
||||||
|
/// @dev Zero address checks are performed directly in the `Treasury`
|
||||||
|
/// contract
|
||||||
|
function addMinter(address minter) external;
|
||||||
|
|
||||||
|
/// @notice Removes a minter from the contract
|
||||||
|
/// @param minter Minter address to remove
|
||||||
|
/// @dev This function can also be called by a minter wishing to revoke
|
||||||
|
/// itself
|
||||||
|
function removeMinter(address minter) external;
|
||||||
|
|
||||||
|
/// @notice Sets a new treasury contract
|
||||||
|
/// @param _treasury New treasury address
|
||||||
|
function setTreasury(address _treasury) external;
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
EXTERNAL FUNCTIONS
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
/// @notice Checks whether an address has the right to mint agTokens
|
||||||
|
/// @param minter Address for which the minting right should be checked
|
||||||
|
/// @return Whether the address has the right to mint agTokens or not
|
||||||
|
function isMinter(address minter) external view returns (bool);
|
||||||
|
|
||||||
|
/// @notice Amount of decimals of the stablecoin
|
||||||
|
function decimals() external view returns (uint8);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ManagerType {
|
||||||
|
EXTERNAL
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WhitelistType {
|
||||||
|
BACKED
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ManagerStorage {
|
||||||
|
IERC20[] subCollaterals; // Subtokens handled by the manager or strategies
|
||||||
|
bytes config; // Additional configuration data
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Collateral {
|
||||||
|
uint8 isManaged; // If the collateral is managed through external strategies
|
||||||
|
uint8 isMintLive; // If minting from this asset is unpaused
|
||||||
|
uint8 isBurnLive; // If burning to this asset is unpaused
|
||||||
|
uint8 decimals; // IERC20Metadata(collateral).decimals()
|
||||||
|
uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem
|
||||||
|
// for this token
|
||||||
|
uint216 normalizedStables; // Normalized amount of stablecoins issued from
|
||||||
|
// this collateral
|
||||||
|
uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[
|
||||||
|
int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint`
|
||||||
|
uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9]
|
||||||
|
int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn`
|
||||||
|
bytes oracleConfig; // Data about the oracle used for the collateral
|
||||||
|
bytes whitelistData; // For whitelisted collateral, data used to verify
|
||||||
|
// whitelists
|
||||||
|
ManagerStorage managerData; // For managed collateral, data used to handle
|
||||||
|
// the strategies
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransmuterStorage {
|
||||||
|
IAgToken agToken; // agToken handled by the system
|
||||||
|
uint8 isRedemptionLive; // If redemption is unpaused
|
||||||
|
uint8 statusReentrant; // If call is reentrant or not
|
||||||
|
uint128 normalizedStables; // Normalized amount of stablecoins issued by the
|
||||||
|
// system
|
||||||
|
uint128 normalizer; // To reconcile `normalizedStables` values with the
|
||||||
|
// actual amount
|
||||||
|
address[] collateralList; // List of collateral assets supported by the
|
||||||
|
// system
|
||||||
|
uint64[] xRedemptionCurve; // Increasing collateral ratios > 0
|
||||||
|
int64[] yRedemptionCurve; // Value of the redemption fees at
|
||||||
|
// `xRedemptionCurve`
|
||||||
|
mapping(address => Collateral) collaterals; // Maps a collateral asset to
|
||||||
|
// its parameters
|
||||||
|
mapping(address => uint256) isTrusted; // If an address is trusted to update
|
||||||
|
// the normalizer value
|
||||||
|
mapping(address => uint256) isSellerTrusted; // If an address is trusted to
|
||||||
|
// sell accruing reward tokens
|
||||||
|
mapping(WhitelistType => mapping(address => uint256)) isWhitelistedForType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IManager {
|
||||||
|
/// @notice Returns the amount of collateral managed by the Manager
|
||||||
|
/// @return balances Balances of all the subCollaterals handled by the
|
||||||
|
/// manager
|
||||||
|
/// @dev MUST NOT revert
|
||||||
|
function totalAssets()
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256[] memory balances, uint256 totalValue);
|
||||||
|
|
||||||
|
/// @notice Hook to invest `amount` of `collateral`
|
||||||
|
/// @dev MUST revert if the manager cannot accept these funds
|
||||||
|
/// @dev MUST have received the funds beforehand
|
||||||
|
function invest(uint256 amount) external;
|
||||||
|
|
||||||
|
/// @notice Sends `amount` of `collateral` to the `to` address
|
||||||
|
/// @dev Called when `agToken` are burnt and during redemptions
|
||||||
|
// @dev MUST revert if there are not funds enough available
|
||||||
|
/// @dev MUST be callable only by the transmuter
|
||||||
|
function release(address asset, address to, uint256 amount) external;
|
||||||
|
|
||||||
|
/// @notice Gives the maximum amount of collateral immediately available for
|
||||||
|
/// a transfer
|
||||||
|
/// @dev Useful for integrators using `quoteIn` and `quoteOut`
|
||||||
|
function maxAvailable() external view returns (uint256);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @title LibManager
|
||||||
|
/// @author Angle Labs, Inc.
|
||||||
|
/// @dev Managed collateral assets may be handled through external smart
|
||||||
|
/// contracts or directly through this library
|
||||||
|
/// @dev There is no implementation at this point for a managed collateral
|
||||||
|
/// handled through this library, and
|
||||||
|
/// a new specific `ManagerType` would need to be added in this case
|
||||||
|
library LibManager {
|
||||||
|
/// @notice Checks to which address managed funds must be transferred
|
||||||
|
function transferRecipient(bytes memory config)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (address recipient)
|
||||||
|
{
|
||||||
|
(ManagerType managerType, bytes memory data) =
|
||||||
|
parseManagerConfig(config);
|
||||||
|
recipient = address(this);
|
||||||
|
if (managerType == ManagerType.EXTERNAL) {
|
||||||
|
return abi.decode(data, (address));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Performs a transfer of `token` for a collateral that is managed
|
||||||
|
/// to a `to` address
|
||||||
|
/// @dev `token` may not be the actual collateral itself, as some
|
||||||
|
/// collaterals have subcollaterals associated
|
||||||
|
/// with it
|
||||||
|
/// @dev Eventually pulls funds from strategies
|
||||||
|
function release(
|
||||||
|
address token,
|
||||||
|
address to,
|
||||||
|
uint256 amount,
|
||||||
|
bytes memory config
|
||||||
|
) internal {
|
||||||
|
(ManagerType managerType, bytes memory data) =
|
||||||
|
parseManagerConfig(config);
|
||||||
|
if (managerType == ManagerType.EXTERNAL) {
|
||||||
|
abi.decode(data, (IManager)).release(token, to, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Gets the balances of all the tokens controlled through
|
||||||
|
/// `managerData`
|
||||||
|
/// @return balances An array of size `subCollaterals` with current balances
|
||||||
|
/// of all subCollaterals
|
||||||
|
/// including the one corresponding to the `managerData` given
|
||||||
|
/// @return totalValue The value of all the `subCollaterals` in `collateral`
|
||||||
|
/// @dev `subCollaterals` must always have as first token (index 0) the
|
||||||
|
/// collateral itself
|
||||||
|
function totalAssets(bytes memory config)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (uint256[] memory balances, uint256 totalValue)
|
||||||
|
{
|
||||||
|
(ManagerType managerType, bytes memory data) =
|
||||||
|
parseManagerConfig(config);
|
||||||
|
if (managerType == ManagerType.EXTERNAL) {
|
||||||
|
return abi.decode(data, (IManager)).totalAssets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calls a hook if needed after new funds have been transfered to a
|
||||||
|
/// manager
|
||||||
|
function invest(uint256 amount, bytes memory config) internal {
|
||||||
|
(ManagerType managerType, bytes memory data) =
|
||||||
|
parseManagerConfig(config);
|
||||||
|
if (managerType == ManagerType.EXTERNAL) {
|
||||||
|
abi.decode(data, (IManager)).invest(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Returns available underlying tokens, for instance if liquidity
|
||||||
|
/// is fully used and
|
||||||
|
/// not withdrawable the function will return 0
|
||||||
|
function maxAvailable(bytes memory config)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (uint256 available)
|
||||||
|
{
|
||||||
|
(ManagerType managerType, bytes memory data) =
|
||||||
|
parseManagerConfig(config);
|
||||||
|
if (managerType == ManagerType.EXTERNAL) {
|
||||||
|
return abi.decode(data, (IManager)).maxAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Decodes the `managerData` associated to a collateral
|
||||||
|
function parseManagerConfig(bytes memory config)
|
||||||
|
internal
|
||||||
|
pure
|
||||||
|
returns (ManagerType managerType, bytes memory data)
|
||||||
|
{
|
||||||
|
(managerType, data) = abi.decode(config, (ManagerType, bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITransmuter {
|
||||||
|
function implementation() external view returns (address);
|
||||||
|
|
||||||
|
function setDummyImplementation(address _implementation) external;
|
||||||
|
|
||||||
|
function facetAddress(bytes4 _functionSelector)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (address facetAddress_);
|
||||||
|
|
||||||
|
function facetAddresses()
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (address[] memory facetAddresses_);
|
||||||
|
|
||||||
|
function facetFunctionSelectors(address _facet)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bytes4[] memory _facetFunctionSelectors);
|
||||||
|
|
||||||
|
function accessControlManager() external view returns (address);
|
||||||
|
|
||||||
|
function agToken() external view returns (address);
|
||||||
|
|
||||||
|
function getCollateralBurnFees(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint64[] memory xFeeBurn, int64[] memory yFeeBurn);
|
||||||
|
|
||||||
|
function getCollateralDecimals(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint8);
|
||||||
|
|
||||||
|
function getCollateralInfo(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (Collateral memory);
|
||||||
|
|
||||||
|
function getCollateralList() external view returns (address[] memory);
|
||||||
|
|
||||||
|
function getCollateralMintFees(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint64[] memory xFeeMint, int64[] memory yFeeMint);
|
||||||
|
|
||||||
|
function getCollateralRatio()
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint64 collatRatio, uint256 stablecoinsIssued);
|
||||||
|
|
||||||
|
function getCollateralWhitelistData(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bytes memory);
|
||||||
|
|
||||||
|
function getIssuedByCollateral(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued);
|
||||||
|
|
||||||
|
function getManagerData(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bool, address[] memory, bytes memory);
|
||||||
|
|
||||||
|
function getOracle(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint8 oracleType,
|
||||||
|
uint8 targetType,
|
||||||
|
bytes memory oracleData,
|
||||||
|
bytes memory targetData
|
||||||
|
);
|
||||||
|
|
||||||
|
function getOracleValues(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 mint,
|
||||||
|
uint256 burn,
|
||||||
|
uint256 ratio,
|
||||||
|
uint256 minRatio,
|
||||||
|
uint256 redemption
|
||||||
|
);
|
||||||
|
|
||||||
|
function getRedemptionFees()
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint64[] memory xRedemptionCurve,
|
||||||
|
int64[] memory yRedemptionCurve
|
||||||
|
);
|
||||||
|
|
||||||
|
function getTotalIssued() external view returns (uint256);
|
||||||
|
|
||||||
|
function isPaused(address collateral, uint8 action)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bool);
|
||||||
|
|
||||||
|
function isTrusted(address sender) external view returns (bool);
|
||||||
|
|
||||||
|
function isTrustedSeller(address sender) external view returns (bool);
|
||||||
|
|
||||||
|
function isValidSelector(bytes4 selector) external view returns (bool);
|
||||||
|
|
||||||
|
function isWhitelistedCollateral(address collateral)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bool);
|
||||||
|
|
||||||
|
function isWhitelistedForCollateral(address collateral, address sender)
|
||||||
|
external
|
||||||
|
returns (bool);
|
||||||
|
|
||||||
|
function isWhitelistedForType(uint8 whitelistType, address sender)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bool);
|
||||||
|
|
||||||
|
function sellRewards(uint256 minAmountOut, bytes memory payload)
|
||||||
|
external
|
||||||
|
returns (uint256 amountOut);
|
||||||
|
|
||||||
|
function addCollateral(address collateral) external;
|
||||||
|
|
||||||
|
function adjustStablecoins(
|
||||||
|
address collateral,
|
||||||
|
uint128 amount,
|
||||||
|
bool increase
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function changeAllowance(address token, address spender, uint256 amount)
|
||||||
|
external;
|
||||||
|
|
||||||
|
function recoverERC20(
|
||||||
|
address collateral,
|
||||||
|
address token,
|
||||||
|
address to,
|
||||||
|
uint256 amount
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function revokeCollateral(address collateral) external;
|
||||||
|
|
||||||
|
function setAccessControlManager(address _newAccessControlManager)
|
||||||
|
external;
|
||||||
|
|
||||||
|
function setOracle(address collateral, bytes memory oracleConfig)
|
||||||
|
external;
|
||||||
|
|
||||||
|
function setWhitelistStatus(
|
||||||
|
address collateral,
|
||||||
|
uint8 whitelistStatus,
|
||||||
|
bytes memory whitelistData
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function toggleTrusted(address sender, uint8 t) external;
|
||||||
|
|
||||||
|
function setFees(
|
||||||
|
address collateral,
|
||||||
|
uint64[] memory xFee,
|
||||||
|
int64[] memory yFee,
|
||||||
|
bool mint
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee)
|
||||||
|
external;
|
||||||
|
|
||||||
|
function togglePause(address collateral, uint8 pausedType) external;
|
||||||
|
|
||||||
|
function toggleWhitelist(uint8 whitelistType, address who) external;
|
||||||
|
|
||||||
|
function quoteIn(uint256 amountIn, address tokenIn, address tokenOut)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256 amountOut);
|
||||||
|
|
||||||
|
function quoteOut(uint256 amountOut, address tokenIn, address tokenOut)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256 amountIn);
|
||||||
|
|
||||||
|
function swapExactInput(
|
||||||
|
uint256 amountIn,
|
||||||
|
uint256 amountOutMin,
|
||||||
|
address tokenIn,
|
||||||
|
address tokenOut,
|
||||||
|
address to,
|
||||||
|
uint256 deadline
|
||||||
|
) external returns (uint256 amountOut);
|
||||||
|
|
||||||
|
function swapExactInputWithPermit(
|
||||||
|
uint256 amountIn,
|
||||||
|
uint256 amountOutMin,
|
||||||
|
address tokenIn,
|
||||||
|
address to,
|
||||||
|
uint256 deadline,
|
||||||
|
bytes memory permitData
|
||||||
|
) external returns (uint256 amountOut);
|
||||||
|
|
||||||
|
function swapExactOutput(
|
||||||
|
uint256 amountOut,
|
||||||
|
uint256 amountInMax,
|
||||||
|
address tokenIn,
|
||||||
|
address tokenOut,
|
||||||
|
address to,
|
||||||
|
uint256 deadline
|
||||||
|
) external returns (uint256 amountIn);
|
||||||
|
|
||||||
|
function swapExactOutputWithPermit(
|
||||||
|
uint256 amountOut,
|
||||||
|
uint256 amountInMax,
|
||||||
|
address tokenIn,
|
||||||
|
address to,
|
||||||
|
uint256 deadline,
|
||||||
|
bytes memory permitData
|
||||||
|
) external returns (uint256 amountIn);
|
||||||
|
|
||||||
|
function quoteRedemptionCurve(uint256 amount)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (address[] memory tokens, uint256[] memory amounts);
|
||||||
|
|
||||||
|
function redeem(
|
||||||
|
uint256 amount,
|
||||||
|
address receiver,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256[] memory minAmountOuts
|
||||||
|
) external returns (address[] memory tokens, uint256[] memory amounts);
|
||||||
|
|
||||||
|
function redeemWithForfeit(
|
||||||
|
uint256 amount,
|
||||||
|
address receiver,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256[] memory minAmountOuts,
|
||||||
|
address[] memory forfeitTokens
|
||||||
|
) external returns (address[] memory tokens, uint256[] memory amounts);
|
||||||
|
|
||||||
|
function updateNormalizer(uint256 amount, bool increase)
|
||||||
|
external
|
||||||
|
returns (uint256);
|
||||||
|
}
|
||||||
35
evm/src/angle/manifest.yaml
Normal file
35
evm/src/angle/manifest.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# information about the author helps us reach out in case of issues.
|
||||||
|
author:
|
||||||
|
name: shadowycoders.dev
|
||||||
|
email: hello@shadowycreators.com
|
||||||
|
|
||||||
|
# Protocol Constants
|
||||||
|
constants:
|
||||||
|
protocol_gas: 30000
|
||||||
|
# minimum capabilities we can expect, individual pools may extend these
|
||||||
|
capabilities:
|
||||||
|
- SellSide
|
||||||
|
- BuySide
|
||||||
|
|
||||||
|
# The file containing the adapter contract
|
||||||
|
contract: AngleAdapter.sol
|
||||||
|
|
||||||
|
# Deployment instances used to generate chain specific bytecode.
|
||||||
|
instances:
|
||||||
|
- chain:
|
||||||
|
name: mainnet
|
||||||
|
id: 0
|
||||||
|
arguments:
|
||||||
|
- "0x00253582b2a3FE112feEC532221d9708c64cEFAb"
|
||||||
|
|
||||||
|
# Specify some automatic test cases in case getPoolIds and
|
||||||
|
# getTokens are not implemented.
|
||||||
|
tests:
|
||||||
|
instances:
|
||||||
|
- pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
|
||||||
|
sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
||||||
|
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||||
|
block: 17000000
|
||||||
|
chain:
|
||||||
|
id: 0
|
||||||
|
name: mainnet
|
||||||
194
evm/test/AngleAdapter.t.sol
Normal file
194
evm/test/AngleAdapter.t.sol
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||||
|
import "src/angle/AngleAdapter.sol";
|
||||||
|
import "src/interfaces/ISwapAdapterTypes.sol";
|
||||||
|
import "src/libraries/FractionMath.sol";
|
||||||
|
import "forge-std/console.sol";
|
||||||
|
|
||||||
|
contract AngleAdapterTest is Test, ISwapAdapterTypes {
|
||||||
|
using FractionMath for Fraction;
|
||||||
|
|
||||||
|
AngleAdapter adapter;
|
||||||
|
IERC20 agEUR;
|
||||||
|
IERC20 constant EURC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c);
|
||||||
|
ITransmuter constant transmuter =
|
||||||
|
ITransmuter(0x00253582b2a3FE112feEC532221d9708c64cEFAb);
|
||||||
|
|
||||||
|
uint256 constant TEST_ITERATIONS = 100;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
uint256 forkBlock = 18921770;
|
||||||
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
adapter = new AngleAdapter(transmuter);
|
||||||
|
agEUR = IERC20(transmuter.agToken());
|
||||||
|
|
||||||
|
vm.label(address(adapter), "AngleAdapter");
|
||||||
|
vm.label(address(agEUR), "agEUR");
|
||||||
|
vm.label(address(EURC), "EURC");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(0);
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, EURC, agEUR);
|
||||||
|
|
||||||
|
if (side == OrderSide.Buy) {
|
||||||
|
vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0);
|
||||||
|
|
||||||
|
deal(address(EURC), address(this), type(uint256).max);
|
||||||
|
EURC.approve(address(adapter), type(uint256).max);
|
||||||
|
} else {
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 0);
|
||||||
|
|
||||||
|
deal(address(EURC), address(this), specifiedAmount);
|
||||||
|
EURC.approve(address(adapter), specifiedAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 eurc_balance = EURC.balanceOf(address(this));
|
||||||
|
uint256 agEUR_balance = agEUR.balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, EURC, agEUR, side, specifiedAmount);
|
||||||
|
|
||||||
|
if (trade.calculatedAmount > 0) {
|
||||||
|
if (side == OrderSide.Buy) {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
agEUR.balanceOf(address(this)) - agEUR_balance
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
eurc_balance - EURC.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
eurc_balance - EURC.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
agEUR.balanceOf(address(this)) - agEUR_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzAngleRedeem(uint256 specifiedAmount, bool isBuy)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(0);
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC);
|
||||||
|
|
||||||
|
if (side == OrderSide.Buy) {
|
||||||
|
vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0);
|
||||||
|
|
||||||
|
deal(address(agEUR), address(this), type(uint256).max);
|
||||||
|
agEUR.approve(address(adapter), type(uint256).max);
|
||||||
|
} else {
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 0);
|
||||||
|
|
||||||
|
deal(address(agEUR), address(this), specifiedAmount);
|
||||||
|
agEUR.approve(address(adapter), specifiedAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 eurc_balance = EURC.balanceOf(address(this));
|
||||||
|
uint256 agEUR_balance = agEUR.balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, agEUR, EURC, side, specifiedAmount);
|
||||||
|
|
||||||
|
if (trade.calculatedAmount > 0) {
|
||||||
|
if (side == OrderSide.Buy) {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
EURC.balanceOf(address(this)) - eurc_balance
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
agEUR_balance - agEUR.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
agEUR_balance - agEUR.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
EURC.balanceOf(address(this)) - eurc_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapSellIncreasingAngle() public {
|
||||||
|
executeIncreasingSwapsAngle(OrderSide.Sell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeIncreasingSwapsAngle(OrderSide side) internal {
|
||||||
|
bytes32 pair = bytes32(0);
|
||||||
|
|
||||||
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
amounts[i] = side == OrderSide.Sell
|
||||||
|
? (100 * i * 10 ** 18)
|
||||||
|
: (100 * i * 10 ** 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trade[] memory trades = new Trade[](TEST_ITERATIONS);
|
||||||
|
uint256 beforeSwap;
|
||||||
|
for (uint256 i = 1; i < TEST_ITERATIONS; i++) {
|
||||||
|
beforeSwap = vm.snapshot();
|
||||||
|
|
||||||
|
if (side == OrderSide.Sell) {
|
||||||
|
deal(address(agEUR), address(this), amounts[i]);
|
||||||
|
agEUR.approve(address(adapter), amounts[i]);
|
||||||
|
} else {
|
||||||
|
deal(address(agEUR), address(this), type(uint256).max);
|
||||||
|
agEUR.approve(address(adapter), type(uint256).max);
|
||||||
|
}
|
||||||
|
trades[i] = adapter.swap(pair, agEUR, EURC, side, amounts[i]);
|
||||||
|
vm.revertTo(beforeSwap);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) {
|
||||||
|
assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
|
||||||
|
assertFalse(
|
||||||
|
trades[i].price.compareFractions(trades[i + 1].price) == -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapBuyIncreasingAngle() public {
|
||||||
|
executeIncreasingSwapsAngle(OrderSide.Buy);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetCapabilitiesAngle(bytes32 pair, address t0, address t1)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
Capability[] memory res =
|
||||||
|
adapter.getCapabilities(pair, IERC20(t0), IERC20(t1));
|
||||||
|
|
||||||
|
assertEq(res.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetTokensAngle() public {
|
||||||
|
IERC20[] memory tokens = adapter.getTokens(bytes32(0));
|
||||||
|
|
||||||
|
assertGe(tokens.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetLimitsAngle() public {
|
||||||
|
bytes32 pair = bytes32(0);
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC);
|
||||||
|
|
||||||
|
assertEq(limits.length, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user