|
// SPDX-License-Identifier: MIT |
|
pragma solidity ^0.8.17; |
|
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; |
|
import "@openzeppelin/contracts/access/Ownable.sol"; |
|
import "forge-std/console.sol"; |
|
|
|
import {Test} from "forge-std/Test.sol"; |
|
|
|
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; |
|
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol"; |
|
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; |
|
|
|
import {PoolManager} from "v4-core/PoolManager.sol"; |
|
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; |
|
|
|
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; |
|
|
|
import {Hooks} from "v4-core/libraries/Hooks.sol"; |
|
import {TickMath} from "v4-core/libraries/TickMath.sol"; |
|
import {SqrtPriceMath} from "v4-core/libraries/SqrtPriceMath.sol"; |
|
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol"; |
|
|
|
import {PookaToken} from "../src/PookaToken.sol"; |
|
import {PookaValuationHook} from "../src/PookaValuationHook.sol"; |
|
|
|
import {Constants} from "@uniswap/v4-core/test/utils/Constants.sol"; |
|
|
|
/// @title TestPookaValuationHook |
|
/// @notice Test for the valuation hook contract for Pooka compatible with our Uniswap V4 hooks and pool examples. |
|
contract TestPookaValuationHook is Test, Deployers { |
|
using CurrencyLibrary for Currency; |
|
|
|
Currency token0Currency; |
|
Currency token1Currency; |
|
|
|
PoolManager public poolManager; |
|
IPoolManager public ipoolManager; |
|
PoolSwapTest public poolSwapTest; |
|
MockERC20 public mockUSDC; |
|
PookaToken public pookaToken; |
|
PookaValuationHook public pookaValuationHook; |
|
|
|
address public owner; |
|
address public user1; |
|
address public user2; |
|
|
|
uint256 public constant INITIAL_SUPPLY = 1_000_000 * 10 ** 18; // 1M Pooka tokens |
|
|
|
/// @dev Converts a uint256 to its ASCII string decimal representation. |
|
function uintToString(uint256 value) internal pure returns (string memory) { |
|
if (value == 0) { |
|
return "0"; |
|
} |
|
uint256 temp = value; |
|
uint256 digits; |
|
while (temp != 0) { |
|
digits++; |
|
temp /= 10; |
|
} |
|
bytes memory buffer = new bytes(digits); |
|
while (value != 0) { |
|
digits -= 1; |
|
buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); |
|
value /= 10; |
|
} |
|
return string(buffer); |
|
} |
|
|
|
/// @dev Pads the fractional string with leading zeros so it has exactly `decimals` digits. |
|
function padFractional( |
|
string memory fractional, |
|
uint256 decimals |
|
) internal pure returns (string memory) { |
|
bytes memory fracBytes = bytes(fractional); |
|
uint256 missingZeros = decimals > fracBytes.length |
|
? decimals - fracBytes.length |
|
: 0; |
|
bytes memory zeros = new bytes(missingZeros); |
|
for (uint256 i = 0; i < missingZeros; i++) { |
|
zeros[i] = "0"; |
|
} |
|
return string(abi.encodePacked(zeros, fractional)); |
|
} |
|
|
|
/// @dev Formats a fixed-point integer as a string with a decimal point. |
|
/// For example, formatFixed(123456789, 6) returns "123.456789". |
|
function formatFixed( |
|
uint256 value, |
|
uint256 decimals |
|
) public pure returns (string memory) { |
|
uint256 factor = 10 ** decimals; |
|
uint256 integerPart = value / factor; |
|
uint256 fractionalPart = value % factor; |
|
return |
|
string( |
|
abi.encodePacked( |
|
uintToString(integerPart), |
|
".", |
|
padFractional(uintToString(fractionalPart), decimals) |
|
) |
|
); |
|
} |
|
|
|
function setUp() public { |
|
// Step 1 + 2 |
|
// Deploy PoolManager and Router contracts |
|
deployFreshManagerAndRouters(); |
|
|
|
owner = address(this); // The test contract itself acts as the deployer/owner |
|
user1 = vm.addr(1); // Create a new address for user1 |
|
user2 = vm.addr(2); // Create a new address for user2 |
|
|
|
// make the pool address the owner |
|
vm.prank(owner); |
|
|
|
// Deploy the PookaToken contract |
|
pookaToken = new PookaToken(INITIAL_SUPPLY); |
|
// Mint a bunch of POOKA to ourselves and to address(1) |
|
pookaToken.mint(owner, 1e6 ether); |
|
pookaToken.mint(user1, 100 ether); |
|
|
|
// Set the token0 and token1 currencies |
|
mockUSDC = new MockERC20("Mock USDC Token", "USDC", 6); |
|
// Mint a bunch of TOKEN to ourselves and to address(1) |
|
mockUSDC.mint(owner, 100e6 * 10 ** 6); |
|
mockUSDC.mint(user1, 100 * 10 ** 6); |
|
|
|
// With this (sort tokens by address): |
|
if (address(mockUSDC) < address(pookaToken)) { |
|
token0Currency = Currency.wrap(address(mockUSDC)); |
|
token1Currency = Currency.wrap(address(pookaToken)); |
|
} else { |
|
token0Currency = Currency.wrap(address(pookaToken)); |
|
token1Currency = Currency.wrap(address(mockUSDC)); |
|
} |
|
|
|
// Deploy hook to an address that has the proper flags set |
|
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG); |
|
deployCodeTo( |
|
"PookaValuationHook.sol", |
|
abi.encode(manager, address(mockUSDC), address(pookaToken)), |
|
address(flags) |
|
); |
|
pookaValuationHook = PookaValuationHook(address(flags)); |
|
|
|
// Approve our TOKEN for spending on the swap router and modify liquidity router |
|
// These variables are coming from the `Deployers` contract |
|
mockUSDC.approve(address(swapRouter), type(uint256).max); |
|
mockUSDC.approve(address(modifyLiquidityRouter), type(uint256).max); |
|
|
|
// Approve our POOKA for spending on the swap router and modify liquidity router |
|
pookaToken.approve(address(swapRouter), type(uint256).max); |
|
pookaToken.approve(address(modifyLiquidityRouter), type(uint256).max); |
|
|
|
require( |
|
address(pookaValuationHook) != address(0), |
|
"pookaValuationHook not deployed!" |
|
); |
|
|
|
// Initialize the USDC POOKA pool |
|
(key, ) = initPool( |
|
token0Currency, // Currency 0 = POOKA |
|
token1Currency, // Currency 1 = USDC |
|
pookaValuationHook, // Hook Contract |
|
3000, // Swap Fees |
|
Constants.SQRT_PRICE_10000_100 // Initial Sqrt(P) value = 1 |
|
); |
|
|
|
// Add initial liquidity to the pool |
|
// Some liquidity from -60 to +60 tick range |
|
modifyLiquidityRouter.modifyLiquidity( |
|
key, |
|
IPoolManager.ModifyLiquidityParams({ |
|
tickLower: -60, |
|
tickUpper: 60, |
|
liquidityDelta: 100_000_000 * 10 ** 6, |
|
salt: bytes32(0) |
|
}), |
|
ZERO_BYTES |
|
); |
|
// Some liquidity from -120 to +120 tick range |
|
modifyLiquidityRouter.modifyLiquidity( |
|
key, |
|
IPoolManager.ModifyLiquidityParams({ |
|
tickLower: -120, |
|
tickUpper: 120, |
|
liquidityDelta: 200 * 10 ** 6, |
|
salt: bytes32(0) |
|
}), |
|
ZERO_BYTES |
|
); |
|
|
|
// Add this check for currency order |
|
require( |
|
key.currency1 == Currency.wrap(address(mockUSDC)) && |
|
key.currency0 == Currency.wrap(address(pookaToken)), |
|
"Token order mismatch" |
|
); |
|
} |
|
|
|
function test_addLiquidity() public { |
|
|
|
// Set user address in hook data |
|
bytes memory hookData = abi.encode(owner); |
|
|
|
uint160 sqrtPriceAtTickLower = TickMath.getSqrtPriceAtTick(-10); |
|
|
|
// Add liquidity of 10 USDC |
|
uint256 usdcToAdd = 100 * 10 ** 6; |
|
uint128 liquidityDelta = LiquidityAmounts.getLiquidityForAmount0( |
|
sqrtPriceAtTickLower, |
|
Constants.SQRT_PRICE_10000_100, |
|
usdcToAdd |
|
); |
|
|
|
// Add liquidity |
|
// @TODO: find out why usdc balance overflows when adding liquidity |
|
// if the amount > 10 * 10 ** 6 |
|
modifyLiquidityRouter.modifyLiquidity{value: usdcToAdd}( |
|
key, |
|
IPoolManager.ModifyLiquidityParams({ |
|
tickLower: -10, |
|
tickUpper: 10, |
|
liquidityDelta: int256(uint256(liquidityDelta)), |
|
salt: bytes32(0) |
|
}), |
|
hookData |
|
); |
|
} |
|
|
|
// a test that makes sure that the owner can swap without fees |
|
function test_ownerSwapPooka2Usdc() public { |
|
// Set user address in hook data |
|
// bytes memory hookData = abi.encode(owner); |
|
vm.startPrank(owner); |
|
|
|
uint256 initialPooka = pookaToken.balanceOf(owner); |
|
uint256 initialUsdc = mockUSDC.balanceOf(owner); |
|
|
|
// Add this check before swapping |
|
require( |
|
key.currency1 == Currency.wrap(address(mockUSDC)) && |
|
key.currency0 == Currency.wrap(address(pookaToken)), |
|
"Token order mismatch" |
|
); |
|
|
|
// Set test settings |
|
PoolSwapTest.TestSettings memory testSettings = PoolSwapTest |
|
.TestSettings({takeClaims: false, settleUsingBurn: false}); |
|
|
|
// Pass the owner's address in hook data so that the hook recognizes the owner. |
|
bytes memory hookData = abi.encode(owner); |
|
|
|
// Instead of using TickMath.MIN_SQRT_PRICE + 1, |
|
// use a moderate tick offset relative to a known reference (e.g., tick 0). |
|
// For token0 -> token1 swap (price decreasing), a lower tick (e.g. -100) is used. |
|
uint160 sqrtPriceLimitX96_10 = TickMath.getSqrtPriceAtTick(-100); // Tick -10 should have liquidity |
|
|
|
// Swap 1 POOKA for USDC |
|
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ |
|
zeroForOne: true, |
|
amountSpecified: -10 ether, // Exact input: 0.001 POOKA |
|
sqrtPriceLimitX96: sqrtPriceLimitX96_10 // Allow price to increase |
|
}); |
|
|
|
swapRouter.swap(key, params, testSettings, hookData); |
|
|
|
// calculate the the difference in balances |
|
uint256 diff = initialPooka - pookaToken.balanceOf(owner); |
|
|
|
string memory formatted = formatFixed(diff, 18); |
|
console.log("Formatted value:", formatted); // Should log: "Formatted value: 123.456789" |
|
|
|
emit log_named_uint("diff pooka", diff); |
|
|
|
// Verify no fee deducted |
|
assertApproxEqAbs( |
|
pookaToken.balanceOf(owner), |
|
initialPooka - 10 ether, // No fees subtracted |
|
0.0001 ether, // error margin for precision loss |
|
"Owner should pay 0% fee" |
|
); |
|
|
|
// Verify USDC received |
|
assertGt(mockUSDC.balanceOf(owner), initialUsdc, "Should receive USDC"); |
|
|
|
vm.stopPrank(); |
|
} |
|
} |