/* '########:'##::::'##:'########:::: ... ##..:: ##:::: ##: ##.....::::: ::: ##:::: ##:::: ##: ##:::::::::: ::: ##:::: #########: ######:::::: ::: ##:::: ##.... ##: ##...::::::: ::: ##:::: ##:::: ##: ##:::::::::: ::: ##:::: ##:::: ##: ########:::: :::..:::::..:::::..::........::::: '##::::'##:'########:'########:::'######::'##::::'##:'########::'####::::'###::::'##::: ##: ###::'###: ##.....:: ##.... ##:'##... ##: ##:::: ##: ##.... ##:. ##::::'## ##::: ###:: ##: ####'####: ##::::::: ##:::: ##: ##:::..:: ##:::: ##: ##:::: ##:: ##:::'##:. ##:: ####: ##: ## ### ##: ######::: ########:: ##::::::: ##:::: ##: ########::: ##::'##:::. ##: ## ## ##: ##. #: ##: ##...:::: ##.. ##::: ##::::::: ##:::: ##: ##.. ##:::: ##:: #########: ##. ####: ##:.:: ##: ##::::::: ##::. ##:: ##::: ##: ##:::: ##: ##::. ##::: ##:: ##.... ##: ##:. ###: ##:::: ##: ########: ##:::. ##:. ######::. #######:: ##:::. ##:'####: ##:::: ##: ##::. ##: ..:::::..::........::..:::::..:::......::::.......:::..:::::..::....::..:::::..::..::::..:: '########::'########:::'#######::::::::'##:'########::'######::'########: ##.... ##: ##.... ##:'##.... ##::::::: ##: ##.....::'##... ##:... ##..:: ##:::: ##: ##:::: ##: ##:::: ##::::::: ##: ##::::::: ##:::..::::: ##:::: ########:: ########:: ##:::: ##::::::: ##: ######::: ##:::::::::: ##:::: ##.....::: ##.. ##::: ##:::: ##:'##::: ##: ##...:::: ##:::::::::: ##:::: ##:::::::: ##::. ##:: ##:::: ##: ##::: ##: ##::::::: ##::: ##:::: ##:::: ##:::::::: ##:::. ##:. #######::. ######:: ########:. ######::::: ##:::: ..:::::::::..:::::..:::.......::::......:::........:::......::::::..::::: @@@@@@@@@@@@@@ @@ .. @@@@@ @@@ .... ,,.. @@@ @@ ./((((((((%%%%%%%%((((( . , @ @@..%%%%%(((.....(((%%%%(%%(( ,, ,,,@@ @@%%%((... ..../%%%%( @ @%%%%(.. ../%%((( ,,,,@ @@%%%((.. /////( ,,,,,//,,@ @@@look(.... /%%// ,,,,,/,,((@ @((%%%**....... %%% ,,,//,,(((((@ @@@@%%%%%*.... .... %%// ,,,,,///,,((,/(@@ @@,%%%%%.. ... .. .. ..//( ,,,,,,,/,,,,,/((((((@@ @@@,,%%%....... ... . ...//(( ,,,////,,,,,,/(((,@@ @@ ,%%((........ ... .....//( ,,,,,,,,,,,%%,,,/(((@ @@@ ,,% ... ..... .. ..///,, ,,,,,,,%@@%%for,,/(@@ @@ ,%%%%(((............ .... ..((, ,,/,,,,, ,,@@@%%%@@ @@ ,,%@@(((....... ....%((,, ...//,,, ((@@@@@%@@ @clues@, ,((%%(((... ..... (( .///////@@@((((@@@@@ @@@@@,(( ,,%((((..... ...((,,. ..////////@@@((((@ @@/////,,(,, ,,(%%((((((( /////@@((/every@@@ @@(((((@@(((,,,,, ,((((((( ,,,,%////%@@@@/@@@@@ @@(((@@/..@@, %%%%/////(@@@@(@@ @@@@@(@@@@@,, /,,@@@////(@@@@@ @@(((@@/////,,.//@@@////@ @@(((//////////@@@@@//@ @@where@ @%%%%@ */ pragma solidity ^0.8.0; // import "./ERC721Enumerable.sol"; // import "./OwnableWithAdmin.sol"; // import "./Pausable.sol"; // import "./Strings.sol"; // import "./rMAGNESIUM.sol"; // TODO mainnet // import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; contract AncientDevice is ERC721Enumerable, OwnableWithAdmin, Pausable { using Strings for uint256; using Strings for uint16; bytes32 public whitelistMerkleRoot; mapping(address => bool) public addressHasClaimed; // track whether whitelisters have minted uint256 constant MAX_DEVICES_SUPPLY = 1375; // TODO mainnet: uint256 constant SECONDS_PER_DAY = 1 days; uint256 constant SECONDS_PER_DAY = 1 minutes; //rinkeby ... TODO delete for mainnet uint constant YIELD_END_TIME = 1647727200; // rinkeby: March 19, 2022, 6pm ET ... TODO : delete for mainnet // TODO mainnet: uint constant YIELD_END_TIME = 1742839200; // March 24, 2025, 19:00 UTC: Mercury's first Inferior Conjunction of 2025 string public baseURI; string public baseExtension = ".json"; mapping(uint256 => Device) public devices; struct Device { uint16 level; // maxval 255 ... yield = (2**(uint256(level-1))) ether uint240 lastClaimTimestamp; // stored in seconds, no chance of overflow } event MagnesiumClaimed(address recipient, uint256 tokenId, uint256 amount); event DeviceUpgradedMagClaimed(address user, uint256 tokenId, uint256 targetLevel, uint256 amount); event AncientDevicesClaimed(address recipient, uint256 amount); rMAGNESIUM public magnesium; // TODO mainnet constructor(address _magnesium) ERC721("Ancient Devices", "ANCIENT DEVICES") { magnesium = rMAGNESIUM(_magnesium); // TODO mainnet _pause(); } /** * @dev include trailing / */ function setBaseURI(string memory _baseURI) public onlyOwnerOrAdmin { baseURI = _baseURI; } /** * @dev default: .json */ function setBaseExtension(string memory _newBaseExtension) public onlyOwnerOrAdmin { baseExtension = _newBaseExtension; } function setWhitelistMerkleRoot(bytes32 _whitelistMerkleRoot) external onlyOwnerOrAdmin { whitelistMerkleRoot = _whitelistMerkleRoot; } // Emergency coin migration mechanism // TODO: Owner decides if necessary function setMagnesiumAddress(address _newMagAddress) public onlyOwnerOrAdmin { magnesium = rMAGNESIUM(_newMagAddress); // TODO mainnet } /** * enables owner or admin to pause / unpause minting, claiming mag, and upgrading */ function setPaused(bool _paused) external onlyOwnerOrAdmin { if (_paused) _pause(); else _unpause(); } // CLAIMING DEVICES function _leaf(string memory payload, string memory allowance) internal pure returns (bytes32) { return keccak256(abi.encodePacked(payload, allowance)); } function _verify(bytes32[] memory proof, bytes32 leaf) internal view returns (bool) { return MerkleProof.verify(proof, whitelistMerkleRoot, leaf); } function confirmAllowance(address to, string memory allowance, bytes32[] calldata proof) external view returns (string memory) { string memory payload = string(abi.encodePacked(to)); require(_verify(proof, _leaf(allowance, payload)), "Invalid Merkle Tree proof supplied."); return allowance; } function claimDevices(uint256 _amount, bytes32[] calldata _merkleProof, uint256 _allowance) external whenNotPaused { uint256 totalSupply = _owners.length; require(totalSupply + _amount <= MAX_DEVICES_SUPPLY, "Exceeds max supply."); string memory payload = string(abi.encodePacked(_msgSender())); require(_verify(_merkleProof, _leaf(Strings.toString(_allowance), payload)), "Invalid Merkle Tree proof supplied."); require(addressHasClaimed[_msgSender()] == false, "You have already claimed devices"); // User can only claim once addressHasClaimed[_msgSender()] = true; for(uint i; i < _amount; i++) { devices[totalSupply + i] = Device({ level: uint16(1), lastClaimTimestamp: uint240(block.timestamp) }); _mint(_msgSender(), totalSupply + i); } emit AncientDevicesClaimed(_msgSender(), _amount); } function claimReserves(address _to, uint _amount) external onlyOwner{ uint256 totalSupply = _owners.length; require(totalSupply + _amount <= MAX_DEVICES_SUPPLY, "Exceeds max supply."); for(uint i; i < _amount; i++) { devices[totalSupply + i] = Device({ level: uint16(1), lastClaimTimestamp: uint240(block.timestamp) }); _mint(_to, totalSupply + i); } emit AncientDevicesClaimed(_msgSender(), _amount); } // TODO remove for mainnet function claimDevicesTest(address _to, uint _amount) external whenNotPaused { uint256 totalSupply = _owners.length; require(totalSupply + _amount <= MAX_DEVICES_SUPPLY, "Exceeds max supply."); addressHasClaimed[_to] = true; for(uint i; i < _amount; i++) { devices[totalSupply + i] = Device({ level: uint16(1), lastClaimTimestamp: uint240(block.timestamp) }); _mint(_to, totalSupply + i); } emit AncientDevicesClaimed(_msgSender(), _amount); } function hasClaimed(address _address) external view returns (bool){ return addressHasClaimed[_address]; } // Claiming MAG /** * the amount of MAG currently available to claim in a device * @param tokenId the token to check the MAG for */ function magnesiumAvailable(uint256 tokenId) public view returns (uint256) { Device memory device = devices[tokenId]; uint256 currentTimestamp = block.timestamp; if (currentTimestamp > YIELD_END_TIME) // if its past the yield end time currentTimestamp = YIELD_END_TIME; // stop the yield at yield end time if (device.lastClaimTimestamp > currentTimestamp) return 0; uint256 elapsedSeconds = currentTimestamp - device.lastClaimTimestamp; uint256 earnedPerSecond = (2**((uint256(device.level))-1)) * 1 ether / SECONDS_PER_DAY; return earnedPerSecond * elapsedSeconds; } /** * the amount of MAG currently available to claim in a set of device * @param tokenIds the tokens to check MAG for */ function magnesiumAvailableInMany(uint256[] calldata tokenIds) public view returns (uint256) { uint256 available; uint256 totalAvailable; for (uint i = 0; i < tokenIds.length; i++) { available = magnesiumAvailable(tokenIds[i]); totalAvailable += available; } return totalAvailable; } /** * claim MAGNESIUM from a device * @param tokenId the token to claim MAG from */ function claimMag(uint256 tokenId) external whenNotPaused { require(ownerOf(tokenId) == _msgSender(), "NOT YOUR DEVICE"); uint256 available = magnesiumAvailable(tokenId); require(available > 0, "NO MAG AVAILABLE"); Device storage device = devices[tokenId]; device.lastClaimTimestamp = uint240(block.timestamp); magnesium.mint(_msgSender(), available); // trusted emit MagnesiumClaimed(_msgSender(), tokenId, available); } function claimMagFromMany(uint256[] calldata tokenIds) external whenNotPaused { uint256 available; uint256 totalAvailable; for (uint i = 0; i < tokenIds.length; i++) { require(ownerOf(tokenIds[i]) == _msgSender(), "NOT YOUR DEVICE(S)"); available = magnesiumAvailable(tokenIds[i]); Device storage device = devices[tokenIds[i]]; device.lastClaimTimestamp = uint240(block.timestamp); emit MagnesiumClaimed(_msgSender(), tokenIds[i], available); totalAvailable += available; } require(totalAvailable > 0, "NO MAG AVAILABLE"); magnesium.mint(_msgSender(), totalAvailable); // trusted } // DEVICE UPGRADES /** * Upgrade a device (One at a time. You can upgrade multiple levels at once) */ function upgradeDeviceAndClaim(uint256 _tokenId, uint256 _targetLevel) external whenNotPaused { require(tx.origin == _msgSender(), "Only EOA"); require(_msgSender() == ownerOf(_tokenId), "NOT YOUR TOKEN"); Device storage device = devices[_tokenId]; require(device.level < _targetLevel, "Must be an upgrade"); require(_targetLevel < 6, "Max known level is 5"); //claim mag from device uint256 available = magnesiumAvailable(_tokenId); device.lastClaimTimestamp = uint240(block.timestamp); magnesium.mint(_msgSender(), available); // upgrade uint256 magCost = upgradeCost(device.level, _targetLevel); magnesium.burn(_msgSender(), magCost); // if user does not have enough MAG, burn requirement will fail device.level = uint16(_targetLevel); emit DeviceUpgradedMagClaimed(_msgSender(), _tokenId, _targetLevel, available); } /* * User can upgrade 1 level at a time or multiple * 1 to 2 : 7 MAG * 2 to 3 : 30 MAG * 3 to 4 : 120 MAG * 4 to 5 : 480 MAG * @param _level current level of device * @param _targetLevel the level the user would like to upgrade to * @return the cost of upgrade */ function upgradeCost(uint _level, uint _targetLevel) internal pure returns (uint256) { uint256 totalCost; while(_level < _targetLevel){ if (_level == 1) totalCost += 7 ether; else if (_level == 2) totalCost += 30 ether; else if (_level == 3) totalCost += 120 ether; else if (_level == 4) totalCost += 480 ether; _level++; } return totalCost; } function getUpgradeCost(uint256 _tokenId, uint _targetLevel) external view returns (uint256) { uint256 totalCost; uint16 level = devices[_tokenId].level; while(level < _targetLevel){ if (level == 1) totalCost += 7 ether; else if (level == 2) totalCost += 30 ether; else if (level == 3) totalCost += 120 ether; else if (level == 4) totalCost += 480 ether; level++; } return totalCost; } function getLevel(uint256 _tokenId) external view returns (uint16) { return devices[_tokenId].level; } function secretUnlockWithClaimIfNeeded(uint256 _tokenId) external whenNotPaused { require(_msgSender() == ownerOf(_tokenId), "Not your device"); require(block.timestamp > YIELD_END_TIME, "Device not ready"); Device storage device = devices[_tokenId]; require(device.level == 5, "must be level 5 to unlock a device"); uint256 available = magnesiumAvailable(_tokenId); // placed here intentionally device.level = uint16(6); if (available > 0){ //if there is mag in the device, claim it device.lastClaimTimestamp = uint240(block.timestamp); magnesium.mint(_msgSender(), available); // call to a trusted contract, no need for a reentrancy guard } } // Overrides /** Override to make sure that transfers can't be frontrun */ function transferFrom(address from, address to, uint256 tokenId) public override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); require(devices[tokenId].lastClaimTimestamp < block.timestamp, "Cannot claim immediately before a transfer"); _transfer(from, to, tokenId); } /** Override to make sure that transfers can't be frontrun */ function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved"); require(devices[tokenId].lastClaimTimestamp < block.timestamp, "Cannot claim immediately before a transfer"); _safeTransfer(from, to, tokenId, _data); } function tokenURI(uint256 _tokenId) public view override returns (string memory) { require(_exists(_tokenId), "ERC721Metadata: URI query for nonexistent token"); return string(abi.encodePacked(baseURI, Strings.toString(devices[_tokenId].level), "/", Strings.toString(_tokenId), baseExtension)); } function _mint(address to, uint256 tokenId) internal virtual override { _owners.push(to); emit Transfer(address(0), to, tokenId); } }
0.4.18