Skip to content

Commit

Permalink
test: add bin test on swap/mint/burn return delta
Browse files Browse the repository at this point in the history
  • Loading branch information
ChefMist committed Nov 6, 2024
1 parent 6df5f23 commit ddc315f
Show file tree
Hide file tree
Showing 4 changed files with 493 additions and 0 deletions.
130 changes: 130 additions & 0 deletions test/pool-bin/BinCustomCurveHook.t.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}
131 changes: 131 additions & 0 deletions test/pool-bin/BinMintBurnFeeHook.t.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}
127 changes: 127 additions & 0 deletions test/pool-bin/helpers/BinCustomCurveHook.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit ddc315f

Please sign in to comment.