diff --git a/.gitmodules b/.gitmodules index 161a97a..0fd3f82 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/safe-contracts"] path = lib/safe-contracts url = https://github.com/safe-global/safe-contracts +[submodule "lib/uniswap-periphery"] + path = lib/uniswap-periphery + url = https://github.com/Uniswap/v3-periphery diff --git a/src/Constants.sol b/src/Constants.sol index 6c740e6..5c729e4 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -9,7 +9,8 @@ address constant WBTC_POLYGON = 0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6; address constant WETH_POLYGON = 0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619; address constant UNISWAP_PERMIT2_POLYGON = 0x000000000022D473030F116dDEE9F6B43aC78BA3; - +address constant UNISWAP_POSITION_MANAGER = 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; +address constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; //Goerli address constant GOERLI_SAFE_PROXY_FACTORY = 0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2; address constant GOERLI_SAFE_LOGIC_SINGLETON = 0x3E5c63644E683549055b9Be8653de26E0B4CD36E; diff --git a/src/PositionObserver.sol b/src/PositionObserver.sol new file mode 100644 index 0000000..bc4e94a --- /dev/null +++ b/src/PositionObserver.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "./Constants.sol"; +import "./interfaces/INonfungiblePositionManager.sol"; +import "./interfaces/IUniswapV3Factory.sol"; +import "./interfaces/IUniswapV3Pool.sol"; +import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; + +contract PositionObserver { + address public safe; + constructor( + address _safe + ){ + safe = _safe; + } + + function positionCount() external view returns (uint256) { + return INonfungiblePositionManager(UNISWAP_POSITION_MANAGER).balanceOf(address(safe)); + } + + function tokenIdByIndex(uint256 index) external view returns (uint256) { + return INonfungiblePositionManager(UNISWAP_POSITION_MANAGER).tokenOfOwnerByIndex(address(safe), index); + } + + function poolAddress(uint256 tokenId) external view returns (address) { + ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) = INonfungiblePositionManager(UNISWAP_POSITION_MANAGER).positions(tokenId); + return IUniswapV3Factory(UNISWAP_V3_FACTORY).getPool(token0, token1, fee); + } + + function calculatePrice(int56 tick0, int56 tick1, int56 secs, uint256 decimals0, uint256 decimals1) internal pure returns (uint256) { + int56 exponent = (tick0 - tick1) / secs; + uint256 base = 10001; + uint256 price = 10000; + while (exponent > 0) { + if (exponent & 1 == 1) { + price = (price * base) / 10000; + } + base = (base * base) / 10000; + exponent >>= 1; + } + price *= 10 ** 18; + if (decimals0 > decimals1) { + price /= 10 ** (decimals0 - decimals1); + } else { + price *= 10 ** (decimals1 - decimals0); + } + return price; + } + + function positionPriceByTokenId(uint256 tokenId) external view returns (uint256) { + ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) = INonfungiblePositionManager(UNISWAP_POSITION_MANAGER).positions(tokenId); + uint8 token0Decimals = IERC20Metadata(token0).decimals(); + uint8 token1Decimals = IERC20Metadata(token1).decimals(); + address pool = IUniswapV3Factory(UNISWAP_V3_FACTORY).getPool(token0, token1, fee); + uint32[] memory secondsAgos = new uint32[](2); + secondsAgos[0] = 0; + secondsAgos[1] = 1; + ( + int56[] memory tickCumulatives, + uint160[] memory secondsPerLiquidityCumulativeX128s + ) = IUniswapV3Pool(pool).observe(secondsAgos); + require(tickCumulatives.length == 2); + return calculatePrice(tickCumulatives[0], tickCumulatives[1], 1, token0Decimals, token1Decimals); + } +} diff --git a/src/interfaces/INonfungiblePositionManager.sol b/src/interfaces/INonfungiblePositionManager.sol new file mode 100644 index 0000000..6651f1e --- /dev/null +++ b/src/interfaces/INonfungiblePositionManager.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +interface INonfungiblePositionManager { + function positions(uint256 tokenId) + external + view + returns ( + uint96 nonce, + address operator, + address token0, + address token1, + uint24 fee, + int24 tickLower, + int24 tickUpper, + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ); + + function balanceOf(address owner) external view returns (uint256); + + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); +} diff --git a/src/interfaces/IUniswapV3Factory.sol b/src/interfaces/IUniswapV3Factory.sol new file mode 100644 index 0000000..1985e9b --- /dev/null +++ b/src/interfaces/IUniswapV3Factory.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +interface IUniswapV3Factory { + + function getPool( + address tokenA, + address tokenB, + uint24 fee + ) external view returns (address pool); + +} diff --git a/src/interfaces/IUniswapV3Pool.sol b/src/interfaces/IUniswapV3Pool.sol new file mode 100644 index 0000000..d5c430d --- /dev/null +++ b/src/interfaces/IUniswapV3Pool.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +interface IUniswapV3Pool { + function observe(uint32[] calldata secondsAgos) + external + view + returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); +} diff --git a/test/PositionObserver.t.sol b/test/PositionObserver.t.sol new file mode 100644 index 0000000..f7d774f --- /dev/null +++ b/test/PositionObserver.t.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.8.0; + +import {Test, console2} from "forge-std/Test.sol"; +import {PositionObserver} from "../src/PositionObserver.sol"; + +contract PositionObserverTest is Test { + PositionObserver observer; + + address constant SAFE_ADDRESS = 0xD4928171A52f420855Bf59bcaF3C7077F42298D5; + + function setUp() public { + observer = new PositionObserver(address(SAFE_ADDRESS)); + } + + function testPositionCount() public { + assertEq(observer.positionCount(), 2); + } + + function testTokenIdByIndex() public { + assertEq(observer.tokenIdByIndex(0), 1071846); + assertEq(observer.tokenIdByIndex(1), 1081648); + } + + function testPoolAddress() public { + assertEq(observer.poolAddress(1071846), 0x0e44cEb592AcFC5D3F09D996302eB4C499ff8c10); + assertEq(observer.poolAddress(1081648), 0x8Fc5e02d85891BA2855af1904dfc5cf1d82e4a44); + } + + function testPositionPrice() public { + console2.log("price", observer.positionPriceByTokenId(1071846)); + console2.log("price", observer.positionPriceByTokenId(1081648)); +// assertEq(observer.positionPriceByTokenId(1071846), 1000000000000000000); +// assertEq(observer.positionPriceByTokenId(1081648), 1000000000000000000); + } +} \ No newline at end of file