From 6b77ef79d7ce5f9e0d0383ae63863d8f81a12ad4 Mon Sep 17 00:00:00 2001 From: zhl Date: Mon, 27 Dec 2021 15:03:18 +0800 Subject: [PATCH] add contract of hero and marketplace --- contracts/CryptoHero.sol | 279 +++++++++++++++++++++++++++++ contracts/IMarketplace.sol | 83 +++++++++ contracts/MarketPlace.sol | 194 ++++++++++++++++++++ migrations/2_deploy_hero.js | 9 + migrations/3_deploy_marketplace.js | 10 ++ truffle-config.js | 10 ++ 6 files changed, 585 insertions(+) create mode 100644 contracts/CryptoHero.sol create mode 100644 contracts/IMarketplace.sol create mode 100644 contracts/MarketPlace.sol create mode 100644 migrations/2_deploy_hero.js create mode 100644 migrations/3_deploy_marketplace.js diff --git a/contracts/CryptoHero.sol b/contracts/CryptoHero.sol new file mode 100644 index 0000000..073ee90 --- /dev/null +++ b/contracts/CryptoHero.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +contract CryptoHero is Ownable, IERC721 { + using SafeMath for uint256; + + uint256 public constant maxGen0Hero = 10; + + uint256 public gen0Counter = 0; + + bytes4 internal constant _ERC721Checksum = bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); + + bytes4 private constant _InterfaceIdERC721 = 0x80ac58cd; + bytes4 private constant _InterfaceIdERC165 = 0x01ffc9a7; + + string private _name; + string private _symbol; + + struct Hero { + uint256 genes; + uint64 birthTime; + uint32 mumId; + uint32 dadId; + uint16 generation; + } + Hero[] heros; + // map of hero and owner + mapping(uint256 => address) public heroOwner; + // hero count of owner + mapping(address => uint256) ownsNumberOfTokens; + // hero in trading + mapping(uint256 => address) public approvalOneHero; + + mapping(address => mapping (address => bool)) private _operatorApprovals; + + event Birth(address owner, uint256 heroId, uint256 mumId, uint256 dadId, uint256 genes); + + constructor(string memory dname, string memory dsymbol) { + _name = dname; + _symbol = dsymbol; + _createHero(0, 0, 0, uint256(0), address(0)); + } + + function getContractOwner() external view returns (address contractowner) { + return owner(); + } + + function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { + return (_interfaceId == _InterfaceIdERC721 || _interfaceId == _InterfaceIdERC165); + } + + function createHeroGen0(uint256 genes) public onlyOwner returns (uint256) { + require(gen0Counter < maxGen0Hero, "Maximum number of Heros is reached. No new hero allowed!"); + gen0Counter = SafeMath.add(gen0Counter, 1); + return _createHero(0, 0, 0, genes, msg.sender); + } + + function _createHero( + uint256 _mumId, + uint256 _dadId, + uint256 _generation, + uint256 _genes, + address _owner + ) internal returns (uint256) { + Hero memory _hero = Hero({ + genes: _genes, + birthTime: uint64(block.timestamp), + mumId: uint32(_mumId), //easier to input 256 and later convert to 32. + dadId: uint32(_dadId), + generation: uint16(_generation) + }); + heros.push(_hero); + uint256 newHeroId = SafeMath.sub(heros.length, 1);//want to start with zero. + _transfer(address(0), _owner, newHeroId);//transfer from nowhere. Creation event. + emit Birth(_owner, newHeroId, _mumId, _dadId, _genes); + return newHeroId; + } + + function getHero(uint256 tokenId) public view returns ( + uint256 genes, + uint256 birthTime, + uint256 mumId, + uint256 dadId, + uint256 generation) //code looks cleaner when the params appear here vs. in the return statement. + { + require(tokenId < heros.length, "Token ID doesn't exist."); + Hero storage hero = heros[tokenId];//saves space over using memory, which would make a copy + + genes = hero.genes; + birthTime = uint256(hero.birthTime); + mumId = uint256(hero.mumId); + dadId = uint256(hero.dadId); + generation = uint256(hero.generation); + } + + function getAllHerosOfOwner(address owner) external view returns(uint256[] memory) { + uint256[] memory allHerosOfOwner = new uint[](ownsNumberOfTokens[owner]); + uint256 j = 0; + for (uint256 i = 0; i < heros.length; i++) { + if (heroOwner[i] == owner) { + allHerosOfOwner[j] = i; + j = SafeMath.add(j, 1); + } + } + return allHerosOfOwner; + } + + function balanceOf(address owner) external view returns (uint256 balance) { + return ownsNumberOfTokens[owner]; + } + + function totalSupply() external view returns (uint256 total) { + return heros.length; + } + + function name() external view returns (string memory tokenName){ + return _name; + } + + function symbol() external view returns (string memory tokenSymbol){ + return _symbol; + } + + function ownerOf(uint256 tokenId) external view returns (address owner) { + require(tokenId < heros.length, "Token ID doesn't exist."); + return heroOwner[tokenId]; + } + + function transfer(address to, uint256 tokenId) external { + require(to != address(0), "Use the burn function to burn tokens!"); + require(to != address(this), "Wrong address, try again!"); + require(heroOwner[tokenId] == msg.sender); + _transfer(msg.sender, to, tokenId); + } + + function _transfer(address _from, address _to, uint256 _tokenId) internal { + require(_to != address(this)); + ownsNumberOfTokens[_to] = SafeMath.add(ownsNumberOfTokens[_to], 1); + heroOwner[_tokenId] = _to; + + if (_from != address(0)) { + ownsNumberOfTokens[_from] = SafeMath.sub(ownsNumberOfTokens[_from], 1); + delete approvalOneHero[_tokenId];//when owner changes, approval must be removed. + } + + emit Transfer(_from, _to, _tokenId); + } + + function approve(address _approved, uint256 _tokenId) external { + require(heroOwner[_tokenId] == msg.sender || _operatorApprovals[heroOwner[_tokenId]][msg.sender] == true, + "You are not authorized to access this function."); + approvalOneHero[_tokenId] = _approved; + emit Approval(msg.sender, _approved, _tokenId); + } + + function setApprovalForAll(address _operator, bool _approved) external { + require(_operator != msg.sender); + _operatorApprovals[msg.sender][_operator] = _approved; + emit ApprovalForAll(msg.sender, _operator, _approved); + } + + function getApproved(uint256 _tokenId) external view returns (address) { + require(_tokenId < heros.length, "Token doesn't exist"); + return approvalOneHero[_tokenId]; + } + + function isApprovedForAll(address _owner, address _operator) external view returns (bool) { + return _operatorApprovals[_owner][_operator]; + } + + + function _safeTransfer(address _from, address _to, uint256 _tokenId, bytes memory _data) internal { + require(_checkERC721Support(_from, _to, _tokenId, _data)); + _transfer(_from, _to, _tokenId); + } + + function _checkERC721Support(address _from, address _to, uint256 _tokenId, bytes memory _data) + internal returns(bool) { + if(!_isContract(_to)) { + return true; + } + bytes4 returnData = IERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data); + //Call onERC721Received in the _to contract + return returnData == _ERC721Checksum; + //Check return value + } + + function _isContract(address _to) internal view returns (bool) { + uint32 size; + assembly{ + size := extcodesize(_to) + } + return size > 0; + //check if code size > 0; wallets have 0 size. + } + + function _isOwnerOrApproved(address _from, address _to, uint256 _tokenId) internal view returns (bool) { + require(_from == msg.sender || + approvalOneHero[_tokenId] == msg.sender || + _operatorApprovals[_from][msg.sender], + "You are not authorized to use this function"); + require(heroOwner[_tokenId] == _from, "Owner incorrect"); + require(_to != address(0), "Error: Operation would delete this token permanently"); + require(_tokenId < heros.length, "Token doesn't exist"); + return true; + } + + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external { + _isOwnerOrApproved(_from, _to, _tokenId); + _safeTransfer(_from, _to, _tokenId, data); + } + + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external { + _isOwnerOrApproved(_from, _to, _tokenId); + _safeTransfer(_from, _to, _tokenId, ""); + } + + function transferFrom(address _from, address _to, uint256 _tokenId) external { + _isOwnerOrApproved(_from, _to, _tokenId); + _transfer(_from, _to, _tokenId); + } + + function _mixDna( + uint256 _dadDna, + uint256 _mumDna, + uint8 random, + uint8 randomSeventeenthDigit, + uint8 randomPair, + uint8 randomNumberForRandomPair + ) internal pure returns (uint256){ + + uint256[9] memory geneArray; + uint256 i; + uint256 counter = 7; // start on the right end + + //DNA example: 11 22 33 44 55 66 77 88 9 + + if(randomSeventeenthDigit == 0){ + geneArray[8] = uint8(_mumDna % 10); //this takes the 17th gene from mum. + } else { + geneArray[8] = uint8(_dadDna % 10); //this takes the 17th gene from dad. + } + + _mumDna = SafeMath.div(_mumDna, 10); // division by 10 removes the last digit + _dadDna = SafeMath.div(_dadDna, 10); // division by 10 removes the last digit + + for (i = 1; i <= 128; i=i*2) { //1, 2 , 4, 8, 16, 32, 64 ,128 + if(random & i == 0){ //00000001 + geneArray[counter] = uint8(_mumDna % 100); //00000010 etc. + } else { //11001011 & + geneArray[counter] = uint8(_dadDna % 100); //00000001 will go through random number bitwise + } //if(1) - dad gene + _mumDna = SafeMath.div(_mumDna, 100); //if(0) - mum gene + _dadDna = SafeMath.div(_dadDna, 100); //division by 100 removes last two digits from genes + if(counter > 0) { + counter = SafeMath.sub(counter, 1); + } + } + + geneArray[randomPair] = randomNumberForRandomPair; //extra randomness for random pair. + + uint256 newGene = 0; + + //geneArray example: [11, 22, 33, 44, 55, 66, 77, 88, 9] + + for (i = 0; i < 8; i++) { //8 is number of pairs in array + newGene = SafeMath.mul(newGene, 100); //adds two digits to newGene; no digits the first time + newGene = SafeMath.add(newGene, geneArray[i]); //adds a pair of genes + } + newGene = SafeMath.mul(newGene, 10); //add seventeenth digit + newGene = SafeMath.add(newGene, geneArray[8]); + return newGene; + } +} diff --git a/contracts/IMarketplace.sol b/contracts/IMarketplace.sol new file mode 100644 index 0000000..45ed5be --- /dev/null +++ b/contracts/IMarketplace.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./CryptoHero.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/* + * Market place to trade heros (should **in theory** be used for any ERC721 token) + * It needs an existing hero contract to interact with + * Note: it does not inherit from the contract + * Note: It takes ownership of the hero for the duration that is is on the marketplace. + */ +interface IMarketPlace { + + event MarketTransaction(string TxType, address owner, uint256 tokenId); + event MonetaryTransaction(string message, address recipient, uint256 amount); + + /** + * Set the current contract address and initialize the instance of the contract. + * Requirement: Only the contract owner can call. + */ + function setContract(address _contractAddress) external; + + /** + * Sets status of _paused to true which affects all functions that have whenNotPaused modifiers. + */ + function pause() external; + + /** + * Sets status of _paused to false which affects all functions that have whenNotPaused modifiers. + */ + function resume() external; + + /** + * Get the details about a offer for _tokenId. Throws an error if there is no active offer for _tokenId. + */ + function getOffer(uint256 _tokenId) external view returns (address seller, uint256 price, uint256 index, uint256 tokenId, bool active); + + /** + * Get all tokenId's that are currently for sale. Returns an empty array if no offer exists. + */ + function getAllTokensOnSale() external view returns (uint256[] memory listOfOffers); + + /** + * Creates a new offer for _tokenId for the price _price. + * Emits the MarketTransaction event with txType "Create offer" + * Requirement: Only the owner of _tokenId can create an offer. + * Requirement: There can only be one active offer for a token at a time. + * Requirement: Marketplace contract (this) needs to be an approved operator when the offer is created. + */ + function setOffer(uint256 _price, uint256 _tokenId) external; + + /** + * Removes an existing offer. + * Emits the MarketTransaction event with txType "Remove offer" + * Requirement: Only the seller of _tokenId can remove an offer. + */ + function removeOffer(uint256 _tokenId) external; + + /** + * Executes the purchase of _tokenId. + * Transfers the token using transferFrom in CryptoHero. + * Transfers funds to the _fundsToBeCollected mapping. + * Removes the offer from the mapping. + * Sets the offer in the array to inactive. + * Emits the MarketTransaction event with txType "Buy". + * Requirement: The msg.value needs to equal the price of _tokenId + * Requirement: There must be an active offer for _tokenId + */ + function buyHero(uint256 _tokenId) external payable; + + /** + * Returns current balance of msg.sender + */ + function getBalance() external view returns (uint256); + + /** + * Send funds to msg.sender. + * Emits a MonetaryTransaction event "Successful Transfer". + * Requirement: msg.sender must have funds in the mapping. + */ + function withdrawFunds() external payable; +} \ No newline at end of file diff --git a/contracts/MarketPlace.sol b/contracts/MarketPlace.sol new file mode 100644 index 0000000..44e8e70 --- /dev/null +++ b/contracts/MarketPlace.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "./CryptoHero.sol"; +import "./IMarketplace.sol"; + +/* + * Market place to trade heros (should **in theory** be used for any ERC721 token) + * It needs an existing hero contract to interact with + * Note: it does not inherit from the contract + * Note: It takes ownership of the hero for the duration that is is on the marketplace. + */ + +contract MarketPlace is Ownable, IMarketPlace { + CryptoHero private _cryptoHero; + + using SafeMath for uint256; + + struct Offer { + address payable seller; + uint256 price; + uint256 index; + uint256 tokenId; + bool active; + } + + bool internal _paused; + + Offer[] offers; + + mapping(uint256 => Offer) tokenIdToOffer; + mapping(address => uint256) internal _fundsToBeCollected; + + //Contract has one event that is already declared in the interface. + + //Contract can be paused by owner to ensure bugs can be fixed after deployment + modifier whenNotPaused() { + require(!_paused); + _; + } + + modifier whenPaused() { + require(_paused); + _; + } + + function setContract(address _contractAddress) onlyOwner public { + _cryptoHero = CryptoHero(_contractAddress); + } + + constructor(address _contractAddress) { + setContract(_contractAddress); + _paused = false; + } + + function pause() public onlyOwner whenNotPaused { + _paused = true; + } + + function resume() public onlyOwner whenPaused { + _paused = false; + } + + function isPaused() public view returns (bool) { + return _paused; + } + + function getOffer(uint256 _tokenId) public view returns ( + address seller, + uint256 price, + uint256 index, + uint256 tokenId, + bool active) { + + require(tokenIdToOffer[_tokenId].active == true, "No active offer at this time"); + + return (tokenIdToOffer[_tokenId].seller, + tokenIdToOffer[_tokenId].price, + tokenIdToOffer[_tokenId].index, + tokenIdToOffer[_tokenId].tokenId, + tokenIdToOffer[_tokenId].active); + } + + function getAllTokensOnSale() public view returns (uint256[] memory listOfOffers) { + uint256 resultId = 0;//index for all heros with active offer status (true) + + for (uint256 index = 0; index < offers.length; index++) { + if (offers[index].active == true) { + resultId = SafeMath.add(resultId, 1);//determine length of array to return + } + } + + if (offers.length == 0) { + return new uint256[](0);//returns empty array + } else { + uint256[] memory allTokensOnSale = new uint256[](resultId); + //initialize new array with correct length + resultId = 0;//reset index of new array + for (uint256 index = 0; index < offers.length; index++) {//iterate through entire offers array + if (offers[index].active == true) { + allTokensOnSale[resultId] = offers[index].tokenId; + resultId = SafeMath.add(resultId, 1); + } + } + return allTokensOnSale; + } + } + + function _ownsHero(address _address, uint256 _tokenId) internal view returns (bool) { + return (_cryptoHero.ownerOf(_tokenId) == _address); + } + + function setOffer(uint256 _price, uint256 _tokenId) public { + require(_ownsHero(msg.sender, _tokenId), + "Only the owner of the hero can initialize an offer"); + require(tokenIdToOffer[_tokenId].active == false, + "You already created an offer for this hero. Please remove it first before creating a new one."); + require(_cryptoHero.isApprovedForAll(msg.sender, address(this)), + "MarketPlace contract must first be an approved operator for your heros"); + + Offer memory _currentOffer = Offer({//set offer + seller: payable(msg.sender), + price: _price, + index: offers.length, + tokenId: _tokenId, + active: true + }); + + tokenIdToOffer[_tokenId] = _currentOffer;//update mapping + offers.push(_currentOffer);//update array + + emit MarketTransaction("Offer created", msg.sender, _tokenId); + } + + function removeOffer(uint256 _tokenId) public { + require(tokenIdToOffer[_tokenId].seller == msg.sender, + "Only the owner of the hero can withdraw the offer."); + + offers[tokenIdToOffer[_tokenId].index].active = false; + //don't iterate through array, simply set active to false. + delete tokenIdToOffer[_tokenId];//delete entry in mapping + + emit MarketTransaction("Offer removed", msg.sender, _tokenId); + } + + function buyHero(uint256 _tokenId) public payable whenNotPaused{ + Offer memory _currentOffer = tokenIdToOffer[_tokenId]; + + //checks + require(_currentOffer.active, "There is no active offer for this hero"); + require(msg.value == _currentOffer.price, "The amount offered is not equal to the requested amount"); + + //effects + delete tokenIdToOffer[_tokenId];//delete entry in mapping + offers[_currentOffer.index].active = false;//don't iterate through array, but simply set active to false. + + //interactions + if (_currentOffer.price > 0) { + _fundsToBeCollected[_currentOffer.seller] = + SafeMath.add(_fundsToBeCollected[_currentOffer.seller], _currentOffer.price); + //instead of sending money to seller it is deposited in a mapping waiting for seller to pull. + } + + _cryptoHero.transferFrom(_currentOffer.seller, msg.sender, _tokenId);//ERC721 ownership transferred + + emit MarketTransaction("Hero successfully purchased", msg.sender, _tokenId); + } + + function getBalance() public view returns (uint256) { + return _fundsToBeCollected[msg.sender]; + } + + function withdrawFunds() public payable whenNotPaused{ + + //check + require(_fundsToBeCollected[msg.sender] > 0, "No funds available at this time"); + + uint256 toWithdraw = _fundsToBeCollected[msg.sender]; + + //effect + _fundsToBeCollected[msg.sender] = 0; + + //interaction + payable(msg.sender).transfer(toWithdraw); + + //making sure transfer executed correctly + assert(_fundsToBeCollected[msg.sender] == 0); + + //emit event + emit MonetaryTransaction("Funds successfully received", msg.sender, toWithdraw); + } +} \ No newline at end of file diff --git a/migrations/2_deploy_hero.js b/migrations/2_deploy_hero.js new file mode 100644 index 0000000..65be87b --- /dev/null +++ b/migrations/2_deploy_hero.js @@ -0,0 +1,9 @@ +const Hero = artifacts.require('CryptoHero'); + +module.exports = async function (deployer) { + await deployer.deploy(Hero, "CryptoHero", "JC"); + const instance = await Hero.deployed(); + if(instance) { + console.log("CryptoHero successfully deployed.") + } +}; \ No newline at end of file diff --git a/migrations/3_deploy_marketplace.js b/migrations/3_deploy_marketplace.js new file mode 100644 index 0000000..653f032 --- /dev/null +++ b/migrations/3_deploy_marketplace.js @@ -0,0 +1,10 @@ +const Hero = artifacts.require('CryptoHero'); +const MarketPlace = artifacts.require('MarketPlace'); + +module.exports = async function (deployer) { + await deployer.deploy(MarketPlace, Hero.address); + const instance = await MarketPlace.deployed(); + if(instance) { + console.log("MarketPlace successfully deployed.") + } +}; \ No newline at end of file diff --git a/truffle-config.js b/truffle-config.js index 3e15968..9bc94f4 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -71,6 +71,16 @@ module.exports = { // network_id: 2111, // This network is yours, in the cloud. // production: true // Treats this network as if it was a public net. (default: false) // } + development: { + host: "192.168.100.22", // Localhost (default: none) + port: 8545, // Standard Ethereum port (default: none) + network_id: "*", // Any network (default: none) + }, + local: { + host: "127.0.0.1", // Localhost (default: none) + port: 7545, // Standard Ethereum port (default: none) + network_id: "*", // Any network (default: none) + } }, // Set default mocha options here, use special reporters etc.