From 642e31ce18a50b1d82f78ddf23374c1913a522ee Mon Sep 17 00:00:00 2001 From: saucepoint Date: Thu, 1 Aug 2024 20:16:42 -0400 Subject: [PATCH] i might just be insane --- src/libraries/CurrencyDeltas.sol | 42 +++++++++++++ test/libraries/CurrencyDeltas.t.sol | 85 ++++++++++++++++++++++++++ test/mocks/MockCurrencyDeltaReader.sol | 63 +++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 src/libraries/CurrencyDeltas.sol create mode 100644 test/libraries/CurrencyDeltas.t.sol create mode 100644 test/mocks/MockCurrencyDeltaReader.sol diff --git a/src/libraries/CurrencyDeltas.sol b/src/libraries/CurrencyDeltas.sol new file mode 100644 index 00000000..194d4366 --- /dev/null +++ b/src/libraries/CurrencyDeltas.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; + +/// @title Currency Deltas +/// @notice Fetch two currency deltas in a single call +library CurrencyDeltas { + using SafeCast for int256; + + /// @notice Get the current delta for a caller in the two given currencies + /// @param caller_ The address of the caller + /// @param currency0 The currency to lookup the delta + /// @param currency1 The other currency to lookup the delta + /// @return BalanceDelta The delta of the two currencies packed + /// amount0 corresponding to currency0 and amount1 corresponding to currency1 + function currencyDeltas(IPoolManager manager, address caller_, Currency currency0, Currency currency1) + internal + view + returns (BalanceDelta) + { + bytes32 tloadSlot0; + bytes32 tloadSlot1; + assembly { + mstore(0, caller_) + mstore(32, currency0) + tloadSlot0 := keccak256(0, 64) + + mstore(0, caller_) + mstore(32, currency1) + tloadSlot1 := keccak256(0, 64) + } + bytes32[] memory slots = new bytes32[](2); + slots[0] = tloadSlot0; + slots[1] = tloadSlot1; + bytes32[] memory result = manager.exttload(slots); + return toBalanceDelta(int256(uint256(result[0])).toInt128(), int256(uint256(result[1])).toInt128()); + } +} diff --git a/test/libraries/CurrencyDeltas.t.sol b/test/libraries/CurrencyDeltas.t.sol new file mode 100644 index 00000000..a6f9f2e3 --- /dev/null +++ b/test/libraries/CurrencyDeltas.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; + +import {MockCurrencyDeltaReader} from "../mocks/MockCurrencyDeltaReader.sol"; + +contract CurrencyDeltasTest is Test, Deployers { + using CurrencyLibrary for Currency; + + MockCurrencyDeltaReader reader; + + function setUp() public { + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + reader = new MockCurrencyDeltaReader(manager); + + IERC20 token0 = IERC20(Currency.unwrap(currency0)); + IERC20 token1 = IERC20(Currency.unwrap(currency1)); + + token0.approve(address(reader), type(uint256).max); + token1.approve(address(reader), type(uint256).max); + + // send tokens to PoolManager so tests can .take() + token0.transfer(address(manager), 1_000_000e18); + token1.transfer(address(manager), 1_000_000e18); + + // convert some ERC20s into ERC6909 + claimsRouter.deposit(currency0, address(this), 1_000_000e18); + claimsRouter.deposit(currency1, address(this), 1_000_000e18); + manager.approve(address(reader), currency0.toId(), type(uint256).max); + manager.approve(address(reader), currency1.toId(), type(uint256).max); + } + + function test_fuzz_currencyDeltas(uint256 depth, uint256 seed, uint128 amount0, uint128 amount1) public { + depth = bound(depth, 1, 200); + seed = bound(seed, 1, 200); + amount0 = uint128(bound(amount0, 1, 100e18)); + amount1 = uint128(bound(amount1, 1, 100e18)); + + int128 delta0Expected = 0; + int128 delta1Expected = 0; + + bytes[] memory calls = new bytes[](depth); + for (uint256 i = 0; i < depth; i++) { + uint256 _seed = i % seed; + if (_seed % 8 == 0) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.settle.selector, currency0, amount0); + delta0Expected += int128(amount0); + } else if (_seed % 8 == 1) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.settle.selector, currency1, amount1); + delta1Expected += int128(amount1); + } else if (_seed % 8 == 2) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.burn.selector, currency0, amount0); + delta0Expected += int128(amount0); + } else if (_seed % 8 == 3) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.burn.selector, currency1, amount1); + delta1Expected += int128(amount1); + } else if (_seed % 8 == 4) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.take.selector, currency0, amount0); + delta0Expected -= int128(amount0); + } else if (_seed % 8 == 5) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.take.selector, currency1, amount1); + delta1Expected -= int128(amount1); + } else if (_seed % 8 == 6) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.mint.selector, currency0, amount0); + delta0Expected -= int128(amount0); + } else if (_seed % 8 == 7) { + calls[i] = abi.encodeWithSelector(MockCurrencyDeltaReader.mint.selector, currency1, amount1); + delta1Expected -= int128(amount1); + } + } + + BalanceDelta delta = reader.execute(calls, currency0, currency1); + assertEq(delta.amount0(), delta0Expected); + assertEq(delta.amount1(), delta1Expected); + } +} diff --git a/test/mocks/MockCurrencyDeltaReader.sol b/test/mocks/MockCurrencyDeltaReader.sol new file mode 100644 index 00000000..20918220 --- /dev/null +++ b/test/mocks/MockCurrencyDeltaReader.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; + +import {CurrencyDeltas} from "../../src/libraries/CurrencyDeltas.sol"; + +/// @dev A minimal helper strictly for testing +contract MockCurrencyDeltaReader { + using CurrencyDeltas for IPoolManager; + using CurrencySettler for Currency; + + IPoolManager public poolManager; + + address sender; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + /// @param calls an array of abi.encodeWithSelector + function execute(bytes[] calldata calls, Currency currency0, Currency currency1) external returns (BalanceDelta) { + sender = msg.sender; + return abi.decode(poolManager.unlock(abi.encode(calls, currency0, currency1)), (BalanceDelta)); + } + + function unlockCallback(bytes calldata data) external returns (bytes memory) { + (bytes[] memory calls, Currency currency0, Currency currency1) = abi.decode(data, (bytes[], Currency, Currency)); + for (uint256 i; i < calls.length; i++) { + (bool success,) = address(this).call(calls[i]); + if (!success) revert("CurrencyDeltaReader"); + } + + // close deltas + BalanceDelta delta = poolManager.currencyDeltas(address(this), currency0, currency1); + if (delta.amount0() < 0) currency0.settle(poolManager, sender, uint256(-int256(delta.amount0())), false); + if (delta.amount1() < 0) currency1.settle(poolManager, sender, uint256(-int256(delta.amount1())), false); + if (delta.amount0() > 0) currency0.take(poolManager, sender, uint256(int256(delta.amount0())), false); + if (delta.amount1() > 0) currency1.take(poolManager, sender, uint256(int256(delta.amount1())), false); + return abi.encode(delta); + } + + function settle(Currency currency, uint256 amount) external { + currency.settle(poolManager, sender, amount, false); + } + + function burn(Currency currency, uint256 amount) external { + currency.settle(poolManager, sender, amount, true); + } + + function take(Currency currency, uint256 amount) external { + currency.take(poolManager, sender, amount, false); + } + + function mint(Currency currency, uint256 amount) external { + currency.take(poolManager, sender, amount, true); + } +}