-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: add bin test on swap/mint/burn return delta
- Loading branch information
Showing
4 changed files
with
493 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.