From 007135d232a7487d3337692dab4e3c6c6354d6ed Mon Sep 17 00:00:00 2001 From: Tim Olson <> Date: Sun, 20 Aug 2023 10:20:47 -0400 Subject: [PATCH] basic test environment works --- foundry.toml | 17 ++++- lib/libs.txt | 3 + src/IERC20Metadata.sol | 22 ------ src/MockERC20.sol | 8 +- src/Util.sol | 14 ++++ test/TestSinglePool.sol | 157 +++++++++++++++++++++++++++++----------- 6 files changed, 148 insertions(+), 73 deletions(-) create mode 100644 lib/libs.txt delete mode 100644 src/IERC20Metadata.sol create mode 100644 src/Util.sol diff --git a/foundry.toml b/foundry.toml index 6c68e02..202ffde 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,9 +1,20 @@ [profile.default] +solc_version = '0.7.6' libs = ['lib'] remappings = [ - '@openzeppelin/contracts/=lib/openzeppelin/contracts/', - '@uniswap/v3-core/=lib/uniswap/v3-core/', - '@uniswap/v3-periphery/=lib/uniswap/v3-periphery/', + '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/', + '@uniswap/v3-core/=lib/v3-core/', + '@uniswap/v3-periphery/=lib/v3-periphery/', ] optimizer=true optimizer_runs=999999999 +sizes = true +via_ir = false +gas_reports = ['*'] +gas_reports_ignore = [] + +[profile.default.rpc_endpoints] +# todo put these into a secrets file +arbitrum_alchemy='https://arb-mainnet.g.alchemy.com/v2/L0eaUwWEoWzszCK9EhqHdl_p7VHStkaC' +arbitrum_ankr='https://rpc.ankr.com/arbitrum/056ed471570655a3bb77cf31b9e3576658d63d2acb88911f84e7acaf211b55ac' +arbitrum_publicnode='https://arbitrum-one.publicnode.com' diff --git a/lib/libs.txt b/lib/libs.txt new file mode 100644 index 0000000..3f27b68 --- /dev/null +++ b/lib/libs.txt @@ -0,0 +1,3 @@ +Uniswap/v3-core +Uniswap/v3-periphery +OpenZeppelin/openzeppelin-contracts@v3.4.2-solc-0.7 diff --git a/src/IERC20Metadata.sol b/src/IERC20Metadata.sol deleted file mode 100644 index afe19f9..0000000 --- a/src/IERC20Metadata.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity =0.7.6; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -// the version of OpenZeppelin required by Uniswap v3 doesn't have IERC20Metadata yet, so we copy it here. -interface IERC20Metadata is IERC20 { - /** - * @dev Returns the name of the token. - */ - function name() external view returns (string memory); - - /** - * @dev Returns the symbol of the token. - */ - function symbol() external view returns (string memory); - - /** - * @dev Returns the decimals places of the token. - */ - function decimals() external view returns (uint8); -} diff --git a/src/MockERC20.sol b/src/MockERC20.sol index 783f371..b111100 100644 --- a/src/MockERC20.sol +++ b/src/MockERC20.sol @@ -1,10 +1,10 @@ -import "openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "./IERC20Metadata.sol"; +pragma solidity =0.7.6; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -contract MockERC20 is ERC20, IERC20Metadata { +contract MockERC20 is ERC20 { - constructor(string name, string symbol, uint8 decimals) + constructor(string memory name, string memory symbol, uint8 decimals) ERC20(name, symbol) { _setupDecimals(decimals); diff --git a/src/Util.sol b/src/Util.sol new file mode 100644 index 0000000..0dc70fb --- /dev/null +++ b/src/Util.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity =0.7.6; +pragma abicoder v2; + +library Util { + function roundTick(int24 tick, int24 window) public pure returns (int24) { + // NOTE: we round half toward zero + int24 mod = tick % window; + if (tick < 0) + return - mod <= window / 2 ? tick - mod : tick - (window + mod); + else + return mod > window / 2 ? tick + (window - mod) : tick - mod; + } +} diff --git a/test/TestSinglePool.sol b/test/TestSinglePool.sol index a68279b..1790fed 100644 --- a/test/TestSinglePool.sol +++ b/test/TestSinglePool.sol @@ -1,71 +1,140 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity =0.7.6; +pragma abicoder v2; import "forge-std/console2.sol"; +import "forge-std/Test.sol"; import "../src/MockERC20.sol"; -import "uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; -import "uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import "uniswap/v3-core/contracts/libraries/TickMath.sol"; -import "uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; -import "uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; -import "uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; +import "../src/Util.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; -contract TestSinglePool { +contract TestSinglePool is Test { - IUniswapV3Factory public factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); INonfungiblePositionManager public nfpm = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); ISwapRouter public swapper = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - IUniswapV3Pool public immutable pool; + IUniswapV3Pool public pool; uint24 public fee; - MockERC20 public WETH; - MockERC20 public USDC; - MockERC20 public token0; // either WETH or USDC depending on the order in the pool - MockERC20 public token1; + MockERC20 public COIN; + MockERC20 public USD; + address public token0; // either WETH or USDC depending on the order in the pool + address public token1; + bool public inverted; function setUp() public { - MockERC20 weth = MockERC20('Mock Wrapped Ethereum', 'WETH', 18); - MockERC20 usdc = MockERC20('Mock USD Coin', 'USDC', 6); - uint24 fee_ = 500; - fee = fee_; - WETH = weth; - USDC = usdc; - IUniswapV3Pool pool_ = UniswapV3Pool(factory.createPool(address(weth), address(usdc), fee_)); - pool = pool_; - token0 = pool_.token0(); - token1 = pool_.token1(); + COIN = new MockERC20('Mock Coin', 'MOCK', 18); + USD = new MockERC20('Universally Supported Dollars', 'USD', 6); + fee = 500; + inverted = address(COIN) > address(USD); + token0 = inverted ? address(USD) : address(COIN); + token1 = inverted ? address(COIN) : address(USD); + uint160 initialPrice = 1 * 2**96; + // if this reverts here make sure Anvil is started and you are running forge with --rpc-url + pool = IUniswapV3Pool(nfpm.createAndInitializePoolIfNecessary(token0, token1, fee, initialPrice)); + int24 ts = pool.tickSpacing(); + (, int24 lower, , , , ,) = pool.slot0(); + int24 upper = lower; + for (int8 i = 0; i < 10; i++) { + lower -= ts; + upper += ts; + stake(1 * 10 ** COIN.decimals(), 1000 * 10 ** USD.decimals(), lower, upper); + } } - // struct MintParams { - // address token0; - // address token1; - // uint24 fee; - // int24 tickLower; - // int24 tickUpper; - // uint256 amount0Desired; - // uint256 amount1Desired; - // uint256 amount0Min; - // uint256 amount1Min; - // address recipient; - // uint256 deadline; - // } - function stake(uint160 liquidity, uint24 lower, uint24 upper) public { + function stake(uint128 liquidity_, int24 lower, int24 upper) public + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(lower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(upper); - (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); - (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(liquidity, sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96); - token0.mint(amount0); - token1.mint(amount1); + (uint160 sqrtPriceX96, , , , , ,) = pool.slot0(); + (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, liquidity_); + return _stake(amount0, amount1, lower, upper); + } + + function testSwap() public { + COIN.mint(address(this), 1 * 10**18); + uint256 usd = swap(COIN, USD, 1 * 10**18); + console2.log(usd); + } + + function stake(uint256 coinAmount, uint256 usdAmount, int24 lower, int24 upper) public + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + return _stake(coinAmount, usdAmount, lower, upper); + } + + function _stake(uint256 coinAmount, uint256 usdAmount, int24 lower, int24 upper) private + returns ( + uint256 tokenId, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + COIN.mint(address(this), coinAmount); + COIN.approve(address(nfpm), coinAmount); + USD.mint(address(this), usdAmount); + USD.approve(address(nfpm), usdAmount); + // struct MintParams { + // address token0; + // address token1; + // uint24 fee; + // int24 tickLower; + // int24 tickUpper; + // uint256 amount0Desired; + // uint256 amount1Desired; + // uint256 amount0Min; + // uint256 amount1Min; + // address recipient; + // uint256 deadline; + // } + int24 ts = pool.tickSpacing(); + lower = Util.roundTick(lower, ts); + upper = Util.roundTick(upper, ts); + (uint256 a0, uint256 a1) = inverted ? (usdAmount, coinAmount) : (coinAmount, usdAmount); INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams( - address(token0), address(token1), fee, lower, upper, amount0, amount1, 0, 0, msg.sender, block.timestamp + token0, token1, fee, lower, upper, a0, a1, 0, 0, msg.sender, block.timestamp ); - nfpm.mint(params); + return nfpm.mint(params); } - function swap() public { + function swap(MockERC20 inToken, MockERC20 outToken, uint256 amountIn) public returns (uint256 amountOut) { + uint160 limit = address(inToken) == pool.token0() ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1; + return swap(inToken, outToken, amountIn, limit); + } + function swap(MockERC20 inToken, MockERC20 outToken, uint256 amountIn, uint160 sqrtPriceLimitX96) public returns (uint256 amountOut) { + inToken.approve(address(swapper), amountIn); + // struct ExactInputSingleParams { + // address tokenIn; + // address tokenOut; + // uint24 fee; + // address recipient; + // uint256 deadline; + // uint256 amountIn; + // uint256 amountOutMinimum; + // uint160 sqrtPriceLimitX96; + // } + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams( + address(inToken), address(outToken), fee, msg.sender, block.timestamp, amountIn, 0, sqrtPriceLimitX96 + ); + return swapper.exactInputSingle(params); } }