// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import {RaidGeldUtils} from "../src/RaidGeldUtils.sol"; import {Army, Player, Raider} from "../src/RaidGeldStructs.sol"; import "../src/Constants.sol"; contract RaidGeld is ERC20, Ownable, Constants { uint256 public constant MANTISSA = 1e4; uint256 public constant BUY_IN_AMOUNT = 0.0005 ether; uint256 public immutable BUY_IN_DAO_TOKEN_AMOUNT; uint256 public constant INITIAL_GELD = 50 * MANTISSA; mapping(address => Player) private players; mapping(address => Army) private armies; // WETH IWETH public immutable weth = IWETH(WETH); // RGCVII token ERC20 public daoToken; // RGCVII pool address public pool; // Uniswap ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER); // 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 functions that should only be available to non initialized players modifier newPlayer() { require(players[msg.sender].created_at == 0, "Whoops, player already exists :)"); _; } constructor(address _daoToken, address _pool) ERC20("Raid Geld", "GELD") Ownable(msg.sender) { daoToken = ERC20(_daoToken); pool = _pool; BUY_IN_DAO_TOKEN_AMOUNT = 50 * 10 ** daoToken.decimals(); } function init_player(address player) private { // Mint some starting tokens to the player _mint(player, INITIAL_GELD); // Set initial states players[player] = Player({total_minted: INITIAL_GELD, created_at: block.timestamp, last_raided_at: block.timestamp}); armies[player] = Army({ moloch_denier: Raider({level: 0}), apprentice: Raider({level: 0}), anointed: Raider({level: 0}), champion: Raider({level: 0}), profit_per_second: 0 }); } // New player want to register with ETH function register_eth() external payable newPlayer { 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 }); router.exactInputSingle(params); init_player(msg.sender); } // New player wants to register with dao function register_dao() external payable newPlayer { //@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" ); // Init player init_player(msg.sender); } // Override for default number of decimals function decimals() public view virtual override returns (uint8) { return 4; } // Allows the owner to withdraw DAO tokens function withdraw() external onlyOwner { uint256 amount = daoToken.balanceOf(address(this)); daoToken.approve(address(this), amount); daoToken.transferFrom(address(this), owner(), amount); } // Manual minting for itchy fingers function raid() external onlyPlayer { 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; // TODO: Pink noise, make it so sometimes its better than expected _mint(player, new_geld); players[player].last_raided_at = block.timestamp; players[player].total_minted += new_geld; } // Function to get Player struct function getPlayer(address addr) public view returns (Player memory) { return players[addr]; } // Function to get Army struct function getArmy(address addr) public view returns (Army memory) { return armies[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 onlyPlayer { 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); // First trigger a raid so player receives what he is due at to this moment uint256 time_past = block.timestamp - players[msg.sender].last_raided_at; uint256 new_geld = armies[msg.sender].profit_per_second * time_past; require(balanceOf(msg.sender) + new_geld > cost, "Not enough GELD to add this unit"); performRaid(msg.sender); // TODO: Since we are first minting then burning the token, this could be simplified // by first calculating the difference and then minting / burning in just one operation // 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); } 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; }