// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/access/Ownable.sol";
import "./libs/ShufflerV3.sol";
contract PPP3 is Ownable {
IDelegateRegistry internal constant registry = IDelegateRegistry(0x00000000000000447e69651d841bD8D104Bed493);
INFT internal immutable CONTRACT_AD; // mainnet: 0x9CF0aB1cc434dB83097B7E9c831a764481DEc747
INFT internal immutable CONTRACT_FPP; // mainnet: 0xA8A425864dB32fCBB459Bf527BdBb8128e6abF21
struct UserStats {
uint256 passCount;
uint256 aDCount;
uint256 availableExclusiveMints;
uint256[10] mintsPerArtist;
uint256[10] mintsAvailable;
}
uint256 public startTime;
uint256 public exclusivePrice = 0.05 ether;
uint256 public publicPrice = 0.1 ether;
uint256 public maxPerArtist = 10;
uint256 public mintsPerPass = 2;
uint256 public stage1Duration = 30 minutes;
uint256 public stage2Duration = 30 minutes;
address public mintable;
bool public paused;
uint256 public totalSold;
mapping(address => uint256[10]) public mintCount;
mapping(address => uint256) public exclusiveMintCount;
mapping(uint256 => ShufflerV3) public collectionToShuffler;
constructor(
uint256 _startTime,
address collectionAddress,
address FPPAddress,
address ADAddress
) Ownable(msg.sender) {
startTime = _startTime;
mintable = collectionAddress;
for (uint256 i; i < 10; i++) {
collectionToShuffler[i] = new ShufflerV3(100);
}
CONTRACT_FPP = INFT(FPPAddress);
CONTRACT_AD = INFT(ADAddress);
}
function mint(
uint256[] calldata wantedCollections,
uint256[] calldata wantedQuantity
) external payable {
require(totalSold < 1000, "Sold out.");
require(!paused, "Sale is paused.");
require(wantedCollections.length == wantedQuantity.length, "Array length mismatch.");
require(
block.timestamp >= startTime,
"Sale not started."
);
uint256 totalQuantity;
uint256 globalMax = fPPCount(msg.sender) * mintsPerPass;
if (block.timestamp >= startTime + stage1Duration) {
globalMax += aDCount(msg.sender) * mintsPerPass;
}
if(isPublic()) {
globalMax = 1000;
}
for (uint256 i; i < wantedCollections.length; i++) {
uint256 collection = wantedCollections[i];
ShufflerV3 shuffler = collectionToShuffler[collection];
uint256 amount = wantedQuantity[i];
uint256 remaining = shuffler.remainingNumbers();
if (amount > remaining) {
amount = remaining;
}
uint256 alreadyMinted = mintCount[msg.sender][collection];
uint256 allowance = maxPerArtist - alreadyMinted;
if (amount > allowance) {
amount = allowance;
}
uint256 alreadyMintedExclusive = exclusiveMintCount[msg.sender];
uint256 globalAllowance = globalMax - alreadyMintedExclusive;
if (amount > globalAllowance) {
amount = globalAllowance;
}
mintCount[msg.sender][collection] += amount;
// public mints don't count against pass limit
if (!isPublic()) {
exclusiveMintCount[msg.sender] += amount;
}
totalQuantity += amount;
for (uint256 j; j < amount; j++) {
INFT(mintable).mint(msg.sender, collection * 100 + shuffler.drawNext());
}
}
totalSold += totalQuantity;
uint256 price = isPublic() ? publicPrice : exclusivePrice;
uint256 totalPrice = price * totalQuantity;
require(totalPrice <= msg.value, "Insufficient ETH sent.");
uint256 refund = msg.value - totalPrice;
if (refund > 0) {
(bool success,) = msg.sender.call{value: refund}("");
require(success, "Refund failed");
}
}
function aDCount(address user) internal view returns (uint256 total) {
total = CONTRACT_AD.balanceOf(user);
IDelegateRegistry.Delegation[] memory delegations = registry.getIncomingDelegations(user);
for(uint256 i; i < delegations.length; i++) {
IDelegateRegistry.Delegation memory dele = delegations[i];
if (dele.type_ == IDelegateRegistry.DelegationType.ALL) {
total += CONTRACT_AD.balanceOf(dele.from);
}
if (dele.type_ == IDelegateRegistry.DelegationType.CONTRACT && dele.contract_ == address(CONTRACT_AD)) {
total += CONTRACT_AD.balanceOf(dele.from);
}
}
}
function fPPCount(address user) internal view returns (uint256 total) {
total = CONTRACT_FPP.balanceOf(user);
IDelegateRegistry.Delegation[] memory delegations = registry.getIncomingDelegations(user);
for(uint256 i; i < delegations.length; i++) {
IDelegateRegistry.Delegation memory dele = delegations[i];
if (dele.type_ == IDelegateRegistry.DelegationType.ALL) {
total += CONTRACT_FPP.balanceOf(dele.from);
}
if (dele.type_ == IDelegateRegistry.DelegationType.CONTRACT && dele.contract_ == address(CONTRACT_FPP)) {
total += CONTRACT_FPP.balanceOf(dele.from);
}
}
}
function isPublic() public view returns (bool) {
return block.timestamp >= startTime + stage1Duration + stage2Duration;
}
function userStats(address user) external view returns (UserStats memory stats) {
uint256 _passCount = fPPCount(user);
uint256 _aDCount = aDCount(user);
uint256[10] memory mintsAvailable;
for (uint256 i; i < 10; i++) {
mintsAvailable[i] = collectionToShuffler[i].remainingNumbers();
}
stats = UserStats(
_passCount,
_aDCount,
isPublic() ? 0 :
(block.timestamp >= startTime + stage1Duration ?
(_passCount + _aDCount) * mintsPerPass : _passCount * mintsPerPass
) - exclusiveMintCount[user],
mintCount[user],
mintsAvailable
);
}
// OWNER FUNCTIONS
function editConfig(
uint256 _startTime,
uint256 _exclusivePrice,
uint256 _publicPrice,
uint256 _maxPerArtist,
uint256 _mintsPerPass,
uint256 _stage1Duration,
uint256 _stage2Duration
) external onlyOwner {
startTime = _startTime;
exclusivePrice = _exclusivePrice;
publicPrice = _publicPrice;
maxPerArtist = _maxPerArtist;
mintsPerPass = _mintsPerPass;
stage1Duration = _stage1Duration;
stage2Duration = _stage2Duration;
}
function pause() external onlyOwner {
paused = true;
}
function unpause() external onlyOwner {
paused = false;
}
function withdraw(address recipient) external onlyOwner {
(bool success,) = recipient.call{value: address(this).balance}("");
require(success, "Withdraw failed.");
}
}
interface INFT {
function balanceOf(address account) external view returns (uint256);
function mint(address to, uint256 tokenId) external;
}
interface IDelegateRegistry {
/// @notice Delegation type, NONE is used when a delegation does not exist or is revoked
enum DelegationType {
NONE,
ALL,
CONTRACT,
ERC721,
ERC20,
ERC1155
}
/// @notice Struct for returning delegations
struct Delegation {
DelegationType type_;
address to;
address from;
bytes32 rights;
address contract_;
uint256 tokenId;
uint256 amount;
}
function getIncomingDelegations(address to) external view returns (Delegation[] memory delegations);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/access/Ownable.sol";
contract ShufflerV3 is Ownable {
uint256 internal immutable totalNumbers;
uint256 public remainingNumbers;
mapping(uint256 => uint256) public numberAtIndex;
constructor(uint256 n) Ownable(msg.sender) {
totalNumbers = n;
remainingNumbers = n;
}
function drawNext() public onlyOwner returns (uint256) {
require(remainingNumbers > 0);
uint256 randomIndex = uint256(keccak256(abi.encodePacked(remainingNumbers, block.prevrandao))) %
remainingNumbers;
uint256 numberToDraw = numberAtIndex[randomIndex];
if (numberToDraw == 0) {
numberToDraw = randomIndex + 1;
}
remainingNumbers -= 1;
uint256 swapNumber = numberAtIndex[remainingNumbers];
if (swapNumber == 0) {
swapNumber = remainingNumbers + 1;
}
numberAtIndex[randomIndex] = swapNumber;
numberAtIndex[remainingNumbers] = numberToDraw;
return numberToDraw;
}
}
{
"compilationTarget": {
"projects/ppp3/PPP3.sol": "PPP3"
},
"evmVersion": "cancun",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"uint256","name":"_startTime","type":"uint256"},{"internalType":"address","name":"collectionAddress","type":"address"},{"internalType":"address","name":"FPPAddress","type":"address"},{"internalType":"address","name":"ADAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"OwnableInvalidOwner","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"OwnableUnauthorizedAccount","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"collectionToShuffler","outputs":[{"internalType":"contract ShufflerV3","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_startTime","type":"uint256"},{"internalType":"uint256","name":"_exclusivePrice","type":"uint256"},{"internalType":"uint256","name":"_publicPrice","type":"uint256"},{"internalType":"uint256","name":"_maxPerArtist","type":"uint256"},{"internalType":"uint256","name":"_mintsPerPass","type":"uint256"},{"internalType":"uint256","name":"_stage1Duration","type":"uint256"},{"internalType":"uint256","name":"_stage2Duration","type":"uint256"}],"name":"editConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"exclusiveMintCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"exclusivePrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isPublic","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxPerArtist","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"wantedCollections","type":"uint256[]"},{"internalType":"uint256[]","name":"wantedQuantity","type":"uint256[]"}],"name":"mint","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"mintCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintable","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintsPerPass","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"publicPrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stage1Duration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stage2Duration","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"startTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSold","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"userStats","outputs":[{"components":[{"internalType":"uint256","name":"passCount","type":"uint256"},{"internalType":"uint256","name":"aDCount","type":"uint256"},{"internalType":"uint256","name":"availableExclusiveMints","type":"uint256"},{"internalType":"uint256[10]","name":"mintsPerArtist","type":"uint256[10]"},{"internalType":"uint256[10]","name":"mintsAvailable","type":"uint256[10]"}],"internalType":"struct PPP3.UserStats","name":"stats","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]