idle_moloch/src/RaidGeld.sol
2024-11-09 22:07:38 +01:00

471 lines
18 KiB
Solidity

// 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;
}