From 1afd975e0fba3a515718bd5e76735ac708c3287f Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Wed, 30 Oct 2024 00:28:48 +0100 Subject: [PATCH] WIP adding bosses --- src/RaidGeld.sol | 132 ++++++++++++++++++++++++---------------- src/RaidGeldStructs.sol | 9 +++ src/RaidGeldUtils.sol | 50 ++++++++++++++- test/RaidGeld.t.sol | 50 +++------------ 4 files changed, 148 insertions(+), 93 deletions(-) diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index 83ef9e4..dcbef9b 100644 --- a/src/RaidGeld.sol +++ b/src/RaidGeld.sol @@ -5,7 +5,7 @@ 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 {Army, Player, Raider, Boss} from "../src/RaidGeldStructs.sol"; import "../src/Constants.sol"; contract RaidGeld is ERC20, Ownable, Constants { @@ -15,6 +15,7 @@ contract RaidGeld is ERC20, Ownable, Constants { uint256 public constant INITIAL_GELD = 500 * MANTISSA; mapping(address => Player) private players; mapping(address => Army) private armies; + mapping(address => Boss) private bosses; // WETH IWETH public immutable weth = IWETH(WETH); @@ -25,12 +26,9 @@ contract RaidGeld is ERC20, Ownable, Constants { // Uniswap ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER); // Events + event PlayerRegistered(address indexed player, uint256 initialGeld); - event RaidPerformed( - address indexed player, - uint256 totalMinted, - uint256 geldBalance - ); + event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance); event UnitAdded( address indexed player, uint8 unitType, @@ -49,25 +47,54 @@ contract RaidGeld is ERC20, Ownable, Constants { _; } - modifier newPlayer() { - require(players[msg.sender].created_at == 0, "Whoops, player already exists :)"); + modifier onlyActiveSession() { + require(players[msg.sender].active_session, "Session is not active, you need to buy into the game first"); + _; + } + + modifier newPlay() { + Player memory player = players[msg.sender]; + bool notRegistered = player.created_at == 0; + bool returningPlayer = player.has_registered && !player.has_active_session; + require(notRegistered || returningPlayer, "Active session already in progress"); _; } 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(); + BUY_IN_DAO_TOKEN_AMOUNT = 500 * 10 ** daoToken.decimals(); } function init_player(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 + reset_player(player); + + if (existing_player) { + // TODO: Emit new run + } else { + // Emit event + emit PlayerRegistered(msg.sender, INITIAL_GELD); + } + } + + function reset_player(address addr) private { + Player memory player = players[addr]; + uint32 current_n_runs = player.n_runs + 1; + uint256 current_total_rewards = player.total_rewards; + bool has_registered = player.is_registered; // Set initial states players[msg.sender] = Player({ total_minted: INITIAL_GELD, created_at: block.timestamp, - last_raided_at: block.timestamp + last_raided_at: block.timestamp, + n_runs: current_n_runs, + total_rewards: current_total_rewards, + active_session: false }); armies[msg.sender] = Army({ moloch_denier: Raider({level: 0}), @@ -76,13 +103,11 @@ contract RaidGeld is ERC20, Ownable, Constants { champion: Raider({level: 0}), profit_per_second: 0 }); - - // Emit event - emit PlayerRegistered(msg.sender, INITIAL_GELD); + bosses[msg.sender] = Boss({level: 0, variant: 0}); } // New player want to register with ETH - function register_eth() external payable newPlayer { + function register_eth() 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); @@ -100,7 +125,7 @@ contract RaidGeld is ERC20, Ownable, Constants { } // New player wants to register with dao - function register_dao() external payable newPlayer { + function register_dao() 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( @@ -123,25 +148,18 @@ contract RaidGeld is ERC20, Ownable, Constants { } // Manual minting for itchy fingers - function raid() external onlyPlayer { - uint256 totalMinted = performRaid(msg.sender); - - // Emit event - emit RaidPerformed(msg.sender, totalMinted, balanceOf(msg.sender)); + function raid() external onlyActiveSession { + performRaid(); } // Helper so we can use it when buying units too - function performRaid(address player) private returns (uint256) { + 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; - - return players[player].total_minted; + emit RaidPerformed(player, players[player].total_minted, balanceOf(player)); } // Function to get Player struct @@ -160,7 +178,7 @@ contract RaidGeld is ERC20, Ownable, Constants { } // Add a unit to your army - function addUnit(uint8 unit, uint16 n_units) external onlyPlayer { + function addUnit(uint8 unit, uint16 n_units) external onlyActiveSession { require(unit <= 3, "Unknown unit"); Army storage army = armies[msg.sender]; @@ -179,24 +197,15 @@ contract RaidGeld is ERC20, Ownable, Constants { 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 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units); - uint256 time_past = block.timestamp - - players[msg.sender].last_raided_at; + // 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" - ); + require(balanceOf(msg.sender) + new_geld >= cost, "Not enough GELD to add this unit"); uint256 totalMinted = performRaid(msg.sender); // Emit event - emit RaidPerformed(msg.sender, totalMinted, balanceOf(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 @@ -236,10 +245,37 @@ contract RaidGeld is ERC20, Ownable, Constants { ); } - receive() external payable { - revert( - "No plain Ether accepted, use register() function to check in :)" + function battle_with_boss() external onlyActiveSession { + // first perform raid + performRaid(); + 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.BOSS_POWERS[boss_to_attack.level] + ? RaidGeldUtils.BOSS_POWERS[boss_to_attack.level] + : balanceOf(msg.sender); + bool hasWonBattle = RaidGeldUtils.calculateBossFight( + boss_to_attack.level, geld_to_burn, block.timestamp, block.difficulty ); + if (hasWonBattle) { + // Burn geld, get some sweet DAO Token and continue + _burn(msg.sender, geld_to_burn); + uint256 reward = RaidGeldUtils.calculateBossReward(boss_to_attack.level); + players[msg.sender].total_rewards += reward; + daoToken.transferFrom(address(this), msg.sender, reward); + } else { + // Whoops u die + player_dies(msg.sender); + } + } + + function player_dies(address player) private { + reset_player(); + players[player].has_active_session = false; + _burn(msg.sender, balanceOf(player)); + } + + 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 @@ -259,10 +295,7 @@ interface ISwapRouter02 { uint160 sqrtPriceLimitX96; } - function exactInputSingle(ExactInputSingleParams calldata params) - external - payable - returns (uint256 amountOut); + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); struct ExactOutputSingleParams { address tokenIn; @@ -274,10 +307,7 @@ interface ISwapRouter02 { uint160 sqrtPriceLimitX96; } - function exactOutputSingle(ExactOutputSingleParams calldata params) - external - payable - returns (uint256 amountIn); + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); } interface IWETH is IERC20 { diff --git a/src/RaidGeldStructs.sol b/src/RaidGeldStructs.sol index 9846c09..5d70a96 100644 --- a/src/RaidGeldStructs.sol +++ b/src/RaidGeldStructs.sol @@ -17,4 +17,13 @@ struct Player { uint256 total_minted; uint256 created_at; uint256 last_raided_at; + uint256 total_rewards; + uint32 n_runs; + bool is_registered; + bool has_active_session; +} + +struct Boss { + uint8 variant; + uint8 level; } diff --git a/src/RaidGeldUtils.sol b/src/RaidGeldUtils.sol index 42d4c98..6f76b78 100644 --- a/src/RaidGeldUtils.sol +++ b/src/RaidGeldUtils.sol @@ -19,6 +19,26 @@ library RaidGeldUtils { uint256 constant ANOINTED_BASE_COST = 30096000; uint256 constant CHAMPION_BASE_COST = 255816000; + // Boss lvels + uint256[7] constant BOSS_POWERS = [ + 9000000, // 900e4 + 90000000, // 900e5 + 900000000, // 900e6 + 9000000000, // 900e7 + 90000000000, // 900e8 + 900000000000, // 900e9 + 9000000000000 // 900e10 + ]; + + // First boss is almost guaranteed, others arent + uint256[7] constant BOSS_CHANCES = [99, 89, 80, 70, 62, 51, 40]; + + // These are cumulative chances from above with precision 18 + uint256[7] constant CUMULATIVE_BOSS_CHANCES = + [99e16, 8811e14, 7048e14, 493416e12, 30591792e10, 1560181392e8, 6240725568e7]; + + uint256 constant RISK_BETA = 15e17; + function calculateUnitPrice(uint8 unit, uint16 currentLevel, uint16 units) internal pure returns (uint256) { require(unit <= 3, "No matching unit found"); uint256 rollingPriceCalculation = MOLOCH_DENIER_BASE_COST; @@ -43,7 +63,6 @@ library RaidGeldUtils { function calculateProfitsPerSecond(Army memory army) internal pure returns (uint256) { // Each next unit scales progressivelly better - uint256 moloch_denier_profit = army.moloch_denier.level * MOLOCH_DENIER_PROFIT; uint256 apprentice_profit = army.apprentice.level * APPRENTICE_PROFIT; uint256 anointed_profit = army.anointed.level * ANOINTED_PROFIT; @@ -51,4 +70,33 @@ library RaidGeldUtils { return moloch_denier_profit + apprentice_profit + anointed_profit + champion_profit; } + + // Returns how much Dao Token player is due for winning + function calculateBossReward(uint256 bossLevel) public view returns (uint256) { + return BOSS_POWERS[bossLevel] * (CUMULATIVE_BOSS_CHANCES[bossLevel] * 10 / 5) ** 2; + } + + // Calculates whether user survives the fight + function calculateBossFight(uint8 bossLevel, uint256 geldBurnt, uint256 timestamp, uint256 difficulty) + internal + pure + returns (uint256) + { + uint256 bossPower = BOSS_POWERS[bossLevel]; + require(geldBurnt <= bossPower, "Cant try to defeat boss with more than what boss power is"); + uint256 random_n = random(timestamp, difficulty, 1, 100); + // Relative power as in, you can only put in 800 geld to defeat 900 geld boss, + // but you will get exponentially worse chances + uint256 relativePower = ((geldBurnt ** 2) * 100) / bossPower ** 2; + uint256 roll = (random_n * relativePower * PRECISION) / (100 * PRECISION); + return roll >= bossPower; + } + + // TODO: Implement actual randomness + function random(uint256 timestamp, uint256 difficulty, uint256 min, uint256 max) internal pure returns (uint256) { + // returns 0 - 100 + require(max >= min, "Max must be greater than or equal to min"); + uint256 range = max - min + 1; + return min + (uint256(keccak256(abi.encodePacked(timestamp, difficulty))) % range); + } } diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index 300acc6..b58297c 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -17,11 +17,7 @@ contract raid_geldTest is Test, Constants { event Approval(address indexed owner, address indexed spender, uint256 value); event PlayerRegistered(address indexed player, uint256 initialGeld); - event RaidPerformed( - address indexed player, - uint256 totalMinted, - uint256 geldBalance - ); + event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance); event UnitAdded( address indexed player, uint8 unitType, @@ -61,7 +57,7 @@ contract raid_geldTest is Test, Constants { function test_00_no_fallback() public { vm.expectRevert(); // Send Ether with some data to trigger fallback - (bool success, ) = address(raid_geld).call{value: 0.1 ether}("0x1234"); + (bool success,) = address(raid_geld).call{value: 0.1 ether}("0x1234"); } function test_01_no_receive() public { @@ -113,7 +109,7 @@ contract raid_geldTest is Test, Constants { uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld)); // Making sure event is emitted when player is registered - // doesnt test player emitted event because other events get emitted before it + // doesnt test player emitted event because other events get emitted before it registerPlayerWithDaoToken(); // Check that initial raid_geld is received by the player @@ -136,7 +132,7 @@ contract raid_geldTest is Test, Constants { assertEq(army.apprentice.level, 0); assertEq(army.anointed.level, 0); assertEq(army.champion.level, 0); - } + } function test_03_dao_token_can_be_withdrawn() public { uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld)); @@ -144,7 +140,7 @@ contract raid_geldTest is Test, Constants { // Switch to Player 1 and register it vm.startPrank(player1); - // doesnt test player emitted event because other events get emitted before it + // doesnt test player emitted event because other events get emitted before it registerPlayerWithDaoToken(); // Switch back to owner and withdraw funds @@ -211,17 +207,7 @@ contract raid_geldTest is Test, Constants { // Making sure event is emitted when player adds a unit vm.expectEmit(address(raid_geld)); - emit UnitAdded( - address(player1), - 0, - 1, - cost, - playerBalance - cost, - 1, - 0, - 0, - 0 - ); + emit UnitAdded(address(player1), 0, 1, cost, playerBalance - cost, 1, 0, 0, 0); // Add 1 unit raid_geld.addUnit(0, 1); @@ -257,17 +243,7 @@ contract raid_geldTest is Test, Constants { // Making sure event is emitted when player adds a unit vm.expectEmit(address(raid_geld)); - emit UnitAdded( - address(player1), - 0, - 1, - cost, - playerBalance - cost, - 1, - 0, - 0, - 0 - ); + emit UnitAdded(address(player1), 0, 1, cost, playerBalance - cost, 1, 0, 0, 0); // bought 1 moloch_denier raid_geld.addUnit(0, 1); @@ -282,11 +258,7 @@ contract raid_geldTest is Test, Constants { // Making sure event is emitted when player performs a raid vm.expectEmit(address(raid_geld)); - emit RaidPerformed( - address(player1), - player.total_minted + amountMinted, - balance + amountMinted - ); + emit RaidPerformed(address(player1), player.total_minted + amountMinted, balance + amountMinted); // Trigger raid funds minting raid_geld.raid(); @@ -302,11 +274,7 @@ contract raid_geldTest is Test, Constants { amountMinted = army.profit_per_second * 15; - emit RaidPerformed( - address(player1), - player.total_minted + amountMinted, - balance + amountMinted - ); + emit RaidPerformed(address(player1), player.total_minted + amountMinted, balance + amountMinted); raid_geld.raid();