├── .DS_Store ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── contracts ├── ArrakisHookV1.sol ├── Counter.sol ├── interfaces │ └── IArrakisHookV1.sol └── libraries │ └── LiquidityAmounts.sol ├── foundry.toml ├── remappings.txt ├── script └── Counter.s.sol └── test ├── ArrakisHookV1.t.sol ├── Counter.t.sol ├── constants └── FeeAmount.sol ├── helper ├── ArrakisHookV1Helper.sol └── UniswapV4Swapper.sol └── utils └── ArrakisHooksV1Factory.sol /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArrakisFinance/uni-v4-playground/5cf3b3e4729253303dce2818bc8a3a47f25903f7/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | foundry-out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/v4-core"] 5 | path = lib/v4-core 6 | url = https://github.com/Uniswap/v4-core 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/openzeppelin/openzeppelin-contracts 10 | [submodule "lib/periphery-next"] 11 | path = lib/periphery-next 12 | url = https://github.com/Uniswap/periphery-next 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uni-v4-playground 2 | -------------------------------------------------------------------------------- /contracts/ArrakisHookV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.8.19; 3 | 4 | import {BaseHook, IPoolManager, Hooks} from "@uniswap/v4-periphery/contracts/BaseHook.sol"; 5 | import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; 6 | import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; 7 | import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol"; 8 | import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; 9 | import {Pool} from "@uniswap/v4-core/contracts/libraries/Pool.sol"; 10 | import {Position} from "@uniswap/v4-core/contracts/libraries/Position.sol"; 11 | import {PoolIdLibrary} from "@uniswap/v4-core/contracts/libraries/PoolId.sol"; 12 | import {Currency, CurrencyLibrary} from "@uniswap/v4-core/contracts/libraries/CurrencyLibrary.sol"; 13 | import {SqrtPriceMath} from "@uniswap/v4-core/contracts/libraries/SqrtPriceMath.sol"; 14 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 15 | import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 16 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 17 | import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 18 | import {IArrakisHookV1} from "./interfaces/IArrakisHookV1.sol"; 19 | import {LiquidityAmounts} from "./libraries/LiquidityAmounts.sol"; 20 | 21 | import "forge-std/console.sol"; 22 | 23 | /// @dev Al N spread V1 24 | contract ArrakisHookV1 is IArrakisHookV1, BaseHook, ERC20, ReentrancyGuard { 25 | using CurrencyLibrary for Currency; 26 | using BalanceDeltaLibrary for BalanceDelta; 27 | using TickMath for int24; 28 | using Pool for Pool.State; 29 | using SafeERC20 for ERC20; 30 | 31 | //#region constants. 32 | 33 | uint16 public immutable c; 34 | uint16 public immutable referenceFee; 35 | 36 | //#endregion constants. 37 | 38 | //#region properties. 39 | 40 | /// @dev should not be settable. 41 | PoolManager.PoolKey public override poolKey; 42 | uint160 public lastSqrtPriceX96; 43 | uint16 public referenceVolatility; 44 | uint24 public rangeSize; 45 | int24 public lowerTick; 46 | int24 public upperTick; 47 | uint16 public ultimateThreshold; 48 | uint16 public allocation; 49 | uint256 public lastBlockNumber; 50 | 51 | uint16 public delta; 52 | bool public impactDirection; 53 | 54 | bool public zeroForOne; // transient. 55 | uint256 public a0; 56 | uint256 public a1; 57 | 58 | //#endregion properties. 59 | 60 | constructor( 61 | InitializeParams memory params_ 62 | ) BaseHook(params_.poolManager) ERC20(params_.name, params_.symbol) { 63 | referenceFee = params_.referenceFee; 64 | referenceVolatility = params_.referenceVolatility; 65 | rangeSize = params_.rangeSize; 66 | lowerTick = params_.lowerTick; 67 | upperTick = params_.upperTick; 68 | ultimateThreshold = params_.ultimateThreshold; 69 | allocation = params_.allocation; 70 | c = params_.c; 71 | } 72 | 73 | // #region pre calls. 74 | 75 | function beforeInitialize( 76 | address, 77 | PoolManager.PoolKey calldata poolKey_, 78 | uint160 sqrtPriceX96_ 79 | ) external override returns (bytes4) { 80 | poolKey = poolKey_; 81 | lastSqrtPriceX96 = sqrtPriceX96_; 82 | lastBlockNumber = block.number; 83 | return this.beforeInitialize.selector; 84 | } 85 | 86 | /// @dev beforeSwap do the new spread computation. 87 | function beforeSwap( 88 | address, 89 | PoolManager.PoolKey calldata poolKey_, 90 | PoolManager.SwapParams calldata swapParams_ 91 | ) external override returns (bytes4) { 92 | /// @dev is first swap. 93 | bool isFirstBlock = block.number > lastBlockNumber; 94 | 95 | if (isFirstBlock) { 96 | // update block numbers tracks. 97 | lastBlockNumber = block.number; 98 | 99 | // compute spread. 100 | (uint160 sqrtPriceX96, , , , , ) = poolManager.getSlot0( 101 | PoolIdLibrary.toId(poolKey_) 102 | ); 103 | 104 | uint256 price = _getPrice(sqrtPriceX96); 105 | uint256 lastPrice = _getPrice(lastSqrtPriceX96); 106 | 107 | delta = SafeCast.toUint16( 108 | FullMath.mulDiv( 109 | c, 110 | ( 111 | price > lastPrice 112 | ? price - lastPrice 113 | : lastPrice - price 114 | ) * 1_000_000, 115 | lastSqrtPriceX96 * 10_000 116 | ) 117 | ); 118 | 119 | impactDirection = price > lastPrice; 120 | } 121 | 122 | zeroForOne = swapParams_.zeroForOne; 123 | 124 | return this.beforeSwap.selector; 125 | } 126 | 127 | // #endregion pre calls. 128 | 129 | // #region IERC20 functions. 130 | 131 | function burn( 132 | uint256 burnAmount_, 133 | address receiver_ 134 | ) external nonReentrant returns (uint256 amount0, uint256 amount1) { 135 | require(burnAmount_ > 0, "burn 0"); 136 | require(totalSupply() > 0, "total supply is 0"); 137 | 138 | bytes memory data = abi.encode( 139 | PoolManagerCallData({ 140 | actionType: 1, 141 | mintAmount: 0, 142 | burnAmount: burnAmount_, 143 | receiver: receiver_, 144 | msgSender: msg.sender 145 | }) 146 | ); 147 | 148 | a0 = a1 = 0; 149 | 150 | poolManager.lock(data); 151 | 152 | amount0 = a0; 153 | amount1 = a1; 154 | 155 | _burn(msg.sender, burnAmount_); 156 | } 157 | 158 | function mint( 159 | uint256 mintAmount_, 160 | address receiver_ 161 | ) external nonReentrant returns (uint256 amount0, uint256 amount1) { 162 | require(mintAmount_ > 0, "mint 0"); 163 | 164 | bytes memory data = abi.encode( 165 | PoolManagerCallData({ 166 | actionType: 0, 167 | mintAmount: mintAmount_, 168 | burnAmount: 0, 169 | receiver: receiver_, 170 | msgSender: msg.sender 171 | }) 172 | ); 173 | 174 | a0 = a1 = 0; 175 | 176 | poolManager.lock(data); 177 | 178 | amount0 = a0; 179 | amount1 = a1; 180 | 181 | _mint(receiver_, mintAmount_); 182 | } 183 | 184 | // #endregion IERC20 functions. 185 | 186 | // #region hook functions 187 | 188 | function lockAcquired( 189 | uint256, 190 | /* id */ bytes calldata data_ 191 | ) external override poolManagerOnly returns (bytes memory) { 192 | PoolManagerCallData memory pMCallData = abi.decode( 193 | data_, 194 | (PoolManagerCallData) 195 | ); 196 | // first case mint 197 | if (pMCallData.actionType == 0) _lockAcquiredMint(pMCallData); 198 | // second case burn action. 199 | if (pMCallData.actionType == 1) _lockAcquiredBurn(pMCallData); 200 | } 201 | 202 | function _lockAcquiredMint(PoolManagerCallData memory pMCallData) internal { 203 | // burn everything positions and erc1155 204 | 205 | uint256 totalSupply = totalSupply(); 206 | 207 | if (totalSupply == 0) { 208 | poolManager.modifyPosition( 209 | poolKey, 210 | IPoolManager.ModifyPositionParams({ 211 | liquidityDelta: SafeCast.toInt256(pMCallData.mintAmount), 212 | tickLower: lowerTick, 213 | tickUpper: upperTick 214 | }) 215 | ); 216 | 217 | uint256 index = poolManager.lockedByLength() - 1; 218 | int256 currency0BalanceRaw = poolManager.getCurrencyDelta( 219 | index, 220 | poolKey.currency0 221 | ); 222 | if (currency0BalanceRaw < 0) { 223 | revert("cannot delta currency0 negative"); 224 | } 225 | uint256 currency0Balance = SafeCast.toUint256(currency0BalanceRaw); 226 | int256 currency1BalanceRaw = poolManager.getCurrencyDelta( 227 | index, 228 | poolKey.currency1 229 | ); 230 | if (currency1BalanceRaw < 0) { 231 | revert("cannot delta currency1 negative"); 232 | } 233 | uint256 currency1Balance = SafeCast.toUint256(currency1BalanceRaw); 234 | 235 | if (currency0Balance > 0) { 236 | ERC20(Currency.unwrap(poolKey.currency0)).safeTransferFrom( 237 | pMCallData.msgSender, 238 | address(poolManager), 239 | currency0Balance 240 | ); 241 | poolManager.settle(poolKey.currency0); 242 | } 243 | if (currency1Balance > 0) { 244 | ERC20(Currency.unwrap(poolKey.currency1)).safeTransferFrom( 245 | pMCallData.msgSender, 246 | address(poolManager), 247 | currency1Balance 248 | ); 249 | poolManager.settle(poolKey.currency1); 250 | } 251 | a0 = currency0Balance; 252 | a1 = currency1Balance; 253 | } else { 254 | Position.Info memory info = PoolManager( 255 | payable(address(poolManager)) 256 | ).getPosition( 257 | PoolIdLibrary.toId(poolKey), 258 | address(this), 259 | lowerTick, 260 | upperTick 261 | ); 262 | 263 | if (info.liquidity > 0) 264 | poolManager.modifyPosition( 265 | poolKey, 266 | IPoolManager.ModifyPositionParams({ 267 | liquidityDelta: -SafeCast.toInt256( 268 | uint256(info.liquidity) 269 | ), 270 | tickLower: lowerTick, 271 | tickUpper: upperTick 272 | }) 273 | ); 274 | 275 | uint256 currency0Id = CurrencyLibrary.toId(poolKey.currency0); 276 | uint256 leftOver0 = poolManager.balanceOf( 277 | address(this), 278 | currency0Id 279 | ); 280 | 281 | if (leftOver0 > 0) 282 | PoolManager(payable(address(poolManager))).onERC1155Received( 283 | address(0), 284 | address(0), 285 | currency0Id, 286 | leftOver0, 287 | "" 288 | ); 289 | 290 | uint256 currency1Id = CurrencyLibrary.toId(poolKey.currency1); 291 | uint256 leftOver1 = poolManager.balanceOf( 292 | address(this), 293 | currency1Id 294 | ); 295 | if (leftOver1 > 0) 296 | PoolManager(payable(address(poolManager))).onERC1155Received( 297 | address(0), 298 | address(0), 299 | currency1Id, 300 | leftOver1, 301 | "" 302 | ); 303 | 304 | // check locker balances. 305 | 306 | uint256 index = poolManager.lockedByLength() - 1; 307 | int256 currency0BalanceRaw = poolManager.getCurrencyDelta( 308 | index, 309 | poolKey.currency0 310 | ); 311 | if (currency0BalanceRaw < 0) { 312 | revert("cannot delta currency0 negative"); 313 | } 314 | uint256 currency0Balance = SafeCast.toUint256(currency0BalanceRaw); 315 | int256 currency1BalanceRaw = poolManager.getCurrencyDelta( 316 | index, 317 | poolKey.currency1 318 | ); 319 | if (currency1BalanceRaw < 0) { 320 | revert("cannot delta currency1 negative"); 321 | } 322 | uint256 currency1Balance = SafeCast.toUint256(currency1BalanceRaw); 323 | 324 | uint256 amount0 = FullMath.mulDiv( 325 | pMCallData.mintAmount, 326 | currency0Balance, 327 | totalSupply 328 | ); 329 | uint256 amount1 = FullMath.mulDiv( 330 | pMCallData.mintAmount, 331 | currency1Balance, 332 | totalSupply 333 | ); 334 | 335 | // safeTransfer to PoolManager. 336 | if (amount0 > 0) { 337 | ERC20(Currency.unwrap(poolKey.currency0)).safeTransferFrom( 338 | pMCallData.msgSender, 339 | address(poolManager), 340 | amount0 341 | ); 342 | poolManager.settle(poolKey.currency0); 343 | } 344 | if (amount1 > 0) { 345 | ERC20(Currency.unwrap(poolKey.currency1)).safeTransferFrom( 346 | pMCallData.msgSender, 347 | address(poolManager), 348 | amount1 349 | ); 350 | poolManager.settle(poolKey.currency1); 351 | } 352 | 353 | a0 = amount0; 354 | a1 = amount1; 355 | 356 | // updated total balances. 357 | currency0BalanceRaw = poolManager.getCurrencyDelta( 358 | index, 359 | poolKey.currency0 360 | ); 361 | if (currency0BalanceRaw < 0) { 362 | revert("cannot delta currency0 negative"); 363 | } 364 | currency0Balance = SafeCast.toUint256(currency0BalanceRaw); 365 | currency1BalanceRaw = poolManager.getCurrencyDelta( 366 | index, 367 | poolKey.currency1 368 | ); 369 | if (currency1BalanceRaw < 0) { 370 | revert("cannot delta currency1 negative"); 371 | } 372 | currency1Balance = SafeCast.toUint256(currency1BalanceRaw); 373 | 374 | // mint back the position. 375 | 376 | (uint160 sqrtPriceX96, , , , , ) = poolManager.getSlot0( 377 | PoolIdLibrary.toId(poolKey) 378 | ); 379 | 380 | uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( 381 | sqrtPriceX96, 382 | TickMath.getSqrtRatioAtTick(lowerTick), 383 | TickMath.getSqrtRatioAtTick(upperTick), 384 | currency0Balance, 385 | currency1Balance 386 | ); 387 | 388 | if (liquidity > 0) 389 | poolManager.modifyPosition( 390 | poolKey, 391 | IPoolManager.ModifyPositionParams({ 392 | liquidityDelta: SafeCast.toInt256(uint256(liquidity)), 393 | tickLower: lowerTick, 394 | tickUpper: upperTick 395 | }) 396 | ); 397 | 398 | leftOver0 = poolManager.balanceOf(address(this), currency0Id); 399 | 400 | leftOver1 = poolManager.balanceOf(address(this), currency1Id); 401 | 402 | if (leftOver0 > 0) { 403 | poolManager.mint(poolKey.currency0, address(this), leftOver0); 404 | } 405 | 406 | if (leftOver1 > 0) { 407 | poolManager.mint(poolKey.currency1, address(this), leftOver1); 408 | } 409 | } 410 | } 411 | 412 | function _lockAcquiredBurn(PoolManagerCallData memory pMCallData) internal { 413 | { 414 | // burn everything positions and erc1155 415 | 416 | uint256 totalSupply = totalSupply(); 417 | 418 | Position.Info memory info = PoolManager( 419 | payable(address(poolManager)) 420 | ).getPosition( 421 | PoolIdLibrary.toId(poolKey), 422 | address(this), 423 | lowerTick, 424 | upperTick 425 | ); 426 | 427 | if (info.liquidity > 0) 428 | poolManager.modifyPosition( 429 | poolKey, 430 | IPoolManager.ModifyPositionParams({ 431 | liquidityDelta: -SafeCast.toInt256( 432 | uint256(info.liquidity) 433 | ), 434 | tickLower: lowerTick, 435 | tickUpper: upperTick 436 | }) 437 | ); 438 | 439 | { 440 | uint256 currency0Id = CurrencyLibrary.toId(poolKey.currency0); 441 | uint256 leftOver0 = poolManager.balanceOf( 442 | address(this), 443 | currency0Id 444 | ); 445 | 446 | if (leftOver0 > 0) 447 | PoolManager(payable(address(poolManager))) 448 | .onERC1155Received( 449 | address(0), 450 | address(0), 451 | currency0Id, 452 | leftOver0, 453 | "" 454 | ); 455 | 456 | uint256 currency1Id = CurrencyLibrary.toId(poolKey.currency1); 457 | uint256 leftOver1 = poolManager.balanceOf( 458 | address(this), 459 | currency1Id 460 | ); 461 | if (leftOver1 > 0) 462 | PoolManager(payable(address(poolManager))) 463 | .onERC1155Received( 464 | address(0), 465 | address(0), 466 | currency1Id, 467 | leftOver1, 468 | "" 469 | ); 470 | } 471 | 472 | // check locker balances. 473 | 474 | uint256 index = poolManager.lockedByLength() - 1; 475 | int256 currency0BalanceRaw = poolManager.getCurrencyDelta( 476 | index, 477 | poolKey.currency0 478 | ); 479 | if (currency0BalanceRaw > 0) { 480 | revert("cannot delta currency0 positive"); 481 | } 482 | uint256 currency0Balance = SafeCast.toUint256(- currency0BalanceRaw); 483 | int256 currency1BalanceRaw = poolManager.getCurrencyDelta( 484 | index, 485 | poolKey.currency1 486 | ); 487 | if (currency1BalanceRaw > 0) { 488 | revert("cannot delta currency1 positive"); 489 | } 490 | uint256 currency1Balance = SafeCast.toUint256(- currency1BalanceRaw); 491 | 492 | { 493 | uint256 amount0 = FullMath.mulDiv( 494 | pMCallData.burnAmount, 495 | currency0Balance, 496 | totalSupply 497 | ); 498 | uint256 amount1 = FullMath.mulDiv( 499 | pMCallData.burnAmount, 500 | currency1Balance, 501 | totalSupply 502 | ); 503 | 504 | // take amounts and send them to receiver 505 | if (amount0 > 0) { 506 | poolManager.take( 507 | poolKey.currency0, 508 | pMCallData.receiver, 509 | amount0 510 | ); 511 | } 512 | if (amount1 > 0) { 513 | poolManager.take( 514 | poolKey.currency1, 515 | pMCallData.receiver, 516 | amount1 517 | ); 518 | } 519 | 520 | a0 = amount0; 521 | a1 = amount1; 522 | } 523 | 524 | // mint back the position. 525 | 526 | // updated total balances. 527 | currency0BalanceRaw = poolManager.getCurrencyDelta( 528 | index, 529 | poolKey.currency0 530 | ); 531 | if (currency0BalanceRaw < 0) { 532 | revert("cannot delta currency0 negative"); 533 | } 534 | currency0Balance = SafeCast.toUint256(currency0BalanceRaw); 535 | currency1BalanceRaw = poolManager.getCurrencyDelta( 536 | index, 537 | poolKey.currency1 538 | ); 539 | if (currency1BalanceRaw < 0) { 540 | revert("cannot delta currency1 negative"); 541 | } 542 | currency1Balance = SafeCast.toUint256(currency1BalanceRaw); 543 | 544 | { 545 | (uint160 sqrtPriceX96, , , , , ) = poolManager.getSlot0( 546 | PoolIdLibrary.toId(poolKey) 547 | ); 548 | 549 | uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( 550 | sqrtPriceX96, 551 | TickMath.getSqrtRatioAtTick(lowerTick), 552 | TickMath.getSqrtRatioAtTick(upperTick), 553 | currency0Balance, 554 | currency1Balance 555 | ); 556 | 557 | if (liquidity > 0) 558 | poolManager.modifyPosition( 559 | poolKey, 560 | IPoolManager.ModifyPositionParams({ 561 | liquidityDelta: SafeCast.toInt256( 562 | uint256(liquidity) 563 | ), 564 | tickLower: lowerTick, 565 | tickUpper: upperTick 566 | }) 567 | ); 568 | } 569 | 570 | { 571 | uint256 currency0Id = CurrencyLibrary.toId(poolKey.currency0); 572 | uint256 currency1Id = CurrencyLibrary.toId(poolKey.currency1); 573 | 574 | uint256 leftOver0 = poolManager.balanceOf( 575 | address(this), 576 | currency0Id 577 | ); 578 | 579 | uint256 leftOver1 = poolManager.balanceOf( 580 | address(this), 581 | currency1Id 582 | ); 583 | 584 | if (leftOver0 > 0) { 585 | poolManager.mint( 586 | poolKey.currency0, 587 | address(this), 588 | leftOver0 589 | ); 590 | } 591 | 592 | if (leftOver1 > 0) { 593 | poolManager.mint( 594 | poolKey.currency1, 595 | address(this), 596 | leftOver1 597 | ); 598 | } 599 | } 600 | } 601 | } 602 | 603 | function getFee( 604 | PoolManager.PoolKey calldata 605 | ) external view returns (uint24) { 606 | return 607 | impactDirection != zeroForOne 608 | ? referenceFee + delta 609 | : referenceFee > delta 610 | ? referenceFee - delta 611 | : 0; 612 | } 613 | 614 | // #endegion hook functions 615 | 616 | //#region view/pure functions. 617 | 618 | function getHooksCalls() public pure override returns (Hooks.Calls memory) { 619 | return 620 | Hooks.Calls({ 621 | beforeInitialize: true, 622 | afterInitialize: false, 623 | beforeModifyPosition: false, 624 | afterModifyPosition: false, 625 | beforeSwap: true, 626 | afterSwap: false, // strategy of the vault 627 | beforeDonate: false, 628 | afterDonate: false 629 | }); 630 | } 631 | 632 | //#endregion view/pure functions. 633 | 634 | //#region internal functions. 635 | 636 | function _getPrice( 637 | uint160 sqrtPriceX96_ 638 | ) internal pure returns (uint256 price) { 639 | price = FullMath.mulDiv(sqrtPriceX96_, sqrtPriceX96_, 2 ** 96); 640 | } 641 | 642 | //#endregion internal functions. 643 | } 644 | -------------------------------------------------------------------------------- /contracts/Counter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | contract Counter { 5 | uint256 public number; 6 | 7 | function setNumber(uint256 newNumber) public { 8 | number = newNumber; 9 | } 10 | 11 | function increment() public { 12 | number++; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/IArrakisHookV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.8.19; 3 | 4 | import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; 5 | import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; 6 | import {Currency} from "@uniswap/v4-core/contracts/libraries/CurrencyLibrary.sol"; 7 | 8 | // libraries/CurrencyLibrary.sol 9 | 10 | /// @dev Arrakis common vault 11 | interface IArrakisHookV1 { 12 | //#region structs. 13 | 14 | struct InitializeParams { 15 | IPoolManager poolManager; 16 | string name; 17 | string symbol; 18 | uint24 rangeSize; 19 | int24 lowerTick; 20 | int24 upperTick; 21 | uint16 referenceFee; 22 | uint16 referenceVolatility; // not use for now 23 | uint16 ultimateThreshold; 24 | uint16 allocation; 25 | uint16 c; 26 | } 27 | 28 | struct PoolManagerCallData { 29 | uint8 actionType; // 0 for mint, 1 for burn. 30 | uint256 mintAmount; 31 | uint256 burnAmount; 32 | address receiver; 33 | address msgSender; 34 | } 35 | 36 | //#endregion structs. 37 | //#region events. 38 | 39 | event LogMint( 40 | address indexed receiver, 41 | uint256 mintAmount, 42 | uint256 amount0In, 43 | uint256 amount1In 44 | ); 45 | 46 | event LogBurn( 47 | address indexed receiver, 48 | uint256 burnAmount, 49 | uint256 amount0Out, 50 | uint256 amount1Out 51 | ); 52 | 53 | event LogCollectedFees(uint256 fee0, uint256 fee1); 54 | 55 | //#endregion events. 56 | 57 | // #region state modifiying functions. 58 | 59 | function mint( 60 | uint256 mintAmount_, 61 | address receiver_ 62 | ) external returns (uint256 amount0, uint256 amount1); 63 | 64 | function burn( 65 | uint256 burnAmount_, 66 | address receiver_ 67 | ) external returns (uint256 amount0, uint256 amount1); 68 | 69 | // #endregion state modifiying functions. 70 | 71 | // #region state reading functions. 72 | 73 | function poolKey() 74 | external 75 | view 76 | returns ( 77 | Currency currency0, 78 | Currency currency1, 79 | uint24 fee, 80 | int24 tickSpacing, 81 | IHooks hooks 82 | ); 83 | 84 | /// @dev Al N delta constant. 85 | function c() external view returns (uint16); 86 | 87 | /// @dev base fee when volatility is average 88 | function referenceFee() external view returns (uint16); 89 | 90 | /// @dev base volatility, above that level we will proportionally increase referenceFee 91 | /// below we will proportionally decrease the referenceFee. 92 | function referenceVolatility() external view returns (uint16); 93 | 94 | /// @dev middle range size. 95 | function rangeSize() external view returns (uint24); 96 | 97 | /// @dev ultimate threshold. 98 | function ultimateThreshold() external view returns (uint16); 99 | 100 | /// @dev percentage of tokens to put in action. 101 | function allocation() external view returns (uint16); 102 | 103 | // #endregion state reading functions. 104 | 105 | // option 1 : fee rebate idea for the swapper that modify the position. 106 | } 107 | -------------------------------------------------------------------------------- /contracts/libraries/LiquidityAmounts.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.8.0; 3 | import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol"; 4 | import {FixedPoint96} from "@uniswap/v4-core/contracts/libraries/FixedPoint96.sol"; 5 | 6 | /// @title Liquidity amount functions 7 | /// @notice Provides functions for computing liquidity amounts from token amounts and prices 8 | library LiquidityAmounts { 9 | function toUint128(uint256 x) private pure returns (uint128 y) { 10 | require((y = uint128(x)) == x); 11 | } 12 | 13 | /// @notice Computes the amount of liquidity received for a given amount of token0 and price range 14 | /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)). 15 | /// @param sqrtRatioAX96 A sqrt price 16 | /// @param sqrtRatioBX96 Another sqrt price 17 | /// @param amount0 The amount0 being sent in 18 | /// @return liquidity The amount of returned liquidity 19 | function getLiquidityForAmount0( 20 | uint160 sqrtRatioAX96, 21 | uint160 sqrtRatioBX96, 22 | uint256 amount0 23 | ) internal pure returns (uint128 liquidity) { 24 | if (sqrtRatioAX96 > sqrtRatioBX96) 25 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 26 | uint256 intermediate = FullMath.mulDiv( 27 | sqrtRatioAX96, 28 | sqrtRatioBX96, 29 | FixedPoint96.Q96 30 | ); 31 | return 32 | toUint128( 33 | FullMath.mulDiv( 34 | amount0, 35 | intermediate, 36 | sqrtRatioBX96 - sqrtRatioAX96 37 | ) 38 | ); 39 | } 40 | 41 | /// @notice Computes the amount of liquidity received for a given amount of token1 and price range 42 | /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). 43 | /// @param sqrtRatioAX96 A sqrt price 44 | /// @param sqrtRatioBX96 Another sqrt price 45 | /// @param amount1 The amount1 being sent in 46 | /// @return liquidity The amount of returned liquidity 47 | function getLiquidityForAmount1( 48 | uint160 sqrtRatioAX96, 49 | uint160 sqrtRatioBX96, 50 | uint256 amount1 51 | ) internal pure returns (uint128 liquidity) { 52 | if (sqrtRatioAX96 > sqrtRatioBX96) 53 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 54 | return 55 | toUint128( 56 | FullMath.mulDiv( 57 | amount1, 58 | FixedPoint96.Q96, 59 | sqrtRatioBX96 - sqrtRatioAX96 60 | ) 61 | ); 62 | } 63 | 64 | /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current 65 | /// pool prices and the prices at the tick boundaries 66 | function getLiquidityForAmounts( 67 | uint160 sqrtRatioX96, 68 | uint160 sqrtRatioAX96, 69 | uint160 sqrtRatioBX96, 70 | uint256 amount0, 71 | uint256 amount1 72 | ) internal pure returns (uint128 liquidity) { 73 | if (sqrtRatioAX96 > sqrtRatioBX96) 74 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 75 | 76 | if (sqrtRatioX96 <= sqrtRatioAX96) { 77 | liquidity = getLiquidityForAmount0( 78 | sqrtRatioAX96, 79 | sqrtRatioBX96, 80 | amount0 81 | ); 82 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 83 | uint128 liquidity0 = getLiquidityForAmount0( 84 | sqrtRatioX96, 85 | sqrtRatioBX96, 86 | amount0 87 | ); 88 | uint128 liquidity1 = getLiquidityForAmount1( 89 | sqrtRatioAX96, 90 | sqrtRatioX96, 91 | amount1 92 | ); 93 | 94 | liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; 95 | } else { 96 | liquidity = getLiquidityForAmount1( 97 | sqrtRatioAX96, 98 | sqrtRatioBX96, 99 | amount1 100 | ); 101 | } 102 | } 103 | 104 | /// @notice Computes the amount of token0 for a given amount of liquidity and a price range 105 | /// @param sqrtRatioAX96 A sqrt price 106 | /// @param sqrtRatioBX96 Another sqrt price 107 | /// @param liquidity The liquidity being valued 108 | /// @return amount0 The amount0 109 | function getAmount0ForLiquidity( 110 | uint160 sqrtRatioAX96, 111 | uint160 sqrtRatioBX96, 112 | uint128 liquidity 113 | ) internal pure returns (uint256 amount0) { 114 | if (sqrtRatioAX96 > sqrtRatioBX96) 115 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 116 | 117 | return 118 | FullMath.mulDiv( 119 | uint256(liquidity) << FixedPoint96.RESOLUTION, 120 | sqrtRatioBX96 - sqrtRatioAX96, 121 | sqrtRatioBX96 122 | ) / sqrtRatioAX96; 123 | } 124 | 125 | /// @notice Computes the amount of token1 for a given amount of liquidity and a price range 126 | /// @param sqrtRatioAX96 A sqrt price 127 | /// @param sqrtRatioBX96 Another sqrt price 128 | /// @param liquidity The liquidity being valued 129 | /// @return amount1 The amount1 130 | function getAmount1ForLiquidity( 131 | uint160 sqrtRatioAX96, 132 | uint160 sqrtRatioBX96, 133 | uint128 liquidity 134 | ) internal pure returns (uint256 amount1) { 135 | if (sqrtRatioAX96 > sqrtRatioBX96) 136 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 137 | 138 | return 139 | FullMath.mulDiv( 140 | liquidity, 141 | sqrtRatioBX96 - sqrtRatioAX96, 142 | FixedPoint96.Q96 143 | ); 144 | } 145 | 146 | /// @notice Computes the token0 and token1 value for a given amount of liquidity, the current 147 | /// pool prices and the prices at the tick boundaries 148 | function getAmountsForLiquidity( 149 | uint160 sqrtRatioX96, 150 | uint160 sqrtRatioAX96, 151 | uint160 sqrtRatioBX96, 152 | uint128 liquidity 153 | ) internal pure returns (uint256 amount0, uint256 amount1) { 154 | if (sqrtRatioAX96 > sqrtRatioBX96) 155 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 156 | 157 | if (sqrtRatioX96 <= sqrtRatioAX96) { 158 | amount0 = getAmount0ForLiquidity( 159 | sqrtRatioAX96, 160 | sqrtRatioBX96, 161 | liquidity 162 | ); 163 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 164 | amount0 = getAmount0ForLiquidity( 165 | sqrtRatioX96, 166 | sqrtRatioBX96, 167 | liquidity 168 | ); 169 | amount1 = getAmount1ForLiquidity( 170 | sqrtRatioAX96, 171 | sqrtRatioX96, 172 | liquidity 173 | ); 174 | } else { 175 | amount1 = getAmount1ForLiquidity( 176 | sqrtRatioAX96, 177 | sqrtRatioBX96, 178 | liquidity 179 | ); 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'contracts' 3 | out = 'foundry-out' 4 | solc_version = '0.8.19' 5 | optimizer_runs = 800 6 | ffi = true 7 | fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] 8 | 9 | [profile.ci] 10 | fuzz_runs = 100000 11 | 12 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 13 | 14 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ 2 | @uniswap/v4-core/=lib/v4-core/ 3 | @uniswap/v4-periphery/=lib/periphery-next/ 4 | forge-std/=lib/forge-std/src/ -------------------------------------------------------------------------------- /script/Counter.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract CounterScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/ArrakisHookV1.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.8.19; 3 | 4 | import "forge-std/Test.sol"; 5 | import "forge-std/Vm.sol"; 6 | import "forge-std/console.sol"; 7 | import "forge-std/StdUtils.sol"; 8 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 10 | import {PoolManager} from "@uniswap/v4-core/contracts/PoolManager.sol"; 11 | import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; 12 | import {ILockCallback} from "@uniswap/v4-core/contracts/interfaces/callback/ILockCallback.sol"; 13 | import {TickMath} from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; 14 | import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; 15 | import {Currency} from "@uniswap/v4-core/contracts/libraries/CurrencyLibrary.sol"; 16 | import {IHooks} from "@uniswap/v4-core/contracts/interfaces/IHooks.sol"; 17 | import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; 18 | import {IArrakisHookV1} from "../contracts/interfaces/IArrakisHookV1.sol"; 19 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 20 | import {ArrakisHookV1} from "../contracts/ArrakisHookV1.sol"; 21 | import "./constants/FeeAmount.sol" as FeeAmount; 22 | import {ArrakisHooksV1Factory} from "./utils/ArrakisHooksV1Factory.sol"; 23 | import {ArrakisHookV1Helper} from "./helper/ArrakisHookV1Helper.sol"; 24 | import {UniswapV4Swapper} from "./helper/UniswapV4Swapper.sol"; 25 | 26 | // import {ArrakisHookV1} from "../contracts/ArrakisHookV1.sol"; 27 | 28 | contract ArrakisHookV1Test is Test, ILockCallback { 29 | //#region constants. 30 | 31 | ArrakisHooksV1Factory public immutable factory; 32 | 33 | //#endregion constants. 34 | 35 | using TickMath for int24; 36 | using BalanceDeltaLibrary for BalanceDelta; 37 | 38 | PoolManager public poolManager; 39 | ArrakisHookV1 public arrakisHookV1; 40 | uint24 public fee; 41 | IPoolManager.PoolKey public poolKey; 42 | 43 | IERC20 public tokenA; 44 | IERC20 public tokenB; 45 | 46 | constructor() { 47 | factory = new ArrakisHooksV1Factory(); 48 | } 49 | 50 | ///@dev let's assume for this test suite the price of tokenA/tokenB is equal to 1. 51 | 52 | function setUp() public { 53 | poolManager = new PoolManager(0); 54 | tokenA = new ERC20("Token A", "TOA"); 55 | tokenB = new ERC20("Token B", "TOB"); 56 | 57 | Hooks.Calls memory calls = Hooks.Calls({ 58 | beforeInitialize: true, 59 | afterInitialize: false, 60 | beforeModifyPosition: false, 61 | afterModifyPosition: false, 62 | beforeSwap: true, 63 | afterSwap: false, // strategy of the vault 64 | beforeDonate: false, 65 | afterDonate: false 66 | }); 67 | 68 | IArrakisHookV1.InitializeParams memory params = IArrakisHookV1 69 | .InitializeParams({ 70 | poolManager: poolManager, 71 | name: "HOOK TOKEN", 72 | symbol: "HOT", 73 | rangeSize: uint24(FeeAmount.HIGH * 2), /// 2% price range. 74 | lowerTick: -FeeAmount.HIGH, 75 | upperTick: FeeAmount.HIGH, 76 | referenceFee: 200, 77 | referenceVolatility: 0, // TODO onced implemented in the hook 78 | ultimateThreshold: 0, // TODO onced implemented in the hook 79 | allocation: 1000, /// @dev in BPS => 10% 80 | c: 5000 /// @dev in BPS also => 50% 81 | }); 82 | 83 | address hookAddress; 84 | (hookAddress, fee) = factory.deployWithPrecomputedHookAddress( 85 | params, 86 | calls 87 | ); 88 | 89 | arrakisHookV1 = ArrakisHookV1(hookAddress); 90 | } 91 | 92 | function test_initialization() public { 93 | int16 tickSpacing = 200; ///@dev like 0.3% fees. 94 | 95 | // #region deploy pool on uniswap v4. 96 | 97 | //#region before assert checks. 98 | 99 | ( 100 | Currency currency0, 101 | Currency currency1, 102 | uint24 f, 103 | int24 tS, 104 | IHooks hook 105 | ) = arrakisHookV1.poolKey(); 106 | uint160 lastSqrtPriceX96 = arrakisHookV1.lastSqrtPriceX96(); 107 | uint256 lastBlockNumber = arrakisHookV1.lastBlockNumber(); 108 | 109 | assertEq(Currency.unwrap(currency0), address(0)); 110 | assertEq(Currency.unwrap(currency1), address(0)); 111 | assertEq(f, 0); 112 | assertEq(tS, 0); 113 | assertEq(address(hook), address(0)); 114 | assertEq(lastSqrtPriceX96, 0); 115 | assertEq(lastBlockNumber, 0); 116 | 117 | //#endregion before assert checks. 118 | 119 | int24 tick = 1; 120 | uint160 sqrtPriceX96 = tick.getSqrtRatioAtTick(); 121 | int24 tickResult = _initialize(sqrtPriceX96, tickSpacing); 122 | 123 | assertEq(tick, tickResult); 124 | 125 | // #region assert check. 126 | 127 | (currency0, currency1, f, tS, hook) = arrakisHookV1.poolKey(); 128 | lastSqrtPriceX96 = arrakisHookV1.lastSqrtPriceX96(); 129 | lastBlockNumber = arrakisHookV1.lastBlockNumber(); 130 | 131 | assertEq(Currency.unwrap(currency0), address(tokenA)); 132 | assertEq(Currency.unwrap(currency1), address(tokenB)); 133 | assertEq(f, fee); 134 | assertEq(tS, tickSpacing); 135 | assertEq(address(hook), address(arrakisHookV1)); 136 | assertEq(lastSqrtPriceX96, sqrtPriceX96); 137 | assertEq(lastBlockNumber, block.number); 138 | 139 | /// @dev we can consider here that beforeInitialize hook is working good. 140 | 141 | // #endregion assert check. 142 | 143 | // #endregion deploy pool on uniswap v4. 144 | } 145 | 146 | function test_beforeSwap() public { 147 | address vb = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; // swapper 148 | 149 | deal(address(tokenA), address(this), 1000); 150 | deal(address(tokenB), address(this), 1000); 151 | 152 | uint160 sqrtPriceX96 = int24(1).getSqrtRatioAtTick(); 153 | int16 tickSpacing = 200; 154 | 155 | _initialize(sqrtPriceX96, tickSpacing); 156 | 157 | ///@dev do swap on the pool to simulate price move for computing dynamic fees. 158 | 159 | // #region create a position on that pool. 160 | 161 | uint160 sqrtPriceX96A = (-FeeAmount.HIGH).getSqrtRatioAtTick(); 162 | uint160 sqrtPriceX96B = FeeAmount.HIGH.getSqrtRatioAtTick(); 163 | 164 | uint128 liquidity = ArrakisHookV1Helper.getLiquidityForAmounts( 165 | sqrtPriceX96, 166 | sqrtPriceX96A, 167 | sqrtPriceX96B, 168 | 1000, 169 | 1000 170 | ); 171 | 172 | bytes memory data = abi.encode( 173 | -FeeAmount.HIGH, 174 | FeeAmount.HIGH, 175 | liquidity 176 | ); 177 | 178 | poolManager.lock(data); 179 | 180 | // #endregion create a position on that pool. 181 | 182 | // #region do swap to move the price. 183 | /// @dev to do multiple swap. 184 | 185 | UniswapV4Swapper swapper = new UniswapV4Swapper(poolManager); 186 | 187 | vm.startPrank(vb); 188 | 189 | deal(address(tokenA), vb, 500); 190 | 191 | tokenA.approve(address(swapper), 500); 192 | 193 | assertEq(200, arrakisHookV1.getFee(poolKey)); /// @dev it's what we set earlier. 194 | 195 | IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ 196 | zeroForOne: true, 197 | amountSpecified: 500, 198 | sqrtPriceLimitX96: (-FeeAmount.HIGH / 2).getSqrtRatioAtTick() 199 | }); 200 | 201 | swapper.swap(poolKey, params); 202 | 203 | vm.stopPrank(); 204 | 205 | assertEq(200, arrakisHookV1.getFee(poolKey)); /// @dev it's what we set earlier. 206 | // #endregion do swap to move the price. 207 | 208 | /// TODO check that swap happening inside the same block has the same fees. 209 | /// TODO move to next block. 210 | vm.roll(block.number + 1); 211 | assertEq(200, arrakisHookV1.getFee(poolKey)); /// @dev it's what we set earlier. 212 | 213 | /// TODO do another swap 214 | 215 | vm.startPrank(vb); 216 | 217 | deal(address(tokenB), vb, 200); 218 | 219 | tokenB.approve(address(swapper), 200); 220 | 221 | assertEq(200, arrakisHookV1.getFee(poolKey)); /// @dev it's what we set earlier. 222 | 223 | params = IPoolManager.SwapParams({ 224 | zeroForOne: false, 225 | amountSpecified: 200, 226 | sqrtPriceLimitX96: int24(1).getSqrtRatioAtTick() 227 | }); 228 | 229 | swapper.swap(poolKey, params); 230 | 231 | vm.stopPrank(); 232 | 233 | /// TODO check different fee charging. 234 | assertEq(0, arrakisHookV1.getFee(poolKey)); 235 | } 236 | 237 | function test_mint() public { 238 | address vb = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; // minter 239 | 240 | deal(address(tokenA), vb, 200); 241 | deal(address(tokenB), vb, 200); 242 | 243 | uint160 sqrtPriceX96 = int24(1).getSqrtRatioAtTick(); 244 | int16 tickSpacing = 200; 245 | 246 | _initialize(sqrtPriceX96, tickSpacing); 247 | 248 | uint160 sqrtPriceX96A = (-FeeAmount.HIGH).getSqrtRatioAtTick(); 249 | uint160 sqrtPriceX96B = FeeAmount.HIGH.getSqrtRatioAtTick(); 250 | 251 | uint128 liquidity = ArrakisHookV1Helper.getLiquidityForAmounts( 252 | sqrtPriceX96, 253 | sqrtPriceX96A, 254 | sqrtPriceX96B, 255 | 200, 256 | 200 257 | ); 258 | 259 | vm.startPrank(vb); 260 | 261 | tokenA.approve(address(arrakisHookV1), 200); 262 | tokenB.approve(address(arrakisHookV1), 200); 263 | 264 | arrakisHookV1.mint(uint256(liquidity), vb); 265 | assertEq(arrakisHookV1.balanceOf(vb), 20_000); 266 | 267 | vm.stopPrank(); 268 | } 269 | 270 | function test_burn() public { 271 | 272 | // #region minting before burning. 273 | 274 | address vb = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; // minter 275 | 276 | deal(address(tokenA), vb, 200); 277 | deal(address(tokenB), vb, 200); 278 | 279 | uint160 sqrtPriceX96 = int24(1).getSqrtRatioAtTick(); 280 | int16 tickSpacing = 200; 281 | 282 | _initialize(sqrtPriceX96, tickSpacing); 283 | 284 | uint160 sqrtPriceX96A = (-FeeAmount.HIGH).getSqrtRatioAtTick(); 285 | uint160 sqrtPriceX96B = FeeAmount.HIGH.getSqrtRatioAtTick(); 286 | 287 | uint128 liquidity = ArrakisHookV1Helper.getLiquidityForAmounts( 288 | sqrtPriceX96, 289 | sqrtPriceX96A, 290 | sqrtPriceX96B, 291 | 200, 292 | 200 293 | ); 294 | 295 | vm.startPrank(vb); 296 | 297 | tokenA.approve(address(arrakisHookV1), 200); 298 | tokenB.approve(address(arrakisHookV1), 200); 299 | 300 | arrakisHookV1.mint(uint256(liquidity), vb); 301 | 302 | 303 | // #endregion minting before burning. 304 | 305 | // #region burning. 306 | 307 | arrakisHookV1.burn(arrakisHookV1.balanceOf(vb), vb); 308 | 309 | // #endregion burning. 310 | 311 | vm.stopPrank(); 312 | 313 | assertGe(199, tokenA.balanceOf(vb)); 314 | assertGe(199, tokenB.balanceOf(vb)); 315 | } 316 | 317 | // #region lockAcquired callback. 318 | 319 | function lockAcquired( 320 | uint256, 321 | bytes calldata data 322 | ) external returns (bytes memory result) { 323 | (int24 tickLower, int24 tickUpper, uint128 liquidity) = abi.decode( 324 | data, 325 | (int24, int24, uint128) 326 | ); 327 | BalanceDelta balanceDelta = poolManager.modifyPosition( 328 | poolKey, 329 | IPoolManager.ModifyPositionParams({ 330 | tickLower: tickLower, 331 | tickUpper: tickUpper, 332 | liquidityDelta: SafeCast.toInt256(uint256(liquidity)) 333 | }) 334 | ); 335 | 336 | result = abi.encode(balanceDelta); 337 | 338 | tokenA.transfer( 339 | address(poolManager), 340 | SafeCast.toUint256(int256(balanceDelta.amount0())) 341 | ); 342 | 343 | poolManager.settle(poolKey.currency0); 344 | 345 | tokenB.transfer( 346 | address(poolManager), 347 | SafeCast.toUint256(int256(balanceDelta.amount1())) 348 | ); 349 | 350 | poolManager.settle(poolKey.currency1); 351 | } 352 | 353 | // #endregion lockAcquired callback. 354 | 355 | // #region internal functions. 356 | 357 | function _initialize( 358 | uint160 sqrtPriceX96_, 359 | int16 tickSpacing_ 360 | ) internal returns (int24) { 361 | poolKey = IPoolManager.PoolKey({ 362 | currency0: Currency.wrap(address(tokenA)), 363 | currency1: Currency.wrap(address(tokenB)), 364 | fee: fee, 365 | tickSpacing: tickSpacing_, 366 | hooks: IHooks(address(arrakisHookV1)) 367 | }); 368 | 369 | return poolManager.initialize(poolKey, sqrtPriceX96_); 370 | } 371 | 372 | // #endregion internal functions. 373 | } 374 | -------------------------------------------------------------------------------- /test/Counter.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../contracts/Counter.sol"; 6 | 7 | contract CounterTest is Test { 8 | Counter public counter; 9 | 10 | function setUp() public { 11 | counter = new Counter(); 12 | counter.setNumber(0); 13 | } 14 | 15 | function testIncrement() public { 16 | counter.increment(); 17 | assertEq(counter.number(), 1); 18 | } 19 | 20 | function testSetNumber(uint256 x) public { 21 | counter.setNumber(x); 22 | assertEq(counter.number(), x); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/constants/FeeAmount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.8.19; 3 | 4 | int24 constant LOW = 10; 5 | int24 constant MEDIUM = 60; 6 | int24 constant HIGH = 200; -------------------------------------------------------------------------------- /test/helper/ArrakisHookV1Helper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.8.19; 3 | 4 | import {FullMath} from "@uniswap/v4-core/contracts/libraries/FullMath.sol"; 5 | import {FixedPoint96} from "@uniswap/v4-core/contracts/libraries/FixedPoint96.sol"; 6 | 7 | 8 | library ArrakisHookV1Helper { 9 | function toUint128(uint256 x) private pure returns (uint128 y) { 10 | require((y = uint128(x)) == x); 11 | } 12 | 13 | /// @notice Computes the amount of liquidity received for a given amount of token0 and price range 14 | /// @dev Calculates amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower)). 15 | /// @param sqrtRatioAX96 A sqrt price 16 | /// @param sqrtRatioBX96 Another sqrt price 17 | /// @param amount0 The amount0 being sent in 18 | /// @return liquidity The amount of returned liquidity 19 | function getLiquidityForAmount0( 20 | uint160 sqrtRatioAX96, 21 | uint160 sqrtRatioBX96, 22 | uint256 amount0 23 | ) internal pure returns (uint128 liquidity) { 24 | if (sqrtRatioAX96 > sqrtRatioBX96) 25 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 26 | uint256 intermediate = FullMath.mulDiv( 27 | sqrtRatioAX96, 28 | sqrtRatioBX96, 29 | FixedPoint96.Q96 30 | ); 31 | return 32 | toUint128( 33 | FullMath.mulDiv( 34 | amount0, 35 | intermediate, 36 | sqrtRatioBX96 - sqrtRatioAX96 37 | ) 38 | ); 39 | } 40 | 41 | /// @notice Computes the amount of liquidity received for a given amount of token1 and price range 42 | /// @dev Calculates amount1 / (sqrt(upper) - sqrt(lower)). 43 | /// @param sqrtRatioAX96 A sqrt price 44 | /// @param sqrtRatioBX96 Another sqrt price 45 | /// @param amount1 The amount1 being sent in 46 | /// @return liquidity The amount of returned liquidity 47 | function getLiquidityForAmount1( 48 | uint160 sqrtRatioAX96, 49 | uint160 sqrtRatioBX96, 50 | uint256 amount1 51 | ) internal pure returns (uint128 liquidity) { 52 | if (sqrtRatioAX96 > sqrtRatioBX96) 53 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 54 | return 55 | toUint128( 56 | FullMath.mulDiv( 57 | amount1, 58 | FixedPoint96.Q96, 59 | sqrtRatioBX96 - sqrtRatioAX96 60 | ) 61 | ); 62 | } 63 | 64 | /// @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current 65 | /// pool prices and the prices at the tick boundaries 66 | function getLiquidityForAmounts( 67 | uint160 sqrtRatioX96, 68 | uint160 sqrtRatioAX96, 69 | uint160 sqrtRatioBX96, 70 | uint256 amount0, 71 | uint256 amount1 72 | ) internal pure returns (uint128 liquidity) { 73 | if (sqrtRatioAX96 > sqrtRatioBX96) 74 | (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); 75 | 76 | if (sqrtRatioX96 <= sqrtRatioAX96) { 77 | liquidity = getLiquidityForAmount0( 78 | sqrtRatioAX96, 79 | sqrtRatioBX96, 80 | amount0 81 | ); 82 | } else if (sqrtRatioX96 < sqrtRatioBX96) { 83 | uint128 liquidity0 = getLiquidityForAmount0( 84 | sqrtRatioX96, 85 | sqrtRatioBX96, 86 | amount0 87 | ); 88 | uint128 liquidity1 = getLiquidityForAmount1( 89 | sqrtRatioAX96, 90 | sqrtRatioX96, 91 | amount1 92 | ); 93 | 94 | liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; 95 | } else { 96 | liquidity = getLiquidityForAmount1( 97 | sqrtRatioAX96, 98 | sqrtRatioBX96, 99 | amount1 100 | ); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/helper/UniswapV4Swapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.8.19; 3 | 4 | import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; 5 | import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; 6 | import {Currency} from "@uniswap/v4-core/contracts/libraries/CurrencyLibrary.sol"; 7 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 9 | 10 | import "forge-std/console.sol"; 11 | 12 | contract UniswapV4Swapper { 13 | // #region usings. 14 | 15 | using BalanceDeltaLibrary for BalanceDelta; 16 | 17 | // #endregion usings. 18 | 19 | // #region errors. 20 | 21 | error ZeroAmountIn( 22 | IPoolManager.PoolKey poolKey, 23 | IPoolManager.SwapParams params 24 | ); 25 | 26 | // #endregion errors. 27 | 28 | IPoolManager public immutable poolManager; 29 | 30 | constructor(IPoolManager poolManager_) { 31 | poolManager = poolManager_; 32 | } 33 | 34 | function swap( 35 | IPoolManager.PoolKey memory poolKey_, 36 | IPoolManager.SwapParams memory params_ 37 | ) external { 38 | if (params_.amountSpecified <= 0) 39 | revert ZeroAmountIn(poolKey_, params_); 40 | 41 | bytes memory data = abi.encode(msg.sender, poolKey_, params_); 42 | poolManager.lock(data); 43 | } 44 | 45 | function lockAcquired( 46 | uint256, 47 | bytes calldata data 48 | ) external returns (bytes memory result) { 49 | ( 50 | address msgSender, 51 | IPoolManager.PoolKey memory poolKey, 52 | IPoolManager.SwapParams memory params 53 | ) = abi.decode( 54 | data, 55 | (address, IPoolManager.PoolKey, IPoolManager.SwapParams) 56 | ); 57 | 58 | BalanceDelta delta = poolManager.swap(poolKey, params); 59 | 60 | if (params.zeroForOne) { 61 | IERC20(Currency.unwrap(poolKey.currency0)).transferFrom( 62 | msgSender, 63 | address(poolManager), 64 | SafeCast.toUint256(int256(delta.amount0())) 65 | ); 66 | poolManager.settle(poolKey.currency0); 67 | poolManager.take( 68 | poolKey.currency1, 69 | msgSender, 70 | SafeCast.toUint256(int256( - delta.amount1())) 71 | ); 72 | } else { 73 | IERC20(Currency.unwrap(poolKey.currency1)).transferFrom( 74 | msgSender, 75 | address(poolManager), 76 | SafeCast.toUint256(int256(delta.amount1())) 77 | ); 78 | poolManager.settle(poolKey.currency1); 79 | poolManager.take( 80 | poolKey.currency0, 81 | msgSender, 82 | SafeCast.toUint256(int256( - delta.amount0())) 83 | ); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/utils/ArrakisHooksV1Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.8.19; 3 | 4 | import {ArrakisHookV1, IArrakisHookV1} from "../../contracts/ArrakisHookV1.sol"; 5 | import {Hooks, IHooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; 6 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 7 | 8 | contract ArrakisHooksV1Factory { 9 | function deployWithPrecomputedHookAddress( 10 | IArrakisHookV1.InitializeParams memory params_, 11 | Hooks.Calls memory calls_ 12 | ) external returns (address, uint24) { 13 | uint160 prefix = _getPrefix(calls_); 14 | for (uint256 i = 0; i < 1500; i++) { 15 | bytes32 salt = bytes32(i); 16 | 17 | bytes32 bytecodeHash = keccak256( 18 | abi.encodePacked( 19 | type(ArrakisHookV1).creationCode, 20 | abi.encode(params_) 21 | ) 22 | ); 23 | bytes32 hash = keccak256( 24 | abi.encodePacked( 25 | bytes1(0xff), 26 | address(this), 27 | salt, 28 | bytecodeHash 29 | ) 30 | ); 31 | 32 | address expectedAddress = address(uint160(uint256(hash))); 33 | 34 | if (_doesAddressStartWith(expectedAddress, prefix)) { 35 | return (_deploy(params_, salt), SafeCast.toUint24(uint256(prefix))); 36 | } 37 | } 38 | 39 | return (address(0), SafeCast.toUint24(uint256(prefix))); 40 | } 41 | 42 | function _doesAddressStartWith( 43 | address address_, 44 | uint160 prefix_ 45 | ) private pure returns (bool) { 46 | return uint160(address_) / (2 ** (8 * (19))) == prefix_; 47 | } 48 | 49 | function _deploy( 50 | IArrakisHookV1.InitializeParams memory params_, 51 | bytes32 salt_ 52 | ) internal returns (address) { 53 | return address(new ArrakisHookV1{salt: salt_}(params_)); 54 | } 55 | 56 | function _getPrefix( 57 | Hooks.Calls memory calls_ 58 | ) internal pure returns (uint160) { 59 | uint160 prefix; 60 | if (calls_.beforeInitialize) prefix = 1 << 159; 61 | if (calls_.afterInitialize) prefix = prefix | (1 << 158); 62 | if (calls_.beforeModifyPosition) prefix = prefix | (1 << 157); 63 | if (calls_.afterModifyPosition) prefix = prefix | (1 << 156); 64 | if (calls_.beforeSwap) prefix = prefix | (1 << 155); 65 | if (calls_.afterSwap) prefix = prefix | (1 << 154); 66 | if (calls_.beforeDonate) prefix = prefix | (1 << 153); 67 | if (calls_.afterDonate) prefix = prefix | (1 << 152); 68 | 69 | return prefix / (2 ** (8 * (19))); 70 | } 71 | } 72 | --------------------------------------------------------------------------------