forked from mico/idle_moloch
350 lines
12 KiB
Solidity
350 lines
12 KiB
Solidity
// 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, Boss, LastBossResult} 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.00005 ether;
|
|
uint256 public BUY_IN_DAO_TOKEN_AMOUNT;
|
|
uint256 public constant INITIAL_GELD = 50 * MANTISSA;
|
|
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;
|
|
// RGCVII pool
|
|
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);
|
|
|
|
// 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 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) ERC20("Raid Geld", "GELD") Ownable(msg.sender) {
|
|
daoToken = ERC20(_daoToken);
|
|
pool = _pool;
|
|
BUY_IN_DAO_TOKEN_AMOUNT = 500 * 10 ** daoToken.decimals();
|
|
}
|
|
|
|
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 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);
|
|
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);
|
|
start_game(msg.sender);
|
|
}
|
|
|
|
// New player wants to register with dao
|
|
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(
|
|
daoToken.transferFrom(msg.sender, address(this), BUY_IN_DAO_TOKEN_AMOUNT), "Failed to transfer DAO tokens"
|
|
);
|
|
start_game(msg.sender);
|
|
}
|
|
|
|
// 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 reward = RaidGeldUtils.calculateBossReward(boss_to_attack.level, BUY_IN_DAO_TOKEN_AMOUNT);
|
|
players[msg.sender].total_rewards += 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;
|
|
}
|
|
|
|
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;
|
|
}
|