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:
Domenico
2025-06-27 12:03:32 +02:00
committed by GitHub
parent 1885467616
commit 7f9a995e8a
5 changed files with 1077 additions and 7 deletions

View File

@@ -6,9 +6,11 @@ import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
import {ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
import "src/interfaces/ISwapAdapterTypes.sol";
import "src/libraries/FractionMath.sol";
import "src/libraries/EfficientERC20.sol";
contract AdapterTest is Test, ISwapAdapterTypes {
using FractionMath for Fraction;
using EfficientERC20 for IERC20;
uint256 constant pricePrecision = 10e24;
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++) {
address[] memory tokens = adapter.getTokens(poolIds[i]);
IERC20(tokens[0]).approve(address(adapter), type(uint256).max);
IERC20(tokens[1]).approve(address(adapter), type(uint256).max);
IERC20(tokens[0]).forceApprove(address(adapter), type(uint256).max);
IERC20(tokens[1]).forceApprove(address(adapter), type(uint256).max);
testPricesForPair(
adapter, poolIds[i], tokens[0], tokens[1], hasPriceImpact
@@ -68,6 +70,9 @@ contract AdapterTest is Test, ISwapAdapterTypes {
);
uint256[] memory amounts =
calculateTestAmounts(sellLimit, hasMarginalPrices);
// TODO: What if the price function is not available? Do we still want
// to run this test?
Fraction[] memory prices =
adapter.price(poolId, tokenIn, tokenOut, amounts);
assertGt(

420
evm/test/CurveAdapter.t.sol Normal file
View 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);
}

View File

@@ -28,7 +28,10 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
receive() external payable {}
function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1) public {
function testPriceFuzzEtherfi(uint256 amount0, uint256 amount1)
public
view
{
bytes32 pair = bytes32(0);
uint256[] memory limits = adapter.getLimits(
pair, address(address(weEth)), address(address(eEth))
@@ -319,10 +322,10 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
weEth_.balanceOf(address(this)) - weEth_balance
);
/// @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
assertLe(
specifiedAmount - 2,
specifiedAmount - 4,
weEth_.balanceOf(address(this)) - weEth_balance
);
assertEq(
@@ -386,6 +389,7 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
function testGetCapabilitiesEtherfi(bytes32 pair, address t0, address t1)
public
view
{
Capability[] memory res =
adapter.getCapabilities(pair, address(t0), address(t1));
@@ -393,14 +397,14 @@ contract EtherfiAdapterTest is Test, ISwapAdapterTypes {
assertEq(res.length, 3);
}
function testGetTokensEtherfi() public {
function testGetTokensEtherfi() public view {
bytes32 pair = bytes32(0);
address[] memory tokens = adapter.getTokens(pair);
assertEq(tokens.length, 3);
}
function testGetLimitsEtherfi() public {
function testGetLimitsEtherfi() public view {
bytes32 pair = bytes32(0);
uint256[] memory limits =
adapter.getLimits(pair, address(eEth), address(weEth));