// SPDX-License-Identifier: MIT// Copyright 2023 PROOF Holdings Incpragmasolidity ^0.8.15;import {BitMaps} from"openzeppelin-contracts/utils/structs/BitMaps.sol";
import {ERC4906} from"ethier/erc721/ERC4906.sol";
import {MythicsEggErrors} from"./MythicsEggErrors.sol";
interfaceMythicEggActivatorEvents{
/**
* @notice Emitted when an egg has been activated.
*/eventEggActivated(uint256indexed tokenId);
}
/**
* @title Mythics: Egg activation module
* @author David Huber (@cxkoda)
* @custom:reviewer Arran Schlosberg (@divergencearran)
*/abstractcontractMythicEggActivatorisERC4906, MythicsEggErrors, MythicEggActivatorEvents{
usingBitMapsforBitMaps.BitMap;
/**
* @notice Throws if activating an egg that has already been activated.
*/errorEggAlreadyActivated(uint256);
/**
* @notice Thrown if one tries to activate too many eggs at once.
*/errorActivatingTooManyEggs(uint256 requested, uint256 numLeft);
/**
* @notice Keeps track of eggs that have already been activated.
*/
BitMaps.BitMap private _activated;
/**
* @notice The maximum number of eggs that can be activated during one day.
*/uint32internal _maxNumActivationsPerDay =50;
/**
* @notice The last day on which an egg was activated.
*/uint32private _lastActivationDay;
/**
* @notice The number of eggs that have been activated today.
* @dev Will be reset to 0 in `_activate` at the start of each day.
*/uint32private _numActivatedToday;
/**
* @notice Helper function to get the current day number.
*/function_currentDay() privateviewreturns (uint32) {
returnuint32(block.timestamp/ (1days));
}
/**
* @notice Sets the maximum number of activations per day.
*/function_setMaxNumActivationsPerDay(uint32 maxNumActivationsPerDay) internal{
_maxNumActivationsPerDay = maxNumActivationsPerDay;
}
/**
* @notice Activates an array of eggs.
* @dev Will revert if the number of eggs to activate exceeds the maximum number of activations per day.
*/function_activate(uint256[] calldata tokenIds) internal{
if (_currentDay() > _lastActivationDay) {
_numActivatedToday =0;
_lastActivationDay = _currentDay();
}
uint256 numLeft = _maxNumActivationsPerDay - _numActivatedToday;
if (tokenIds.length> numLeft) {
revert ActivatingTooManyEggs(tokenIds.length, numLeft);
}
for (uint256 i; i < tokenIds.length; ++i) {
uint256 tokenId = tokenIds[i];
if (activated(tokenId)) {
revert EggAlreadyActivated(tokenId);
}
if (!_exists(tokenId)) {
revert NonexistentEgg(tokenId);
}
_activated.set(tokenId);
_refreshMetadata(tokenId);
emit EggActivated(tokenId);
}
_numActivatedToday +=uint32(tokenIds.length);
}
/**
* @notice Returns whether an egg has already been activated.
*/functionactivated(uint256 tokenId) publicviewreturns (bool) {
if (!_exists(tokenId)) {
revert NonexistentEgg(tokenId);
}
return _activated.get(tokenId);
}
/**
* @notice Returns whether a token exists.
*/function_exists(uint256 tokenId) internalviewvirtualreturns (bool);
}
Contract Source Code
File 29 of 38: MythicEggSampler.sol
// SPDX-License-Identifier: MIT// Copyright 2023 PROOF Holdings Incpragmasolidity ^0.8.15;import {IEntropyOracle} from"proof/entropy/IEntropyOracle.sol";
import {MythicsEggErrors} from"./MythicsEggErrors.sol";
import {
StochasticSampler, StochasticSamplerWithCDFStorage, StochasticSamplerWithOracle
} from"./StochasticSampling.sol";
/**
* @title Mythics: Egg type sampling module
* @author David Huber (@cxkoda)
* @custom:reviewer Arran Schlosberg (@divergencearran)
*/abstractcontractMythicEggSamplerisStochasticSamplerWithCDFStorage, StochasticSamplerWithOracle, MythicsEggErrors{
/**
* @notice The different types of eggs.
*/enumEggType {
Stone,
Runic,
Legendary
}
/**
* @notice Number of egg types
*/uint8publicconstant NUM_EGG_TYPES =3;
/**
* @notice Trait ID for the egg type
*/uint8privateconstant _EGG_TYPE_TRAIT_ID =0;
/**
* @notice Token-specific parameters for sampling the egg type
* @dev Will be determined at mint.
* @param revealBlockNumber Number of the block whose entropy will be used to reaveal the egg type.
* @param distributionVersion The version/index of probability distribution to sample the egg type.
* @param mixHash Part of the block mixHash to blind the entropy oracle.
*/structSamplingParams {
uint64 revealBlockNumber;
uint16 distributionVersion;
uint128 mixHash;
}
/**
* @notice Egg-type sampling parameters keyed by token ID.
*/mapping(uint256=> SamplingParams) private _samplingParams;
/**
* @dev Constructor helper function.
*/function_numPerTrait() privatepurereturns (uint256[] memory) {
uint256[] memory numPerTrait =newuint256[](1);
numPerTrait[_EGG_TYPE_TRAIT_ID] = NUM_EGG_TYPES;
return numPerTrait;
}
constructor(IEntropyOracle oracle)
StochasticSamplerWithCDFStorage(_numPerTrait())
StochasticSamplerWithOracle(oracle)
{}
/**
* @notice Returns the egg-type sampling parameters for a given token ID.
*/functionsamplingParams(uint256 tokenId) publicviewreturns (SamplingParams memory) {
if (!_exists(tokenId)) {
revert NonexistentEgg(tokenId);
}
return _samplingParams[tokenId];
}
/**
* @inheritdoc StochasticSamplerWithCDFStorage
* @dev Reads the token-specific parameters.
*/function_distributionVersion(uint256 tokenId, uint256 traitId) internalviewvirtualoverridereturns (uint256) {
assert(traitId == _EGG_TYPE_TRAIT_ID);
return _samplingParams[tokenId].distributionVersion;
}
/**
* @inheritdoc StochasticSamplerWithOracle
* @dev Reads the token-specific parameters.
*/function_revealBlockNumber(uint256 tokenId) internalviewvirtualoverridereturns (uint256) {
return _samplingParams[tokenId].revealBlockNumber;
}
/**
* @notice Registers a token for egg-type sampling using the currently set probability distribution.
* @dev Must be called upon token mint.
*/function_registerForSampling(uint256 tokenId) internal{
uint256 revealBlockNumber =block.number;
_samplingParams[tokenId] = SamplingParams({
revealBlockNumber: uint64(revealBlockNumber),
distributionVersion: uint16(_latestDistributionVersion(_EGG_TYPE_TRAIT_ID)),
// Smearing out single-bit-of-influence from the prevrandao since we're just using 128 bits (mainly to// prevent the forge fuzzer from finding breaking runs which would force us to add circular testing logic).
mixHash: uint128(uint256(keccak256(abi.encode(block.prevrandao))))
});
entropyOracle.requestEntropy(revealBlockNumber);
}
/**
* @notice Sets the probability distribution for egg types.
*/function_setEggProbabilities(uint64[NUM_EGG_TYPES] memory pdf) internal{
uint64[] memory p =newuint64[](NUM_EGG_TYPES);
for (uint256 i =0; i < NUM_EGG_TYPES; i++) {
p[i] = pdf[i];
}
_pushProbabilities(_EGG_TYPE_TRAIT_ID, p);
}
/**
* @inheritdoc StochasticSamplerWithOracle
* @dev Mixes the seed with the token-specific parameters to blind the EntropyOracle.
*/function_seed(uint256 tokenId)
internalviewvirtualoverride(StochasticSampler, StochasticSamplerWithOracle)
returns (bytes32, bool)
{
(bytes32 seed, bool revealed) = StochasticSamplerWithOracle._seed(tokenId);
return (keccak256(abi.encode(seed, samplingParams(tokenId))), revealed);
}
/**
* @notice Returns the egg type of a given token ID and a boolean flag to indicate whether it was already revealed.
*/functioneggType(uint256 tokenId) publicviewreturns (EggType, bool) {
(uint256 sample, bool revealed) = _sampleTrait(tokenId, _EGG_TYPE_TRAIT_ID);
return (EggType(sample), revealed);
}
/**
* @notice Returns whether a token exists.
*/function_exists(uint256 tokenId) internalviewvirtualreturns (bool);
}
Contract Source Code
File 30 of 38: MythicsEgg.sol
// SPDX-License-Identifier: MIT// Copyright 2023 PROOF Holdings Incpragmasolidity ^0.8.15;import {ERC721A, ERC721ACommon, BaseTokenURI, ERC721ACommonBaseTokenURI} from"ethier/erc721/BaseTokenURI.sol";
import {OperatorFilterOS} from"ethier/erc721/OperatorFilterOS.sol";
import {ERC4906} from"ethier/erc721/ERC4906.sol";
import {IEntropyOracle} from"proof/entropy/IEntropyOracle.sol";
import {RedeemableERC721ACommon} from"proof/redemption/voucher/RedeemableERC721ACommon.sol";
import {SellableERC721ACommon} from"proof/sellers/sellable/SellableERC721ACommon.sol";
import {MythicEggSampler} from"./MythicEggSampler.sol";
import {MythicEggActivator} from"./MythicEggActivator.sol";
/**
* @title Mythics: Egg
* @notice A redeemable token claimable by all diamond nested Moonbirds.
* @author David Huber (@cxkoda)
* @custom:reviewer Arran Schlosberg (@divergencearran)
*/contractMythicsEggisERC721ACommonBaseTokenURI,
OperatorFilterOS,
SellableERC721ACommon,
RedeemableERC721ACommon,
MythicEggSampler,
MythicEggActivator{
constructor(address admin, address steerer, addresspayable secondaryReceiver, IEntropyOracle oracle)
ERC721ACommon(admin, steerer, "Mythics: Egg", "EGG", secondaryReceiver, 500)
BaseTokenURI("https://metadata.proof.xyz/mythics/egg/")
MythicEggSampler(oracle)
{
_setEggProbabilities([uint64(0), uint64(40), uint64(60)]);
}
// =================================================================================================================// Information Getter// =================================================================================================================/**
* @notice Encodes information about a token.
* @dev Intended to be used off-chain.
*/structTokenInfo {
bool revealed;
EggType eggType;
bool activated;
}
/**
* @notice Returns information about given egg token.
* @dev Not optimised, intended to be used off-chain only.
*/functiontokenInfos(uint256[] calldata tokenIds) externalviewreturns (TokenInfo[] memory) {
TokenInfo[] memory infos =new TokenInfo[](tokenIds.length);
for (uint256 i =0; i < tokenIds.length; i++) {
(EggType eggT, bool revealed) = eggType(tokenIds[i]);
infos[i] = TokenInfo({revealed: revealed, eggType: eggT, activated: activated(tokenIds[i])});
}
return infos;
}
// =================================================================================================================// Steering// =================================================================================================================/**
* @notice Sets the probability distribution for egg types.
*/functionsetEggProbabilities(uint64[NUM_EGG_TYPES] memory pdf) externalonlyRole(DEFAULT_STEERING_ROLE) {
_setEggProbabilities(pdf);
}
/**
* @notice Sets the entropy oracle.
*/functionsetEntropyOracle(IEntropyOracle newOracle) externalonlyRole(DEFAULT_STEERING_ROLE) {
entropyOracle = newOracle;
}
/**
* @notice Sets the maximum number of activations per day.
*/functionsetMaxNumActivationsPerDay(uint32 maxNumActivationsPerDay) externalonlyRole(DEFAULT_STEERING_ROLE) {
_setMaxNumActivationsPerDay(maxNumActivationsPerDay);
}
/**
* @notice Activates an array of eggs.
*/functionactivate(uint256[] calldata tokenIds) externalonlyRole(DEFAULT_STEERING_ROLE) {
_activate(tokenIds);
}
// =================================================================================================================// Inheritance Resolution// =================================================================================================================/**
* @inheritdoc SellableERC721ACommon
* @dev Registers the minted tokens for sampling.
*/function_handleSale(address to, uint64 num, bytescalldata data) internalvirtualoverride{
uint256 startTokenId = _nextTokenId();
for (uint256 i; i < num; ++i) {
_registerForSampling(startTokenId + i);
}
super._handleSale(to, num, data);
}
function_exists(uint256 tokenId)
internalviewvirtualoverride(ERC721A, MythicEggActivator, MythicEggSampler)
returns (bool)
{
return ERC721A._exists(tokenId);
}
functionsupportsInterface(bytes4 interfaceId)
publicviewvirtualoverride(ERC721ACommon, ERC721ACommonBaseTokenURI, SellableERC721ACommon, RedeemableERC721ACommon, ERC4906)
returns (bool)
{
return RedeemableERC721ACommon.supportsInterface(interfaceId)
|| SellableERC721ACommon.supportsInterface(interfaceId) || ERC4906.supportsInterface(interfaceId)
|| ERC721ACommonBaseTokenURI.supportsInterface(interfaceId);
}
function_baseURI() internalviewvirtualoverride(ERC721A, ERC721ACommonBaseTokenURI) returns (stringmemory) {
return ERC721ACommonBaseTokenURI._baseURI();
}
functionsetApprovalForAll(address operator, bool approved) publicvirtualoverride(ERC721A, OperatorFilterOS) {
OperatorFilterOS.setApprovalForAll(operator, approved);
}
functionapprove(address operator, uint256 tokenId) publicpayablevirtualoverride(ERC721A, OperatorFilterOS) {
OperatorFilterOS.approve(operator, tokenId);
}
functiontransferFrom(addressfrom, address to, uint256 tokenId)
publicpayablevirtualoverride(ERC721A, OperatorFilterOS)
{
OperatorFilterOS.transferFrom(from, to, tokenId);
}
functionsafeTransferFrom(addressfrom, address to, uint256 tokenId)
publicpayablevirtualoverride(ERC721A, OperatorFilterOS)
{
OperatorFilterOS.safeTransferFrom(from, to, tokenId);
}
functionsafeTransferFrom(addressfrom, address to, uint256 tokenId, bytesmemory data)
publicpayablevirtualoverride(ERC721A, OperatorFilterOS)
{
OperatorFilterOS.safeTransferFrom(from, to, tokenId, data);
}
}
Contract Source Code
File 31 of 38: MythicsEggErrors.sol
// SPDX-License-Identifier: MIT// Copyright 2023 PROOF Holdings Incpragmasolidity ^0.8.15;interfaceMythicsEggErrors{
/**
* @notice Thrown if one attempts an action on a nonexistent egg.
*/errorNonexistentEgg(uint256 tokenId);
}
Contract Source Code
File 32 of 38: OperatorFilterOS.sol
Contract Source Code
File 33 of 38: OperatorFilterer.sol
Contract Source Code
File 34 of 38: Pausable.sol
Contract Source Code
File 35 of 38: RedeemableERC721ACommon.sol
Contract Source Code
File 36 of 38: SellableERC721ACommon.sol
Contract Source Code
File 37 of 38: StochasticSampling.sol
// SPDX-License-Identifier: MIT// Copyright 2023 PROOF Holdings Incpragmasolidity ^0.8.15;import {IEntropyOracle} from"proof/entropy/IEntropyOracle.sol";
/**
* @notice Helper libray for sampling from a discrete probability distribution.
*/libraryStochasticSamplingLib{
/**
* @notice Computes the cumulative probability distribution from a discrete probability distribution.
*/functioncomputeCDF(uint64[] memory pdf) internalpurereturns (uint64[] memory) {
uint64[] memory cdf =newuint64[](pdf.length);
cdf[0] = pdf[0];
for (uint256 i =1; i < pdf.length; ++i) {
cdf[i] = cdf[i -1] + pdf[i];
}
return cdf;
}
/**
* @notice Samples from a discrete cumulative probability distribution.
* @dev This function assumes that rand is uniform in [0,2^256) and that `cdf[cdf.length - 1] << 2^256`. If not the
* outcome will be biased
*/functionsampleWithCDF(uint256 rand, uint64[] memory cdf) internalpurereturns (uint256) {
rand = rand % cdf[cdf.length-1];
for (uint256 i; i < cdf.length; ++i) {
if (rand < cdf[i]) {
return i;
}
}
// This will never be reached given the above bounds of rand.assert(false);
return0;
}
}
/**
* @notice A contract that can sample token traits from discrete probability distributions.
* @dev The probability distributions and seed derivation functions are implemented in the inheriting contracts.
* @dev The functions defined here might be gas-heavy and are therefore intended to be used in view-calls only.
*/abstractcontractStochasticSampler{
/**
* @notice Returns a random seed for a given token and a boolean indicating whether the seed is available.
*/function_seed(uint256 tokenId) internalviewvirtualreturns (bytes32, bool);
/**
* @notice Returns the cumulative probability distribution for a given trait of a given token.
*/function_cdf(uint256 tokenId, uint256 traitId) internalviewvirtualreturns (uint64[] memory);
/**
* @notice Samples a trait for a given token.
* @dev Returns the sampled trait and a boolean indicating whether the trait was already revealed (i.e. if the seed
* for the given token is available).
*/function_sampleTrait(uint256 tokenId, uint256 traitId) internalviewreturns (uint256, bool) {
(bytes32 seed, bool revealed) = _seed(tokenId);
seed =keccak256(abi.encodePacked(seed, traitId));
return (StochasticSamplingLib.sampleWithCDF(uint256(seed), _cdf(tokenId, traitId)), revealed);
}
}
/**
* @notice A contract that can sample token traits from discrete probability distributions loaded from storage.
*/abstractcontractStochasticSamplerWithCDFStorageisStochasticSampler{
usingStochasticSamplingLibforuint64[];
/**
* @notice Thrown if the traitId is invalid, i.e. if it exceeds the number of traits.
*/errorInvalidTraitId(uint256 traitId);
/**
* @notice Thrown if the length of the given PDF does not match the number of realisations in a given trait.
*/errorIncorrectPDFLength(uint256 gotLength, uint256 traitId, uint256 wantLength);
/**
* @notice Thrown if the given PDF cannot be normalised, i.e. if the sum of the probabilities is zero.
*/errorConstantZeroPDF();
/**
* @notice The number of realisations for each trait.
*/uint256[] private _numPerTrait;
/**
* @notice The cumulative probability distributions for each trait.
* @dev Indexed by traitId, distributionVersion, sample.
* @dev The distributionVersion is intended to allow having multiple "versions" of the probability distributions.
*/uint64[][][] private _cdfs;
constructor(uint256[] memory numPerTrait) {
_numPerTrait = numPerTrait;
for (uint256 i; i < numPerTrait.length; ++i) {
_cdfs.push(newuint64[][](0));
}
assert(_cdfs.length== numPerTrait.length);
}
/**
* @notice Adds a new probability distribution for a given trait.
*/function_pushProbabilities(uint256 traitId, uint64[] memory pdf) internal{
if (traitId >= _numPerTrait.length) {
revert InvalidTraitId(traitId);
}
if (pdf.length!= _numPerTrait[traitId]) {
revert IncorrectPDFLength(pdf.length, traitId, _numPerTrait[traitId]);
}
uint64[] memory cdf = pdf.computeCDF();
if (cdf[cdf.length-1] ==0) {
revert ConstantZeroPDF();
}
_cdfs[traitId].push(cdf);
}
/**
* @notice Returns the version/index of the latest probability distribution for a given trait.
*/function_latestDistributionVersion(uint256 traitId) internalviewreturns (uint256) {
return _cdfs[traitId].length-1;
}
/**
* @notice Returns the version/index of the probability distribution that is used for a given token and trait.
* @dev This function is intended to be overridden by inheriting contracts.
*/function_distributionVersion(uint256 tokenId, uint256 traitId) internalviewvirtualreturns (uint256);
/**
* @inheritdoc StochasticSampler
* @dev Returns the probability distribution that is index by `_distributionVersion`.
*/function_cdf(uint256 tokenId, uint256 traitId) internalviewvirtualoverridereturns (uint64[] memory) {
if (traitId >= _numPerTrait.length) {
revert InvalidTraitId(traitId);
}
return _cdfs[traitId][_distributionVersion(tokenId, traitId)];
}
}
/**
* @notice A contract that can sample token traits from discrete probability distributions using entropy provided by the
* EntropyOracle.
*/abstractcontractStochasticSamplerWithOracleisStochasticSampler{
/**
* @notice The entropy oracle.
*/
IEntropyOracle public entropyOracle;
constructor(IEntropyOracle entropyOracle_) {
entropyOracle = entropyOracle_;
}
/**
* @inheritdoc StochasticSampler
* @dev Uses the entropy of the block at `_revealBlockNumber(tokenId)`.
*/function_seed(uint256 tokenId) internalviewvirtualoverridereturns (bytes32, bool) {
bytes32 entropy = entropyOracle.blockEntropy(_revealBlockNumber(tokenId));
return (keccak256(abi.encode(entropy, tokenId)), entropy !=0);
}
/**
* @notice The blocknumber at which a given token will be revealed.
* @dev The entropy provided by `entropyOracle` for this block will be used as seed for trait sampling.
*/function_revealBlockNumber(uint256 tokenId) internalviewvirtualreturns (uint256);
}