Curve Adapter (Stable and Crypto Swap) (#29)
* feat: Initial manifest structure * feat: Adapter initial development and price function implementation * feat: Implemented getPoolIDs * feat: Implemented getTokens * feat: Implemented getCapabilities * feat: Implemented getLimits * feat: Implemented swap * feat: Created Adapter version for CurveV2 pools uint256 * fix: Review Fixes * chore: Formatted code and code adjustments * feat: Separated contracts and finished tests for Curve Exchange(Crypto and StableSwap) * feat: Adjusted naming and formatted tests code * chore: Initial merging of swaps * merge CurveCryptoSwapAdapter and CurveStableSwapAdapter into CurveAdapter * Fix and Test: fixes on CurveAdapter.sol and created CurveAdapter.t.sol * review and fixes * removed unused test and parameters * chore: Initial MetaPool underlying swap and ETH native implementation * chore: expanded swap and sell functions to support seap for underlying tokens and LPS * chore: expanded functionalities of CurveAdapter and fixed getTokens function * fix: fixed registry.is_meta * chore: fixes * chore: extended adapter and implementing tests for new pools * chore: updated Adapter, implementing final tests * fixing eth transfer * fix and tests: fixed adapter and finishing tests implementation * using adapter with try catch * feat: Final fixes for pools support, using try-catch * chore: Removed chunk files * chore: Formatted code and removed unused condition * fix: Fixed calculatedAmount in sell function when receiveToken=ETH * chore: Adjusted ETH pools check * corrected metaregistry address * fix: fixed int128 conversion to unit256 * feat: Removed registry from adapter * feat: Implemented price() * fix: Propeller review fixes * review fixes * fix: Final fixes for custom custom int128 pools * chore: Removed unused test * fix: Fixed price error in custom pools * feat: Improved isInt128Pool function to support any coin * fix: Fixed price for custom pools using ETH balance when token0 is WETH * fix: Fixed price function and added AdapterTest support in test * fix: Fixed divisions in getPriceAt * feat: Added secondary ETH pool support, e.g. stETH * refactor(curve-adapter): Avoid calling WETH contract if possible. (#65) * refactor(curve-adapter): Avoid calling WETH contract if possible. This PR aims to use native ETH instead of WETH when possible, this is to avoid having to index WETH contract. * style(adapter): apply `forge fmt` * refactor(curve): change limit factor to 2 --------- Co-authored-by: Florian Pellissier <111426680+flopell@users.noreply.github.com> * refactor: fix Curve Adapter test after rebase * fix: remove PriceFunction capability for Curve This was wrongly implemented: it return the price for a quote and not the marginal price after the swap. * refactor: improve adapter test and fix failing test for etherfi --------- Co-authored-by: domenicodev <domenico.romeo3919@gmail.com> Co-authored-by: mp-web3 <mp.web3.t@gmail.com> Co-authored-by: Zizou <111426680+zizou0x@users.noreply.github.com> Co-authored-by: Florian Pellissier <111426680+flopell@users.noreply.github.com> Co-authored-by: Diana Carvalho <diana@propellerheads.xyz>
This commit is contained in:
606
evm/src/curve/CurveAdapter.sol
Normal file
606
evm/src/curve/CurveAdapter.sol
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
||||||
|
import {
|
||||||
|
IERC20,
|
||||||
|
SafeERC20
|
||||||
|
} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "src/libraries/FractionMath.sol";
|
||||||
|
|
||||||
|
/// @dev custom RESERVE_LIMIT_FACTOR for limits for this adapter(underestimate)
|
||||||
|
uint256 constant RESERVE_LIMIT_FACTOR = 2;
|
||||||
|
|
||||||
|
/// @title Curve Finance Adapter
|
||||||
|
/// @dev This contract supports both CryptoSwap and StableSwap Curve pools
|
||||||
|
contract CurveAdapter is ISwapAdapter {
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
using FractionMath for Fraction;
|
||||||
|
|
||||||
|
struct SellParamsCache {
|
||||||
|
address poolAddress; // address of the pool to swap in
|
||||||
|
address sellToken; // address of the token to sell
|
||||||
|
address buyToken; // address of the token to buy
|
||||||
|
int128 sellTokenIndex; // index of the token being sold
|
||||||
|
int128 buyTokenIndex; // index of the token being bought
|
||||||
|
uint256 specifiedAmount; // amount to trade
|
||||||
|
bool isInt128Pool; // pool is int128
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PoolCoins {
|
||||||
|
address[8] addresses;
|
||||||
|
uint256 coinsLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 constant PRECISION = 10 ** 5;
|
||||||
|
|
||||||
|
address constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
||||||
|
address constant ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/// @dev enable receive as this contract supports ETH
|
||||||
|
receive() external payable {}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function price(
|
||||||
|
bytes32 poolId,
|
||||||
|
address sellToken,
|
||||||
|
address buyToken,
|
||||||
|
uint256[] memory specifiedAmounts
|
||||||
|
) external override returns (Fraction[] memory prices) {
|
||||||
|
revert NotImplemented("CurveAdapter.price");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function swap(
|
||||||
|
bytes32 poolId,
|
||||||
|
address sellToken,
|
||||||
|
address buyToken,
|
||||||
|
OrderSide side,
|
||||||
|
uint256 specifiedAmount
|
||||||
|
) external override returns (Trade memory trade) {
|
||||||
|
if (specifiedAmount == 0) {
|
||||||
|
return trade;
|
||||||
|
}
|
||||||
|
|
||||||
|
SellParamsCache memory sellParams;
|
||||||
|
{
|
||||||
|
sellParams.poolAddress = address(bytes20(poolId));
|
||||||
|
sellParams.sellToken = sellToken;
|
||||||
|
sellParams.buyToken = buyToken;
|
||||||
|
sellParams.specifiedAmount = specifiedAmount;
|
||||||
|
|
||||||
|
bool isEthPool; // pool is native ETH pool
|
||||||
|
PoolCoins memory coins = getCoins(sellParams.poolAddress);
|
||||||
|
sellParams.isInt128Pool =
|
||||||
|
isInt128Pool(sellParams.poolAddress, coins);
|
||||||
|
|
||||||
|
/// @dev Support for Native ETH pools, ETH pools cannot be Meta
|
||||||
|
/// therefore we can directly access coins without using underlying
|
||||||
|
if (sellToken == address(0)) {
|
||||||
|
for (uint256 i = 0; i < coins.coinsLength; i++) {
|
||||||
|
if (
|
||||||
|
coins.addresses[i] == ETH_ADDRESS
|
||||||
|
|| coins.addresses[i] == WETH_ADDRESS
|
||||||
|
) {
|
||||||
|
sellParams.sellToken = ETH_ADDRESS;
|
||||||
|
if (coins.addresses[i] == ETH_ADDRESS) {
|
||||||
|
isEthPool = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (buyToken == address(0)) {
|
||||||
|
for (uint256 i = 0; i < coins.coinsLength; i++) {
|
||||||
|
if (
|
||||||
|
coins.addresses[i] == ETH_ADDRESS
|
||||||
|
|| coins.addresses[i] == WETH_ADDRESS
|
||||||
|
) {
|
||||||
|
sellParams.buyToken = ETH_ADDRESS;
|
||||||
|
if (coins.addresses[i] == ETH_ADDRESS) {
|
||||||
|
isEthPool = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(sellParams.sellTokenIndex, sellParams.buyTokenIndex) =
|
||||||
|
getCoinsIndices(
|
||||||
|
sellParams.sellToken, sellParams.buyToken, coins, isEthPool
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 gasBefore = gasleft();
|
||||||
|
|
||||||
|
if (side == OrderSide.Sell) {
|
||||||
|
trade.calculatedAmount = sell(sellParams);
|
||||||
|
} else {
|
||||||
|
revert Unavailable(
|
||||||
|
"OrderSide.Buy is not available for this adapter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
trade.gasUsed = gasBefore - gasleft();
|
||||||
|
trade.price = getPriceAt(sellParams, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function getLimits(bytes32 poolId, address sellToken, address buyToken)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
override
|
||||||
|
returns (uint256[] memory limits)
|
||||||
|
{
|
||||||
|
address poolAddress = address(bytes20(poolId));
|
||||||
|
ICurveStableSwapPool pool = ICurveStableSwapPool(poolAddress);
|
||||||
|
address sellToken_ = sellToken;
|
||||||
|
address buyToken_ = buyToken;
|
||||||
|
bool isEthPool;
|
||||||
|
PoolCoins memory coins = getCoins(poolAddress);
|
||||||
|
|
||||||
|
/// @dev Support for Native ETH pools, ETH pools cannot be Meta
|
||||||
|
/// therefore we can directly access coins without using underlying
|
||||||
|
if (sellToken == address(0)) {
|
||||||
|
for (uint256 i = 0; i < coins.coinsLength; i++) {
|
||||||
|
if (
|
||||||
|
coins.addresses[i] == ETH_ADDRESS
|
||||||
|
|| coins.addresses[i] == WETH_ADDRESS
|
||||||
|
) {
|
||||||
|
sellToken_ = ETH_ADDRESS;
|
||||||
|
if (coins.addresses[i] == ETH_ADDRESS) {
|
||||||
|
isEthPool = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (buyToken == address(0)) {
|
||||||
|
for (uint256 i = 0; i < coins.coinsLength; i++) {
|
||||||
|
if (
|
||||||
|
coins.addresses[i] == ETH_ADDRESS
|
||||||
|
|| coins.addresses[i] == WETH_ADDRESS
|
||||||
|
) {
|
||||||
|
buyToken_ = ETH_ADDRESS;
|
||||||
|
if (coins.addresses[i] == ETH_ADDRESS) {
|
||||||
|
isEthPool = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(int128 sellTokenIndex, int128 buyTokenIndex) =
|
||||||
|
getCoinsIndices(sellToken_, buyToken_, coins, isEthPool);
|
||||||
|
|
||||||
|
limits = new uint256[](2);
|
||||||
|
uint256 sellTokenIndexUint = uint256(uint128(sellTokenIndex));
|
||||||
|
uint256 buyTokenIndexUint = uint256(uint128(buyTokenIndex));
|
||||||
|
try pool.balances(sellTokenIndexUint) returns (uint256 bal) {
|
||||||
|
limits[0] = bal / RESERVE_LIMIT_FACTOR;
|
||||||
|
limits[1] = pool.balances(buyTokenIndexUint) / RESERVE_LIMIT_FACTOR;
|
||||||
|
} catch {
|
||||||
|
limits[0] = ICurveCustomInt128Pool(poolAddress).balances(
|
||||||
|
sellTokenIndex
|
||||||
|
) / RESERVE_LIMIT_FACTOR;
|
||||||
|
limits[1] = ICurveCustomInt128Pool(poolAddress).balances(
|
||||||
|
buyTokenIndex
|
||||||
|
) / RESERVE_LIMIT_FACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function getCapabilities(bytes32, address, address)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (Capability[] memory capabilities)
|
||||||
|
{
|
||||||
|
capabilities = new Capability[](2);
|
||||||
|
capabilities[0] = Capability.SellOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function getTokens(bytes32 poolId)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
override
|
||||||
|
returns (address[] memory tokens)
|
||||||
|
{
|
||||||
|
PoolCoins memory coins = getCoins(address(bytes20(poolId)));
|
||||||
|
address[] memory tokensTmp = new address[](coins.coinsLength);
|
||||||
|
bool containsETH;
|
||||||
|
for (uint256 j = 0; j < coins.coinsLength; j++) {
|
||||||
|
if (coins.addresses[j] == WETH_ADDRESS) {
|
||||||
|
containsETH = true;
|
||||||
|
}
|
||||||
|
if (coins.addresses[j] == ETH_ADDRESS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokensTmp[j] = coins.addresses[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsETH) {
|
||||||
|
tokens = new address[](coins.coinsLength + 1);
|
||||||
|
for (uint256 k = 0; k < coins.coinsLength; k++) {
|
||||||
|
tokens[k] = tokensTmp[k];
|
||||||
|
}
|
||||||
|
tokens[coins.coinsLength] = address(0);
|
||||||
|
} else {
|
||||||
|
tokens = tokensTmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPoolIds(uint256, uint256)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (bytes32[] memory)
|
||||||
|
{
|
||||||
|
revert NotImplemented("CurveAdapter.getPoolIds");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calculates pool prices for specified amounts
|
||||||
|
/// @param sellParams Params for the price(see: struct SellParamsCache).
|
||||||
|
/// @param useGenericAmount Determine if a amount used to determine the
|
||||||
|
/// price is a small amount of the reserve(true) or
|
||||||
|
/// sellParams.specifiedAmount(false)
|
||||||
|
/// @return (Fraction) price as a fraction corresponding to the provided
|
||||||
|
/// amount.
|
||||||
|
function getPriceAt(
|
||||||
|
SellParamsCache memory sellParams,
|
||||||
|
bool useGenericAmount
|
||||||
|
) internal view returns (Fraction memory) {
|
||||||
|
uint256 amountIn;
|
||||||
|
uint256 sellTokenIndexUint = uint256(uint128(sellParams.sellTokenIndex));
|
||||||
|
uint256 buyTokenIndexUint = uint256(uint128(sellParams.buyTokenIndex));
|
||||||
|
if (sellParams.isInt128Pool) {
|
||||||
|
try ICurveStableSwapPool(sellParams.poolAddress).balances(
|
||||||
|
sellTokenIndexUint
|
||||||
|
) returns (uint256 bal) {
|
||||||
|
amountIn = useGenericAmount
|
||||||
|
? (bal / PRECISION)
|
||||||
|
: sellParams.specifiedAmount;
|
||||||
|
} catch {
|
||||||
|
amountIn = useGenericAmount
|
||||||
|
? (
|
||||||
|
ICurveCustomInt128Pool(sellParams.poolAddress).balances(
|
||||||
|
sellParams.sellTokenIndex
|
||||||
|
) / PRECISION
|
||||||
|
)
|
||||||
|
: sellParams.specifiedAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fraction(
|
||||||
|
ICurveStableSwapPool(sellParams.poolAddress).get_dy(
|
||||||
|
sellParams.sellTokenIndex,
|
||||||
|
sellParams.buyTokenIndex,
|
||||||
|
amountIn
|
||||||
|
),
|
||||||
|
amountIn
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
amountIn = useGenericAmount
|
||||||
|
? (
|
||||||
|
ICurveCryptoSwapPool(sellParams.poolAddress).balances(
|
||||||
|
sellTokenIndexUint
|
||||||
|
) / PRECISION
|
||||||
|
)
|
||||||
|
: sellParams.specifiedAmount;
|
||||||
|
|
||||||
|
return Fraction(
|
||||||
|
ICurveCryptoSwapPool(sellParams.poolAddress).get_dy(
|
||||||
|
sellTokenIndexUint, buyTokenIndexUint, amountIn
|
||||||
|
),
|
||||||
|
amountIn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Executes a sell order on a given pool.
|
||||||
|
/// @dev Only metapools available (with LP token as counter pair) are
|
||||||
|
/// Stable,
|
||||||
|
/// but after some research we've found that Curve deployed some pools that
|
||||||
|
/// are Crypto and use the int128 interface, therefore we optimistically
|
||||||
|
/// support them too.
|
||||||
|
/// @param sellParams Params for the trade(see: struct SellParamsCache).
|
||||||
|
/// @return calculatedAmount The amount of tokens received.
|
||||||
|
function sell(SellParamsCache memory sellParams)
|
||||||
|
internal
|
||||||
|
returns (uint256 calculatedAmount)
|
||||||
|
{
|
||||||
|
IERC20 buyToken = IERC20(sellParams.buyToken);
|
||||||
|
IERC20 sellToken = IERC20(sellParams.sellToken);
|
||||||
|
uint256 nativeTokenBalBefore = address(this).balance;
|
||||||
|
uint256 buyTokenBalBefore = (sellParams.buyToken == ETH_ADDRESS)
|
||||||
|
? address(this).balance
|
||||||
|
: buyToken.balanceOf(address(this));
|
||||||
|
|
||||||
|
if (sellParams.isInt128Pool) {
|
||||||
|
if (sellParams.sellToken == ETH_ADDRESS) {
|
||||||
|
// ETH Pool
|
||||||
|
ICurveStableSwapPoolEth(sellParams.poolAddress).exchange{
|
||||||
|
value: sellParams.specifiedAmount
|
||||||
|
}(
|
||||||
|
sellParams.sellTokenIndex,
|
||||||
|
sellParams.buyTokenIndex,
|
||||||
|
sellParams.specifiedAmount,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sellToken.safeTransferFrom(
|
||||||
|
msg.sender, address(this), sellParams.specifiedAmount
|
||||||
|
);
|
||||||
|
sellToken.safeIncreaseAllowance(
|
||||||
|
sellParams.poolAddress, sellParams.specifiedAmount
|
||||||
|
);
|
||||||
|
ICurveStableSwapPool(sellParams.poolAddress).exchange(
|
||||||
|
sellParams.sellTokenIndex,
|
||||||
|
sellParams.buyTokenIndex,
|
||||||
|
sellParams.specifiedAmount,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uint256 sellTokenIndexUint =
|
||||||
|
uint256(uint128(sellParams.sellTokenIndex));
|
||||||
|
uint256 buyTokenIndexUint =
|
||||||
|
uint256(uint128(sellParams.buyTokenIndex));
|
||||||
|
if (sellParams.sellToken == ETH_ADDRESS) {
|
||||||
|
ICurveCryptoSwapPoolEth(sellParams.poolAddress).exchange{
|
||||||
|
value: sellParams.specifiedAmount
|
||||||
|
}(
|
||||||
|
sellTokenIndexUint,
|
||||||
|
buyTokenIndexUint,
|
||||||
|
sellParams.specifiedAmount,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
address(this)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sellToken.safeTransferFrom(
|
||||||
|
msg.sender, address(this), sellParams.specifiedAmount
|
||||||
|
);
|
||||||
|
sellToken.safeIncreaseAllowance(
|
||||||
|
sellParams.poolAddress, sellParams.specifiedAmount
|
||||||
|
);
|
||||||
|
// @dev if available try to swap with use_eth set to true.
|
||||||
|
try ICurveCryptoSwapPoolEth(sellParams.poolAddress).exchange(
|
||||||
|
sellTokenIndexUint,
|
||||||
|
buyTokenIndexUint,
|
||||||
|
sellParams.specifiedAmount,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
address(this)
|
||||||
|
) {
|
||||||
|
// @dev we can't use catch here because some Curve pool have
|
||||||
|
// a fallback function implemented. So this call succeed
|
||||||
|
// without doing anything.
|
||||||
|
uint256 maybeNativeReceived =
|
||||||
|
address(this).balance - nativeTokenBalBefore;
|
||||||
|
if (maybeNativeReceived > 0) {
|
||||||
|
calculatedAmount = maybeNativeReceived; // ETH received
|
||||||
|
(bool sent,) = address(msg.sender).call{
|
||||||
|
value: maybeNativeReceived
|
||||||
|
}("");
|
||||||
|
require(sent, "Eth transfer failed");
|
||||||
|
} else {
|
||||||
|
calculatedAmount = buyToken.balanceOf(address(this))
|
||||||
|
- buyTokenBalBefore;
|
||||||
|
buyToken.safeTransfer(
|
||||||
|
address(msg.sender), calculatedAmount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (calculatedAmount > 0) {
|
||||||
|
return calculatedAmount;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
// @dev else use the generic interface.
|
||||||
|
ICurveCryptoSwapPool(sellParams.poolAddress).exchange(
|
||||||
|
sellTokenIndexUint,
|
||||||
|
buyTokenIndexUint,
|
||||||
|
sellParams.specifiedAmount,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sellParams.buyToken == ETH_ADDRESS) {
|
||||||
|
calculatedAmount = address(this).balance - buyTokenBalBefore;
|
||||||
|
(bool sent,) = address(msg.sender).call{value: calculatedAmount}("");
|
||||||
|
require(sent, "Eth transfer failed");
|
||||||
|
} else {
|
||||||
|
calculatedAmount =
|
||||||
|
buyToken.balanceOf(address(this)) - buyTokenBalBefore;
|
||||||
|
buyToken.safeTransfer(address(msg.sender), calculatedAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Check whether a pool supports int128 inputs or uint256(excluded
|
||||||
|
/// custom)
|
||||||
|
/// @param poolAddress address of the pool
|
||||||
|
/// @param coins list of coin addresses in the pool
|
||||||
|
function isInt128Pool(address poolAddress, PoolCoins memory coins)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
// @dev We avoid using ETH/WETH as a token here because it might create
|
||||||
|
// a requirement to index WETH when it's not needed.
|
||||||
|
uint256 sampleTokenIndex = (
|
||||||
|
coins.addresses[0] == ETH_ADDRESS
|
||||||
|
|| coins.addresses[0] == WETH_ADDRESS
|
||||||
|
) ? 1 : 0;
|
||||||
|
uint256 sampleAmount =
|
||||||
|
IERC20(coins.addresses[sampleTokenIndex]).balanceOf(poolAddress);
|
||||||
|
|
||||||
|
try ICurveCryptoSwapPool(poolAddress).get_dy(
|
||||||
|
sampleTokenIndex == 0 ? 0 : 1,
|
||||||
|
sampleTokenIndex == 0 ? 1 : 0,
|
||||||
|
sampleAmount / 10
|
||||||
|
) returns (uint256) {
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Check whether a pool is a custom int128 pool(balances, coins, ...
|
||||||
|
/// accept int128 as input)
|
||||||
|
/// @param poolAddress address of the pool
|
||||||
|
function isCustomInt128Pool(address poolAddress)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
try ICurveStableSwapPool(poolAddress).balances(0) returns (uint256) {
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get coins inside a pool
|
||||||
|
/// @param poolAddress The address of the pool
|
||||||
|
function getCoins(address poolAddress)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (PoolCoins memory output)
|
||||||
|
{
|
||||||
|
uint256 len;
|
||||||
|
|
||||||
|
/// @dev as of registry, max addresses that can be included in a pool is
|
||||||
|
/// always 8, therefore we limit the loop to it.
|
||||||
|
if (!isCustomInt128Pool(poolAddress)) {
|
||||||
|
// Pool with coins(uint256)
|
||||||
|
for (len; len < 8; len++) {
|
||||||
|
try ICurveStableSwapPool(poolAddress).coins(len) returns (
|
||||||
|
address coin
|
||||||
|
) {
|
||||||
|
output.addresses[len] = coin;
|
||||||
|
output.coinsLength++;
|
||||||
|
} catch {
|
||||||
|
// Pool has no coins, or the last coin has been found
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (len; len < 8; len++) {
|
||||||
|
// Pool supports coins(int128)
|
||||||
|
try ICurveCustomInt128Pool(poolAddress).coins(
|
||||||
|
int128(uint128(len))
|
||||||
|
) returns (address coin) {
|
||||||
|
output.addresses[len] = coin;
|
||||||
|
output.coinsLength++;
|
||||||
|
} catch {
|
||||||
|
// Pool has no coins, or the last coin has been found
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Get indices of coins to swap
|
||||||
|
/// @dev If the pool is meta the registry.get_coin_indices includes the
|
||||||
|
/// underlying addresses (appended to the array from index 1 to length-1)
|
||||||
|
/// @param sellToken The token being sold
|
||||||
|
/// @param buyToken The token being bought
|
||||||
|
/// @param coins output of getCoins()
|
||||||
|
/// @param isEthPool determine if pool has native ETH inside
|
||||||
|
function getCoinsIndices(
|
||||||
|
address sellToken,
|
||||||
|
address buyToken,
|
||||||
|
PoolCoins memory coins,
|
||||||
|
bool isEthPool
|
||||||
|
) internal pure returns (int128 sellTokenIndex, int128 buyTokenIndex) {
|
||||||
|
address sellToken_ = sellToken;
|
||||||
|
address buyToken_ = buyToken;
|
||||||
|
if (sellToken == ETH_ADDRESS && !isEthPool) {
|
||||||
|
sellToken_ = WETH_ADDRESS;
|
||||||
|
}
|
||||||
|
if (buyToken == ETH_ADDRESS && !isEthPool) {
|
||||||
|
buyToken_ = WETH_ADDRESS;
|
||||||
|
}
|
||||||
|
for (uint256 i; i < coins.coinsLength; i++) {
|
||||||
|
if (coins.addresses[i] == sellToken_) {
|
||||||
|
sellTokenIndex = int128(uint128(i));
|
||||||
|
} else if (coins.addresses[i] == buyToken_) {
|
||||||
|
buyTokenIndex = int128(uint128(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Wrapped ported version of Curve Plain Pool to Solidity
|
||||||
|
/// For params informations see:
|
||||||
|
/// https://docs.curve.fi/cryptoswap-exchange/cryptoswap/pools/crypto-pool/
|
||||||
|
interface ICurveCryptoSwapPool {
|
||||||
|
function get_dy(uint256 i, uint256 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy)
|
||||||
|
external
|
||||||
|
payable;
|
||||||
|
|
||||||
|
function balances(uint256 arg0) external view returns (uint256);
|
||||||
|
|
||||||
|
function fee() external view returns (uint256);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICurveCryptoSwapPoolEth is ICurveCryptoSwapPool {
|
||||||
|
function exchange(
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
bool use_eth,
|
||||||
|
address receiver
|
||||||
|
) external payable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Wrapped ported version of Curve Plain Pool to Solidity
|
||||||
|
/// For params informations see:
|
||||||
|
/// https://docs.curve.fi/stableswap-exchange/stableswap/pools/plain_pools/
|
||||||
|
interface ICurveStableSwapPool {
|
||||||
|
function get_dy(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external;
|
||||||
|
|
||||||
|
function balances(uint256 arg0) external view returns (uint256);
|
||||||
|
|
||||||
|
function fee() external view returns (uint256);
|
||||||
|
|
||||||
|
function coins(uint256 i) external view returns (address);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICurveStableSwapPoolEth {
|
||||||
|
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external
|
||||||
|
payable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev TODO future implementation, not used at the moment since StableSwap
|
||||||
|
/// Meta Pools are not supported yet
|
||||||
|
interface ICurveStableSwapMetaPool is ICurveStableSwapPool {
|
||||||
|
function get_dy_underlying(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external
|
||||||
|
returns (uint256);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICurveCustomInt128Pool {
|
||||||
|
function coins(int128 arg0) external view returns (address);
|
||||||
|
function balances(int128 arg0) external view returns (uint256);
|
||||||
|
}
|
||||||
35
evm/src/curve/manifest.yaml
Normal file
35
evm/src/curve/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
|
||||||
|
- PriceFunction
|
||||||
|
|
||||||
|
# The file containing the adapter contract
|
||||||
|
contract: CurveAdapter.sol
|
||||||
|
|
||||||
|
# Deployment instances used to generate chain specific bytecode.
|
||||||
|
instances:
|
||||||
|
- chain:
|
||||||
|
name: mainnet
|
||||||
|
id: 1
|
||||||
|
arguments:
|
||||||
|
- none
|
||||||
|
|
||||||
|
# Specify some automatic test cases in case getPoolIds and
|
||||||
|
# getTokens are not implemented.
|
||||||
|
tests:
|
||||||
|
instances:
|
||||||
|
- pool_id: "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7"
|
||||||
|
sell_token: "0xdAC17F958D2ee523a2206206994597C13D831ec7"
|
||||||
|
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||||
|
block: 19719570
|
||||||
|
chain:
|
||||||
|
id: 1
|
||||||
|
name: mainnet
|
||||||
@@ -6,9 +6,11 @@ import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
|||||||
import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
||||||
import "src/interfaces/ISwapAdapterTypes.sol";
|
import "src/interfaces/ISwapAdapterTypes.sol";
|
||||||
import "src/libraries/FractionMath.sol";
|
import "src/libraries/FractionMath.sol";
|
||||||
|
import "src/libraries/EfficientERC20.sol";
|
||||||
|
|
||||||
contract AdapterTest is Test, ISwapAdapterTypes {
|
contract AdapterTest is Test, ISwapAdapterTypes {
|
||||||
using FractionMath for Fraction;
|
using FractionMath for Fraction;
|
||||||
|
using EfficientERC20 for IERC20;
|
||||||
|
|
||||||
uint256 constant pricePrecision = 10e24;
|
uint256 constant pricePrecision = 10e24;
|
||||||
string[] public stringPctgs = ["0%", "0.1%", "50%", "100%"];
|
string[] public stringPctgs = ["0%", "0.1%", "50%", "100%"];
|
||||||
@@ -29,8 +31,8 @@ contract AdapterTest is Test, ISwapAdapterTypes {
|
|||||||
);
|
);
|
||||||
for (uint256 i = 0; i < poolIds.length; i++) {
|
for (uint256 i = 0; i < poolIds.length; i++) {
|
||||||
address[] memory tokens = adapter.getTokens(poolIds[i]);
|
address[] memory tokens = adapter.getTokens(poolIds[i]);
|
||||||
IERC20(tokens[0]).approve(address(adapter), type(uint256).max);
|
IERC20(tokens[0]).forceApprove(address(adapter), type(uint256).max);
|
||||||
IERC20(tokens[1]).approve(address(adapter), type(uint256).max);
|
IERC20(tokens[1]).forceApprove(address(adapter), type(uint256).max);
|
||||||
|
|
||||||
testPricesForPair(
|
testPricesForPair(
|
||||||
adapter, poolIds[i], tokens[0], tokens[1], hasPriceImpact
|
adapter, poolIds[i], tokens[0], tokens[1], hasPriceImpact
|
||||||
@@ -68,6 +70,9 @@ contract AdapterTest is Test, ISwapAdapterTypes {
|
|||||||
);
|
);
|
||||||
uint256[] memory amounts =
|
uint256[] memory amounts =
|
||||||
calculateTestAmounts(sellLimit, hasMarginalPrices);
|
calculateTestAmounts(sellLimit, hasMarginalPrices);
|
||||||
|
|
||||||
|
// TODO: What if the price function is not available? Do we still want
|
||||||
|
// to run this test?
|
||||||
Fraction[] memory prices =
|
Fraction[] memory prices =
|
||||||
adapter.price(poolId, tokenIn, tokenOut, amounts);
|
adapter.price(poolId, tokenIn, tokenOut, amounts);
|
||||||
assertGt(
|
assertGt(
|
||||||
|
|||||||
420
evm/test/CurveAdapter.t.sol
Normal file
420
evm/test/CurveAdapter.t.sol
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "./AdapterTest.sol";
|
||||||
|
import "src/curve/CurveAdapter.sol";
|
||||||
|
|
||||||
|
contract CurveAdapterTest is Test, ISwapAdapterTypes, AdapterTest {
|
||||||
|
using FractionMath for Fraction;
|
||||||
|
|
||||||
|
CurveAdapter adapter;
|
||||||
|
|
||||||
|
// tokens
|
||||||
|
address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
|
||||||
|
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||||
|
address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
||||||
|
address constant ETH = address(0);
|
||||||
|
address constant WBETH = 0xa2E3356610840701BDf5611a53974510Ae27E2e1;
|
||||||
|
address constant MIM = 0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3;
|
||||||
|
address constant THREE_CRV_TOKEN =
|
||||||
|
0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490;
|
||||||
|
address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
|
||||||
|
address constant stETH = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
|
||||||
|
|
||||||
|
// pools
|
||||||
|
address constant STABLE_POOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7;
|
||||||
|
address constant CRYPTO_POOL = 0x80466c64868E1ab14a1Ddf27A676C3fcBE638Fe5;
|
||||||
|
address constant STABLE_META_POOL =
|
||||||
|
0x5a6A4D54456819380173272A5E8E9B9904BdF41B;
|
||||||
|
address constant ETH_POOL = 0xBfAb6FA95E0091ed66058ad493189D2cB29385E6;
|
||||||
|
address constant STETH_POOL = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022;
|
||||||
|
address[] ADDITIONAL_POOLS_FOR_TESTING;
|
||||||
|
|
||||||
|
uint256 constant TEST_ITERATIONS = 100;
|
||||||
|
IwstETH constant wstETH =
|
||||||
|
IwstETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
uint256 forkBlock = 20234346;
|
||||||
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
adapter = new CurveAdapter();
|
||||||
|
|
||||||
|
// Additional pools that include custom Int128 pools
|
||||||
|
ADDITIONAL_POOLS_FOR_TESTING = [
|
||||||
|
0xEcd5e75AFb02eFa118AF914515D6521aaBd189F1,
|
||||||
|
0xEd279fDD11cA84bEef15AF5D39BB4d4bEE23F0cA,
|
||||||
|
0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c,
|
||||||
|
0x9EfE1A1Cbd6Ca51Ee8319AFc4573d253C3B732af,
|
||||||
|
0x4807862AA8b2bF68830e4C8dc86D0e9A998e085a,
|
||||||
|
0xd632f22692FaC7611d2AA1C0D552930D43CAEd3B,
|
||||||
|
0xA5407eAE9Ba41422680e2e00537571bcC53efBfD,
|
||||||
|
0x5a6A4D54456819380173272A5E8E9B9904BdF41B,
|
||||||
|
0x3211C6cBeF1429da3D0d58494938299C92Ad5860,
|
||||||
|
0xDB6925eA42897ca786a045B252D95aA7370f44b4,
|
||||||
|
0xf861483fa7E511fbc37487D91B6FAa803aF5d37c,
|
||||||
|
0x1e098B32944292969fB58c85bDC85545DA397117,
|
||||||
|
0xe0e970a99bc4F53804D8145beBBc7eBc9422Ba7F,
|
||||||
|
0x6bfE880Ed1d639bF80167b93cc9c56a39C1Ba2dC,
|
||||||
|
0xBDFAe7D2cF2E69E27b75a5287ECD3808F62B5a76,
|
||||||
|
0xfB8814D005C5f32874391e888da6eB2fE7a27902,
|
||||||
|
0x0f3159811670c117c372428D4E69AC32325e4D0F,
|
||||||
|
0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14,
|
||||||
|
0x0E9B5B092caD6F1c5E6bc7f89Ffe1abb5c95F1C2,
|
||||||
|
0x21410232B484136404911780bC32756D5d1a9Fa9,
|
||||||
|
0x322135Dd9cBAE8Afa84727d9aE1434b5B3EBA44B,
|
||||||
|
0xC26b89A667578ec7b3f11b2F98d6Fd15C07C54ba,
|
||||||
|
0x9409280DC1e6D33AB7A8C6EC03e5763FB61772B5,
|
||||||
|
0x5FAE7E604FC3e24fd43A72867ceBaC94c65b404A
|
||||||
|
// 0x87650D7bbfC3A9F10587d7778206671719d9910D // Uses a token
|
||||||
|
// that can't be `deal`ed with foundry
|
||||||
|
// https://etherscan.io/token/0x2a8e1e676ec238d8a992307b495b45b3feaa5e86
|
||||||
|
// 0x50f3752289e1456BfA505afd37B241bca23e685d, // Uses a token
|
||||||
|
// that can't be `deal`ed with foundry
|
||||||
|
// https://etherscan.io/token/0x3472A5A71965499acd81997a54BBA8D852C6E53d
|
||||||
|
];
|
||||||
|
|
||||||
|
vm.label(address(adapter), "CurveAdapter");
|
||||||
|
vm.label(USDT, "USDT");
|
||||||
|
vm.label(USDC, "USDC");
|
||||||
|
vm.label(STABLE_POOL, "STABLE_POOL");
|
||||||
|
vm.label(WETH, "WETH");
|
||||||
|
vm.label(CRYPTO_POOL, "CRYPTO_POOL");
|
||||||
|
}
|
||||||
|
|
||||||
|
receive() external payable {}
|
||||||
|
|
||||||
|
function testSwapsForAdditionalPools() public {
|
||||||
|
uint256 len = ADDITIONAL_POOLS_FOR_TESTING.length;
|
||||||
|
for (uint256 i = 0; i < len; i++) {
|
||||||
|
bytes32 pair = bytes32(bytes20(ADDITIONAL_POOLS_FOR_TESTING[i]));
|
||||||
|
address[] memory tokens = adapter.getTokens(pair);
|
||||||
|
uint256[] memory amounts = new uint256[](1);
|
||||||
|
|
||||||
|
try ICurveStableSwapPool(ADDITIONAL_POOLS_FOR_TESTING[i]).balances(
|
||||||
|
0
|
||||||
|
) returns (uint256 bal) {
|
||||||
|
amounts[0] = bal / 10;
|
||||||
|
} catch {
|
||||||
|
amounts[0] = ICurveCustomInt128Pool(
|
||||||
|
ADDITIONAL_POOLS_FOR_TESTING[i]
|
||||||
|
).balances(int128(0)) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
deal(tokens[0], address(this), amounts[0]);
|
||||||
|
IERC20(tokens[0]).approve(address(adapter), amounts[0]);
|
||||||
|
|
||||||
|
// Test Swap
|
||||||
|
Trade memory trade = adapter.swap(
|
||||||
|
pair,
|
||||||
|
tokens[0],
|
||||||
|
tokens[1],
|
||||||
|
ISwapAdapterTypes.OrderSide.Sell,
|
||||||
|
amounts[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test Limits
|
||||||
|
uint256[] memory limits =
|
||||||
|
adapter.getLimits(pair, tokens[0], tokens[1]);
|
||||||
|
|
||||||
|
assertGt(trade.calculatedAmount, 0);
|
||||||
|
assertGt(trade.price.numerator, 0);
|
||||||
|
assertGt(trade.price.denominator, 0);
|
||||||
|
assertGt(limits[0], 0);
|
||||||
|
assertGt(limits[1], 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzCurveStEthPool(
|
||||||
|
uint256 specifiedAmount,
|
||||||
|
bool invertedSides
|
||||||
|
) public {
|
||||||
|
(address sellToken, address buyToken) =
|
||||||
|
!invertedSides ? (ETH, stETH) : (stETH, ETH);
|
||||||
|
(uint256 sellTokenBalBefore, uint256 buyTokenBalBefore) = (0, 0);
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(bytes20(STETH_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, sellToken, buyToken);
|
||||||
|
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10 ** 5);
|
||||||
|
|
||||||
|
if (sellToken == ETH) {
|
||||||
|
deal(address(adapter), specifiedAmount);
|
||||||
|
sellTokenBalBefore = address(adapter).balance;
|
||||||
|
buyTokenBalBefore = IERC20(buyToken).balanceOf(address(this));
|
||||||
|
} else {
|
||||||
|
dealStEthTokens(specifiedAmount);
|
||||||
|
IERC20(sellToken).approve(address(adapter), specifiedAmount);
|
||||||
|
sellTokenBalBefore = IERC20(sellToken).balanceOf(address(this));
|
||||||
|
buyTokenBalBefore = address(this).balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Trade memory trade = adapter.swap(
|
||||||
|
pair, sellToken, buyToken, OrderSide.Sell, specifiedAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sellToken == ETH) {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount, sellTokenBalBefore - address(adapter).balance
|
||||||
|
);
|
||||||
|
assertGe(
|
||||||
|
trade.calculatedAmount + 3,
|
||||||
|
IERC20(buyToken).balanceOf(address(this)) - buyTokenBalBefore
|
||||||
|
);
|
||||||
|
assertLe(
|
||||||
|
trade.calculatedAmount - 3,
|
||||||
|
IERC20(buyToken).balanceOf(address(this)) - buyTokenBalBefore
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assertGe(
|
||||||
|
specifiedAmount + 3,
|
||||||
|
sellTokenBalBefore - IERC20(sellToken).balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertLe(
|
||||||
|
specifiedAmount - 3,
|
||||||
|
sellTokenBalBefore - IERC20(sellToken).balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
address(this).balance - buyTokenBalBefore
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzCurveStableSwap(uint256 specifiedAmount) public {
|
||||||
|
OrderSide side = OrderSide.Sell;
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(bytes20(STABLE_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, USDC, USDT);
|
||||||
|
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10 ** 4);
|
||||||
|
|
||||||
|
deal(USDC, address(this), specifiedAmount);
|
||||||
|
IERC20(USDC).approve(address(adapter), specifiedAmount);
|
||||||
|
|
||||||
|
uint256 usdc_balance = IERC20(USDC).balanceOf(address(this));
|
||||||
|
uint256 USDT_balance = IERC20(USDT).balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, USDC, USDT, side, specifiedAmount);
|
||||||
|
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
usdc_balance - IERC20(USDC).balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
IERC20(USDT).balanceOf(address(this)) - USDT_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzCurveCryptoSwap(uint256 specifiedAmount) public {
|
||||||
|
OrderSide side = OrderSide.Sell;
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(bytes20(CRYPTO_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, WETH, USDT);
|
||||||
|
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10 ** 6);
|
||||||
|
|
||||||
|
deal(WETH, address(this), specifiedAmount);
|
||||||
|
IERC20(WETH).approve(address(adapter), specifiedAmount);
|
||||||
|
|
||||||
|
uint256 WETH_balance = IERC20(WETH).balanceOf(address(this));
|
||||||
|
uint256 USDT_balance = IERC20(USDT).balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, WETH, USDT, side, specifiedAmount);
|
||||||
|
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
WETH_balance - IERC20(WETH).balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
IERC20(USDT).balanceOf(address(this)) - USDT_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzCurveCryptoSwapUsingEth(uint256 specifiedAmount)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
OrderSide side = OrderSide.Sell;
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(bytes20(CRYPTO_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, ETH, USDT);
|
||||||
|
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10 ** 6);
|
||||||
|
|
||||||
|
deal(address(adapter), specifiedAmount);
|
||||||
|
|
||||||
|
uint256 ETH_balance = address(adapter).balance;
|
||||||
|
uint256 USDT_balance = IERC20(USDT).balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, ETH, USDT, side, specifiedAmount);
|
||||||
|
|
||||||
|
assertEq(specifiedAmount, ETH_balance - address(adapter).balance);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
IERC20(USDT).balanceOf(address(this)) - USDT_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzCurveStablePoolEthWithEth(uint256 specifiedAmount)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
OrderSide side = OrderSide.Sell;
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(bytes20(ETH_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, ETH, WBETH);
|
||||||
|
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10 ** 14);
|
||||||
|
|
||||||
|
deal(address(adapter), specifiedAmount);
|
||||||
|
|
||||||
|
uint256 eth_balance = address(adapter).balance;
|
||||||
|
uint256 WBETH_balance = IERC20(WBETH).balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, ETH, WBETH, side, specifiedAmount);
|
||||||
|
|
||||||
|
assertEq(specifiedAmount, eth_balance - address(adapter).balance);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
IERC20(WBETH).balanceOf(address(this)) - WBETH_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzzCurveStablePoolEthWithToken(uint256 specifiedAmount)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
OrderSide side = OrderSide.Sell;
|
||||||
|
|
||||||
|
bytes32 pair = bytes32(bytes20(ETH_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, WBETH, ETH);
|
||||||
|
|
||||||
|
vm.assume(specifiedAmount < limits[0] && specifiedAmount > 10 ** 14);
|
||||||
|
|
||||||
|
deal(address(WBETH), address(this), specifiedAmount);
|
||||||
|
IERC20(WBETH).approve(address(adapter), specifiedAmount);
|
||||||
|
|
||||||
|
uint256 eth_balance = address(this).balance;
|
||||||
|
uint256 WBETH_balance = IERC20(WBETH).balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, WBETH, ETH, side, specifiedAmount);
|
||||||
|
|
||||||
|
assertEq(trade.calculatedAmount, address(this).balance - eth_balance);
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
WBETH_balance - IERC20(WBETH).balanceOf(address(this))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapSellIncreasingSwapsCurve() public {
|
||||||
|
executeIncreasingSwapsStableSwap(OrderSide.Sell);
|
||||||
|
executeIncreasingSwapsCryptoSwap(OrderSide.Sell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeIncreasingSwapsStableSwap(OrderSide side) internal {
|
||||||
|
bytes32 pair = bytes32(bytes20(CRYPTO_POOL));
|
||||||
|
|
||||||
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
amounts[i] = 1000 * i * 10 ** 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
Trade[] memory trades = new Trade[](TEST_ITERATIONS);
|
||||||
|
uint256 beforeSwap;
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
beforeSwap = vm.snapshot();
|
||||||
|
|
||||||
|
deal(WETH, address(this), amounts[i]);
|
||||||
|
IERC20(WETH).approve(address(adapter), amounts[i]);
|
||||||
|
|
||||||
|
trades[i] = adapter.swap(pair, WETH, USDT, side, amounts[i]);
|
||||||
|
vm.revertTo(beforeSwap);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) {
|
||||||
|
assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
|
||||||
|
assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeIncreasingSwapsCryptoSwap(OrderSide side) internal {
|
||||||
|
bytes32 pair = bytes32(bytes20(CRYPTO_POOL));
|
||||||
|
|
||||||
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
amounts[i] = 1000 * i * 10 ** 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
Trade[] memory trades = new Trade[](TEST_ITERATIONS);
|
||||||
|
uint256 beforeSwap;
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
beforeSwap = vm.snapshot();
|
||||||
|
|
||||||
|
deal(WETH, address(this), amounts[i]);
|
||||||
|
IERC20(WETH).approve(address(adapter), amounts[i]);
|
||||||
|
|
||||||
|
trades[i] = adapter.swap(pair, WETH, USDT, side, amounts[i]);
|
||||||
|
vm.revertTo(beforeSwap);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) {
|
||||||
|
assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
|
||||||
|
assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetCapabilitiesCurveSwap(bytes32 pair, address t0, address t1)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
Capability[] memory res = adapter.getCapabilities(pair, t0, t1);
|
||||||
|
|
||||||
|
assertEq(res.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetTokensCurveStableSwap() public {
|
||||||
|
bytes32 pair = bytes32(bytes20(STABLE_POOL));
|
||||||
|
address[] memory tokens = adapter.getTokens(pair);
|
||||||
|
|
||||||
|
assertGe(tokens.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetTokensCurveCryptoSwap() public {
|
||||||
|
bytes32 pair = bytes32(bytes20(CRYPTO_POOL));
|
||||||
|
address[] memory tokens = adapter.getTokens(pair);
|
||||||
|
|
||||||
|
assertGe(tokens.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetLimitsCurveStableSwap() public {
|
||||||
|
bytes32 pair = bytes32(bytes20(STABLE_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, USDC, USDT);
|
||||||
|
|
||||||
|
assertEq(limits.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetLimitsCurveCryptoSwap() public {
|
||||||
|
bytes32 pair = bytes32(bytes20(CRYPTO_POOL));
|
||||||
|
uint256[] memory limits = adapter.getLimits(pair, WETH, USDT);
|
||||||
|
|
||||||
|
assertEq(limits.length, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev custom function to 'deal' stETH tokens as normal deal won't work
|
||||||
|
function dealStEthTokens(uint256 amount) internal {
|
||||||
|
uint256 wstETHAmount = wstETH.getStETHByWstETH(amount);
|
||||||
|
deal(address(wstETH), address(this), wstETHAmount);
|
||||||
|
wstETH.unwrap(wstETHAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IwstETH is IERC20 {
|
||||||
|
function unwrap(uint256 _wstETHAmount) external returns (uint256);
|
||||||
|
function getStETHByWstETH(uint256 _wstETHAmount)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
}
|
||||||
@@ -28,7 +28,10 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
|
|||||||
|
|
||||||
receive() external payable {}
|
receive() external payable {}
|
||||||
|
|
||||||
function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1) public {
|
function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
{
|
||||||
bytes32 pair = bytes32(0);
|
bytes32 pair = bytes32(0);
|
||||||
uint256[] memory limits = adapter.getLimits(
|
uint256[] memory limits = adapter.getLimits(
|
||||||
pair, address(address(weEth)), address(address(eEth))
|
pair, address(address(weEth)), address(address(eEth))
|
||||||
@@ -319,10 +322,10 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
|
|||||||
weEth_.balanceOf(address(this)) - weEth_balance
|
weEth_.balanceOf(address(this)) - weEth_balance
|
||||||
);
|
);
|
||||||
/// @dev Transfer function contains rounding errors because of
|
/// @dev Transfer function contains rounding errors because of
|
||||||
/// rewards in eETH contract, therefore we assume a +/-2
|
/// rewards in eETH contract, therefore we assume a +/-4
|
||||||
/// tolerance
|
/// tolerance
|
||||||
assertLe(
|
assertLe(
|
||||||
specifiedAmount - 2,
|
specifiedAmount - 4,
|
||||||
weEth_.balanceOf(address(this)) - weEth_balance
|
weEth_.balanceOf(address(this)) - weEth_balance
|
||||||
);
|
);
|
||||||
assertEq(
|
assertEq(
|
||||||
@@ -386,6 +389,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
|
|||||||
|
|
||||||
function testGetCapabilitiesEtherfi(bytes32 pair, address t0, address t1)
|
function testGetCapabilitiesEtherfi(bytes32 pair, address t0, address t1)
|
||||||
public
|
public
|
||||||
|
view
|
||||||
{
|
{
|
||||||
Capability[] memory res =
|
Capability[] memory res =
|
||||||
adapter.getCapabilities(pair, address(t0), address(t1));
|
adapter.getCapabilities(pair, address(t0), address(t1));
|
||||||
@@ -393,14 +397,14 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
|
|||||||
assertEq(res.length, 3);
|
assertEq(res.length, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testGetTokensEtherfi() public {
|
function testGetTokensEtherfi() public view {
|
||||||
bytes32 pair = bytes32(0);
|
bytes32 pair = bytes32(0);
|
||||||
address[] memory tokens = adapter.getTokens(pair);
|
address[] memory tokens = adapter.getTokens(pair);
|
||||||
|
|
||||||
assertEq(tokens.length, 3);
|
assertEq(tokens.length, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testGetLimitsEtherfi() public {
|
function testGetLimitsEtherfi() public view {
|
||||||
bytes32 pair = bytes32(0);
|
bytes32 pair = bytes32(0);
|
||||||
uint256[] memory limits =
|
uint256[] memory limits =
|
||||||
adapter.getLimits(pair, address(eEth), address(weEth));
|
adapter.getLimits(pair, address(eEth), address(weEth));
|
||||||
|
|||||||
Reference in New Issue
Block a user