diff --git a/foundry.toml b/foundry.toml index 25b918f..9b61783 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,3 +4,8 @@ out = "out" libs = ["lib"] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + +remappings = [ + "@uniswap/v3-core/=lib/v3-core/", + "@uniswap/v3-periphery=lib/v3-periphery/", +] \ No newline at end of file diff --git a/script/RaidGeld.s.sol b/script/RaidGeld.s.sol index cacd86d..84e52db 100644 --- a/script/RaidGeld.s.sol +++ b/script/RaidGeld.s.sol @@ -12,7 +12,7 @@ contract RaidGeldScript is Script, Constants { function run() public { vm.startBroadcast(); - raidgeld = new RaidGeld(DAO_TOKEN, POOL, BAAL); + raidgeld = new RaidGeld(DAO_TOKEN, POOL, BAAL, NFT_POSITION_MANAGER); vm.stopBroadcast(); } } diff --git a/src/Constants.sol b/src/Constants.sol index e1ea2bb..b099212 100644 --- a/src/Constants.sol +++ b/src/Constants.sol @@ -8,4 +8,5 @@ contract Constants { address public constant BAAL = 0x4d5A5B4a679b10038e1677C84Cb675d10d29fFFD; address public constant WETH = 0x4200000000000000000000000000000000000006; address public constant SWAP_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481; + address public constant NFT_POSITION_MANAGER = 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1; } diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index 59c0693..ec19970 100644 --- a/src/RaidGeld.sol +++ b/src/RaidGeld.sol @@ -1,13 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IBaal} from "lib/Baal/contracts/interfaces/IBaal.sol"; -import {RaidGeldUtils} from "../src/RaidGeldUtils.sol"; -import {Army, Player, Raider, Boss} from "../src/RaidGeldStructs.sol"; -import "../src/Constants.sol"; +import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import { IERC20, TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; +import { INonfungiblePositionManager } from "./lib/INonfungiblePositionManager.sol"; +import { CustomMath} from "./lib/CustomMath.sol"; + +import {RaidGeldUtils } from "../src/RaidGeldUtils.sol"; +import { Army, Player, Raider, Boss } from "../src/RaidGeldStructs.sol"; +import { Constants} from "../src/Constants.sol"; contract RaidGeld is ERC20, Ownable, Constants { uint256 public constant MANTISSA = 1e4; @@ -24,6 +29,12 @@ contract RaidGeld is ERC20, Ownable, Constants { // RGCVII token ERC20 public daoToken; IBaal public baal; + address public DAO; + bool poolDeployed = false; + int24 internal maxTick; + int24 internal minTick; + uint24 internal poolFee = 10000; + INonfungiblePositionManager public nonfungiblePositionManager; // RGCVII pool address public pool; // Uniswap @@ -45,13 +56,27 @@ contract RaidGeld is ERC20, Ownable, Constants { uint16 championLevel ); event DaoTokenBuyInAmountSet(address indexed owner, uint256 oldAmount, uint256 newAmount); - + event UniswapPositionCreated( + address indexed pool, + uint256 indexed positionId, + uint160 sqrtPriceX96, + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ); // Modifier for functions that should only be available to registered players modifier onlyPlayer() { require(players[msg.sender].created_at != 0, "Not an initiated player"); _; } + // Modifier for dao to deploy the swap pool + modifier onlyDaoOnlyOnce() { + require(msg.sender == DAO && poolDeployed == false, "Not DAO"); + _; + poolDeployed = true; + } + modifier onlyActiveSession() { require(players[msg.sender].has_active_session, "Session is not active, you need to buy into the game first"); _; @@ -65,11 +90,19 @@ contract RaidGeld is ERC20, Ownable, Constants { _; } - constructor(address _daoToken, address _pool, address _baal) ERC20("Raid Geld", "GELD") Ownable(msg.sender) { + constructor(address _daoToken, address _pool, address _baal, address _nftPositionManager) ERC20("Raid Geld", "GELD") Ownable(msg.sender) { daoToken = ERC20(_daoToken); pool = _pool; baal = IBaal(_baal); BUY_IN_DAO_TOKEN_AMOUNT = 500 * 10 ** daoToken.decimals(); + nonfungiblePositionManager = INonfungiblePositionManager(_nftPositionManager); + + IUniswapV3Factory factory = IUniswapV3Factory(nonfungiblePositionManager.factory()); + int24 tickSpacing = factory.feeAmountTickSpacing(poolFee); + require(tickSpacing != 0, "InvalidPoolFee"); + maxTick = (887272 / tickSpacing) * tickSpacing; + minTick = -maxTick; + } function start_game(address player) private { @@ -143,7 +176,7 @@ contract RaidGeld is ERC20, Ownable, Constants { function performSacrifice(uint256 _baseAmount) private { uint256 amount = _baseAmount * SACRIFICE_SHARE / MANTISSA; address[] memory tokens = new address[](0); - baal.ragequit(address(this), amount, 0, tokens); + _ragequit(amount); } // Override for default number of decimals @@ -292,6 +325,80 @@ contract RaidGeld is ERC20, Ownable, Constants { BUY_IN_DAO_TOKEN_AMOUNT = newAmount; } + function deploySwapPool(uint256 _geldAmount, uint256 _daoTokenAmount) external onlyDaoOnlyOnce { + + uint256 daoBalanceBefore = daoToken.balanceOf(address(this)); + uint256 geldBalanceBefore = balanceOf(address(this)); + _mint(address(this), _geldAmount); + + bool isGeldFirst = address(this) < address(daoToken); + // Ensure correct order of tokens based on their addresses + (address token0, address token1, uint256 liquidityAmount0, uint256 liquidityAmount1) = isGeldFirst + ? (address(this), address(daoToken), _geldAmount, _daoTokenAmount) + : (address(daoToken), address(this), _daoTokenAmount, _geldAmount); + + // calculate the sqrtPriceX96 + uint160 sqrtPriceX96 = CustomMath.calculateSqrtPriceX96(liquidityAmount0, liquidityAmount1); + // Create and initialize the pool if necessary + pool = nonfungiblePositionManager.createAndInitializePoolIfNecessary(token0, token1, poolFee, sqrtPriceX96); + // console.log("pool", pool); + + // approve weth and shares with NonfungiblePositionManager (taken from univ3 docs) + TransferHelper.safeApprove(token0, address(nonfungiblePositionManager), liquidityAmount0); + TransferHelper.safeApprove(token1, address(nonfungiblePositionManager), liquidityAmount1); + // console.log("approve OK"); + + + // Set up mintParams with full range for volatile token + // tick upper and lower need to be a valid tick per fee (divisible by 200 for 1%) + // position receipt NFT goes to the vault + INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({ + token0: token0, + token1: token1, + fee: poolFee, + tickLower: minTick, + tickUpper: maxTick, + amount0Desired: liquidityAmount0, + amount1Desired: liquidityAmount1, + amount0Min: 0, + amount1Min: 0, + recipient: _msgSender(), // baalVaultOnly ensures vault is the caller + deadline: block.timestamp + 15 minutes // Ensure a reasonable deadline + }); + + // Mint the position + (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = nonfungiblePositionManager.mint( + mintParams + ); + + // Remove allowance and refund in both assets. + uint256 daoRefund = daoBalanceBefore - daoToken.balanceOf(address(this)); + if (daoRefund > 0) { + TransferHelper.safeApprove(address(daoToken), address(nonfungiblePositionManager), 0); + _ragequit(daoRefund); + } + uint256 geldRefund = geldBalanceBefore - balanceOf(address(this)); + if (geldRefund > 0) { + TransferHelper.safeApprove(address(this), address(nonfungiblePositionManager), 0); + _burn(address(this), geldRefund); + } + + + + + emit UniswapPositionCreated(pool, tokenId, sqrtPriceX96, liquidity, amount0, amount1); + } + + + + function _ragequit(uint256 _amount) private { + address[] memory tokens = new address[](0); + baal.ragequit(address(this), _amount, 0, tokens); + } + function setDaoAddress(address _dao) external onlyOwner { + DAO = _dao; + } + receive() external payable { revert("No plain Ether accepted, use register() function to check in :)"); } diff --git a/src/lib/CustomMath.sol b/src/lib/CustomMath.sol new file mode 100644 index 0000000..7a1550d --- /dev/null +++ b/src/lib/CustomMath.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7 <0.9.0; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +/** + * @title Math utility functions + * @author DAOHaus + * @notice Includes math functions to calculate prices on Uniswap V3 + */ +library CustomMath { + /// @dev Scale used by Uniswap for working with Q64.96 (binary fixed-point) numbers + uint256 constant Q96 = 2 ** 96; + + /** + * @notice Calculates the squeare root of provided value + * @param x value + * @return SQRT(`value`) + */ + function sqrt(uint256 x) internal pure returns (uint256) { + if (x == 0) return 0; + uint256 z = (x + 1) / 2; + uint256 y = x; + while (z < y) { + y = z; + z = (x / z + z) / 2; + } + return y; + } + + /** + * @dev Calculates the sqrtPriceX96 value for Uniswap V3 pools. + * + * This function computes the square root of the price ratio between two tokens + * and adjusts it to the Uniswap V3 format, which requires the square root price + * to be scaled by 2^96. This format is used by Uniswap V3 to facilitate high-precision + * and low-cost arithmetic operations within the protocol. + * + * @param amount0 The amount of token0, where token0 is the token with a numerically lower address. + * @param amount1 The amount of token1, where token1 is the token with a numerically higher address. + * + * The price ratio is calculated as the number of units of token1 equivalent to one unit of token0, + * scaled up by 1e18 to maintain precision during the division operation. + * + * @return The square root of the price ratio, adjusted to the Uniswap V3 fixed-point format (sqrtPriceX96). + * + * Requirements: + * - Both `amount0` and `amount1` must be greater than zero to avoid division by zero errors + * and ensure meaningful price calculations. + * + */ + function calculateSqrtPriceX96(uint256 amount0, uint256 amount1) internal pure returns (uint160) { + require(amount0 > 0 && amount1 > 0, "Token amounts cannot be zero"); + + // Calculate the price ratio as amount1 / amount0 + // Here, `amount1` is multiplied by 1e18 to retain precision after dividing by `amount0`. + uint256 priceRatio = (amount1 * 1e18) / amount0; + + // Compute the square root of the price ratio. + uint256 sqrtPrice = sqrt(priceRatio); + + // Adjust the square root price to the Uniswap V3 fixed-point format by scaling up by 2^96, + // then dividing by 1e9 to correct for the initial scaling by 1e18. + uint256 sqrtPriceX96 = (sqrtPrice * Q96) / 1e9; + + // Return the result as a uint160, conforming to the Uniswap V3 type requirement for sqrtPriceX96. + return uint160(sqrtPriceX96); + } +} \ No newline at end of file diff --git a/src/lib/INonfungiblePositionManager.sol b/src/lib/INonfungiblePositionManager.sol new file mode 100644 index 0000000..40c4de6 --- /dev/null +++ b/src/lib/INonfungiblePositionManager.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.7 <0.9.0; + +// @notice taken from RaidGuild s7 Yeet24ShamanModule +// @notice https://basescan.org/address/0x22952b522e72015B671B4715599F7522073E37c1#code + +interface INonfungiblePositionManager { + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + address recipient; + uint256 deadline; + } + + function factory() external view returns (address); + + function ownerOf(uint256 tokenId) external view returns (address); + + /// @notice Creates a new position wrapped in a NFT + /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized + /// a method does not exist, i.e. the pool is assumed to be initialized. + /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata + /// @return tokenId The ID of the token that represents the minted position + /// @return liquidity The amount of liquidity for this position + /// @return amount0 The amount of token0 + /// @return amount1 The amount of token1 + function mint( + MintParams calldata params + ) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + + function createAndInitializePoolIfNecessary( + address token0, + address token1, + uint24 fee, + uint160 sqrtPriceX96 + ) external payable returns (address pool); +} \ No newline at end of file diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index 4cb6d2b..05ca611 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -36,7 +36,7 @@ contract raid_geldTest is Test, Constants { vm.deal(owner, 10 ether); fundAccount(player1); vm.prank(owner); - raid_geld = new RaidGeld(DAO_TOKEN, POOL, BAAL); + raid_geld = new RaidGeld(DAO_TOKEN, POOL, BAAL, NFT_POSITION_MANAGER); raid_geld.weth().deposit{value: 5 ether}(); }