diff --git a/foundry.toml b/foundry.toml index 25b918f..da273b1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,10 @@ src = "src" out = "out" libs = ["lib"] +evm_version = "cancun" +optimizer_runs = 800 +via_ir = false +ffi = true +verbosity = 2 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/script/Counter.s.sol b/script/Counter.s.solx similarity index 100% rename from script/Counter.s.sol rename to script/Counter.s.solx diff --git a/script/HookDonation.s.sol b/script/HookDonation.s.sol new file mode 100644 index 0000000..df70d99 --- /dev/null +++ b/script/HookDonation.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; +import {AfterSwapDonationHook} from "../src/HookDonation.sol"; + +contract CounterScript is Script { + AfterSwapDonationHook public donationHook; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + // donationHook = new AfterSwapDonationHook(); + + vm.stopBroadcast(); + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index cae77ad..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/HookDonation.sol b/src/HookDonation.sol new file mode 100644 index 0000000..6e64222 --- /dev/null +++ b/src/HookDonation.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {BaseHook} from "lib/v4-periphery/src/base/hooks/BaseHook.sol"; +import {PoolKey} from "lib/v4-periphery/lib/v4-core/src/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "lib/v4-periphery/lib/v4-core/src/types/BalanceDelta.sol"; +import {Hooks} from "lib/v4-periphery/lib/v4-core/src/libraries/Hooks.sol"; +import {CurrencyLibrary, Currency} from "v4-core/types/Currency.sol"; + +contract AfterSwapDonationHook is BaseHook { + using CurrencyLibrary for Currency; + struct DonationMapping { + bool enabled; + address payable recipient; + uint256 percent; // how much to donate + } + address public owner; + address public pool; + mapping(address => DonationMapping) donationMap; + +// -------------- begin donation associated functions --------------- + function disableDonation() public { + // Reset the value to the default value. + delete donationMap[msg.sender]; + } + + function enableDonation(address recipient, uint256 percent) public { + DonationMapping memory local; + local.recipient = payable(recipient); + local.percent = percent; + + donationMap[msg.sender] = local; + } + + // the following should all have internal view, not public + // but have been changed to public view for testing + + function donationEnabled(address payee) public view returns (bool) { + return (donationMap[payee].recipient != payable(0x0)); + } + + function donationPercent(address payee) public view returns (uint256) { + return (donationMap[payee].percent); + } + + function donationRecipient(address payee) public view returns (address) { + return (donationMap[payee].recipient); + } +// -------------- end donation associated functions --------------- + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) { + owner = msg.sender; + } + + // Modifier to restrict access to the owner + modifier onlyOwner() { + require(msg.sender == owner, "Not authorized"); + _; + } + + /// @notice The hook called after a swap + /// @param sender The initial msg.sender for the swap call + /// @param key The key for the pool + /// @param swapParams The parameters for the swap + /// @param delta The amount owed to the caller (positive) or owed to the pool (negative) + /// @param ...Arbitrary data handed into the PoolManager by the swapper to be passed on to the hook + /// @return bytes4 The function selector for the hook + /// @return int128 The hook's delta in unspecified currency. Positive: the hook is owed/took currency, negative: the hook owes/sent currency + function afterSwap( + address sender, + PoolKey calldata key, + IPoolManager.SwapParams calldata swapParams, + BalanceDelta delta, + bytes calldata + ) external override returns (bytes4, int128) { + require(msg.sender == address(pool), "Unauthorized caller"); + + // Check that donation is enabled for the sender + if (!donationEnabled(sender)) + return (this.afterSwap.selector, 0); + + uint256 spendAmount = swapParams.amountSpecified < 0 + ? uint256(-swapParams.amountSpecified) + : uint256(int256(-delta.amount0())); + + uint256 donationAmount = (spendAmount * donationPercent(sender)) / 100; + address recipient = donationRecipient(sender); + + key.currency0.transfer(recipient, donationAmount); + + return (this.afterSwap.selector, 0); + } + + // Function to update the pool address if needed + function updatePool(address _newPool) external onlyOwner { + pool = _newPool; + } + + // Only for other apps. Uniswap doesn't call this. + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return + Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: false, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 667e106..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/HookDonation.t.sol b/test/HookDonation.t.sol new file mode 100644 index 0000000..47a9d1b --- /dev/null +++ b/test/HookDonation.t.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +// forge-std/=lib/v4-periphery/lib/v4-core/lib/forge-std/src/ +import "lib/v4-periphery/lib/v4-core/lib/forge-std/src/Test.sol"; +import "lib/v4-periphery/lib/v4-core/lib/forge-std/src/console.sol"; +import {Hooks} from "lib/v4-core/src/libraries/Hooks.sol"; +import {Deployers} from "lib/v4-core/test/utils/Deployers.sol"; +import {IPoolManager} from "lib/v4-core/src/interfaces/IPoolManager.sol"; +import {AfterSwapDonationHook} from "../src/HookDonation.sol"; +import {Currency, CurrencyLibrary} from "lib/v4-core/src/types/Currency.sol"; +// solmate/=lib/v4-core/lib/solmate/ +import {MockERC20} from "lib/v4-core/lib/solmate/src/test/utils/mocks/MockERC20.sol"; +import {PoolKey} from "lib/v4-core/src/types/PoolKey.sol"; +import {IHooks} from "lib/v4-core/src/interfaces/IHooks.sol"; + +contract DonationTest is Test, Deployers { + using CurrencyLibrary for Currency; + + struct DonationMapping { + bool enabled; + address payable recipient; + uint256 percent; // how much to donate + } + mapping(address => DonationMapping) donationMap; + + // address constant USDT_MOCK_ADDRESS = address(0xEce6af52f8eDF69dd2C216b9C3f184e5b31750e9); // mock address + // address constant USDC_MOCK_ADDRESS = address(0x63ba29cAF4c40DaDA8a61D10AB5D2728c806b61f); // mock address + + AfterSwapDonationHook donationHook; + // The two currencies (tokens) from the pool + Currency token0; + Currency token1; + PoolKey globalKey; + address constant RECIPIENT = address(0x01); + + event HookAddress(address indexed hookAddress); + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + (currency0, currency1) = Deployers.deployMintAndApprove2Currencies(); + (token0, token1) = (currency0, currency1); + + // Deploy the hook to an address with the correct flags + uint160 flags = uint160( + Hooks.AFTER_SWAP_FLAG + ); + + address hookAddress = address(flags); + deployCodeTo( + "HookDonation.sol", + abi.encode(manager, ""), + hookAddress + ); + emit HookAddress(hookAddress); + donationHook = AfterSwapDonationHook(hookAddress); + console.log("setUp Hook Address: ", hookAddress); + console.log("donation Hook: ", address(donationHook)); + + // Approve our hook address to spend these tokens as well + MockERC20(Currency.unwrap(token0)).approve( + address(donationHook), + type(uint256).max + ); + MockERC20(Currency.unwrap(token1)).approve( + address(donationHook), + type(uint256).max + ); + // Approve swapRouter to spend these tokens as well + MockERC20(Currency.unwrap(token0)).approve( + address(swapRouter), + type(uint256).max + ); + MockERC20(Currency.unwrap(token1)).approve( + address(swapRouter), + type(uint256).max + ); + + // Initialize a pool with these two tokens + (key, ) = initPool(token0, token1, IHooks(hookAddress), 3000, SQRT_PRICE_1_1, ZERO_BYTES); + globalKey = key; + } + + function donationEnabled(address payee) public view returns (bool) { + return (donationMap[payee].recipient != payable(0x0)); + } + + function donationRecipient(address payee) public view returns (address) { + return (donationMap[payee].recipient); + } + + function enableDonation(address recipient, uint256 percent) public { + DonationMapping memory local; + local.recipient = payable(recipient); + local.percent = percent; + + donationMap[msg.sender] = local; + } + + function test_internalEnableDonation() public { + address payee = msg.sender; + bool enabled = donationEnabled(payee); + address recipient = donationRecipient(payee); + console.log("Before enabling donation"); + console.log("--------------------------------------------------------------------------------"); + console.log("enabled: %s", enabled); + console.log("recipient: %s", recipient); + console.log(); + + enableDonation(RECIPIENT, 10); // recipient = 0x01, 10 percent + console.log("After enabling donation"); + console.log("--------------------------------------------------------------------------------"); + enabled = donationEnabled(payee); + recipient = donationRecipient(payee); + console.log("enabled: %s", enabled); + console.log("recipient: %s", recipient); + } + + function test_enableDonation() public { + address payee = msg.sender; + bool enabled = donationHook.donationEnabled(payee); + address recipient = donationHook.donationRecipient(payee); + console.log("Before enabling donation"); + console.log("--------------------------------------------------------------------------------"); + console.log("enabled: %s", enabled); + console.log("recipient: %s", recipient); + console.log(); + + donationHook.enableDonation(RECIPIENT, 10); // recipient = 0x01, 10 percent + console.log("After enabling donation"); + console.log("--------------------------------------------------------------------------------"); + enabled = donationHook.donationEnabled(payee); + recipient = donationHook.donationRecipient(payee); + console.log("enabled: %s", enabled); + console.log("recipient: %s", recipient); + } + + function test_Donation() public { + bool zeroForOne = true; + // PoolKey memory pool = PoolKey( + // token0, token1, 3000, 60, IHooks(address(donationHook)) + // ); + PoolKey memory pool = globalKey; + bytes memory data = abi.encode(msg.sender); + address recipient = address(0x01); + console.log("Donation not enabled"); + console.log("Donation enabled for %s: %s", msg.sender, donationHook.donationEnabled(msg.sender)); + console.log("Donation recipient for %s: %s", msg.sender, donationHook.donationRecipient(msg.sender)); + + console.log("Donation enabled: 10%%"); + donationHook.enableDonation(RECIPIENT, 10); // recipient = 0x01, 10 percent + console.log("Test Donation sender: ", msg.sender); + + console.log("Donation enabled for %s: %s", msg.sender, donationHook.donationEnabled(msg.sender)); + console.log("Donation recipient for %s: %s", msg.sender, donationHook.donationRecipient(msg.sender)); + + console.log("beforeSwap Balance token0: ", token0.balanceOf(msg.sender)); + console.log("beforeSwap Balance token1: ", token1.balanceOf(msg.sender)); + console.log(); + console.log("beforeSwap Balance token0: ", token0.balanceOf(recipient)); + console.log("beforeSwap Balance token1: ", token1.balanceOf(recipient)); + + + int256 amountSpecified = 10; + Deployers.swap(pool, zeroForOne, amountSpecified, data); + + console.log(" afterSwap Balance token0: ", token0.balanceOf(msg.sender)); + console.log(" afterSwap Balance token1: ", token1.balanceOf(msg.sender)); + console.log(" afterSwap Balance token0: ", token0.balanceOf(recipient)); + console.log(" afterSwap Balance token1: ", token1.balanceOf(recipient)); + } + +} diff --git a/test/utils/HookMiner.sol b/test/utils/HookMiner.sol new file mode 100644 index 0000000..4f1d696 --- /dev/null +++ b/test/utils/HookMiner.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +/// @title HookMiner - a library for mining hook addresses +/// @dev This library is intended for `forge test` environments. There may be gotchas when using salts in `forge script` or `forge create` +library HookMiner { + // mask to slice out the top 10 bits of the address + uint160 constant FLAG_MASK = 0x3FF << 150; + + // Maximum number of iterations to find a salt, avoid infinite loops + uint256 constant MAX_LOOP = 10_000; + + /// @notice Find a salt that produces a hook address with the desired `flags` + /// @param deployer The address that will deploy the hook. + /// In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param flags The desired flags for the hook address + /// @param seed Use 0 for as a default. An optional starting salt when linearly searching for a salt + /// Useful for finding salts for multiple hooks with the same flags + /// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode` + /// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))` + /// @return hookAddress salt and corresponding address that was found + /// The salt can be used in `new Hook{salt: salt}()` + function find( + address deployer, + uint160 flags, + uint256 seed, + bytes memory creationCode, + bytes memory constructorArgs + ) external pure returns (address, bytes32) { + address hookAddress; + bytes memory creationCodeWithArgs = abi.encodePacked( + creationCode, + constructorArgs + ); + + uint256 salt = seed; + for (salt; salt < MAX_LOOP; ) { + hookAddress = computeAddress(deployer, salt, creationCodeWithArgs); + if (uint160(hookAddress) & FLAG_MASK == flags) { + return (hookAddress, bytes32(salt)); + } + + unchecked { + ++salt; + } + } + + revert("HookMiner: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE2 + /// @param deployer The address that will deploy the hook + /// In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param salt The salt used to deploy the hook + /// @param creationCode The creation code of a hook contract + function computeAddress( + address deployer, + uint256 salt, + bytes memory creationCode + ) public pure returns (address hookAddress) { + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xFF), + deployer, + salt, + keccak256(creationCode) + ) + ) + ) + ) + ); + } +}