Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/slither.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ jobs:
- uses: crytic/slither-action@f197989dea5b53e986d0f88c60a034ddd77ec9a8
with:
target: 'foundry/'
slither-args: '--filter-paths foundry/lib/'
slither-args: '--filter-paths foundry/lib/ --exclude unindexed-event-address'
4 changes: 2 additions & 2 deletions config/executor_addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"uniswap_v4_hooks": "0xE49B916032c734CD89cDfe80A868805c738A6ceB",
"vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411",
"ekubo_v2": "0x263DD7AD20983b5E0392bf1F09C4493500EDb333",
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
"vm:curve": "0xc8031d1457D19D5F0e074f74960bAF2010beA795",
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B",
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
"rfq:bebop": "0xFE42BFb115eD9671011cA52BDD23A52A2e077a7c",
Expand All @@ -35,4 +35,4 @@
"velodrome_slipstreams": "0x772f58a21a1e64c5677cdf16630205264cad2b6a",
"vm:curve": "0x21A97f2Aa8D9AE7144BfD080266Ee87cC2369656"
}
}
}
10 changes: 6 additions & 4 deletions foundry/scripts/deploy-executors.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,12 @@ const executors_to_deploy = {
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
]
},
// Args: ETH address in curve pools, Permit2
// Args: ETH address in curve pools, Permit2, stETH address
{
exchange: "CurveExecutor", args: [
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
"0x000000000022D473030F116dDEE9F6B43aC78BA3",
"0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"
]
},
// Args: factory, permit2
Expand Down Expand Up @@ -194,11 +195,12 @@ const executors_to_deploy = {
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
]
},
// Args: ETH address in curve pools, Permit2
// Args: ETH address in curve pools, Permit2, stETH address
{
exchange: "CurveExecutor", args: [
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
"0x000000000022D473030F116dDEE9F6B43aC78BA3",
"0x0000000000000000000000000000000000000000" // No stETH on unichain
]
},
// Aerodrome Slipstreams - Args: Old Factory, New Factory, Permit2
Expand Down
23 changes: 22 additions & 1 deletion foundry/src/executors/CurveExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,23 @@ contract CurveExecutor is IExecutor, RestrictTransferFrom {
using SafeERC20 for IERC20;

address public immutable nativeToken;
address public immutable stEthAddress;
bool public immutable hasStETH;

constructor(address _nativeToken, address _permit2)
constructor(address _nativeToken, address _permit2, address _stEthAddress)
RestrictTransferFrom(_permit2)
{
if (_nativeToken == address(0)) {
revert CurveExecutor__AddressZero();
}
nativeToken = _nativeToken;

if (_stEthAddress != address(0)) {
hasStETH = true;
} else {
hasStETH = false;
}
stEthAddress = _stEthAddress;
}

// slither-disable-next-line locked-ether
Expand Down Expand Up @@ -78,6 +87,7 @@ contract CurveExecutor is IExecutor, RestrictTransferFrom {
_transfer(address(this), transferType, tokenIn, amountIn);

/// Inspired by Curve's router contract: https://github.com/curvefi/curve-router-ng/blob/9ab006ca848fc7f1995b6fbbecfecc1e0eb29e2a/contracts/Router.vy#L44

uint256 balanceBefore = _balanceOf(tokenOut);

uint256 ethAmount = 0;
Expand Down Expand Up @@ -105,15 +115,26 @@ contract CurveExecutor is IExecutor, RestrictTransferFrom {
}

uint256 balanceAfter = _balanceOf(tokenOut);

uint256 amountOut = balanceAfter - balanceBefore;

uint256 castRemainderWei = 0;

if (receiver != address(this)) {
if (tokenOut == nativeToken) {
Address.sendValue(payable(receiver), amountOut);
} else {
// Due to rounding errors, 1 wei might get lost
IERC20(tokenOut).safeTransfer(receiver, amountOut);
}

if (hasStETH && tokenOut == stEthAddress) {
castRemainderWei = IERC20(stEthAddress).balanceOf(address(this))
- balanceBefore;
amountOut -= castRemainderWei;
}
}

return amountOut;
}

Expand Down
3 changes: 2 additions & 1 deletion foundry/test/TychoRouterTestSetup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS);
ekuboExecutor =
new EkuboExecutor(ekuboCore, ekuboMevResist, PERMIT2_ADDRESS);
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
curveExecutor =
new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS, STETH_ADDR);
maverickv2Executor =
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);
balancerV3Executor = new BalancerV3Executor(PERMIT2_ADDRESS);
Expand Down
50 changes: 42 additions & 8 deletions foundry/test/protocols/Curve.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ interface MetaRegistry {
}

contract CurveExecutorExposed is CurveExecutor {
constructor(address _nativeToken, address _permit2)
CurveExecutor(_nativeToken, _permit2)
constructor(address _nativeToken, address _permit2, address _stEthAddress)
CurveExecutor(_nativeToken, _permit2, _stEthAddress)
{}

function decodeData(bytes calldata data)
Expand Down Expand Up @@ -55,8 +55,9 @@ contract CurveExecutorTest is Test, TestUtils, Constants {
function setUp() public {
uint256 forkBlock = 22031795;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
curveExecutorExposed =
new CurveExecutorExposed(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
curveExecutorExposed = new CurveExecutorExposed(
ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS, STETH_ADDR
);
metaRegistry = MetaRegistry(CURVE_META_REGISTRY);
}

Expand Down Expand Up @@ -134,11 +135,44 @@ contract CurveExecutorTest is Test, TestUtils, Constants {

uint256 amountOut = curveExecutorExposed.swap(amountIn, data);

assertEq(amountOut, 1001072414418410897);
assertEq(
IERC20(STETH_ADDR).balanceOf(ALICE),
amountOut - 1 // there is something weird in this pool, but won't investigate for now because we don't currently support it in the simulation
assertEq(amountOut, 1001072414418410896);
assertEq(IERC20(STETH_ADDR).balanceOf(ALICE), amountOut);
}

// This test verifies that amountOut for stETH is calculated correctly by
// accounting for an existing stETH balance in the executor prior to the swap
function testStEthPoolWithInitialstETH() public {
// Swapping ETH -> stETH on StEthPool 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022 twice
uint256 amountIn = 2 ether;
deal(address(curveExecutorExposed), amountIn);

uint256 amountInForTest = 1 ether;

bytes memory data1 = _getData(
ETH_ADDR_FOR_CURVE,
STETH_ADDR,
STETH_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);

uint256 amountOut1 = curveExecutorExposed.swap(amountInForTest, data1);

bytes memory data2 = _getData(
ETH_ADDR_FOR_CURVE,
STETH_ADDR,
STETH_POOL,
1,
ALICE,
RestrictTransferFrom.TransferType.None
);

uint256 amountOut2 = curveExecutorExposed.swap(amountInForTest, data2);

assertEq(amountOut1, 1001072414418410896);
assertEq(amountOut2, 1001072213238226892);
assertEq(IERC20(STETH_ADDR).balanceOf(ALICE), amountOut1 + amountOut2);
}

function testTricrypto2Pool() public {
Expand Down
96 changes: 96 additions & 0 deletions foundry/test/protocols/Lido.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,99 @@ contract TychoRouterForLidoTest is TychoRouterTestSetup {
assertEq(ALICE.balance, 0);
}
}

/**
*
*
* These tests demonstrate that the rounding issue causing 1–2 wei discrepancies
* in stETH transfers exists in LidoV3, as it did previously in LidoV2.
*
* Only tests relevant to this issue are included.
*/
contract LidoExecutorV3Test is Constants, Permit2TestHelper, TestUtils {
using SafeERC20 for IERC20;

LidoExecutorExposed LidoExposed;

function setUp() public {
uint256 forkBlock = 24238735;
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
LidoExposed =
new LidoExecutorExposed(STETH_ADDR, WSTETH_ADDR, PERMIT2_ADDRESS);
}

function testStaking() public {
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 999999999999999998;

bytes memory protocolData = abi.encodePacked(
BOB,
RestrictTransferFrom.TransferType.None,
LidoPoolType.stETH,
LidoPoolDirection.Stake,
false
);

deal(BOB, amountIn);
vm.prank(BOB);
uint256 calculatedAmount =
LidoExposed.swap{value: amountIn}(amountIn, protocolData);

uint256 finalBalance = IERC20(STETH_ADDR).balanceOf(BOB);
assertEq(calculatedAmount, finalBalance);
assertEq(finalBalance, expectedAmountOut);
assertEq(BOB.balance, 0);
}

function testWrapping() public {
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 816702140251455050;

// Need to mint STETH before, just dealing won't work because stETH does some internal accounting
deal(address(LidoExposed), amountIn);
vm.startPrank(address(LidoExposed));
// slither-disable-next-line arbitrary-send-eth
LidoPool(STETH_ADDR).submit{value: amountIn}(address(this));
uint256 stETHAmount = IERC20(STETH_ADDR).balanceOf(address(LidoExposed));

bytes memory protocolData = abi.encodePacked(
BOB,
RestrictTransferFrom.TransferType.None,
LidoPoolType.wstETH,
LidoPoolDirection.Wrap,
true
);

uint256 amountOut = LidoExposed.swap(stETHAmount, protocolData);

uint256 finalBalance = IERC20(WSTETH_ADDR).balanceOf(BOB);
assertEq(amountOut, expectedAmountOut);
assertEq(finalBalance, expectedAmountOut);
// there is 1 wei left in the contract
assertEq(IERC20(STETH_ADDR).balanceOf(address(LidoExposed)), 1);

vm.stopPrank();
}

function testUnwrapping() public {
uint256 amountIn = 1 ether;
uint256 expectedAmountOut = 1224436610013179625;

deal(WSTETH_ADDR, address(LidoExposed), amountIn);
bytes memory protocolData = abi.encodePacked(
BOB,
RestrictTransferFrom.TransferType.None,
LidoPoolType.wstETH,
LidoPoolDirection.Unwrap,
false
);
vm.startPrank(address(LidoExposed));
uint256 amountOut = LidoExposed.swap(amountIn, protocolData);

uint256 finalBalance = IERC20(STETH_ADDR).balanceOf(BOB);
assertEq(amountOut, expectedAmountOut);
assertEq(finalBalance, expectedAmountOut);
assertEq(IERC20(WSTETH_ADDR).balanceOf(address(LidoExposed)), 0);
vm.stopPrank();
}
}
Loading