// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; 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 { 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, LastBossResult} from "../src/RaidGeldStructs.sol"; import { Constants} from "../src/Constants.sol"; contract RaidGeld is ERC20, Ownable, Constants { uint256 public constant MANTISSA = 1e4; uint256 public constant BUY_IN_AMOUNT = 0.00045 ether; uint256 public BUY_IN_DAO_TOKEN_AMOUNT; uint256 public constant INITIAL_GELD = 50 * MANTISSA; uint256 public constant SACRIFICE_SHARE = 1e3; // 10% mapping(address => Player) private players; mapping(address => Army) private armies; mapping(address => Boss) private bosses; mapping(address => LastBossResult) private lastBossResults; // WETH IWETH public immutable weth = IWETH(WETH); // 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 DAOWETHpool; address public pool; // Uniswap ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER); // Events event PlayerRegistered(address indexed player, uint256 initialGeld); event PlayerStrikesAgain(address indexed player, uint256 totalRewards, uint256 initialGeld); event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance); event UnitAdded( address indexed player, uint8 unitType, uint16 nUnits, uint256 cost, uint256 geldBalance, uint16 molochDenierLevel, uint16 apprenticeLevel, uint16 anointedLevel, uint16 championLevel ); event DaoTokenBuyInAmountSet(address indexed owner, uint256 oldAmount, uint256 newAmount); event PrestigeGained(address indexed player, uint32 prestigeLevel); event BossDefeated(address indexed player, uint8 bossLevel, uint256 earnings); event BossBattle(address indexed player, uint8 bossLevel, bool hasWon); /* * @notice emitted when the UniV3 pool is created and the initial liquidity position is minted * @param pool pool address * @param positionId NFT position Id * @param sqrtPriceX96 initial token price * @param liquidity final liquidity provided * @param amount0 final amount of liquidity provided for token0 * @param amount1 final amount of liquidity provided for token1 * */ 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 or pool already deployed"); _; poolDeployed = true; } modifier onlyActiveSession() { require(players[msg.sender].has_active_session, "Session is not active, you need to buy into the game first"); _; } modifier newPlay() { Player storage player = players[msg.sender]; bool notRegistered = player.created_at == 0; bool returningPlayer = player.is_registered && !player.has_active_session; require(notRegistered || returningPlayer, "Active session already in progress"); _; } constructor(address _daoToken, address _pool, address _baal, address _nftPositionManager) ERC20("Raid Geld", "GELD") Ownable(msg.sender) { daoToken = ERC20(_daoToken); DAOWETHpool = _pool; baal = IBaal(_baal); BUY_IN_DAO_TOKEN_AMOUNT = 400 * 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 { bool existing_player = players[player].is_registered; // Mint some starting tokens to the player _mint(player, INITIAL_GELD); // Reset or set player resetPlayer(player); players[player].has_active_session = true; players[player].is_registered = true; players[player].n_runs += 1; if (existing_player) { emit PlayerStrikesAgain(player, players[player].total_rewards, INITIAL_GELD); } else { // Emit event emit PlayerRegistered(msg.sender, INITIAL_GELD); } } function resetPlayer(address _playerAddress) private { Player storage player = players[_playerAddress]; // Set initial states player.total_minted = INITIAL_GELD; player.created_at = block.timestamp; player.last_raided_at = block.timestamp; player.has_active_session = false; armies[_playerAddress] = Army({ moloch_denier: Raider({level: 0}), apprentice: Raider({level: 0}), anointed: Raider({level: 0}), champion: Raider({level: 0}), profit_per_second: 0 }); bosses[_playerAddress] = Boss({level: 0, variants: RaidGeldUtils.generate_boss_variants(block.prevrandao)}); // dont change lastBossResult // that only changes after boss battles and on init } // New player want to register with ETH function registerEth() external payable newPlay { require(msg.value == BUY_IN_AMOUNT, "Incorrect buy in amount"); weth.deposit{value: BUY_IN_AMOUNT}(); weth.approve(address(router), BUY_IN_AMOUNT); ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02.ExactInputSingleParams({ tokenIn: WETH, tokenOut: DAO_TOKEN, fee: 10000, recipient: address(this), amountIn: BUY_IN_AMOUNT, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }); uint256 daoTokenAmount = router.exactInputSingle(params); performSacrifice(daoTokenAmount); start_game(msg.sender); } // New player wants to register with dao function registerDaoToken() external payable newPlay { //@notice this is not safe for arbitrary tokens, which may not follow the interface eg. USDT //@notice but should be fine for the DAO token require( daoToken.transferFrom(msg.sender, address(this), BUY_IN_DAO_TOKEN_AMOUNT), "Failed to transfer DAO tokens" ); performSacrifice(BUY_IN_DAO_TOKEN_AMOUNT); start_game(msg.sender); } function performSacrifice(uint256 _baseAmount) private { uint256 amount = _baseAmount * SACRIFICE_SHARE / MANTISSA; _ragequit(amount); } // Override for default number of decimals function decimals() public view virtual override returns (uint8) { return 4; } // Manual minting for itchy fingers function raid() external onlyActiveSession { performRaid(msg.sender); } // Helper so we can use it when buying units too function performRaid(address player) private { uint256 time_past = block.timestamp - players[player].last_raided_at; uint256 new_geld = armies[player].profit_per_second * time_past; _mint(player, new_geld); players[player].last_raided_at = block.timestamp; players[player].total_minted += new_geld; emit RaidPerformed(player, players[player].total_minted, balanceOf(player)); } // Function to get the Player struct function getPlayer(address addr) public view returns (Player memory) { return players[addr]; } // Function to get the Army struct function getArmy(address addr) public view returns (Army memory) { return armies[addr]; } // Function to get the Boss struct function getBoss(address addr) public view returns (Boss memory) { return bosses[addr]; } // Function to get the Boss struct function getLastBossResult(address addr) public view returns (LastBossResult memory) { return lastBossResults[addr]; } // Quick fn to check if user is registered function isRegistered(address addr) public view returns (bool) { return players[addr].created_at != 0; } // Add a unit to your army function addUnit(uint8 unit, uint16 n_units) external onlyActiveSession { require(unit <= 3, "Unknown unit"); Army storage army = armies[msg.sender]; uint16 currentLevel = 0; if (unit == 0) { // moloch_denier currentLevel = army.moloch_denier.level; } else if (unit == 1) { // apprentice currentLevel = army.apprentice.level; } else if (unit == 2) { // anointed currentLevel = army.anointed.level; } else if (unit == 3) { // champion currentLevel = army.champion.level; } uint256 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units); performRaid(msg.sender); // TODO: Since we are first minting then burning the token, this could be simplified require(balanceOf(msg.sender) >= cost, "Not enough GELD to add this unit"); // then burn the cost of the new army _burn(msg.sender, cost); // Increase level if (unit == 0) { // moloch_denier army.moloch_denier.level += n_units; } else if (unit == 1) { // apprentice army.apprentice.level += n_units; } else if (unit == 2) { // anointed army.anointed.level += n_units; } else if (unit == 3) { // champion army.champion.level += n_units; } // update profite per second army.profit_per_second = RaidGeldUtils.calculateProfitsPerSecond(army); // Emit event emit UnitAdded( msg.sender, unit, n_units, cost, balanceOf(msg.sender), army.moloch_denier.level, army.apprentice.level, army.anointed.level, army.champion.level ); } function battle_with_boss() external onlyActiveSession returns (bool[2] memory hasWonOrAscended) { // first perform raid performRaid(msg.sender); Boss memory boss_to_attack = bosses[msg.sender]; // calculate how much the player will put into battle uint256 geld_to_burn = balanceOf(msg.sender) >= RaidGeldUtils.getBossPower(boss_to_attack.level) ? RaidGeldUtils.getBossPower(boss_to_attack.level) : balanceOf(msg.sender); bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao); emit BossBattle(msg.sender, boss_to_attack.level, hasWonBattle); lastBossResults[msg.sender] = LastBossResult({ battled_at: block.timestamp, level: boss_to_attack.level, variant: boss_to_attack.variants[boss_to_attack.level], prestigeGained: false, reward: 0 }); if (hasWonBattle) { // Burn geld, send some sweet DAO Token and continue _burn(msg.sender, geld_to_burn); uint256 baseReward = (BUY_IN_DAO_TOKEN_AMOUNT - BUY_IN_DAO_TOKEN_AMOUNT * SACRIFICE_SHARE / MANTISSA); uint256 wholeReward = RaidGeldUtils.calculateBossReward(boss_to_attack.level, baseReward); uint256 treasuryShare = wholeReward * SACRIFICE_SHARE / MANTISSA; uint256 reward = wholeReward - treasuryShare; // send a share to dao treasury daoToken.transfer(DAO_TREASURY, treasuryShare); players[msg.sender].total_rewards += reward; // send user its reward daoToken.transfer(msg.sender, reward); emit BossDefeated(msg.sender, boss_to_attack.level, reward); lastBossResults[msg.sender].reward = reward; if (boss_to_attack.level == 6) { // User ascends! Moloch is defeated, user can start a new run players[msg.sender].prestige_level += 1; emit PrestigeGained(msg.sender, players[msg.sender].prestige_level); player_dies(msg.sender); lastBossResults[msg.sender].prestigeGained = true; return [hasWonBattle, true /* New prestige level! */ ]; } else { // else go to next boss bosses[msg.sender].level += 1; } } else { // Whoops u died, boss defeated you lastBossResults[msg.sender].reward = 0; player_dies(msg.sender); } return [hasWonBattle, false /* hasnt gotten prestige level */ ]; } function player_dies(address player) private { resetPlayer(player); players[player].has_active_session = false; _burn(msg.sender, balanceOf(player)); } function setDaoTokenBuyInAmount(uint256 newAmount) external onlyOwner { require(newAmount > 0, "Amount must be greater than 0"); emit DaoTokenBuyInAmountSet(msg.sender, BUY_IN_DAO_TOKEN_AMOUNT, newAmount); BUY_IN_DAO_TOKEN_AMOUNT = newAmount; } function deploySwapPool(uint256 _geldAmount, uint256 _daoTokenAmount) external onlyDaoOnlyOnce { daoToken.transferFrom(DAO, address(this), _daoTokenAmount); _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: DAO, deadline: block.timestamp + 15 minutes // Ensure a reasonable deadline }); // Mint the position (uint256 tokenId, uint128 liquidity, uint256 amount0 , uint256 amount1) = nonfungiblePositionManager.mint( mintParams ); // daoToken dust will join the reward poool // this contract should not hold any $GELD so we burn all remaining balance _burn(address(this), balanceOf(address(this))); 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 :)"); } // Revert any non-function-call Ether transfers or calls to non-existent functions fallback() external payable { revert("No fallback calls accepted"); } } interface ISwapRouter02 { struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; } function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); struct ExactOutputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 amountOut; uint256 amountInMaximum; uint160 sqrtPriceLimitX96; } function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); } interface IWETH is IERC20 { function deposit() external payable; function withdraw(uint256 amount) external; }