Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi CurrencyDeltas Library #245

Merged
merged 5 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
saucepoint marked this conversation as resolved.
Show resolved Hide resolved
/// @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());
}
}
85 changes: 85 additions & 0 deletions test/libraries/CurrencyDeltas.t.sol
Original file line number Diff line number Diff line change
@@ -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 {
saucepoint marked this conversation as resolved.
Show resolved Hide resolved
depth = bound(depth, 1, 200);
seed = bound(seed, 1, 200);
saucepoint marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
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);
}
}
Loading