mirror of
https://github.com/lux-amm/v4-core.git
synced 2026-10-30 20:54:53 +00:00
376 lines
15 KiB
Solidity
376 lines
15 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
|
|
|
|
import {Deployers} from "./utils/Deployers.sol";
|
|
import {IHooks} from "../src/interfaces/IHooks.sol";
|
|
import {Currency} from "../src/types/Currency.sol";
|
|
import {IPoolManager} from "../src/interfaces/IPoolManager.sol";
|
|
import {PoolSwapTest} from "../src/test/PoolSwapTest.sol";
|
|
import {IUnlockCallback} from "../src/interfaces/callback/IUnlockCallback.sol";
|
|
import {PoolKey} from "../src/types/PoolKey.sol";
|
|
import {ModifyLiquidityParams} from "../src/types/PoolOperation.sol";
|
|
import {ActionsRouter, Actions} from "../src/test/ActionsRouter.sol";
|
|
import {SafeCast} from "../src/libraries/SafeCast.sol";
|
|
import {CurrencyReserves} from "../src/libraries/CurrencyReserves.sol";
|
|
import {StateLibrary} from "../src/libraries/StateLibrary.sol";
|
|
import {TransientStateLibrary} from "../src/libraries/TransientStateLibrary.sol";
|
|
import {NativeERC20} from "../src/test/NativeERC20.sol";
|
|
import {IPoolManager} from "../src/interfaces/IPoolManager.sol";
|
|
import {CurrencyLibrary} from "../src/types/Currency.sol";
|
|
|
|
contract SyncTest is Test, Deployers {
|
|
using StateLibrary for IPoolManager;
|
|
using TransientStateLibrary for IPoolManager;
|
|
|
|
// PoolManager has no balance of currency2.
|
|
Currency currency2;
|
|
|
|
function setUp() public {
|
|
initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0)));
|
|
currency2 = deployMintAndApproveCurrency();
|
|
}
|
|
|
|
function test_sync_multiple_unlocked() public noIsolate {
|
|
manager.sync(currency1);
|
|
assertEq(Currency.unwrap(currency1), Currency.unwrap(manager.getSyncedCurrency()));
|
|
manager.sync(currency0);
|
|
assertEq(Currency.unwrap(currency0), Currency.unwrap(manager.getSyncedCurrency()));
|
|
}
|
|
|
|
function test_sync_balanceIsZero() public {
|
|
assertEq(currency2.balanceOf(address(manager)), uint256(0));
|
|
|
|
Actions[] memory actions = new Actions[](2);
|
|
bytes[] memory params = new bytes[](2);
|
|
|
|
actions[0] = Actions.SYNC;
|
|
params[0] = abi.encode(currency2);
|
|
|
|
actions[1] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[1] = abi.encode(0);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
|
|
assertEq(currency2.balanceOf(address(manager)), uint256(0));
|
|
}
|
|
|
|
function test_sync_balanceIsNonzero() public {
|
|
uint256 currency0Balance = currency0.balanceOf(address(manager));
|
|
assertGt(currency0Balance, uint256(0));
|
|
|
|
Actions[] memory actions = new Actions[](4);
|
|
bytes[] memory params = new bytes[](4);
|
|
|
|
actions[0] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[0] = abi.encode(0);
|
|
|
|
actions[1] = Actions.SYNC;
|
|
params[1] = abi.encode(currency0);
|
|
|
|
actions[2] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[2] = abi.encode(currency0Balance);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
|
|
uint256 balance = currency0.balanceOf(address(manager));
|
|
assertEq(balance, currency0Balance, "balance not equal");
|
|
}
|
|
|
|
function test_settle_withStartingBalance() public {
|
|
assertGt(currency0.balanceOf(address(manager)), uint256(0));
|
|
|
|
PoolSwapTest.TestSettings memory testSettings =
|
|
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
|
|
|
|
// Sync has not been called.
|
|
assertEq(manager.getSyncedReserves(), 0);
|
|
|
|
swapRouter.swap(key, SWAP_PARAMS, testSettings, new bytes(0));
|
|
(uint256 balanceCurrency0) = currency0.balanceOf(address(manager));
|
|
|
|
Actions[] memory actions = new Actions[](2);
|
|
bytes[] memory params = new bytes[](2);
|
|
|
|
actions[0] = Actions.SYNC;
|
|
params[0] = abi.encode(currency0);
|
|
|
|
actions[1] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[1] = abi.encode(balanceCurrency0);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
}
|
|
|
|
function test_settle_withNoStartingBalance() public {
|
|
assertEq(currency2.balanceOf(address(manager)), uint256(0));
|
|
|
|
(Currency cur0, Currency cur1) = currency0 < currency2 ? (currency0, currency2) : (currency2, currency0);
|
|
PoolKey memory key2 =
|
|
PoolKey({currency0: cur0, currency1: cur1, fee: 3000, tickSpacing: 60, hooks: IHooks(address(0))});
|
|
|
|
manager.initialize(key2, SQRT_PRICE_1_1);
|
|
|
|
modifyLiquidityRouter.modifyLiquidity(key2, ModifyLiquidityParams(-60, 60, 100, 0), new bytes(0));
|
|
(uint256 balanceCurrency2) = currency2.balanceOf(address(manager));
|
|
|
|
Actions[] memory actions = new Actions[](2);
|
|
bytes[] memory params = new bytes[](2);
|
|
|
|
actions[0] = Actions.SYNC;
|
|
params[0] = abi.encode(currency2);
|
|
|
|
actions[1] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[1] = abi.encode(balanceCurrency2);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
}
|
|
|
|
function test_settle_payOnBehalf(address taker, uint256 amount) public {
|
|
vm.assume(taker != address(actionsRouter));
|
|
amount = bound(amount, 1, uint256(int256(type(int128).max)));
|
|
MockERC20(Currency.unwrap(currency2)).approve(address(actionsRouter), type(uint256).max);
|
|
MockERC20(Currency.unwrap(currency2)).mint(address(manager), amount);
|
|
|
|
Actions[] memory actions = new Actions[](6);
|
|
bytes[] memory params = new bytes[](6);
|
|
|
|
actions[0] = Actions.PRANK_TAKE_FROM;
|
|
params[0] = abi.encode(currency2, taker, taker, amount);
|
|
|
|
actions[1] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[1] = abi.encode(currency2, taker, int256(amount) * -1);
|
|
|
|
actions[2] = Actions.SYNC;
|
|
params[2] = abi.encode(currency2);
|
|
|
|
actions[3] = Actions.TRANSFER_FROM;
|
|
params[3] = abi.encode(currency2, address(this), address(manager), amount);
|
|
|
|
actions[4] = Actions.SETTLE_FOR;
|
|
params[4] = abi.encode(taker);
|
|
|
|
actions[5] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[5] = abi.encode(currency2, taker, 0);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
}
|
|
|
|
/// @notice When there is no balance and reserves are set to 0, no delta should be applied.
|
|
function test_settle_noBalanceInPool_shouldNotApplyDelta() public {
|
|
assertEq(currency2.balanceOf(address(manager)), uint256(0));
|
|
|
|
Actions[] memory actions = new Actions[](4);
|
|
bytes[] memory params = new bytes[](4);
|
|
|
|
actions[0] = Actions.SYNC;
|
|
params[0] = abi.encode(currency2);
|
|
|
|
actions[1] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[1] = abi.encode(0);
|
|
|
|
actions[2] = Actions.SETTLE;
|
|
|
|
actions[3] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[3] = abi.encode(currency2, address(actionsRouter), 0);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
}
|
|
|
|
/// @notice When there is a balance, no delta should be applied.
|
|
function test_settle_balanceInPool_shouldNotApplyDelta() public {
|
|
uint256 currency0Balance = currency0.balanceOf(address(manager));
|
|
assertGt(currency0Balance, uint256(0));
|
|
|
|
Actions[] memory actions = new Actions[](5);
|
|
bytes[] memory params = new bytes[](5);
|
|
|
|
actions[0] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[0] = abi.encode(0);
|
|
|
|
actions[1] = Actions.SYNC;
|
|
params[1] = abi.encode(currency0);
|
|
|
|
actions[2] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[2] = abi.encode(currency0Balance);
|
|
|
|
actions[3] = Actions.SETTLE;
|
|
|
|
actions[4] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[4] = abi.encode(currency0, address(actionsRouter), 0);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
}
|
|
|
|
// @notice This tests expected behavior if you DO NOT call sync before a non native settle. (ie. Do not interact with the pool manager properly. You can lose funds.)
|
|
function test_settle_nonNative_withoutSync_loseFunds() public {
|
|
MockERC20(Currency.unwrap(currency0)).approve(address(actionsRouter), type(uint256).max);
|
|
uint256 managerCurrency0BalanceBefore = currency0.balanceOf(address(manager));
|
|
uint256 userCurrency0BalanceBefore = currency0.balanceOf(address(this));
|
|
|
|
Actions[] memory actions = new Actions[](9);
|
|
bytes[] memory params = new bytes[](9);
|
|
|
|
vm.startSnapshotGas("getReserves");
|
|
uint256 reserves = manager.getSyncedReserves();
|
|
vm.stopSnapshotGas();
|
|
assertEq(reserves, 0); // reserves are 0.
|
|
|
|
actions[0] = Actions.TAKE;
|
|
params[0] = abi.encode(currency0, address(this), 10);
|
|
|
|
// Assert that the delta open on the actionsRouter is -10. (The user owes 10 to the pool).
|
|
actions[1] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[1] = abi.encode(currency0, address(actionsRouter), -10);
|
|
|
|
actions[2] = Actions.TRANSFER_FROM; // NOT syned before sending tokens
|
|
params[2] = abi.encode(currency0, address(this), manager, 10);
|
|
|
|
actions[3] = Actions.SETTLE; // calling settle without sync is expecting a native token, but msg.value == 0 so it settles for 0.
|
|
|
|
actions[4] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[4] = abi.encode(currency0, address(actionsRouter), -10);
|
|
|
|
actions[5] = Actions.SYNC;
|
|
params[5] = abi.encode(currency0);
|
|
|
|
// To now settle the delta, the user owes 10 to the pool.
|
|
actions[6] = Actions.TRANSFER_FROM;
|
|
params[6] = abi.encode(currency0, address(this), manager, 10);
|
|
|
|
actions[7] = Actions.SETTLE;
|
|
|
|
actions[8] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[8] = abi.encode(currency0, address(actionsRouter), 0);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
|
|
// The manager gained 10 currency0.
|
|
assertEq(currency0.balanceOf(address(manager)), managerCurrency0BalanceBefore + 10);
|
|
// The user lost 10 currency0, and can never claim it back.
|
|
assertEq(currency0.balanceOf(address(this)), userCurrency0BalanceBefore - 10);
|
|
}
|
|
|
|
function test_settle_failsWithNativeERC20IfNotSyncedInOrder(uint256 value) public {
|
|
value = bound(value, 1, uint256(int256(type(int128).max / 2)));
|
|
vm.deal(address(this), value);
|
|
vm.deal(address(manager), value);
|
|
NativeERC20 nativeERC20 = new NativeERC20();
|
|
|
|
uint256 nativeERC20Balance = nativeERC20.balanceOf(address(manager));
|
|
|
|
Actions[] memory actions = new Actions[](3);
|
|
bytes[] memory params = new bytes[](3);
|
|
|
|
actions[0] = Actions.SYNC;
|
|
params[0] = abi.encode(nativeERC20);
|
|
|
|
actions[1] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[1] = abi.encode(nativeERC20Balance);
|
|
|
|
actions[2] = Actions.SETTLE_NATIVE;
|
|
params[2] = abi.encode(value);
|
|
|
|
vm.expectRevert(IPoolManager.NonzeroNativeValue.selector);
|
|
actionsRouter.executeActions{value: value}(actions, params);
|
|
|
|
// Reference only - see OZ C01 report - previous test confirming vulnerability
|
|
// uint256 balanceBefore = address(this).balance;
|
|
|
|
// actions[1] = Actions.SETTLE;
|
|
// params[1] = abi.encode(Currency.wrap(address(nativeERC20)));
|
|
|
|
// actions[2] = Actions.ASSERT_DELTA_EQUALS;
|
|
// params[2] = abi.encode(Currency.wrap(address(0)), address(actionsRouter), value);
|
|
|
|
// actions[3] = Actions.ASSERT_DELTA_EQUALS;
|
|
// params[3] = abi.encode(Currency.wrap(address(nativeERC20)), address(actionsRouter), value);
|
|
|
|
// actions[4] = Actions.TAKE;
|
|
// params[4] = abi.encode(Currency.wrap(address(0)), address(this), value);
|
|
|
|
// actions[5] = Actions.TAKE;
|
|
// params[5] = abi.encode(Currency.wrap(address(nativeERC20)), address(this), value);
|
|
|
|
// uint256 balanceAfter = address(this).balance;
|
|
// assertEq(balanceAfter - balanceBefore, value);
|
|
}
|
|
|
|
function test_settle_native_afterERC20Sync_succeeds(uint256 currency2Balance, uint256 ethBalance) public {
|
|
currency2Balance = bound(currency2Balance, 1, uint256(int256(type(int128).max / 2)));
|
|
ethBalance = bound(ethBalance, 1, uint256(int256(type(int128).max / 2)));
|
|
|
|
vm.deal(address(this), ethBalance);
|
|
// ensure the reserves balance is non 0
|
|
currency2.transfer(address(manager), currency2Balance);
|
|
|
|
Actions[] memory actions = new Actions[](8);
|
|
bytes[] memory params = new bytes[](8);
|
|
|
|
actions[0] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[0] = abi.encode(0);
|
|
|
|
actions[1] = Actions.SYNC;
|
|
params[1] = abi.encode(currency2);
|
|
|
|
actions[2] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[2] = abi.encode(currency2Balance);
|
|
|
|
actions[3] = Actions.SYNC;
|
|
params[3] = abi.encode(CurrencyLibrary.ADDRESS_ZERO);
|
|
|
|
// Under the hood this is non-zero but our transient state library overrides the value if the currency is address(0)
|
|
actions[4] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[4] = abi.encode(0);
|
|
|
|
// This calls settle with a value, of ethBalance. Since the synedCurrency slot is address(0), the call should successfully apply a positive delta on the native currency.
|
|
actions[5] = Actions.SETTLE_NATIVE;
|
|
params[5] = abi.encode(ethBalance);
|
|
|
|
actions[6] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[6] = abi.encode(CurrencyLibrary.ADDRESS_ZERO, address(actionsRouter), ethBalance);
|
|
|
|
// take the eth to close the deltas
|
|
actions[7] = Actions.TAKE;
|
|
params[7] = abi.encode(CurrencyLibrary.ADDRESS_ZERO, address(this), ethBalance);
|
|
|
|
actionsRouter.executeActions{value: ethBalance}(actions, params);
|
|
}
|
|
|
|
function test_settle_twice_doesNotApplyDelta(uint256 value) public {
|
|
value = bound(value, 1, uint256(int256(type(int128).max / 2)));
|
|
currency2.transfer(address(manager), value);
|
|
|
|
Actions[] memory actions = new Actions[](8);
|
|
bytes[] memory params = new bytes[](8);
|
|
|
|
actions[0] = Actions.SYNC;
|
|
params[0] = abi.encode(currency2);
|
|
|
|
actions[1] = Actions.ASSERT_RESERVES_EQUALS;
|
|
params[1] = abi.encode(value);
|
|
|
|
actions[2] = Actions.TRANSFER_FROM;
|
|
params[2] = abi.encode(currency2, address(this), address(manager), value);
|
|
|
|
// This settles the syncedCurrency, currency2.
|
|
actions[3] = Actions.SETTLE;
|
|
|
|
actions[4] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[4] = abi.encode(currency2, address(actionsRouter), value);
|
|
|
|
actions[5] = Actions.TAKE;
|
|
params[5] = abi.encode(currency2, address(this), value);
|
|
|
|
// This settles the syncedCurrency, which has been cleared to address(0).
|
|
actions[6] = Actions.SETTLE;
|
|
|
|
// Calling settle on address(0) does not apply a delta when called with no value.
|
|
actions[7] = Actions.ASSERT_DELTA_EQUALS;
|
|
params[7] = abi.encode(CurrencyLibrary.ADDRESS_ZERO, address(actionsRouter), 0);
|
|
|
|
actionsRouter.executeActions(actions, params);
|
|
}
|
|
}
|