Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
d2e5ed8
Rehypothecation
luiz-lvj Apr 24, 2025
c90b828
adding beforeSwap
luiz-lvj Apr 24, 2025
bd12f3c
add afterSwap
luiz-lvj Apr 24, 2025
d8297de
get currency deltas
luiz-lvj Apr 24, 2025
299fd31
rehypothecated liquidity
luiz-lvj Apr 25, 2025
7f74e51
Merge branch 'master' of github.com:luiz-lvj/uniswap-hooks into rehyp…
luiz-lvj Jun 25, 2025
951c751
new rehypothecation flow
luiz-lvj Jun 25, 2025
9000ea2
update on add and removal of liquidity
luiz-lvj Jun 25, 2025
231946a
use IERC4626
luiz-lvj Jul 3, 2025
ca53b4e
forge install: openzeppelin-contracts
luiz-lvj Jul 18, 2025
a46c6b3
merge master
luiz-lvj Jul 18, 2025
0ef4408
test
luiz-lvj Aug 26, 2025
904af14
add ERC20
luiz-lvj Aug 26, 2025
d6db0a8
forge fmt
luiz-lvj Aug 26, 2025
00225f6
add tests
luiz-lvj Aug 27, 2025
79721ae
add utility library
luiz-lvj Aug 27, 2025
7a8e573
test swap with interest
luiz-lvj Aug 27, 2025
394977c
add docs
luiz-lvj Aug 27, 2025
84e53ea
removed unused imports
luiz-lvj Aug 27, 2025
4d2ce5c
add docs for library
luiz-lvj Aug 27, 2025
d86a054
add to docs
luiz-lvj Aug 27, 2025
46de8a8
lint
luiz-lvj Aug 27, 2025
15cca18
remove console
luiz-lvj Aug 27, 2025
0199ae6
Merge branch 'master' of github.com:luiz-lvj/uniswap-hooks into rehyp…
luiz-lvj Aug 27, 2025
387738a
update rehypothecation hook to lint
luiz-lvj Aug 27, 2025
d579ea7
remove unecessary import
luiz-lvj Aug 27, 2025
d632f71
up
luiz-lvj Aug 27, 2025
8eb5b6f
up
luiz-lvj Aug 27, 2025
37c4d5f
up
luiz-lvj Aug 27, 2025
8d75cc7
up
luiz-lvj Aug 27, 2025
a4baaf4
up
luiz-lvj Aug 27, 2025
818fc88
forge install: v4-periphery
luiz-lvj Aug 27, 2025
7cfe5c8
install v4 periphery
luiz-lvj Aug 27, 2025
3eaf122
up
luiz-lvj Aug 27, 2025
96f8769
up
luiz-lvj Aug 27, 2025
6cf26f8
up
luiz-lvj Aug 27, 2025
ce11fdb
up
luiz-lvj Aug 27, 2025
6c76454
up
luiz-lvj Aug 27, 2025
27b8e2f
up
luiz-lvj Aug 27, 2025
eda9055
up
luiz-lvj Aug 27, 2025
735aab2
up
luiz-lvj Aug 27, 2025
c32c080
up
luiz-lvj Aug 27, 2025
5d3b2cb
up
luiz-lvj Aug 27, 2025
e8d7868
test
luiz-lvj Aug 27, 2025
32d26d1
up
luiz-lvj Aug 27, 2025
15d6857
siplify mock via construction, rename errors
gonzaotc Sep 5, 2025
48c5bc8
up events params
gonzaotc Sep 5, 2025
b0b5911
up natspec
gonzaotc Sep 5, 2025
01e664b
update rehypothecationhook
gonzaotc Sep 5, 2025
e3fb439
merge w origin
gonzaotc Sep 5, 2025
4a0490a
rename getTickUpper and getTickLower
gonzaotc Sep 5, 2025
83b8e42
up
gonzaotc Sep 5, 2025
5722900
up tests imports
gonzaotc Sep 5, 2025
63c0823
move erc4626 logic to mock
gonzaotc Sep 5, 2025
cc2fa7b
merge master
luiz-lvj Sep 5, 2025
a6de5c1
update remappings
luiz-lvj Sep 5, 2025
ee573bc
up
gonzaotc Sep 5, 2025
7eecfb8
simplify function names
gonzaotc Sep 8, 2025
38fd8a0
refactor liquidity to shares
gonzaotc Sep 8, 2025
08ef691
add rebalancing
gonzaotc Sep 10, 2025
4acd611
add abstract vault
gonzaotc Sep 11, 2025
c54d42f
up tests
gonzaotc Sep 15, 2025
dd37725
up
gonzaotc Sep 15, 2025
1bfb924
up
gonzaotc Sep 15, 2025
bd0fe7b
iterate
gonzaotc Sep 15, 2025
52f2f9d
up
gonzaotc Sep 16, 2025
1937a6e
remove rebalancing and refactor liquidity addition and removal
gonzaotc Sep 16, 2025
5c72328
up
gonzaotc Sep 16, 2025
e84fd32
up codespell
gonzaotc Sep 17, 2025
2d81294
remove unused fn and up docs
gonzaotc Sep 17, 2025
eafb8a6
up docs
gonzaotc Sep 17, 2025
f29e167
up tests
gonzaotc Sep 17, 2025
c676aa5
up tests
gonzaotc Sep 17, 2025
40df160
up docs
gonzaotc Sep 17, 2025
8f464d4
lint
gonzaotc Sep 22, 2025
3f99593
remove base test in favor of erc4626 mocked test
gonzaotc Sep 22, 2025
1af1504
up codespell
gonzaotc Sep 22, 2025
c189bc1
up rehypothecation hook docs
gonzaotc Sep 25, 2025
98eec0c
add ReHypothecationHookNativeMock + refactor the hook to handle nativ…
gonzaotc Sep 25, 2025
2a47c59
lint
gonzaotc Sep 25, 2025
5924dbe
add nonReentrant transient modifier to transferFromSenderToHook
gonzaotc Sep 26, 2025
2ca9d04
add nonReentrant modifier to add and remove rehypothecated liquidity …
gonzaotc Oct 1, 2025
9ab90e0
Merge pull request #7 from gonzaotc/rehypothecation-hook
luiz-lvj Oct 1, 2025
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
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
forge-std/=lib/forge-std/src/
v4-core/=lib/v4-core/
v4-periphery/=lib/v4-periphery/
openzeppelin/=lib/openzeppelin-contracts/contracts/
2 changes: 2 additions & 0 deletions src/general/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ Ready-to-use hooks built on top of the base and fee abstract contracts
* {LiquidityPenaltyHook}: Hook resistant to just-in-time liquidity attacks
* {AntiSandwichHook}: Hook resistant to sandwich attacks on swaps
* {LimitOrderHook}: Hook to enable limit order placing on liquidity pools
* {ReHypothecationHook}: Hook that enables rehypothecation of liquidity positions.

== Hooks

{{LiquidityPenaltyHook}}
{{AntiSandwichHook}}
{{LimitOrderHook}}
{{ReHypothecationHook}}
440 changes: 440 additions & 0 deletions src/general/ReHypothecationHook.sol

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions src/mocks/ReHypothecationMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {ReHypothecationHook} from "src/general/ReHypothecationHook.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
import {ERC4626} from "openzeppelin/token/ERC20/extensions/ERC4626.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";

contract ERC4626Mock is ERC4626 {
constructor(IERC20 token, string memory name, string memory symbol) ERC4626(token) ERC20(name, symbol) {}
}

contract ReHypothecationMock is ReHypothecationHook {
address private _yieldSource0;
address private _yieldSource1;

constructor(IPoolManager poolManager_) ReHypothecationHook(poolManager_) ERC20("ReHypothecationMock", "RHM") {}

function setYieldSources(address yieldSource0_, address yieldSource1_) external {
_yieldSource0 = yieldSource0_;
_yieldSource1 = yieldSource1_;
}

// overrides for testing
function getYieldSourceForCurrency(Currency currency) public view override returns (address) {
PoolKey memory poolKey = getPoolKey();
if (currency == poolKey.currency0) {
return _yieldSource0;
}
if (currency == poolKey.currency1) {
return _yieldSource1;
}
revert InvalidCurrency();
}

// Exclude from coverage report
function test() public {}
}
55 changes: 55 additions & 0 deletions src/utils/LiquidityMath.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Uniswap Hooks (last updated v1.1.0) (src/utils/LiquidityMath.sol)

pragma solidity ^0.8.24;

import {BalanceDelta, toBalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {SqrtPriceMath} from "v4-core/src/libraries/SqrtPriceMath.sol";

import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol";

/**
* @dev Library with helper functions for liquidity math.
*/
library LiquidityMath {
using SafeCast for *;

/**
* @dev Calculates the delta necessary to provide a given `liquidity` amount to a pool, based
* on the pool's `currentTick` and `currentSqrtPriceX96` and the `tickLower` and `tickUpper`
* boundaries.
*/
function calculateDeltaForLiquidity(
uint128 liquidity,
int24 currentTick,
int24 tickLower,
int24 tickUpper,
uint160 currentSqrtPriceX96
) internal pure returns (BalanceDelta delta) {
if (currentTick < tickLower) {
delta = toBalanceDelta(
SqrtPriceMath.getAmount0Delta(
TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), int128(liquidity)
).toInt128(),
0
);
} else if (currentTick < tickUpper) {
delta = toBalanceDelta(
SqrtPriceMath.getAmount0Delta(
currentSqrtPriceX96, TickMath.getSqrtPriceAtTick(tickUpper), int128(liquidity)
).toInt128(),
SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtPriceAtTick(tickLower), currentSqrtPriceX96, int128(liquidity)
).toInt128()
);
} else {
delta = toBalanceDelta(
0,
SqrtPriceMath.getAmount1Delta(
TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), int128(liquidity)
).toInt128()
);
}
}
}
2 changes: 2 additions & 0 deletions src/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ NOTE: This document is better viewed on the docs page.
Libraries and general purpose utilities to help develop hooks.

* {CurrencySettler}: Library used to interact with the `PoolManager` to settle any open deltas, with support for ERC-6909 and native currencies.
* {LiquidityMath}: Library with helper functions for liquidity math.

== Libraries

{{CurrencySettler}}
{{LiquidityMath}}
248 changes: 248 additions & 0 deletions test/general/ReHypothecationHook.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {ReHypothecationHook} from "src/general/ReHypothecationHook.sol";
import {ReHypothecationMock, ERC4626Mock} from "src/mocks/ReHypothecationMock.sol";
import {HookTest} from "../utils/HookTest.sol";
import {BalanceDeltaAssertions} from "../utils/BalanceDeltaAssertions.sol";
import {IERC4626} from "openzeppelin/interfaces/IERC4626.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";

contract ReHypothecationHookTest is HookTest, BalanceDeltaAssertions {
using StateLibrary for IPoolManager;

ReHypothecationMock hook;
uint24 fee = 1000; // 0.1%

IERC4626 yieldSource0;
IERC4626 yieldSource1;

PoolKey noHookKey;

function setUp() public {
deployFreshManagerAndRouters();
deployMintAndApprove2Currencies();

yieldSource0 = IERC4626(new ERC4626Mock(IERC20(Currency.unwrap(currency0)), "Yield Source 0", "Y0"));
yieldSource1 = IERC4626(new ERC4626Mock(IERC20(Currency.unwrap(currency1)), "Yield Source 1", "Y1"));

hook = ReHypothecationMock(
address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG))
);
deployCodeTo("src/mocks/ReHypothecationMock.sol:ReHypothecationMock", abi.encode(manager), address(hook));

(key,) = initPool(currency0, currency1, IHooks(address(hook)), fee, SQRT_PRICE_1_1);
(noHookKey,) = initPool(currency0, currency1, IHooks(address(0)), fee, SQRT_PRICE_1_1);

hook.setYieldSources(address(yieldSource0), address(yieldSource1));

IERC20(Currency.unwrap(currency0)).approve(address(hook), type(uint256).max);
IERC20(Currency.unwrap(currency1)).approve(address(hook), type(uint256).max);

vm.label(Currency.unwrap(currency0), "currency0");
vm.label(Currency.unwrap(currency1), "currency1");
}

function test_already_initialized_reverts() public {
vm.expectRevert();
initPool(currency0, currency1, IHooks(address(hook)), fee, SQRT_PRICE_1_1);
}

function test_full_cycle() public {
uint128 liquidity = 1e15;
BalanceDelta delta = hook.addReHypothecatedLiquidity(liquidity);

assertEq(
IERC4626(address(yieldSource0)).balanceOf(address(hook)),
uint256(liquidity),
"YieldSource0 balance should be the same as the liquidity"
);
assertEq(
IERC4626(address(yieldSource1)).balanceOf(address(hook)),
uint256(liquidity),
"YieldSource1 balance should be the same as the liquidity"
);

assertEq(manager.getLiquidity(key.toId()), 0, "Liquidity should be 0");

assertEq(hook.balanceOf(address(this)), liquidity, "Hook balance should be the same as the liquidity");

// add rehypothecated liquidity should be equal to modifyPoolLiquidity with a pool with the same state
BalanceDelta expectedDelta =
modifyPoolLiquidity(noHookKey, hook.getTickLower(), hook.getTickUpper(), int256(uint256(liquidity)), 0);
assertEq(delta, expectedDelta, "Delta should be equal");

BalanceDelta swapDelta = swap(key, false, 1e14, ZERO_BYTES);
BalanceDelta noHookSwapDelta = swap(noHookKey, false, 1e14, ZERO_BYTES);

assertEq(swapDelta, noHookSwapDelta, "Swap delta should be equal");
assertEq(manager.getLiquidity(key.toId()), 0, "Liquidity should be 0");

assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(hook)), 0, "Currency0 balance should be 0");
assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(hook)), 0, "Currency1 balance should be 0");

assertApproxEqAbs(
IERC4626(address(yieldSource0)).balanceOf(address(hook)),
uint256(liquidity - uint128(swapDelta.amount0())),
2,
"YieldSource0 balance should go to user"
);
assertApproxEqAbs(
IERC4626(address(yieldSource1)).balanceOf(address(hook)),
uint256(uint128(int128(liquidity) - swapDelta.amount1())),
2,
"YieldSource1 balance should go to user"
);

delta = hook.removeReHypothecatedLiquidity(address(this));

expectedDelta =
modifyPoolLiquidity(noHookKey, hook.getTickLower(), hook.getTickUpper(), int256(-int128(liquidity)), 0);

assertEq(delta, expectedDelta, "Delta should be equal");

assertEq(manager.getLiquidity(key.toId()), 0, "Liquidity should be 0");

assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(hook)), 0, "Currency0 balance should be 0");
assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(hook)), 0, "Currency1 balance should be 0");

assertEq(IERC4626(address(yieldSource0)).balanceOf(address(hook)), 0, "YieldSource0 balance should be 0");
assertEq(IERC4626(address(yieldSource1)).balanceOf(address(hook)), 0, "YieldSource1 balance should be 0");

assertEq(hook.balanceOf(address(this)), 0, "Hook balance should be 0");
}

function test_swap_with_increased_shares() public {
uint128 liquidity = 1e15;
BalanceDelta delta = hook.addReHypothecatedLiquidity(liquidity);

IERC20(Currency.unwrap(currency0)).transfer(address(yieldSource0), 1e14); // 10% increase on currency0
IERC20(Currency.unwrap(currency1)).transfer(address(yieldSource1), 1e14); // 10% increase on currency1

assertEq(
IERC4626(address(yieldSource0)).balanceOf(address(hook)),
uint256(liquidity),
"YieldSource0 balance should be the same as the liquidity"
);
assertEq(
IERC4626(address(yieldSource1)).balanceOf(address(hook)),
uint256(liquidity),
"YieldSource1 balance should be the same as the liquidity"
);

assertEq(manager.getLiquidity(key.toId()), 0, "Liquidity should be 0");

assertEq(hook.balanceOf(address(this)), liquidity, "Hook balance should be the same as the liquidity");

// add rehypothecated liquidity should be equal to modifyPoolLiquidity with a pool with the same state
BalanceDelta expectedDelta =
modifyPoolLiquidity(noHookKey, hook.getTickLower(), hook.getTickUpper(), int256(uint256(liquidity)), 0);
assertEq(delta, expectedDelta, "Delta should be equal");

assertEq(manager.getLiquidity(key.toId()), 0, "Liquidity should be 0");

assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(hook)), 0, "Currency0 balance should be 0");
assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(hook)), 0, "Currency1 balance should be 0");

delta = hook.removeReHypothecatedLiquidity(address(this));

expectedDelta =
modifyPoolLiquidity(noHookKey, hook.getTickLower(), hook.getTickUpper(), int256(-int128(liquidity)), 0);

assertEq(manager.getLiquidity(key.toId()), 0, "Liquidity should be 0");

assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(hook)), 0, "Currency0 balance should be 0");
assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(hook)), 0, "Currency1 balance should be 0");

assertEq(IERC4626(address(yieldSource0)).balanceOf(address(hook)), 0, "YieldSource0 balance should be 0");
assertEq(IERC4626(address(yieldSource1)).balanceOf(address(hook)), 0, "YieldSource1 balance should be 0");

assertEq(hook.balanceOf(address(this)), 0, "Hook balance should be 0");
}

function test_add_rehypothecated_liquidity_zero_liquidity_reverts() public {
vm.expectRevert(ReHypothecationHook.ZeroLiquidity.selector);
hook.addReHypothecatedLiquidity(0);
}

function test_add_rehypothecated_liquidity_uninitialized_pool_key_reverts() public {
ReHypothecationMock newHook = ReHypothecationMock(
address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG) + 2 ** 96)
);

deployCodeTo(
"src/mocks/ReHypothecationMock.sol:ReHypothecationMock",
abi.encode(manager, address(yieldSource0), address(yieldSource1)),
address(newHook)
);
vm.expectRevert(ReHypothecationHook.PoolKeyNotInitialized.selector);
newHook.addReHypothecatedLiquidity(1e15);
}

function test_add_rehypothecated_liquidity_msg_value_reverts() public {
vm.expectRevert(ReHypothecationHook.InvalidMsgValue.selector);
hook.addReHypothecatedLiquidity{value: 1e5}(1e15);
}

function test_remove_rehypothecated_liquidity_zero_liquidity_reverts() public {
vm.expectRevert(ReHypothecationHook.ZeroLiquidity.selector);
hook.removeReHypothecatedLiquidity(address(this));
}

function test_remove_rehypothecated_liquidity_uninitialized_pool_key_reverts() public {
ReHypothecationMock newHook = ReHypothecationMock(
address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG) + 2 ** 96)
);

deployCodeTo(
"src/mocks/ReHypothecationMock.sol:ReHypothecationMock",
abi.encode(manager, address(yieldSource0), address(yieldSource1)),
address(newHook)
);
vm.expectRevert(ReHypothecationHook.PoolKeyNotInitialized.selector);
newHook.removeReHypothecatedLiquidity(address(this));
}

function test_add_rehypothecated_liquidity_invalid_currency_reverts() public {
ReHypothecationMock newHook = ReHypothecationMock(
address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG) + 2 ** 96)
);

deployCodeTo(
"src/mocks/ReHypothecationMock.sol:ReHypothecationMock",
abi.encode(manager, address(yieldSource0), address(yieldSource1)),
address(newHook)
);

initPool(Currency.wrap(address(0)), currency1, IHooks(address(newHook)), fee, SQRT_PRICE_1_1);

IERC20(Currency.unwrap(currency1)).approve(address(newHook), type(uint256).max);

vm.expectRevert(ReHypothecationHook.InvalidCurrency.selector);
newHook.addReHypothecatedLiquidity{value: 1e15}(1e15);
}

function test_add_rehypothecated_liquidity_native_reverts() public {
ReHypothecationMock newHook = ReHypothecationMock(
address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG) + 2 ** 96)
);

deployCodeTo(
"src/mocks/ReHypothecationMock.sol:ReHypothecationMock",
abi.encode(manager, address(yieldSource0), address(yieldSource1)),
address(newHook)
);

initPool(Currency.wrap(address(0)), currency1, IHooks(address(newHook)), fee, SQRT_PRICE_1_1);

vm.expectRevert(ReHypothecationHook.InvalidMsgValue.selector);
newHook.addReHypothecatedLiquidity{value: 1e14}(1e15);
}
}
Loading