-
Notifications
You must be signed in to change notification settings - Fork 504
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
205 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,88 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.24; | ||
|
||
import {BaseHook} from "../../BaseHook.sol"; | ||
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; | ||
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; | ||
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; | ||
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; | ||
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; | ||
import {Owned} from "solmate/auth/Owned.sol"; | ||
|
||
contract TakingFee is BaseHook, Owned { | ||
using PoolIdLibrary for PoolKey; | ||
using SafeCast for uint256; | ||
|
||
uint128 private constant TOTAL_BIPS = 10000; | ||
uint128 private constant MAX_BIPS = 100; | ||
uint128 public swapFeeBips; | ||
address public treasury = msg.sender; | ||
|
||
constructor( | ||
IPoolManager _poolManager, | ||
uint128 _swapFeeBips, | ||
address _treasury | ||
) BaseHook(_poolManager) Owned(msg.sender) { | ||
swapFeeBips = _swapFeeBips; | ||
treasury = _treasury; | ||
} | ||
|
||
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: false, | ||
afterSwap: true, | ||
beforeDonate: false, | ||
afterDonate: false, | ||
beforeSwapReturnDelta: false, | ||
afterSwapReturnDelta: true, | ||
afterAddLiquidityReturnDelta: false, | ||
afterRemoveLiquidityReturnDelta: false | ||
}); | ||
} | ||
|
||
function afterSwap( | ||
address, | ||
PoolKey calldata key, | ||
IPoolManager.SwapParams calldata params, | ||
BalanceDelta delta, | ||
bytes calldata | ||
) external override returns (bytes4, int128) { | ||
// fee will be in the unspecified token of the swap | ||
bool specifiedTokenIs0 = (params.amountSpecified < 0 == | ||
params.zeroForOne); | ||
(Currency feeCurrency, int128 swapAmount) = (specifiedTokenIs0) | ||
? (key.currency1, delta.amount1()) | ||
: (key.currency0, delta.amount0()); | ||
// if fee is on output, get the absolute output amount | ||
if (swapAmount < 0) swapAmount = -swapAmount; | ||
|
||
uint256 feeAmount = (uint128(swapAmount) * swapFeeBips) / TOTAL_BIPS; | ||
poolManager.take(feeCurrency, treasury, feeAmount); | ||
|
||
return (BaseHook.afterSwap.selector, feeAmount.toInt128()); | ||
} | ||
|
||
function setSwapFeeBips(uint128 _swapFeeBips) external onlyOwner { | ||
require(_swapFeeBips <= MAX_BIPS); | ||
swapFeeBips = _swapFeeBips; | ||
} | ||
|
||
function setTreasury(address _treasury) external onlyOwner { | ||
treasury = _treasury; | ||
} | ||
} |
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,101 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {Test} from "forge-std/Test.sol"; | ||
import {GetSender} from "./shared/GetSender.sol"; | ||
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; | ||
import {TakingFee} from "../contracts/hooks/examples/TakingFee.sol"; | ||
import {TakingFeeImplementation} from "./shared/implementation/TakingFeeImplementation.sol"; | ||
import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; | ||
import {TestERC20} from "@uniswap/v4-core/src/test/TestERC20.sol"; | ||
import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol"; | ||
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; | ||
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; | ||
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; | ||
import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; | ||
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; | ||
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; | ||
|
||
contract TakingFeeTest is Test, Deployers { | ||
using PoolIdLibrary for PoolKey; | ||
using StateLibrary for IPoolManager; | ||
|
||
uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; | ||
|
||
address constant TREASURY = address(0x1234567890123456789012345678901234567890); | ||
uint128 private constant TOTAL_BIPS = 10000; | ||
|
||
HookEnabledSwapRouter router; | ||
TestERC20 token0; | ||
TestERC20 token1; | ||
TakingFee takingFee = TakingFee(address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG))); | ||
PoolId id; | ||
|
||
function setUp() public { | ||
deployFreshManagerAndRouters(); | ||
(currency0, currency1) = deployMintAndApprove2Currencies(); | ||
|
||
router = new HookEnabledSwapRouter(manager); | ||
token0 = TestERC20(Currency.unwrap(currency0)); | ||
token1 = TestERC20(Currency.unwrap(currency1)); | ||
|
||
vm.record(); | ||
TakingFeeImplementation impl = new TakingFeeImplementation(manager, 25, TREASURY, takingFee); | ||
(, bytes32[] memory writes) = vm.accesses(address(impl)); | ||
vm.etch(address(takingFee), address(impl).code); | ||
// for each storage key that was written during the hook implementation, copy the value over | ||
unchecked { | ||
for (uint256 i = 0; i < writes.length; i++) { | ||
bytes32 slot = writes[i]; | ||
vm.store(address(takingFee), slot, vm.load(address(impl), slot)); | ||
} | ||
} | ||
|
||
// key = PoolKey(currency0, currency1, 3000, 60, takingFee); | ||
(key, id) = initPoolAndAddLiquidity(currency0, currency1, takingFee, 3000, SQRT_PRICE_1_1, ZERO_BYTES); | ||
|
||
token0.approve(address(takingFee), type(uint256).max); | ||
token1.approve(address(takingFee), type(uint256).max); | ||
token0.approve(address(router), type(uint256).max); | ||
token1.approve(address(router), type(uint256).max); | ||
} | ||
|
||
function testSwapHooks() public { | ||
// rounding for tests | ||
uint128 ROUND_FACTOR = 8; | ||
|
||
// positions were created in setup() | ||
assertEq(currency0.balanceOf(TREASURY), 0); | ||
assertEq(currency1.balanceOf(TREASURY), 0); | ||
|
||
// Perform a test swap // | ||
bool zeroForOne = true; | ||
int256 amountSpecified = -1e12; // negative number indicates exact input swap | ||
BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); | ||
// ------------------- // | ||
|
||
uint128 output = uint128(swapDelta.amount1()); | ||
assertFalse(output == 0); | ||
|
||
uint256 expectedFee = output * TOTAL_BIPS/(TOTAL_BIPS - takingFee.swapFeeBips()) - output; | ||
|
||
assertEq(currency0.balanceOf(TREASURY), 0); | ||
assertEq(currency1.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee / ROUND_FACTOR); | ||
|
||
// Perform a test swap // | ||
bool zeroForOne2 = true; | ||
int256 amountSpecified2 = 1e12; // positive number indicates exact output swap | ||
BalanceDelta swapDelta2 = swap(key, zeroForOne2, amountSpecified2, ZERO_BYTES); | ||
// ------------------- // | ||
|
||
uint128 input = uint128(-swapDelta2.amount0()); | ||
assertFalse(input == 0); | ||
|
||
uint128 expectedFee2 = (input * takingFee.swapFeeBips()) / (TOTAL_BIPS + takingFee.swapFeeBips()); | ||
|
||
assertEq(currency0.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee2 / ROUND_FACTOR); | ||
assertEq(currency1.balanceOf(TREASURY) / ROUND_FACTOR, expectedFee / ROUND_FACTOR); | ||
} | ||
} |
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,16 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.19; | ||
|
||
import {BaseHook} from "../../../contracts/BaseHook.sol"; | ||
import {TakingFee} from "../../../contracts/hooks/examples/TakingFee.sol"; | ||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; | ||
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; | ||
|
||
contract TakingFeeImplementation is TakingFee { | ||
constructor(IPoolManager _poolManager, uint128 _swapFeeBips, address _treasury, TakingFee addressToEtch) TakingFee(_poolManager, _swapFeeBips, _treasury) { | ||
Hooks.validateHookPermissions(addressToEtch, getHookPermissions()); | ||
} | ||
|
||
// make this a no-op in testing | ||
function validateHookAddress(BaseHook _this) internal pure override {} | ||
} |