Skip to content

Commit

Permalink
Multi CurrencyDeltas Library (#245)
Browse files Browse the repository at this point in the history
* i might just be insane

* additional assertion

* fix pseudorandom seeding

* do not bound seed

* pr feedback
  • Loading branch information
saucepoint authored Aug 2, 2024
1 parent 598b02e commit fff8413
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 0 deletions.
42 changes: 42 additions & 0 deletions src/libraries/CurrencyDeltas.sol
Original file line number Diff line number Diff line change
@@ -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());
}
}
82 changes: 82 additions & 0 deletions test/libraries/CurrencyDeltas.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
71 changes: 71 additions & 0 deletions test/mocks/MockCurrencyDeltaReader.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit fff8413

Please sign in to comment.