Skip to content
This repository was archived by the owner on Aug 14, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ jobs:
check:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]

name: Foundry project
runs-on: ubuntu-latest
Expand All @@ -36,7 +38,7 @@ jobs:

- name: Run Forge build
run: |
forge build --sizes
forge build
id: build

- name: Run Forge tests
Expand Down
16 changes: 14 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@
url = https://github.com/foundry-rs/forge-std.git
[submodule "lib/v4-core"]
path = lib/v4-core
url = https://github.com/Uniswap/v4-core
url = https://github.com/uniswap/v4-core
[submodule "lib/v4-periphery"]
path = lib/v4-periphery
url = https://github.com/Uniswap/v4-periphery
url = https://github.com/uniswap/v4-periphery
[submodule "lib/permit2"]
path = lib/permit2
url = https://github.com/uniswap/permit2
[submodule "lib/universal-router"]
path = lib/universal-router
url = https://github.com/Uniswap/universal-router
[submodule "lib/v3-core"]
path = lib/v3-core
url = https://github.com/uniswap/v3-core
[submodule "lib/v2-core"]
path = lib/v2-core
url = https://github.com/uniswap/v2-core
10 changes: 8 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
[profile.default]
viaIR = true

src = "src"
out = "out"
libs = ["lib"]

remappings = [
"@openzeppelin/contracts=lib/openzeppelin-contracts/contracts",
"forge-std/=lib/forge-std/src/",
"@uniswap/v4-core/=lib/v4-core/",
"forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/",
"forge-std/=lib/v4-core/lib/forge-std/src/",
"permit2/=lib/v4-periphery/lib/permit2/",
"solmate/=lib/v4-core/lib/solmate/",
"v4-core/=lib/v4-core/",
"v4-periphery/=lib/v4-periphery/",
"@uniswap/permit2/=lib/permit2/",
"@uniswap/universal-router/=lib/universal-router/",
"@uniswap/v3-core/=lib/v3-core/",
"@uniswap/v2-core/=lib/v2-core/",


]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts
1 change: 1 addition & 0 deletions lib/permit2
Submodule permit2 added at cc56ad
1 change: 1 addition & 0 deletions lib/universal-router
Submodule universal-router added at 3663f6
1 change: 1 addition & 0 deletions lib/v2-core
Submodule v2-core added at 4dd590
1 change: 1 addition & 0 deletions lib/v3-core
Submodule v3-core added at e3589b
1 change: 1 addition & 0 deletions lib/v4-core
Submodule v4-core added at e50237
1 change: 1 addition & 0 deletions lib/v4-periphery
Submodule v4-periphery added at ad04c9
42 changes: 42 additions & 0 deletions src/uniV4Intergration/ChamaFeeHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.24;

import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";

contract ChamaFeeHook {
uint256 public baseFee = 30; // 0.3%
uint256 public stablecoinReducedFee = 10; // 0.1% for stablecoin swaps

mapping(PoolId => uint24) public customFees;
mapping(address => bool) public isStablecoin;

constructor() {
// These are placeholders, for now
isStablecoin[0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48] = true; // USDC
isStablecoin[0xdAC17F958D2ee523a2206206994597C13D831ec7] = true; // USDT
isStablecoin[0x6B175474E89094C44Da98b954EedeAC495271d0F] = true; // DAI
}

function getDynamicFee(address token0, address token1) public view returns (uint256) {
// If both tokens are stablecoins, apply reduced fee
if (isStablecoin[token0] && isStablecoin[token1]) {
return stablecoinReducedFee;
}

return baseFee;
}

/// TODO Add some access control on this
function updateFeeParameters(uint256 _baseFee, uint256 _stablecoinReducedFee) external {
require(_baseFee <= 100, "Fee too high"); // Max 1%
require(_stablecoinReducedFee <= _baseFee, "Reduced fee must be <= base fee");

baseFee = _baseFee;
stablecoinReducedFee = _stablecoinReducedFee;
}

function setStablecoin(address token, bool status) external {
isStablecoin[token] = status;
}
}
133 changes: 133 additions & 0 deletions src/uniV4Intergration/ChamaYieldHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ChamaFeeHook} from "./ChamaFeeHook.sol";
import {IPoolManager, Currency} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";

// For now we are using morpho vaults, which are ERC4626 compliant
interface ILendingProtocol is IERC4626 {}

import {
BaseHook, Hooks, IPoolManager, SwapParams, PoolKey, BeforeSwapDelta
} from "v4-periphery/src/utils/BaseHook.sol";

contract ChamaYieldHook is ChamaFeeHook, BaseHook {
using SafeERC20 for IERC20;
using PoolIdLibrary for PoolKey;

mapping(address => bool) public approvedYieldProtocols;
mapping(address => address) public tokenYieldProtocol; //current yield protocols for each token
// Minimum token balance to keep liquid (not rehypothecated)
mapping(address => uint256) public minLiquidBalance;
// Amount of tokens currently deployed in yield protocols
mapping(address => uint256) public deployed;

event TokensWithdrawn(address indexed token, address indexed protocol, uint256 amount);
event TokensDeployed(address indexed token, address indexed protocol, uint256 amount);

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

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: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}

function _beforeSwap(address, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// Before a swap, make sure we have enough liquidity by withdrawing from yield protocol if needed
address tokenOut = params.zeroForOne ? Currency.unwrap(key.currency1) : Currency.unwrap(key.currency0);

// Ensure enough liquidity for tokenOut
ensureLiquidity(tokenOut, params.amountSpecified);

// Here we update the fee based on whether it is a stablecoin pool
poolManager.updateDynamicLPFee(
key, uint24(getDynamicFee(Currency.unwrap(key.currency0), Currency.unwrap(key.currency1)))
);

return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), uint24(0));
}

/// @notice The following function should be overiden but that bringes an error figure out what the hell is the issue
function _afterSwap(address, PoolKey calldata key, SwapParams calldata, BalanceDelta, bytes calldata)
internal
override
returns (bytes4, int128)
{
address token0 = Currency.unwrap(key.currency0);
address token1 = Currency.unwrap(key.currency1);

deployExcessLiquidity(token0);
deployExcessLiquidity(token1);
return (BaseHook.afterSwap.selector, 0);
}

function ensureLiquidity(address token, int256 amountNeeded) internal {
if (amountNeeded <= 0) return;

uint256 currentBalance = IERC20(token).balanceOf(address(this));

// If we don't have enough liquid tokens, withdraw from lending protocol
if (currentBalance < uint256(amountNeeded) && deployed[token] > 0) {
address yieldProtocol = tokenYieldProtocol[token];

if (yieldProtocol != address(0)) {
uint256 amountToWithdraw = uint256(amountNeeded) - currentBalance;
if (amountToWithdraw > deployed[token]) {
amountToWithdraw = deployed[token];
}

// Withdraw from lending protocol
ILendingProtocol(yieldProtocol).withdraw(amountToWithdraw, token, msg.sender);
deployed[token] -= amountToWithdraw;

emit TokensWithdrawn(token, yieldProtocol, amountToWithdraw);
}
}
}

function deployExcessLiquidity(address token) internal {
address yieldProtocol = tokenYieldProtocol[token];

if (yieldProtocol != address(0)) {
uint256 currentBalance = IERC20(token).balanceOf(address(this));
uint256 excessAmount = 0;

if (currentBalance > minLiquidBalance[token]) {
excessAmount = currentBalance - minLiquidBalance[token];

if (excessAmount > 0) {
IERC20(token).safeIncreaseAllowance(yieldProtocol, excessAmount);
ILendingProtocol(yieldProtocol).deposit(excessAmount, token);
deployed[token] += excessAmount;

emit TokensDeployed(token, yieldProtocol, excessAmount);
}
}
}
}
}
Loading