From fff84137f0e7fbcc555cf92ba6517053c9a7d6e4 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:56:23 -0400 Subject: [PATCH] Multi CurrencyDeltas Library (#245) * i might just be insane * additional assertion * fix pseudorandom seeding * do not bound seed * pr feedback --- src/libraries/CurrencyDeltas.sol | 42 +++++++++++++ test/libraries/CurrencyDeltas.t.sol | 82 ++++++++++++++++++++++++++ test/mocks/MockCurrencyDeltaReader.sol | 71 ++++++++++++++++++++++ 3 files changed, 195 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..2a7b85f8 --- /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..53dad9e4 --- /dev/null +++ b/test/libraries/CurrencyDeltas.t.sol @@ -0,0 +1,82 @@ +// 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(uint8 depth, uint256 seed, uint128 amount0, uint128 amount1) public { + int128 delta0Expected = 0; + int128 delta1Expected = 0; + + bytes[] memory calls = new bytes[](depth); + for (uint256 i = 0; i < depth; i++) { + amount0 = uint128(bound(amount0, 1, 100e18)); + amount1 = uint128(bound(amount1, 1, 100e18)); + uint256 _seed = seed % (i + 1); + 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..94aa3db7 --- /dev/null +++ b/test/mocks/MockCurrencyDeltaReader.sol @@ -0,0 +1,71 @@ +// 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 {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.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 TransientStateLibrary for IPoolManager; + 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"); + } + + BalanceDelta delta = poolManager.currencyDeltas(address(this), currency0, currency1); + int256 delta0 = poolManager.currencyDelta(address(this), currency0); + int256 delta1 = poolManager.currencyDelta(address(this), currency1); + + // confirm agreement between currencyDeltas and single-read currencyDelta + require(delta.amount0() == int128(delta0), "CurrencyDeltaReader: delta0"); + require(delta.amount1() == int128(delta1), "CurrencyDeltaReader: delta1"); + + // close deltas + 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); + } +}