You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
9.1 KiB
Solidity
228 lines
9.1 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.13;
|
|
|
|
import {Owned} from "solmate/auth/Owned.sol";
|
|
import {Duck} from "./Duck.sol";
|
|
import {Zapper} from "./Zapper.sol";
|
|
|
|
/**
|
|
@title Punkhunt
|
|
@author lzamenace.eth
|
|
@notice This is the main contract interface for...
|
|
*/
|
|
contract Punkhunt is Owned {
|
|
|
|
/// Track the number of kills for each address
|
|
mapping(address => uint256) public killCount;
|
|
/// Index addresses to form a basic leaderboard
|
|
mapping(uint256 => address) public leaderboard;
|
|
/// Point to the latest leaderboard update
|
|
uint256 public leaderboardPointer;
|
|
/// Price of the Duck ERC-721 token
|
|
uint256 public duckPrice = 0.01 ether;
|
|
/// Price of the Zapper ERC-1155 token
|
|
uint256 public zapperPrice = 0.0025 ether;
|
|
/// If PvP can commence
|
|
bool public mayhem;
|
|
/// Duck contract
|
|
Duck public duck;
|
|
/// Zapper contract
|
|
Zapper public zapper;
|
|
|
|
/// ShotDuck event is for recording the results of sendZappers for real-time feedback to a frontend interface
|
|
/// @param from Sender of the zappers
|
|
/// @param tokenId Duck token which was targeted
|
|
/// @param hit Whether or not the zapper killed the token or not (was a dud / already killed)
|
|
/// @param owned Whether or not the sender was the owner of the DUCK token
|
|
event ShotDuck(address indexed from, uint256 indexed tokenId, bool hit, bool owned);
|
|
|
|
constructor() Owned(msg.sender) {}
|
|
|
|
// =========================================================================
|
|
// Admin
|
|
// =========================================================================
|
|
|
|
/// Withdraw funds to contract owner and game winners
|
|
function withdraw() external onlyOwner {
|
|
// kill leader gets 25%
|
|
// surviving duck owner gets 25%
|
|
// contract deployer gets remaining 50%
|
|
uint256 balance = address(this).balance;
|
|
(bool success, ) = payable(msg.sender).call{value: balance}("");
|
|
require(success, "failed to withdraw");
|
|
}
|
|
|
|
/// Set price per DUCK
|
|
/// @param _price Price in wei to mint DUCK token
|
|
function setDuckPrice(uint256 _price) external onlyOwner {
|
|
duckPrice = _price;
|
|
}
|
|
|
|
/// Set price per ZAPPER
|
|
/// @param _price Price in wei to mint ZAPPER token
|
|
function setZapperPrice(uint256 _price) external onlyOwner {
|
|
zapperPrice = _price;
|
|
}
|
|
|
|
/// Set contract address for Duck tokens
|
|
/// @param _address Address of the Duck / DUCK contract
|
|
function setDuckContract(address _address) external onlyOwner {
|
|
duck = Duck(_address);
|
|
}
|
|
|
|
/// Set contract address for Zapper tokens
|
|
/// @param _address Address of the Zapper / ZAPPER contract
|
|
function setZapperContract(address _address) external onlyOwner {
|
|
zapper = Zapper(_address);
|
|
}
|
|
|
|
/// Toggle mayhem switch to enable shooting
|
|
function toggleMayhem() external onlyOwner {
|
|
mayhem = !mayhem;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Modifiers
|
|
// =========================================================================
|
|
|
|
/// This modifier prevents actions once the Duck survivor count is breached.
|
|
/// The game stops; no more zappering/killing. Survivors make it to the next round.
|
|
modifier gameNotCompleted {
|
|
require(
|
|
duck.totalSupply() > 1,
|
|
"game is completed"
|
|
);
|
|
_;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Getters
|
|
// =========================================================================
|
|
|
|
/// Get DUCK token balance of wallet
|
|
/// @param _address Wallet address to query balance of DUCK token
|
|
/// @return balance Amount of DUCK tokens owned by _address
|
|
function duckBalanceOf(address _address) public view returns (uint256) {
|
|
return duck.balanceOf(_address);
|
|
}
|
|
|
|
/// Get DUCK amount minted (including ones that have been burned/killed)
|
|
/// @param _address Wallet address to query the amount of DUCK token minted
|
|
/// @return balance Amount of DUCK tokens that have been minted by _address
|
|
function ducksMintedBy(address _address) public view returns (uint256) {
|
|
return duck.tokensMintedByWallet(_address);
|
|
}
|
|
|
|
/// Get DUCK token total supply
|
|
/// @return supply Amount of DUCK tokens minted in total
|
|
function totalDucksMinted() public view returns (uint256) {
|
|
return duck.minted();
|
|
}
|
|
|
|
/// Get DUCK ducks killed
|
|
/// @return killCount Amount of DUCK tokens killed
|
|
function totalDucksKilled() public view returns (uint256) {
|
|
return duck.burned();
|
|
}
|
|
|
|
/// Get DUCK supply (minted - burned)
|
|
/// @return totalSupply Amount of DUCK tokens killed
|
|
function totalDuckSupply() public view returns (uint256) {
|
|
return duck.totalSupply();
|
|
}
|
|
|
|
/// Get DUCK token survivor count
|
|
/// @return survivorCount Maximum amount of DUCK survivor tokens that can ever exist
|
|
function duckMaxSurvivorCount() public view returns (uint256) {
|
|
return duck.MAX_SURVIVOR_COUNT();
|
|
}
|
|
|
|
/// Get DUCK token max mint amount per wallet
|
|
/// @return mintAmount Maximum amount of DUCK tokens that can be minted per wallet
|
|
function duckMaxMintPerWallet() public view returns (uint256) {
|
|
return duck.MAX_MINT_AMOUNT();
|
|
}
|
|
|
|
/// Get ZAPPER token balance of wallet
|
|
/// @param _address Wallet address to query balance of ZAPPER token
|
|
/// @return balance Amount of ZAPPER tokens owned by _address
|
|
function zapperBalanceOf(address _address) public view returns (uint256) {
|
|
return zapper.balanceOf(_address, 1);
|
|
}
|
|
|
|
/// Get ZAPPER token supply
|
|
/// @return supply Amount of ZAPPER tokens ever minted
|
|
function totalZappersMinted() public view returns (uint256) {
|
|
return zapper.zappersMinted();
|
|
}
|
|
|
|
/// Get ZAPPER burned amount
|
|
/// @return exploded Amount of ZAPPER tokens that have burned
|
|
function totalZappersBurned() public view returns (uint256) {
|
|
return zapper.zappersBurned();
|
|
}
|
|
|
|
// =========================================================================
|
|
// Tokens
|
|
// =========================================================================
|
|
|
|
/// Mint a duck and give 1 free zapper
|
|
/// @param _amount Amount of Ducks to mint
|
|
function mintDucks(uint256 _amount) external payable gameNotCompleted {
|
|
require(msg.value >= _amount * duckPrice, "not enough ether");
|
|
duck.mint(msg.sender, _amount);
|
|
zapper.mint(msg.sender, _amount);
|
|
}
|
|
|
|
/// Mint additional Zappers to kill ducks
|
|
/// @param _amount Amount of Zappers to mint
|
|
function mintZappers(uint256 _amount) external payable gameNotCompleted {
|
|
require(msg.value >= _amount * zapperPrice, "not enough ether");
|
|
zapper.mint(msg.sender, _amount);
|
|
}
|
|
|
|
/// Send N zappers to pseudo-random Duck tokenIds to kill them.
|
|
/// If the Duck is already dead, the zapper is considered a dud.
|
|
/// Update a leaderboard with updated kill counts.
|
|
/// @dev Pick a pseudo-random tokenID from Duck contract and toggle a mapping value
|
|
/// @dev The likelihood of killing a boomer decreases as time goes on - i.e. more duds
|
|
/// @param _amount Amount of zappers to send to kill Ducks (dead pfps)
|
|
function shootDucks(uint256 _amount) external gameNotCompleted {
|
|
// Require mayhem is set (allow time to mint and trade)
|
|
require(mayhem, "not ready for mayhem");
|
|
// Ensure _amount will not exceed wallet balance of zappers, Duck supply, and active Ducks
|
|
uint256 supply = totalDuckSupply();
|
|
uint256 bal = zapperBalanceOf(msg.sender);
|
|
require(_amount <= bal, "not enough zappers");
|
|
for (uint256 i; i < _amount; i++) {
|
|
// Pick a pseudo-random Duck token - imperfectly derives token IDs so that repeats are probable
|
|
uint256 randomDuck = (uint256(keccak256(abi.encodePacked(i, supply, bal, msg.sender))) % supply) + 1;
|
|
// Capture owner
|
|
address _owner = duck.ownerOf(randomDuck);
|
|
// Check if it was already killed
|
|
bool dud = _owner == address(0);
|
|
// Check if the sender owns it (misfired, killed own pfp)
|
|
bool senderOwned = msg.sender == _owner;
|
|
// Kill it (does nothing if already toggled as dead)
|
|
duck.die(randomDuck);
|
|
// Emit event for displaying in web app
|
|
emit ShotDuck(msg.sender, randomDuck, !dud, senderOwned);
|
|
// Increment kill count if successfully killed another player's Duck
|
|
if(!dud && !senderOwned) {
|
|
killCount[msg.sender]++;
|
|
}
|
|
}
|
|
// Update the leaderboard and pointer for tracking the highest amount of kills for wallets
|
|
uint256 kills = killCount[msg.sender];
|
|
address leader = leaderboard[leaderboardPointer];
|
|
if (kills > killCount[leader]) {
|
|
if (leader != msg.sender) {
|
|
leaderboardPointer++;
|
|
leaderboard[leaderboardPointer] = msg.sender;
|
|
}
|
|
}
|
|
// Burn ERC-1155 ZAPPER tokens
|
|
zapper.burn(msg.sender, _amount);
|
|
}
|
|
|
|
} |