diff --git a/foundry/foundry.toml b/foundry/foundry.toml index a3c8d7b..3d55450 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -3,7 +3,7 @@ src = 'src' out = 'out' libs = ['lib'] solc = "0.8.28" -evm_version = 'shanghai' +evm_version = 'cancun' optimizer = true optimizer_runs = 1000 via_ir = true diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index cc81acc..99f9e0d 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -12,17 +12,22 @@ import {PoolKey} from "@uniswap/v4-core/types/PoolKey.sol"; import {BalanceDelta} from "@uniswap/v4-core/types/BalanceDelta.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {IHooks} from "@uniswap/v4-core/interfaces/IHooks.sol"; -import {IUnlockCallback} from "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; +import {IUnlockCallback} from + "@uniswap/v4-core/interfaces/callback/IUnlockCallback.sol"; +import {TransientStateLibrary} from + "@uniswap/v4-core/libraries/TransientStateLibrary.sol"; error UniswapV4Executor__InvalidDataLength(); error UniswapV4Executor__SwapFailed(); error UniswapV4Executor__InsufficientOutput(); +error UniswapV4Executor__ManagerMismatch(); contract UniswapV4Executor is IExecutor, IUnlockCallback { using SafeERC20 for IERC20; using CurrencyLibrary for Currency; using SafeCast for int128; using SafeCast for int256; + using TransientStateLibrary for IPoolManager; uint256 private constant MIN_SQRT_RATIO = 4295128739; uint256 private constant MAX_SQRT_RATIO = @@ -37,7 +42,8 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { } function swap(uint256 amountIn, bytes calldata data) - external payable + external + payable returns (uint256 amountOut) { ( @@ -54,19 +60,19 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { currency1: Currency.wrap(zeroForOne ? tokenOut : tokenIn), fee: fee, tickSpacing: 60, // Standard tick spacing - hooks: IHooks(address(0)) // No hooks needed for basic swaps + hooks: IHooks(address(0)) // No hooks needed for basic swaps }); IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: zeroForOne, amountSpecified: int256(amountIn), - sqrtPriceLimitX96: uint160(zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1) + sqrtPriceLimitX96: uint160( + zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1 + ) }); - // Transfer tokens from sender to this contract + // TODO: Find a better place IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); - - // Approve PoolManager to spend tokens IERC20(tokenIn).approve(target, amountIn); SwapCallbackData memory callbackData = SwapCallbackData({ @@ -78,10 +84,12 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { }); IPoolManager poolManager = IPoolManager(target); - - try poolManager.unlock(abi.encode(callbackData)) returns (bytes memory result) { + + try poolManager.unlock(abi.encode(callbackData)) returns ( + bytes memory result + ) { amountOut = abi.decode(result, (uint256)); - + if (amountOut == 0) revert UniswapV4Executor__InsufficientOutput(); // Transfer output tokens to receiver if not this contract @@ -93,24 +101,119 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { } } - function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + // Dev notes: This is inspired by the Uniswap V4 PoolSwapTest.sol + function unlockCallback(bytes calldata rawData) + external + returns (bytes memory) + { SwapCallbackData memory data = abi.decode(rawData, (SwapCallbackData)); - + IPoolManager poolManager = IPoolManager(msg.sender); - - + + // Check initial balances + (,, int256 deltaBefore0) = _fetchBalances( + data.key.currency0, data.receiver, address(this), poolManager + ); + (,, int256 deltaBefore1) = _fetchBalances( + data.key.currency1, data.receiver, address(this), poolManager + ); + + require(deltaBefore0 == 0, "deltaBefore0 not zero"); + require(deltaBefore1 == 0, "deltaBefore1 not zero"); + BalanceDelta delta = poolManager.swap(data.key, data.params, ""); - + // Check final balances and validate based on swap direction + (,, int256 deltaAfter0) = _fetchBalances( + data.key.currency0, data.receiver, address(this), poolManager + ); + (,, int256 deltaAfter1) = _fetchBalances( + data.key.currency1, data.receiver, address(this), poolManager + ); + uint256 amountOut; if (data.params.zeroForOne) { - amountOut = delta.amount1() < 0 ? (-delta.amount1()).toUint256() : 0; + if (data.params.amountSpecified < 0) { + // exact input, 0 for 1 + require( + deltaAfter0 >= data.params.amountSpecified, + "insufficient input amount" + ); + require(delta.amount0() == deltaAfter0, "delta mismatch"); + require(deltaAfter1 >= 0, "negative output amount"); + amountOut = deltaAfter1 > 0 ? uint256(deltaAfter1) : 0; + } else { + // exact output, 0 for 1 + require(deltaAfter0 <= 0, "positive input amount"); + require(delta.amount1() == deltaAfter1, "delta mismatch"); + require( + deltaAfter1 <= data.params.amountSpecified, + "excessive output amount" + ); + amountOut = uint256((-delta.amount1()).toUint256()); + } } else { - amountOut = delta.amount0() < 0 ? (-delta.amount0()).toUint256() : 0; + if (data.params.amountSpecified < 0) { + // exact input, 1 for 0 + require( + deltaAfter1 >= data.params.amountSpecified, + "insufficient input amount" + ); + require(delta.amount1() == deltaAfter1, "delta mismatch"); + require(deltaAfter0 >= 0, "negative output amount"); + amountOut = deltaAfter0 > 0 ? uint256(deltaAfter0) : 0; + } else { + // exact output, 1 for 0 + require(deltaAfter1 <= 0, "positive input amount"); + require(delta.amount0() == deltaAfter0, "delta mismatch"); + require( + deltaAfter0 <= data.params.amountSpecified, + "excessive output amount" + ); + amountOut = uint256((-delta.amount0()).toUint256()); + } } - - poolManager.settle(); + if (deltaAfter0 < 0) { + poolManager.settle{ + value: data.key.currency0.isAddressZero() + ? uint256(-deltaAfter0) + : 0 + }(); + if (!data.key.currency0.isAddressZero()) { + IERC20(Currency.unwrap(data.key.currency0)).transfer( + address(poolManager), uint256(-deltaAfter0) + ); + } + } + if (deltaAfter1 < 0) { + poolManager.settle{ + value: data.key.currency1.isAddressZero() + ? uint256(-deltaAfter1) + : 0 + }(); + if (!data.key.currency1.isAddressZero()) { + IERC20(Currency.unwrap(data.key.currency1)).transfer( + address(poolManager), uint256(-deltaAfter1) + ); + } + } + if (deltaAfter0 > 0) { + poolManager.take( + data.key.currency0, data.receiver, uint256(deltaAfter0) + ); + } + if (deltaAfter1 > 0) { + poolManager.take( + data.key.currency1, data.receiver, uint256(deltaAfter1) + ); + } + + // Handle any remaining ETH balance + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + CurrencyLibrary.ADDRESS_ZERO.transfer(data.receiver, ethBalance); + } return abi.encode(amountOut); } @@ -138,4 +241,19 @@ contract UniswapV4Executor is IExecutor, IUnlockCallback { target = address(bytes20(data[63:83])); zeroForOne = uint8(data[83]) > 0; } + + function _fetchBalances( + Currency currency, + address user, + address deltaHolder, + IPoolManager manager + ) + internal + view + returns (uint256 userBalance, uint256 poolBalance, int256 delta) + { + userBalance = currency.balanceOf(user); + poolBalance = currency.balanceOf(address(manager)); + delta = manager.currencyDelta(deltaHolder, currency); + } }