Skip to content

Instantly share code, notes, and snippets.

@claytantor
Last active February 19, 2025 04:19
Show Gist options
  • Select an option

  • Save claytantor/ba68deb4e9b62eaba29b0b237878eccd to your computer and use it in GitHub Desktop.

Select an option

Save claytantor/ba68deb4e9b62eaba29b0b237878eccd to your computer and use it in GitHub Desktop.
Uniswap V4 Hook Example - PookaValuationHook

Key Mechanics

  1. AMM Integration:

    // Price determined by pool reserves
    POOKA_Price = (USDC_Reserves * 10^18) / POOKA_Supply
    • Maintains Uniswap's x * y = k invariant
    • All swaps modify pool reserves normally
  2. Owner Privileges:

    function adjustWarchest(...) {
        // Owner can inject/withdraw USDC through fee-free swaps
        _executePrivilegedSwap(...);
    }
    • Owner swaps bypass fees (0% vs normal 0.3%)
    • Large swaps move price significantly with minimal slippage
  3. Price Adjustment Workflow:

    • Profit Taking:

      // Owner adds 1000 USDC to pool
      hook.adjustWarchest(poolKey, true, 1000e6);
      • Increases USDC reserves → POOKA price rises
      • Executes USDC→POOKA swap with 0% fee
    • Loss Recovery:

      // Owner withdraws 500 USDC from pool
      hook.adjustWarchest(poolKey, false, 500e6);
      • Decreases USDC reserves → POOKA price drops
      • Executes POOKA→USDC swap with 0% fee
  4. Normal User Swaps:

    function _handleUserSwap(...) {
        return (..., 3000); // 0.3% fee
    }
    • Regular users pay standard fees
    • Their swaps affect reserves normally

Advantages Of Approach

  1. True AMM Compliance:

    • Maintains x * y = k invariant
    • Pool liquidity directly impacts pricing
    • No artificial minting/burning
  2. Transparent Price Impact:

    Before adjustment:
    USDC Reserves = 10,000
    POOKA Supply = 10,000
    Price = 1.00 USDC
    
    After owner deposits 1,000 USDC:
    USDC Reserves = 11,000
    POOKA Supply = 9,166.66 (via swap)
    New Price = 11,000 / 9,166.66 ≈ 1.20 USDC (+20%)
    
  3. Reduced Centralization Risk:

    • Price changes require actual capital movement
    • Owner can't arbitrarily set prices
    • All adjustments visible on-chain

This implementation creates a hybrid system where:

  • Regular users interact with a normal Uniswap V4 pool
  • The owner can strategically adjust prices through large, privileged swaps
  • All price changes are organic results of reserve changes via AMM math

Usage

  1. Deploy Contracts:

    • Deploy POOKA with the Hook's address.
    • Deploy PookaValuationHook with USDC and POOKA addresses.
  2. Create Uniswap V4 Pool:

    • Use the PookaValuationHook address when creating the USDC/POOKA pool.
  3. Swapping:

    • Users swap USDC for POOKA (minting) or POOKA for USDC (burning) through the pool, with the Hook managing the pricing. The owner can adjust prices by injecting/withdrawing USDC which they can use to make trades.
  4. POOKA Dilution for USDC Trading:

    • The owner trades the accumulated USDC on external DEXs and updates externalWarchest via adjustWarchest to reflect profits/losses, dynamically adjusting POOKA's value.

This setup creates a liquidity pool where POOKA's value is directly tied to the owner's USDC balance, functioning as a managed "warchest" with dynamic token valuation.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
// lib/v4-core/src/interfaces/IHooks.sol
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {BaseHook} from "v4-periphery/base/hooks/BaseHook.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/types/BeforeSwapDelta.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {PookaToken} from "./PookaToken.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "v4-core/interfaces/IPoolManager.sol";
import "forge-std/console.sol";
/// @title PookaValuationHook
/// @notice A sample hook that triggers a buyback on the warchest pool if the designated AI agent’s swap produces profit.
contract PookaValuationHook is BaseHook, Ownable {
/// The designated AI agent address.
/// Token interfaces.
IERC20 public immutable USDC;
PookaToken public immutable POOKA;
uint256 public externalWarchest; // USDC balance owned by the pool owner for external trading
error OnlyOwner();
error InvalidSwap();
constructor(
IPoolManager _poolManager,
address _usdc,
address _pooka
) BaseHook(_poolManager) Ownable(msg.sender) {
USDC = IERC20(_usdc);
POOKA = PookaToken(_pooka);
}
// --- Core Price Adjustment Mechanism ---
function adjustWarchest(
PoolKey calldata key,
bool addToPool, // true = deposit profits, false = withdraw losses
uint256 usdcAmount
) external onlyOwner {
require(
((Currency.unwrap(key.currency0) == _getUSDCAddress() &&
Currency.unwrap(key.currency1) == _getPookaAddress()) ||
(Currency.unwrap(key.currency1) == _getUSDCAddress() &&
Currency.unwrap(key.currency0) == _getPookaAddress())), // Ensure order doesn't matter
"Invalid pool key: token pair mismatch"
);
if (addToPool) {
// Owner adds USDC to pool (increases POOKA value)
_executePrivilegedSwap(
key,
true, // USDC -> POOKA
usdcAmount
);
externalWarchest -= usdcAmount;
} else {
// Owner withdraws USDC from pool (decreases POOKA value)
_executePrivilegedSwap(
key,
false, // POOKA -> USDC
usdcAmount
);
externalWarchest += usdcAmount;
}
}
function getHookPermissions()
public
pure
override
returns (Hooks.Permissions memory)
{
return
Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
function beforeSwap(
address,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData
) public view override returns (bytes4, BeforeSwapDelta, uint24) {
if (hookData.length == 0)
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
0 // 0% swap fee
);
// Extract user address from hookData
address user = abi.decode(hookData, (address));
// If there is hookData but not in the format we're expecting and user address is zero
// nobody gets any points
if (user == address(0))
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
0 // 0% swap fee
);
// Apply different logic for owner vs regular users
if (user == owner()) {
console.log("Owner swap");
return _handleOwnerSwap(key, params);
}
console.log("User swap");
return _handleUserSwap(key, params);
}
// --- Internal Functions ---
function _executePrivilegedSwap(
PoolKey calldata key,
bool zeroForOne,
uint256 amount
) internal {
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: zeroForOne,
amountSpecified: int256(amount),
sqrtPriceLimitX96: zeroForOne
? TickMath.MIN_SQRT_PRICE + 1
: TickMath.MIN_SQRT_PRICE - 1
});
poolManager.swap(key, params, "");
}
function _getUSDCAddress() internal view returns (address) {
return address(USDC);
}
function _getPookaAddress() internal view returns (address) {
return address(POOKA);
}
function _handleOwnerSwap(
PoolKey calldata /*key*/,
IPoolManager.SwapParams calldata /*params*/
) internal pure returns (bytes4, BeforeSwapDelta, uint24) {
// Owner swaps bypass fees and price limits
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
0 // 0% swap fee
);
}
function _handleUserSwap(
PoolKey calldata /*key*/,
IPoolManager.SwapParams calldata /*params*/
) internal pure returns (bytes4, BeforeSwapDelta, uint24) {
// Regular users pay standard 0.3% fee
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
3000 // 0.3% fee
);
}
}
// 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();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment