feat: Improve adapter and tests

This commit is contained in:
Ale personal
2024-03-16 19:45:15 +01:00
parent f18dce5c04
commit 511ca59f59
2 changed files with 404 additions and 244 deletions

View File

@@ -3,36 +3,34 @@ pragma experimental ABIEncoderV2;
pragma solidity ^0.8.13; pragma solidity ^0.8.13;
import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol"; 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 /// @dev custom reserve limit factor to prevent revert errors in OrderSide.Buy
uint256 constant RESERVE_LIMIT_FACTOR = 10; uint256 constant RESERVE_LIMIT_FACTOR = 10;
uint256 constant STANDARD_TOKEN_DECIMALS = 10 ** 18;
/// @title AngleAdapter /// @title AngleAdapter
/// @dev Information about prices: When swapping collateral to agEUR, the trade price will not decrease(amountOut); /// @dev Information about prices: When swapping collateral to agEUR, the trade
/// Instead, when swapping agEUR to collateral, it will, because agEUR is minted, and this mechanism is used to /// 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. /// stabilize the agEUR price.
contract AngleAdapter is ISwapAdapter { contract AngleAdapter is ISwapAdapter {
ITransmuter immutable transmuter;
ITransmuter transmuter;
constructor(ITransmuter _transmuter) { constructor(ITransmuter _transmuter) {
transmuter = _transmuter; transmuter = _transmuter;
} }
/// @inheritdoc ISwapAdapter /// @inheritdoc ISwapAdapter
function price( function price(bytes32, IERC20, IERC20, uint256[] memory)
bytes32, external
IERC20 _sellToken, pure
IERC20 _buyToken, override
uint256[] memory _specifiedAmounts returns (Fraction[] memory)
) external view override returns (Fraction[] memory _prices) { {
_prices = new Fraction[](_specifiedAmounts.length); revert NotImplemented("AngleAdapter.price");
address sellTokenAddress = address(_sellToken);
address buyTokenAddress = address(_buyToken);
for (uint256 i = 0; i < _specifiedAmounts.length; i++) {
_prices[i] = getPriceAt(_specifiedAmounts[i], sellTokenAddress, buyTokenAddress, OrderSide.Sell);
}
} }
/// @inheritdoc ISwapAdapter /// @inheritdoc ISwapAdapter
@@ -49,21 +47,27 @@ contract AngleAdapter is ISwapAdapter {
uint256 gasBefore = gasleft(); uint256 gasBefore = gasleft();
if (side == OrderSide.Sell) { if (side == OrderSide.Sell) {
trade.calculatedAmount = trade.calculatedAmount = sell(sellToken, buyToken, specifiedAmount);
sell(sellToken, buyToken, specifiedAmount);
} else { } else {
trade.calculatedAmount = trade.calculatedAmount = buy(sellToken, buyToken, specifiedAmount);
buy(sellToken, buyToken, specifiedAmount);
} }
trade.price = getPriceAt(specifiedAmount, address(sellToken), address(buyToken), side);
trade.gasUsed = gasBefore - gasleft(); 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 /// @inheritdoc ISwapAdapter
/// @dev mint may have no limits, but we underestimate them to make sure, with the same amount of sellToken. /// @dev mint may have no limits, but we underestimate them to make sure,
/// We use the quoteIn (incl. fee), because calculating fee requires a part of the implementation of /// with the same amount of sellToken.
/// the Angle Diamond Storage, and therefore redundant functions and excessive contract size, with an high complexity. /// We use the quoteIn (incl. fee), because calculating fee requires a part
/// In addition, we underestimate to / RESERVE_LIMIT_FACTOR to ensure swaps with OrderSide.Buy won't fail anyway. /// 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) function getLimits(bytes32, IERC20 sellToken, IERC20 buyToken)
external external
view view
@@ -75,29 +79,33 @@ contract AngleAdapter is ISwapAdapter {
address buyTokenAddress = address(buyToken); address buyTokenAddress = address(buyToken);
address transmuterAddress = address(transmuter); address transmuterAddress = address(transmuter);
if(buyTokenAddress == transmuter.agToken()) { // mint(buy agToken) if (buyTokenAddress == transmuter.agToken()) {
Collateral memory collatInfo = transmuter.getCollateralInfo(sellTokenAddress); // mint(buy agToken)
if(collatInfo.isManaged > 0) { Collateral memory collatInfo =
limits[0] = LibManager.maxAvailable(collatInfo.managerData.config); transmuter.getCollateralInfo(sellTokenAddress);
} if (collatInfo.isManaged > 0) {
else { limits[0] =
LibManager.maxAvailable(collatInfo.managerData.config);
} else {
limits[0] = sellToken.balanceOf(transmuterAddress); limits[0] = sellToken.balanceOf(transmuterAddress);
} }
limits[1] = transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress); limits[1] =
transmuter.quoteIn(limits[0], sellTokenAddress, buyTokenAddress);
limits[1] = limits[1] / RESERVE_LIMIT_FACTOR; limits[1] = limits[1] / RESERVE_LIMIT_FACTOR;
limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; limits[0] = limits[0] / RESERVE_LIMIT_FACTOR;
} } else {
else { // burn(sell agToken) // burn(sell agToken)
Collateral memory collatInfo = transmuter.getCollateralInfo(buyTokenAddress); Collateral memory collatInfo =
uint256 collatLimit; transmuter.getCollateralInfo(buyTokenAddress);
if(collatInfo.isManaged > 0) { if (collatInfo.isManaged > 0) {
collatLimit = LibManager.maxAvailable(collatInfo.managerData.config); limits[1] =
LibManager.maxAvailable(collatInfo.managerData.config);
} else {
limits[1] = buyToken.balanceOf(transmuterAddress);
} }
else { limits[0] =
collatLimit = buyToken.balanceOf(transmuterAddress); transmuter.quoteIn(limits[1], buyTokenAddress, sellTokenAddress);
} limits[1] = limits[1] / RESERVE_LIMIT_FACTOR;
limits[0] = transmuter.quoteIn(collatLimit, buyTokenAddress, sellTokenAddress);
limits[1] = collatLimit / RESERVE_LIMIT_FACTOR;
limits[0] = limits[0] / RESERVE_LIMIT_FACTOR; limits[0] = limits[0] / RESERVE_LIMIT_FACTOR;
} }
} }
@@ -112,11 +120,11 @@ contract AngleAdapter is ISwapAdapter {
capabilities = new Capability[](3); capabilities = new Capability[](3);
capabilities[0] = Capability.SellOrder; capabilities[0] = Capability.SellOrder;
capabilities[1] = Capability.BuyOrder; capabilities[1] = Capability.BuyOrder;
capabilities[2] = Capability.PriceFunction;
} }
/// @inheritdoc ISwapAdapter /// @inheritdoc ISwapAdapter
/// @dev Since Angle has no pool IDs but supports 3 tokens(agToken and the collaterals), /// @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) /// we return all the available collaterals and the agToken(agEUR)
function getTokens(bytes32) function getTokens(bytes32)
external external
@@ -126,7 +134,7 @@ contract AngleAdapter is ISwapAdapter {
{ {
address[] memory collateralsAddresses = transmuter.getCollateralList(); address[] memory collateralsAddresses = transmuter.getCollateralList();
tokens = new IERC20[](collateralsAddresses.length + 1); tokens = new IERC20[](collateralsAddresses.length + 1);
for(uint256 i = 0; i < collateralsAddresses.length; i++) { for (uint256 i = 0; i < collateralsAddresses.length; i++) {
tokens[i] = IERC20(collateralsAddresses[i]); tokens[i] = IERC20(collateralsAddresses[i]);
} }
tokens[collateralsAddresses.length] = IERC20(transmuter.agToken()); tokens[collateralsAddresses.length] = IERC20(transmuter.agToken());
@@ -142,30 +150,27 @@ contract AngleAdapter is ISwapAdapter {
} }
/// @notice Calculates pool prices for specified amounts /// @notice Calculates pool prices for specified amounts
/// @param amount The amount of the token being sold(if side == Sell) or bought(if side == Buy)
/// @param tokenIn The token being sold /// @param tokenIn The token being sold
/// @param tokenOut The token being bought /// @param tokenOut The token being bought
/// @param side Order side /// @param side Order side
/// @param decimals Decimals of the sell token
/// @return The price as a fraction corresponding to the provided amount. /// @return The price as a fraction corresponding to the provided amount.
function getPriceAt(uint256 amount, address tokenIn, address tokenOut, OrderSide side) function getPriceAt(
internal address tokenIn,
view address tokenOut,
returns (Fraction memory) OrderSide side,
{ uint8 decimals
) internal view returns (Fraction memory) {
uint256 amountOut; uint256 amountOut;
uint256 amountIn; uint256 amountIn;
if(side == OrderSide.Sell) { if (side == OrderSide.Sell) {
amountIn = amount; amountIn = 10 ** decimals;
amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut); amountOut = transmuter.quoteIn(amountIn, tokenIn, tokenOut);
} else {
amountOut = 10 ** decimals;
amountIn = transmuter.quoteOut(amountOut, tokenIn, tokenOut);
} }
else { return Fraction(amountOut, amountIn);
amountOut = amount;
amountIn = transmuter.quoteOut(amount, tokenIn, tokenOut);
}
return Fraction(
amountOut,
amountIn
);
} }
/// @notice Executes a sell order on the contract. /// @notice Executes a sell order on the contract.
@@ -173,20 +178,20 @@ contract AngleAdapter is ISwapAdapter {
/// @param buyToken The token being bought. /// @param buyToken The token being bought.
/// @param amount The amount to be traded. /// @param amount The amount to be traded.
/// @return calculatedAmount The amount of tokens received. /// @return calculatedAmount The amount of tokens received.
function sell( function sell(IERC20 sellToken, IERC20 buyToken, uint256 amount)
IERC20 sellToken, internal
IERC20 buyToken, returns (uint256 calculatedAmount)
uint256 amount {
) internal returns (uint256 calculatedAmount) {
address sellTokenAddress = address(sellToken); address sellTokenAddress = address(sellToken);
address buyTokenAddress = address(buyToken); address buyTokenAddress = address(buyToken);
uint256 amountOut = transmuter.quoteIn(amount, sellTokenAddress, buyTokenAddress); calculatedAmount =
transmuter.quoteIn(amount, sellTokenAddress, buyTokenAddress);
// TODO: use safeTransferFrom
sellToken.transferFrom(msg.sender, address(this), amount); sellToken.transferFrom(msg.sender, address(this), amount);
sellToken.approve(address(transmuter), amount); sellToken.approve(address(transmuter), amount);
transmuter.swapExactInput(amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0); transmuter.swapExactInput(
return amountOut; amount, 0, sellTokenAddress, buyTokenAddress, msg.sender, 0
);
} }
/// @notice Executes a buy order on the contract. /// @notice Executes a buy order on the contract.
@@ -194,26 +199,31 @@ contract AngleAdapter is ISwapAdapter {
/// @param buyToken The token being bought. /// @param buyToken The token being bought.
/// @param amountOut The amount of buyToken to receive. /// @param amountOut The amount of buyToken to receive.
/// @return calculatedAmount The amount of tokens received. /// @return calculatedAmount The amount of tokens received.
function buy( function buy(IERC20 sellToken, IERC20 buyToken, uint256 amountOut)
IERC20 sellToken, internal
IERC20 buyToken, returns (uint256 calculatedAmount)
uint256 amountOut {
) internal returns (uint256 calculatedAmount) {
address sellTokenAddress = address(sellToken); address sellTokenAddress = address(sellToken);
address buyTokenAddress = address(buyToken); address buyTokenAddress = address(buyToken);
uint256 amountIn = transmuter.quoteOut(amountOut, sellTokenAddress, buyTokenAddress); calculatedAmount =
transmuter.quoteOut(amountOut, sellTokenAddress, buyTokenAddress);
// TODO: use safeTransferFrom sellToken.transferFrom(msg.sender, address(this), calculatedAmount);
sellToken.transferFrom(msg.sender, address(this), amountIn); sellToken.approve(address(transmuter), calculatedAmount);
sellToken.approve(address(transmuter), amountIn); transmuter.swapExactOutput(
transmuter.swapExactOutput(amountOut, type(uint256).max, sellTokenAddress, buyTokenAddress, msg.sender, 0); amountOut,
return amountIn; type(uint256).max,
sellTokenAddress,
buyTokenAddress,
msg.sender,
0
);
} }
} }
interface IAgToken is IERC20 { interface IAgToken is IERC20 {
/*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
MINTER ROLE ONLY FUNCTIONS MINTER ROLE ONLY FUNCTIONS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
/// @notice Lets a whitelisted contract mint agTokens /// @notice Lets a whitelisted contract mint agTokens
@@ -221,34 +231,42 @@ interface IAgToken is IERC20 {
/// @param amount Amount to mint /// @param amount Amount to mint
function mint(address account, uint256 amount) external; function mint(address account, uint256 amount) external;
/// @notice Burns `amount` tokens from a `burner` address after being asked to by `sender` /// @notice Burns `amount` tokens from a `burner` address after being asked
/// to by `sender`
/// @param amount Amount of tokens to burn /// @param amount Amount of tokens to burn
/// @param burner Address to burn from /// @param burner Address to burn from
/// @param sender Address which requested the burn from `burner` /// @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 /// @dev This method is to be called by a contract with the minter right
/// to do so by a `sender` address willing to burn tokens from another `burner` address /// after being requested
/// @dev The method checks the allowance between the `sender` and the `burner` /// to do so by a `sender` address willing to burn tokens from another
function burnFrom(uint256 amount, address burner, address sender) external; /// `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 /// @notice Burns `amount` tokens from a `burner` address
/// @param amount Amount of tokens to burn /// @param amount Amount of tokens to burn
/// @param burner Address to burn from /// @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 /// @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 /// requested to do so by an address willing to burn tokens from its address
function burnSelf(uint256 amount, address burner) external; function burnSelf(uint256 amount, address burner) external;
/*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
TREASURY ONLY FUNCTIONS TREASURY ONLY FUNCTIONS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
/// @notice Adds a minter in the contract /// @notice Adds a minter in the contract
/// @param minter Minter address to add /// @param minter Minter address to add
/// @dev Zero address checks are performed directly in the `Treasury` contract /// @dev Zero address checks are performed directly in the `Treasury`
/// contract
function addMinter(address minter) external; function addMinter(address minter) external;
/// @notice Removes a minter from the contract /// @notice Removes a minter from the contract
/// @param minter Minter address to remove /// @param minter Minter address to remove
/// @dev This function can also be called by a minter wishing to revoke itself /// @dev This function can also be called by a minter wishing to revoke
/// itself
function removeMinter(address minter) external; function removeMinter(address minter) external;
/// @notice Sets a new treasury contract /// @notice Sets a new treasury contract
@@ -256,7 +274,7 @@ interface IAgToken is IERC20 {
function setTreasury(address _treasury) external; function setTreasury(address _treasury) external;
/*////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
EXTERNAL FUNCTIONS EXTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////*/
/// @notice Checks whether an address has the right to mint agTokens /// @notice Checks whether an address has the right to mint agTokens
@@ -277,46 +295,61 @@ enum WhitelistType {
} }
struct ManagerStorage { struct ManagerStorage {
IERC20[] subCollaterals; // Subtokens handled by the manager or strategies IERC20[] subCollaterals; // Subtokens handled by the manager or strategies
bytes config; // Additional configuration data bytes config; // Additional configuration data
} }
struct Collateral { struct Collateral {
uint8 isManaged; // If the collateral is managed through external strategies uint8 isManaged; // If the collateral is managed through external strategies
uint8 isMintLive; // If minting from this asset is unpaused uint8 isMintLive; // If minting from this asset is unpaused
uint8 isBurnLive; // If burning to this asset is unpaused uint8 isBurnLive; // If burning to this asset is unpaused
uint8 decimals; // IERC20Metadata(collateral).decimals() uint8 decimals; // IERC20Metadata(collateral).decimals()
uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem for this token uint8 onlyWhitelisted; // If only whitelisted addresses can burn or redeem
uint216 normalizedStables; // Normalized amount of stablecoins issued from this collateral // for this token
uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[ uint216 normalizedStables; // Normalized amount of stablecoins issued from
int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint` // this collateral
uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9] uint64[] xFeeMint; // Increasing exposures in [0,BASE_9[
int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn` int64[] yFeeMint; // Mint fees at the exposures specified in `xFeeMint`
bytes oracleConfig; // Data about the oracle used for the collateral uint64[] xFeeBurn; // Decreasing exposures in ]0,BASE_9]
bytes whitelistData; // For whitelisted collateral, data used to verify whitelists int64[] yFeeBurn; // Burn fees at the exposures specified in `xFeeBurn`
ManagerStorage managerData; // For managed collateral, data used to handle the strategies 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 { struct TransmuterStorage {
IAgToken agToken; // agToken handled by the system IAgToken agToken; // agToken handled by the system
uint8 isRedemptionLive; // If redemption is unpaused uint8 isRedemptionLive; // If redemption is unpaused
uint8 statusReentrant; // If call is reentrant or not uint8 statusReentrant; // If call is reentrant or not
uint128 normalizedStables; // Normalized amount of stablecoins issued by the system uint128 normalizedStables; // Normalized amount of stablecoins issued by the
uint128 normalizer; // To reconcile `normalizedStables` values with the actual amount // system
address[] collateralList; // List of collateral assets supported by the system uint128 normalizer; // To reconcile `normalizedStables` values with the
uint64[] xRedemptionCurve; // Increasing collateral ratios > 0 // actual amount
int64[] yRedemptionCurve; // Value of the redemption fees at `xRedemptionCurve` address[] collateralList; // List of collateral assets supported by the
mapping(address => Collateral) collaterals; // Maps a collateral asset to its parameters // system
mapping(address => uint256) isTrusted; // If an address is trusted to update the normalizer value uint64[] xRedemptionCurve; // Increasing collateral ratios > 0
mapping(address => uint256) isSellerTrusted; // If an address is trusted to sell accruing reward tokens 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; mapping(WhitelistType => mapping(address => uint256)) isWhitelistedForType;
} }
interface IManager { interface IManager {
/// @notice Returns the amount of collateral managed by the Manager /// @notice Returns the amount of collateral managed by the Manager
/// @return balances Balances of all the subCollaterals handled by the manager /// @return balances Balances of all the subCollaterals handled by the
/// manager
/// @dev MUST NOT revert /// @dev MUST NOT revert
function totalAssets() external view returns (uint256[] memory balances, uint256 totalValue); function totalAssets()
external
view
returns (uint256[] memory balances, uint256 totalValue);
/// @notice Hook to invest `amount` of `collateral` /// @notice Hook to invest `amount` of `collateral`
/// @dev MUST revert if the manager cannot accept these funds /// @dev MUST revert if the manager cannot accept these funds
@@ -329,121 +362,224 @@ interface IManager {
/// @dev MUST be callable only by the transmuter /// @dev MUST be callable only by the transmuter
function release(address asset, address to, uint256 amount) external; function release(address asset, address to, uint256 amount) external;
/// @notice Gives the maximum amount of collateral immediately available for a transfer /// @notice Gives the maximum amount of collateral immediately available for
/// a transfer
/// @dev Useful for integrators using `quoteIn` and `quoteOut` /// @dev Useful for integrators using `quoteIn` and `quoteOut`
function maxAvailable() external view returns (uint256); function maxAvailable() external view returns (uint256);
} }
/// @title LibManager /// @title LibManager
/// @author Angle Labs, Inc. /// @author Angle Labs, Inc.
/// @dev Managed collateral assets may be handled through external smart contracts or directly through this library /// @dev Managed collateral assets may be handled through external smart
/// @dev There is no implementation at this point for a managed collateral handled through this library, and /// 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 /// a new specific `ManagerType` would need to be added in this case
library LibManager { library LibManager {
/// @notice Checks to which address managed funds must be transferred /// @notice Checks to which address managed funds must be transferred
function transferRecipient(bytes memory config) internal view returns (address recipient) { function transferRecipient(bytes memory config)
(ManagerType managerType, bytes memory data) = parseManagerConfig(config); internal
view
returns (address recipient)
{
(ManagerType managerType, bytes memory data) =
parseManagerConfig(config);
recipient = address(this); recipient = address(this);
if (managerType == ManagerType.EXTERNAL) return abi.decode(data, (address)); 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 /// @notice Performs a transfer of `token` for a collateral that is managed
/// @dev `token` may not be the actual collateral itself, as some collaterals have subcollaterals associated /// to a `to` address
/// @dev `token` may not be the actual collateral itself, as some
/// collaterals have subcollaterals associated
/// with it /// with it
/// @dev Eventually pulls funds from strategies /// @dev Eventually pulls funds from strategies
function release(address token, address to, uint256 amount, bytes memory config) internal { function release(
(ManagerType managerType, bytes memory data) = parseManagerConfig(config); address token,
if (managerType == ManagerType.EXTERNAL) abi.decode(data, (IManager)).release(token, to, amount); 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` /// @notice Gets the balances of all the tokens controlled through
/// @return balances An array of size `subCollaterals` with current balances of all subCollaterals /// `managerData`
/// @return balances An array of size `subCollaterals` with current balances
/// of all subCollaterals
/// including the one corresponding to the `managerData` given /// including the one corresponding to the `managerData` given
/// @return totalValue The value of all the `subCollaterals` in `collateral` /// @return totalValue The value of all the `subCollaterals` in `collateral`
/// @dev `subCollaterals` must always have as first token (index 0) the collateral itself /// @dev `subCollaterals` must always have as first token (index 0) the
function totalAssets(bytes memory config) internal view returns (uint256[] memory balances, uint256 totalValue) { /// collateral itself
(ManagerType managerType, bytes memory data) = parseManagerConfig(config); function totalAssets(bytes memory config)
if (managerType == ManagerType.EXTERNAL) return abi.decode(data, (IManager)).totalAssets(); 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 /// @notice Calls a hook if needed after new funds have been transfered to a
/// manager
function invest(uint256 amount, bytes memory config) internal { function invest(uint256 amount, bytes memory config) internal {
(ManagerType managerType, bytes memory data) = parseManagerConfig(config); (ManagerType managerType, bytes memory data) =
if (managerType == ManagerType.EXTERNAL) abi.decode(data, (IManager)).invest(amount); 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 /// @notice Returns available underlying tokens, for instance if liquidity
/// is fully used and
/// not withdrawable the function will return 0 /// not withdrawable the function will return 0
function maxAvailable(bytes memory config) internal view returns (uint256 available) { function maxAvailable(bytes memory config)
(ManagerType managerType, bytes memory data) = parseManagerConfig(config); internal
if (managerType == ManagerType.EXTERNAL) return abi.decode(data, (IManager)).maxAvailable(); 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 /// @notice Decodes the `managerData` associated to a collateral
function parseManagerConfig( function parseManagerConfig(bytes memory config)
bytes memory config internal
) internal pure returns (ManagerType managerType, bytes memory data) { pure
returns (ManagerType managerType, bytes memory data)
{
(managerType, data) = abi.decode(config, (ManagerType, bytes)); (managerType, data) = abi.decode(config, (ManagerType, bytes));
} }
} }
abstract contract ITransmuter { abstract contract ITransmuter {
function implementation() external view returns (address) {} function implementation() external view returns (address) {}
function setDummyImplementation(address _implementation) external {} function setDummyImplementation(address _implementation) external {}
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_) {} function facetAddress(bytes4 _functionSelector)
external
view
returns (address facetAddress_)
{}
function facetAddresses() external view returns (address[] memory facetAddresses_) {} function facetAddresses()
external
view
returns (address[] memory facetAddresses_)
{}
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory _facetFunctionSelectors) {} function facetFunctionSelectors(address _facet)
external
view
returns (bytes4[] memory _facetFunctionSelectors)
{}
function accessControlManager() external view returns (address) {} function accessControlManager() external view returns (address) {}
function agToken() external view returns (address) {} function agToken() external view returns (address) {}
function getCollateralBurnFees( function getCollateralBurnFees(address collateral)
address collateral external
) external view returns (uint64[] memory xFeeBurn, int64[] memory yFeeBurn) {} view
returns (uint64[] memory xFeeBurn, int64[] memory yFeeBurn)
{}
function getCollateralDecimals(address collateral) external view returns (uint8) {} function getCollateralDecimals(address collateral)
external
view
returns (uint8)
{}
function getCollateralInfo(address collateral) external view returns (Collateral memory) {} function getCollateralInfo(address collateral)
external
view
returns (Collateral memory)
{}
function getCollateralList() external view returns (address[] memory) {} function getCollateralList() external view returns (address[] memory) {}
function getCollateralMintFees( function getCollateralMintFees(address collateral)
address collateral external
) external view returns (uint64[] memory xFeeMint, int64[] memory yFeeMint) {} view
returns (uint64[] memory xFeeMint, int64[] memory yFeeMint)
{}
function getCollateralRatio() external view returns (uint64 collatRatio, uint256 stablecoinsIssued) {} function getCollateralRatio()
external
view
returns (uint64 collatRatio, uint256 stablecoinsIssued)
{}
function getCollateralWhitelistData(address collateral) external view returns (bytes memory) {} function getCollateralWhitelistData(address collateral)
external
view
returns (bytes memory)
{}
function getIssuedByCollateral( function getIssuedByCollateral(address collateral)
address collateral external
) external view returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued) {} view
returns (uint256 stablecoinsFromCollateral, uint256 stablecoinsIssued)
{}
function getManagerData(address collateral) external view returns (bool, address[] memory, bytes memory) {} function getManagerData(address collateral)
external
view
returns (bool, address[] memory, bytes memory)
{}
function getOracle( function getOracle(address collateral)
address collateral external
) external view returns (uint8 oracleType, uint8 targetType, bytes memory oracleData, bytes memory targetData) {} view
returns (
uint8 oracleType,
uint8 targetType,
bytes memory oracleData,
bytes memory targetData
)
{}
function getOracleValues( function getOracleValues(address collateral)
address collateral external
) external view returns (uint256 mint, uint256 burn, uint256 ratio, uint256 minRatio, uint256 redemption) {} view
returns (
uint256 mint,
uint256 burn,
uint256 ratio,
uint256 minRatio,
uint256 redemption
)
{}
function getRedemptionFees() function getRedemptionFees()
external external
view view
returns (uint64[] memory xRedemptionCurve, int64[] memory yRedemptionCurve) returns (
uint64[] memory xRedemptionCurve,
int64[] memory yRedemptionCurve
)
{} {}
function getTotalIssued() external view returns (uint256) {} function getTotalIssued() external view returns (uint256) {}
function isPaused(address collateral, uint8 action) external view returns (bool) {} function isPaused(address collateral, uint8 action)
external
view
returns (bool)
{}
function isTrusted(address sender) external view returns (bool) {} function isTrusted(address sender) external view returns (bool) {}
@@ -451,43 +587,91 @@ abstract contract ITransmuter {
function isValidSelector(bytes4 selector) external view returns (bool) {} function isValidSelector(bytes4 selector) external view returns (bool) {}
function isWhitelistedCollateral(address collateral) external view returns (bool) {} function isWhitelistedCollateral(address collateral)
external
view
returns (bool)
{}
function isWhitelistedForCollateral(address collateral, address sender) external returns (bool) {} function isWhitelistedForCollateral(address collateral, address sender)
external
returns (bool)
{}
function isWhitelistedForType(uint8 whitelistType, address sender) external view returns (bool) {} function isWhitelistedForType(uint8 whitelistType, address sender)
external
view
returns (bool)
{}
function sellRewards(uint256 minAmountOut, bytes memory payload) external returns (uint256 amountOut) {} function sellRewards(uint256 minAmountOut, bytes memory payload)
external
returns (uint256 amountOut)
{}
function addCollateral(address collateral) external {} function addCollateral(address collateral) external {}
function adjustStablecoins(address collateral, uint128 amount, bool increase) external {} function adjustStablecoins(
address collateral,
uint128 amount,
bool increase
) external {}
function changeAllowance(address token, address spender, uint256 amount) external {} function changeAllowance(address token, address spender, uint256 amount)
external
{}
function recoverERC20(address collateral, address token, address to, uint256 amount) external {} function recoverERC20(
address collateral,
address token,
address to,
uint256 amount
) external {}
function revokeCollateral(address collateral) external {} function revokeCollateral(address collateral) external {}
function setAccessControlManager(address _newAccessControlManager) external {} function setAccessControlManager(address _newAccessControlManager)
external
{}
function setOracle(address collateral, bytes memory oracleConfig) external {} function setOracle(address collateral, bytes memory oracleConfig)
external
{}
function setWhitelistStatus(address collateral, uint8 whitelistStatus, bytes memory whitelistData) external {} function setWhitelistStatus(
address collateral,
uint8 whitelistStatus,
bytes memory whitelistData
) external {}
function toggleTrusted(address sender, uint8 t) external {} function toggleTrusted(address sender, uint8 t) external {}
function setFees(address collateral, uint64[] memory xFee, int64[] memory yFee, bool mint) external {} function setFees(
address collateral,
uint64[] memory xFee,
int64[] memory yFee,
bool mint
) external {}
function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee) external {} function setRedemptionCurveParams(uint64[] memory xFee, int64[] memory yFee)
external
{}
function togglePause(address collateral, uint8 pausedType) external {} function togglePause(address collateral, uint8 pausedType) external {}
function toggleWhitelist(uint8 whitelistType, address who) external {} function toggleWhitelist(uint8 whitelistType, address who) external {}
function quoteIn(uint256 amountIn, address tokenIn, address tokenOut) external view returns (uint256 amountOut) {} 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 quoteOut(uint256 amountOut, address tokenIn, address tokenOut)
external
view
returns (uint256 amountIn)
{}
function swapExactInput( function swapExactInput(
uint256 amountIn, uint256 amountIn,
@@ -525,9 +709,11 @@ abstract contract ITransmuter {
bytes memory permitData bytes memory permitData
) external returns (uint256 amountIn) {} ) external returns (uint256 amountIn) {}
function quoteRedemptionCurve( function quoteRedemptionCurve(uint256 amount)
uint256 amount external
) external view returns (address[] memory tokens, uint256[] memory amounts) {} view
returns (address[] memory tokens, uint256[] memory amounts)
{}
function redeem( function redeem(
uint256 amount, uint256 amount,
@@ -544,5 +730,8 @@ abstract contract ITransmuter {
address[] memory forfeitTokens address[] memory forfeitTokens
) external returns (address[] memory tokens, uint256[] memory amounts) {} ) external returns (address[] memory tokens, uint256[] memory amounts) {}
function updateNormalizer(uint256 amount, bool increase) external returns (uint256) {} function updateNormalizer(uint256 amount, bool increase)
external
returns (uint256)
{}
} }

View File

@@ -6,6 +6,7 @@ import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
import "src/angle/AngleAdapter.sol"; import "src/angle/AngleAdapter.sol";
import "src/interfaces/ISwapAdapterTypes.sol"; import "src/interfaces/ISwapAdapterTypes.sol";
import "src/libraries/FractionMath.sol"; import "src/libraries/FractionMath.sol";
import "forge-std/console.sol";
contract AngleAdapterTest is Test, ISwapAdapterTypes { contract AngleAdapterTest is Test, ISwapAdapterTypes {
using FractionMath for Fraction; using FractionMath for Fraction;
@@ -13,15 +14,15 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes {
AngleAdapter adapter; AngleAdapter adapter;
IERC20 agEUR; IERC20 agEUR;
IERC20 constant EURC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c); IERC20 constant EURC = IERC20(0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c);
ITransmuter constant transmuter = ITransmuter(0x00253582b2a3FE112feEC532221d9708c64cEFAb); ITransmuter constant transmuter =
ITransmuter(0x00253582b2a3FE112feEC532221d9708c64cEFAb);
uint256 constant TEST_ITERATIONS = 100; uint256 constant TEST_ITERATIONS = 100;
function setUp() public { function setUp() public {
uint256 forkBlock = 18921770; uint256 forkBlock = 18921770;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
adapter = new adapter = new AngleAdapter(transmuter);
AngleAdapter(transmuter);
agEUR = IERC20(transmuter.agToken()); agEUR = IERC20(transmuter.agToken());
vm.label(address(adapter), "AngleAdapter"); vm.label(address(adapter), "AngleAdapter");
@@ -29,43 +30,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes {
vm.label(address(EURC), "EURC"); vm.label(address(EURC), "EURC");
} }
function testPriceFuzzAngle(uint256 amount0, uint256 amount1) public { function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy)
bytes32 pair = bytes32(0); public
uint256[] memory limits = adapter.getLimits(pair, EURC, agEUR); {
vm.assume(amount0 < limits[0] && amount0 > 0);
vm.assume(amount1 < limits[0] && amount1 > 0);
uint256[] memory amounts = new uint256[](2);
amounts[0] = amount0;
amounts[1] = amount1;
Fraction[] memory prices = adapter.price(pair, EURC, agEUR, amounts);
for (uint256 i = 0; i < prices.length; i++) {
assertGt(prices[i].numerator, 0);
assertGt(prices[i].denominator, 0);
}
}
function testPriceDecreasingAngle() public {
bytes32 pair = bytes32(0);
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
amounts[i] = 1000 * (i+1) * 10 ** 18;
}
Fraction[] memory prices = adapter.price(pair, agEUR, EURC, amounts);
for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) {
assertEq(prices[i].compareFractions(prices[i + 1]), 1);
assertGt(prices[i].denominator, 0);
assertGt(prices[i + 1].denominator, 0);
}
}
function testSwapFuzzAngleMint(uint256 specifiedAmount, bool isBuy) public {
OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
bytes32 pair = bytes32(0); bytes32 pair = bytes32(0);
@@ -112,12 +79,13 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes {
} }
} }
function testSwapFuzzAngleRedeem(uint256 specifiedAmount, bool isBuy) public { function testSwapFuzzAngleRedeem(uint256 specifiedAmount, bool isBuy)
public
{
OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell; OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
bytes32 pair = bytes32(0); bytes32 pair = bytes32(0);
uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC); uint256[] memory limits = adapter.getLimits(pair, agEUR, EURC);
console.log(limits[0], limits[1]);
if (side == OrderSide.Buy) { if (side == OrderSide.Buy) {
vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0); vm.assume(specifiedAmount < limits[1] && specifiedAmount > 0);
@@ -169,8 +137,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes {
uint256[] memory amounts = new uint256[](TEST_ITERATIONS); uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
for (uint256 i = 0; i < TEST_ITERATIONS; i++) { for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
amounts[i] = amounts[i] = side == OrderSide.Sell
side == OrderSide.Sell ? (100 * i * 10 ** 18) : (100 * i * 10 ** 6); ? (100 * i * 10 ** 18)
: (100 * i * 10 ** 6);
} }
Trade[] memory trades = new Trade[](TEST_ITERATIONS); Trade[] memory trades = new Trade[](TEST_ITERATIONS);
@@ -178,22 +147,22 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes {
for (uint256 i = 1; i < TEST_ITERATIONS; i++) { for (uint256 i = 1; i < TEST_ITERATIONS; i++) {
beforeSwap = vm.snapshot(); beforeSwap = vm.snapshot();
if(side == OrderSide.Sell) { if (side == OrderSide.Sell) {
deal(address(agEUR), address(this), amounts[i]); deal(address(agEUR), address(this), amounts[i]);
agEUR.approve(address(adapter), amounts[i]); agEUR.approve(address(adapter), amounts[i]);
} } else {
else {
deal(address(agEUR), address(this), type(uint256).max); deal(address(agEUR), address(this), type(uint256).max);
agEUR.approve(address(adapter), type(uint256).max); agEUR.approve(address(adapter), type(uint256).max);
} }
trades[i] = adapter.swap(pair, agEUR, EURC, side, amounts[i]); trades[i] = adapter.swap(pair, agEUR, EURC, side, amounts[i]);
vm.revertTo(beforeSwap); vm.revertTo(beforeSwap);
} }
for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) { for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) {
assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1); assertFalse(
trades[i].price.compareFractions(trades[i + 1].price) == -1
);
} }
} }
@@ -201,7 +170,9 @@ contract AngleAdapterTest is Test, ISwapAdapterTypes {
executeIncreasingSwapsAngle(OrderSide.Buy); executeIncreasingSwapsAngle(OrderSide.Buy);
} }
function testGetCapabilitiesAngle(bytes32 pair, address t0, address t1) public { function testGetCapabilitiesAngle(bytes32 pair, address t0, address t1)
public
{
Capability[] memory res = Capability[] memory res =
adapter.getCapabilities(pair, IERC20(t0), IERC20(t1)); adapter.getCapabilities(pair, IERC20(t0), IERC20(t1));