From ddc315fc2bc1e9c97124de358d8026b3aaf68687 Mon Sep 17 00:00:00 2001 From: ChefMist <133624774+ChefMist@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:25:22 +0800 Subject: [PATCH] test: add bin test on swap/mint/burn return delta --- test/pool-bin/BinCustomCurveHook.t.sol | 130 ++++++++++++++++++ test/pool-bin/BinMintBurnFeeHook.t.sol | 131 +++++++++++++++++++ test/pool-bin/helpers/BinCustomCurveHook.sol | 127 ++++++++++++++++++ test/pool-bin/helpers/BinMintBurnFeeHook.sol | 105 +++++++++++++++ 4 files changed, 493 insertions(+) create mode 100644 test/pool-bin/BinCustomCurveHook.t.sol create mode 100644 test/pool-bin/BinMintBurnFeeHook.t.sol create mode 100644 test/pool-bin/helpers/BinCustomCurveHook.sol create mode 100644 test/pool-bin/helpers/BinMintBurnFeeHook.sol diff --git a/test/pool-bin/BinCustomCurveHook.t.sol b/test/pool-bin/BinCustomCurveHook.t.sol new file mode 100644 index 00000000..e78b1611 --- /dev/null +++ b/test/pool-bin/BinCustomCurveHook.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; +import {IBinPoolManager} from "../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {Vault} from "../../src/Vault.sol"; +import {Currency} from "../../src/types/Currency.sol"; +import {PoolKey} from "../../src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../../src/types/BalanceDelta.sol"; +import {BinPoolManager} from "../../src/pool-bin/BinPoolManager.sol"; +import {BinPool} from "../../src/pool-bin/libraries/BinPool.sol"; +import {BinPoolParametersHelper} from "../../src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {BinSwapHelper} from "./helpers/BinSwapHelper.sol"; +import {BinTestHelper} from "./helpers/BinTestHelper.sol"; +import {Hooks} from "../../src/libraries/Hooks.sol"; +import {BinCustomCurveHook} from "./helpers/BinCustomCurveHook.sol"; + +contract BinHookReturnsDelta is Test, GasSnapshot, BinTestHelper { + using BinPoolParametersHelper for bytes32; + + Vault public vault; + BinPoolManager public poolManager; + BinCustomCurveHook public binCustomCurveHook; + + BinSwapHelper public binSwapHelper; + + uint24 activeId = 2 ** 23; // where token0 and token1 price is the same + + PoolKey key; + bytes32 poolParam; + MockERC20 token0; + MockERC20 token1; + Currency currency0; + Currency currency1; + + function setUp() public { + vault = new Vault(); + poolManager = new BinPoolManager(IVault(address(vault))); + + vault.registerApp(address(poolManager)); + + token0 = new MockERC20("TestA", "A", 18); + token1 = new MockERC20("TestB", "B", 18); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + currency0 = Currency.wrap(address(token0)); + currency1 = Currency.wrap(address(token1)); + + IBinPoolManager iBinPoolManager = IBinPoolManager(address(poolManager)); + IVault iVault = IVault(address(vault)); + + binSwapHelper = new BinSwapHelper(iBinPoolManager, iVault); + token0.approve(address(binSwapHelper), 1000 ether); + token1.approve(address(binSwapHelper), 1000 ether); + + binCustomCurveHook = new BinCustomCurveHook(iVault, iBinPoolManager); + token0.approve(address(binCustomCurveHook), 1000 ether); + token1.approve(address(binCustomCurveHook), 1000 ether); + + key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: binCustomCurveHook, + poolManager: IPoolManager(address(poolManager)), + fee: uint24(3000), // 3000 = 0.3% + parameters: bytes32(uint256(binCustomCurveHook.getHooksRegistrationBitmap())).setBinStep(10) + }); + + binCustomCurveHook.setPoolKey(key); + poolManager.initialize(key, activeId); + } + + /// @dev only meant for sanity test for the hook example + function test_addLiquidity_removeLiquidity() external { + // pre-req: mint token on this contract + token0.mint(address(this), 10 ether); + token1.mint(address(this), 10 ether); + + assertEq(token0.balanceOf(address(this)), 10 ether); + assertEq(token1.balanceOf(address(this)), 10 ether); + assertEq(token0.balanceOf(address(vault)), 0 ether); + assertEq(token1.balanceOf(address(vault)), 0 ether); + + // add liquidity and verify tokens are in the vault + binCustomCurveHook.addLiquidity(1 ether, 2 ether); + assertEq(token0.balanceOf(address(this)), 9 ether); + assertEq(token1.balanceOf(address(this)), 8 ether); + assertEq(token0.balanceOf(address(vault)), 1 ether); + assertEq(token1.balanceOf(address(vault)), 2 ether); + + // remove liquidity and verify tokens are returned to this contract + binCustomCurveHook.removeLiquidity(1 ether, 1 ether); + assertEq(token0.balanceOf(address(this)), 10 ether); + assertEq(token1.balanceOf(address(this)), 9 ether); + assertEq(token0.balanceOf(address(vault)), 0 ether); + assertEq(token1.balanceOf(address(vault)), 1 ether); + } + + function test_Swap_CustomCurve(uint256 _amtIn) public { + // preq-req: add liqudiity + token0.mint(address(this), 10 ether); + token1.mint(address(this), 10 ether); + binCustomCurveHook.addLiquidity(4 ether, 8 ether); + + // before verify + assertEq(token0.balanceOf(address(this)), 6 ether); + assertEq(token1.balanceOf(address(this)), 2 ether); + assertEq(token0.balanceOf(address(vault)), 4 ether); + assertEq(token1.balanceOf(address(vault)), 8 ether); + + // swap exactIn token0 for token1 + uint128 amtIn = uint128(bound(_amtIn, 0.1 ether, 6 ether)); // 6 as token0.balanceOf(address(this) == 6 ethers + BalanceDelta delta = binSwapHelper.swap(key, true, -int128(amtIn), BinSwapHelper.TestSettings(true, true), ""); + + // verify 1:1 swap + assertEq(delta.amount0(), -int128(amtIn)); + assertEq(delta.amount1(), int128(amtIn)); + + // after verify + assertEq(token0.balanceOf(address(this)), 6 ether - amtIn); + assertEq(token1.balanceOf(address(this)), 2 ether + amtIn); + assertEq(token0.balanceOf(address(vault)), 4 ether + amtIn); + assertEq(token1.balanceOf(address(vault)), 8 ether - amtIn); + } + + receive() external payable {} +} diff --git a/test/pool-bin/BinMintBurnFeeHook.t.sol b/test/pool-bin/BinMintBurnFeeHook.t.sol new file mode 100644 index 00000000..b40c08ea --- /dev/null +++ b/test/pool-bin/BinMintBurnFeeHook.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {IPoolManager} from "../../src/interfaces/IPoolManager.sol"; +import {IBinPoolManager} from "../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {Vault} from "../../src/Vault.sol"; +import {Currency} from "../../src/types/Currency.sol"; +import {PoolKey} from "../../src/types/PoolKey.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../../src/types/BalanceDelta.sol"; +import {BinPoolManager} from "../../src/pool-bin/BinPoolManager.sol"; +import {BinPool} from "../../src/pool-bin/libraries/BinPool.sol"; +import {BinPoolParametersHelper} from "../../src/pool-bin/libraries/BinPoolParametersHelper.sol"; +import {BinLiquidityHelper} from "./helpers/BinLiquidityHelper.sol"; +import {BinTestHelper} from "./helpers/BinTestHelper.sol"; +import {Hooks} from "../../src/libraries/Hooks.sol"; +import {BinMintBurnFeeHook} from "./helpers/BinMintBurnFeeHook.sol"; + +contract BinHookReturnsDelta is Test, GasSnapshot, BinTestHelper { + using BinPoolParametersHelper for bytes32; + + Vault public vault; + BinPoolManager public poolManager; + BinMintBurnFeeHook public binMintBurnFeeHook; + + BinLiquidityHelper public binLiquidityHelper; + + uint24 activeId = 2 ** 23; // where token0 and token1 price is the same + + PoolKey key; + bytes32 poolParam; + MockERC20 token0; + MockERC20 token1; + Currency currency0; + Currency currency1; + + function setUp() public { + vault = new Vault(); + poolManager = new BinPoolManager(IVault(address(vault))); + + vault.registerApp(address(poolManager)); + + token0 = new MockERC20("TestA", "A", 18); + token1 = new MockERC20("TestB", "B", 18); + (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); + currency0 = Currency.wrap(address(token0)); + currency1 = Currency.wrap(address(token1)); + + IBinPoolManager iBinPoolManager = IBinPoolManager(address(poolManager)); + IVault iVault = IVault(address(vault)); + + binLiquidityHelper = new BinLiquidityHelper(iBinPoolManager, iVault); + token0.approve(address(binLiquidityHelper), 1000 ether); + token1.approve(address(binLiquidityHelper), 1000 ether); + + binMintBurnFeeHook = new BinMintBurnFeeHook(iVault, iBinPoolManager); + token0.approve(address(binMintBurnFeeHook), 1000 ether); + token1.approve(address(binMintBurnFeeHook), 1000 ether); + + key = PoolKey({ + currency0: currency0, + currency1: currency1, + hooks: binMintBurnFeeHook, + poolManager: IPoolManager(address(poolManager)), + fee: uint24(3000), // 3000 = 0.3% + parameters: bytes32(uint256(binMintBurnFeeHook.getHooksRegistrationBitmap())).setBinStep(10) + }); + + poolManager.initialize(key, activeId); + } + + function test_Mint() external { + token0.mint(address(this), 10 ether); + token1.mint(address(this), 10 ether); + + // before + assertEq(token0.balanceOf(address(this)), 10 ether); + assertEq(token1.balanceOf(address(this)), 10 ether); + assertEq(token0.balanceOf(address(vault)), 0 ether); + assertEq(token1.balanceOf(address(vault)), 0 ether); + assertEq(token0.balanceOf(address(binMintBurnFeeHook)), 0 ether); + assertEq(token1.balanceOf(address(binMintBurnFeeHook)), 0 ether); + + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 1 ether, 1 ether); + BalanceDelta delta = binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + assertEq(token0.balanceOf(address(this)), 7 ether); + assertEq(token1.balanceOf(address(this)), 7 ether); + assertEq(token0.balanceOf(address(vault)), 3 ether); + assertEq(token1.balanceOf(address(vault)), 3 ether); + + // hook mint VaultToken instead of taking token from vault as vault does not have token in this case + assertEq(vault.balanceOf(address(binMintBurnFeeHook), key.currency0), 2 ether); + assertEq(vault.balanceOf(address(binMintBurnFeeHook), key.currency1), 2 ether); + } + + function test_Burn() external { + token0.mint(address(this), 10 ether); + token1.mint(address(this), 10 ether); + + IBinPoolManager.MintParams memory mintParams = _getSingleBinMintParams(activeId, 1 ether, 1 ether); + BalanceDelta delta = binLiquidityHelper.mint(key, mintParams, abi.encode(0)); + + assertEq(token0.balanceOf(address(this)), 7 ether); + assertEq(token1.balanceOf(address(this)), 7 ether); + assertEq(token0.balanceOf(address(vault)), 3 ether); + assertEq(token1.balanceOf(address(vault)), 3 ether); + assertEq(vault.balanceOf(address(binMintBurnFeeHook), key.currency0), 2 ether); + assertEq(vault.balanceOf(address(binMintBurnFeeHook), key.currency1), 2 ether); + + IBinPoolManager.BurnParams memory burnParams = + _getSingleBinBurnLiquidityParams(key, poolManager, activeId, address(binLiquidityHelper), 100); + + binLiquidityHelper.burn(key, burnParams, ""); + + // +1 from remove liqudiity, -4 from hook fee + assertEq(token0.balanceOf(address(this)), 7 ether + 1 ether - 4 ether); + assertEq(token1.balanceOf(address(this)), 7 ether + 1 ether - 4 ether); + + // -1 from remove liquidity, +4 from hook calling vault.mint + assertEq(token0.balanceOf(address(vault)), 3 ether - 1 ether + 4 ether); + assertEq(token1.balanceOf(address(vault)), 3 ether - 1 ether + 4 ether); + assertEq(vault.balanceOf(address(binMintBurnFeeHook), key.currency0), 2 ether + 4 ether); + assertEq(vault.balanceOf(address(binMintBurnFeeHook), key.currency1), 2 ether + 4 ether); + } + + receive() external payable {} +} diff --git a/test/pool-bin/helpers/BinCustomCurveHook.sol b/test/pool-bin/helpers/BinCustomCurveHook.sol new file mode 100644 index 00000000..bbd31cd0 --- /dev/null +++ b/test/pool-bin/helpers/BinCustomCurveHook.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IVault} from "../../../src/interfaces/IVault.sol"; +import {Hooks} from "../../../src/libraries/Hooks.sol"; +import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {PoolKey} from "../../../src/types/PoolKey.sol"; +import {Currency} from "../../../src/types/Currency.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {toBalanceDelta, BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, toBeforeSwapDelta} from "../../../src/types/BeforeSwapDelta.sol"; +import {BaseBinTestHook} from "./BaseBinTestHook.sol"; +import {CurrencySettlement} from "../../helpers/CurrencySettlement.sol"; + +contract BinCustomCurveHook is BaseBinTestHook { + error InvalidAction(); + + using CurrencySettlement for Currency; + using Hooks for bytes32; + + IVault public immutable vault; + IBinPoolManager public immutable poolManager; + PoolKey key; + + constructor(IVault _vault, IBinPoolManager _poolManager) { + vault = _vault; + poolManager = _poolManager; + } + + function setPoolKey(PoolKey memory _poolKey) external { + key = _poolKey; + } + + function getHooksRegistrationBitmap() external pure override returns (uint16) { + return _hooksRegistrationBitmapFrom( + Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeMint: false, + afterMint: false, + beforeBurn: false, + afterBurn: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnsDelta: true, + afterSwapReturnsDelta: false, + afterMintReturnsDelta: false, + afterBurnReturnsDelta: false + }) + ); + } + + /// @dev assume user call hook to add liquidity + function addLiquidity(uint256 amt0, uint256 amt1) public { + // 1. Take input currency and amount from user + IERC20(Currency.unwrap(key.currency0)).transferFrom(msg.sender, address(this), amt0); + IERC20(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), amt1); + + // 2. Mint -- so vault has token balance + vault.lock(abi.encode("mint", abi.encode(amt0, amt1))); + } + + /// @dev assume user call hook to remove liquidity + function removeLiquidity(uint256 amt0, uint256 amt1) public { + // 2. Mint -- so vault has token balance + vault.lock(abi.encode("burn", abi.encode(amt0, amt1))); + + IERC20(Currency.unwrap(key.currency0)).transfer(msg.sender, amt0); + IERC20(Currency.unwrap(key.currency1)).transfer(msg.sender, amt1); + } + + function lockAcquired(bytes calldata callbackData) external returns (bytes memory) { + (bytes memory action, bytes memory rawCallbackData) = abi.decode(callbackData, (bytes, bytes)); + + if (keccak256(action) == keccak256("mint")) { + (uint256 amt0, uint256 amt1) = abi.decode(rawCallbackData, (uint256, uint256)); + + // transfer token to the vault and mint VaultToken + key.currency0.settle(vault, address(this), amt0, false); + key.currency0.take(vault, address(this), amt0, true); + + key.currency1.settle(vault, address(this), amt1, false); + key.currency1.take(vault, address(this), amt1, true); + } else if (keccak256(action) == keccak256("burn")) { + (uint256 amt0, uint256 amt1) = abi.decode(rawCallbackData, (uint256, uint256)); + + // take token from the vault and burn VaultToken + key.currency0.take(vault, address(this), amt0, false); + key.currency0.settle(vault, address(this), amt0, true); + + key.currency1.take(vault, address(this), amt1, false); + key.currency1.settle(vault, address(this), amt1, true); + } + } + + /// @dev 1:1 swap + function beforeSwap(address, PoolKey calldata key, bool swapForY, int128 amountSpecified, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + (Currency inputCurrency, Currency outputCurrency, uint256 amount) = + _getInputOutputAndAmount(key, swapForY, amountSpecified); + + // 1. Take input currency and amount + inputCurrency.take(vault, address(this), amount, true); + + // 2. Give output currency and amount achieving a 1:1 swap + outputCurrency.settle(vault, address(this), amount, true); + + BeforeSwapDelta hookDelta = toBeforeSwapDelta(-amountSpecified, amountSpecified); + return (this.beforeSwap.selector, hookDelta, 0); + } + + /// @notice Get input, output currencies and amount from swap params + function _getInputOutputAndAmount(PoolKey calldata _key, bool swapForY, int128 amountSpecified) + internal + pure + returns (Currency input, Currency output, uint256 amount) + { + (input, output) = swapForY ? (_key.currency0, _key.currency1) : (_key.currency1, _key.currency0); + + amount = amountSpecified < 0 ? uint128(-amountSpecified) : uint128(amountSpecified); + } +} diff --git a/test/pool-bin/helpers/BinMintBurnFeeHook.sol b/test/pool-bin/helpers/BinMintBurnFeeHook.sol new file mode 100644 index 00000000..373a86e9 --- /dev/null +++ b/test/pool-bin/helpers/BinMintBurnFeeHook.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {IVault} from "../../../src/interfaces/IVault.sol"; +import {Hooks} from "../../../src/libraries/Hooks.sol"; +import {IBinPoolManager} from "../../../src/pool-bin/interfaces/IBinPoolManager.sol"; +import {PoolKey} from "../../../src/types/PoolKey.sol"; +import {Currency} from "../../../src/types/Currency.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {toBalanceDelta, BalanceDelta, BalanceDeltaLibrary} from "../../../src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, toBeforeSwapDelta} from "../../../src/types/BeforeSwapDelta.sol"; +import {BaseBinTestHook} from "./BaseBinTestHook.sol"; +import {CurrencySettlement} from "../../helpers/CurrencySettlement.sol"; + +import {console2} from "forge-std/console2.sol"; + +/// @dev A hook which take a fee on every mint/burn +contract BinMintBurnFeeHook is BaseBinTestHook { + error InvalidAction(); + + using CurrencySettlement for Currency; + using Hooks for bytes32; + + IVault public immutable vault; + IBinPoolManager public immutable poolManager; + + constructor(IVault _vault, IBinPoolManager _poolManager) { + vault = _vault; + poolManager = _poolManager; + } + + function getHooksRegistrationBitmap() external pure override returns (uint16) { + return _hooksRegistrationBitmapFrom( + Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeMint: false, + afterMint: true, + beforeBurn: false, + afterBurn: true, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnsDelta: false, + afterSwapReturnsDelta: false, + afterMintReturnsDelta: true, + afterBurnReturnsDelta: true + }) + ); + } + + /// @dev take 2x of the mint amount as fee + /// meant for https://github.com/pancakeswap/pancake-v4-core/pull/203 to ensure reserveOfApp underflow won't happen + function afterMint( + address, + PoolKey calldata key, + IBinPoolManager.MintParams calldata, + BalanceDelta delta, + bytes calldata + ) external override returns (bytes4, BalanceDelta) { + // take fee from mint + int128 amt0Fee; + if (delta.amount0() < 0) { + amt0Fee = (-delta.amount0()) * 2; + key.currency0.take(vault, address(this), uint128(amt0Fee), true); + } + int128 amt1Fee = 0; + if (delta.amount1() < 0) { + amt1Fee = (-delta.amount1()) * 2; + key.currency1.take(vault, address(this), uint128(amt1Fee), true); + } + + BalanceDelta hookDelta = toBalanceDelta(amt0Fee, amt1Fee); + return (this.afterMint.selector, hookDelta); + } + + /// @dev take 4x the burn amount as fee + /// meant for https://github.com/pancakeswap/pancake-v4-core/pull/203 to ensure reserveOfApp underflow won't happen + function afterBurn( + address, + PoolKey calldata key, + IBinPoolManager.BurnParams calldata, + BalanceDelta delta, + bytes calldata + ) external override returns (bytes4, BalanceDelta) { + console2.log("afterBurn delta"); + console2.logInt(delta.amount0()); + console2.logInt(delta.amount1()); + + int128 amt0Fee; + if (delta.amount0() > 0) { + amt0Fee = (delta.amount0()) * 4; + key.currency0.take(vault, address(this), uint128(amt0Fee), true); + } + int128 amt1Fee = 0; + if (delta.amount1() > 0) { + amt1Fee = (delta.amount1()) * 4; + key.currency1.take(vault, address(this), uint128(amt1Fee), true); + } + + BalanceDelta hookDelta = toBalanceDelta(amt0Fee, amt1Fee); + return (this.afterBurn.selector, hookDelta); + } +}