diff --git a/members/Georgiafab/task3/ERC20Token.sol b/members/Georgiafab/task3/ERC20Token.sol new file mode 100644 index 000000000..285ae5b77 --- /dev/null +++ b/members/Georgiafab/task3/ERC20Token.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.2 <0.9.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +contract MTK is ERC20 { + constructor(address owner) ERC20("MILKToken", "MTK") { + _mint(owner, 21000000 * 10 ** 18); + } +} diff --git a/members/Georgiafab/task3/ERC721Token.sol b/members/Georgiafab/task3/ERC721Token.sol new file mode 100644 index 000000000..920eeeb4c --- /dev/null +++ b/members/Georgiafab/task3/ERC721Token.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.2 <0.9.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract MILKNT is ERC721 { + uint256 private _nextId; + + constructor() ERC721("MILKNFT", "MILKNT") {} + + function saftMint(address to) public { + uint256 tokenId = _nextId++; + _safeMint(to, tokenId); + } +} diff --git a/members/Georgiafab/task3/NFTMarket.sol b/members/Georgiafab/task3/NFTMarket.sol new file mode 100644 index 000000000..2f906ff5c --- /dev/null +++ b/members/Georgiafab/task3/NFTMarket.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.8.2 <0.9.0; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +contract NFTMarket is ReentrancyGuard { + IERC20 public tokenContract; + using Math for uint256; + + struct Listing { + uint256 tokenId; // NFT的Token ID + address seller; // 上架者地址 + address nftContract; // NFT 合约地址 + uint256 price; // 上架价格(使用ERC20代币) + bool active; // 上架状态 + } + + // 存储所有上架的NFT信息 + mapping(address => mapping(uint256 => Listing)) public listings; + + // 记录所有有上架NFT的合约地址 + address[] private nftContracts; + // 记录每个合约地址对应的tokenId数组 + mapping(address => uint256[]) private nftTokenIds; + + // 上架NFT事件 + event NFTListed( + uint256 indexed tokenId, + address indexed seller, + address indexed nftContract, + uint256 price + ); + + // 下架NFT事件 + event NFTUnListed(uint256 indexed tokenId, address indexed seller); + + // 购买NFT事件 + event NFTSold( + uint256 tokenId, + address indexed buyer, + address indexed seller, + address nftContract, + uint256 price + ); + + // 合约构造函数,初始化ERC20代币合约地址 + constructor(address _tokenContractAddress) { + tokenContract = IERC20(_tokenContractAddress); + } + + function getTokenContract() external view returns (IERC20) { + return tokenContract; + } + + // 上架NFT + function listNFT( + address _nftContract, + uint256 _tokenId, + uint256 _price + ) external { + // 确保上架者是NFT的所有者 + IERC721 nftContract = IERC721(_nftContract); + require(nftContract.ownerOf(_tokenId) == msg.sender, "Not token owner"); + // 确保上架价格大于零 + require(_price > 0, "Price must be greater than zero"); + + // 将NFT信息添加到上架列表中 + listings[_nftContract][_tokenId] = Listing( + _tokenId, + msg.sender, + _nftContract, + _price, + true + ); + + // 如果该合约地址第一次上架NFT,则添加到nftContracts数组中 + if (nftTokenIds[_nftContract].length == 0) { + nftContracts.push(_nftContract); + } + + // 将tokenId添加到该合约地址对应的tokenId数组中 + nftTokenIds[_nftContract].push(_tokenId); + + // 触发上架NFT事件 + emit NFTListed(_tokenId, msg.sender, _nftContract, _price); + } + + // 下架NFT + function unListNFT(address _nftContract, uint256 _tokenId) external { + // 获取上架信息 + Listing storage listing = listings[_nftContract][_tokenId]; + // 确保上架状态为激活 + require(listing.active, "Listing not active"); + // 确保下架人是拥有者 + require(listing.seller == msg.sender, "Not token owner"); + + // 修改上架状态为非激活 + listing.active = false; + + emit NFTUnListed(_tokenId, msg.sender); + } + + // 购买NFT + function buyNFT( + address _nftContract, + uint256 _tokenId + ) external nonReentrant { + Listing storage listing = listings[_nftContract][_tokenId]; + require(listing.active, "Listing not active"); + + IERC721 nft = IERC721(_nftContract); + // 确保买家拥有足够的ERC20代币用于购买 + require( + tokenContract.balanceOf(msg.sender) >= listing.price, + "Insufficient balance" + ); + // 从买家转移ERC20代币给卖家 + tokenContract.transferFrom(msg.sender, listing.seller, listing.price); + // 从卖家转移NFT给买家 + nft.transferFrom(listing.seller, msg.sender, _tokenId); + + // 修改上架状态为非激活 + listing.active = false; + + emit NFTSold( + _tokenId, + msg.sender, + listing.seller, + _nftContract, + listing.price + ); + } + + // 获取所有上架的NFT信息 + function getAllListings() external view returns (Listing[] memory) { + uint256 totalListings = 0; + + // 计算所有上架的NFT总数 + for (uint i = 0; i < nftContracts.length; i++) { + address nftContract = nftContracts[i]; + totalListings += nftTokenIds[nftContract].length; + } + + // 创建一个数组来存储所有的上架信息 + Listing[] memory allListings = new Listing[](totalListings); + uint256 currentIndex = 0; + + // 填充数组 + for (uint i = 0; i < nftContracts.length; i++) { + address nftContract = nftContracts[i]; + uint256[] storage tokenIds = nftTokenIds[nftContract]; + for (uint j = 0; j < tokenIds.length; j++) { + uint256 tokenId = tokenIds[j]; + Listing storage listing = listings[nftContract][tokenId]; + if (listing.active) { + allListings[currentIndex] = listing; + currentIndex++; + } + } + } + + return allListings; + } +} diff --git a/members/Georgiafab/task3/README.md b/members/Georgiafab/task3/README.md new file mode 100644 index 000000000..4593773d5 --- /dev/null +++ b/members/Georgiafab/task3/README.md @@ -0,0 +1,8 @@ +## task3 介绍 + +1. 部署在 Sepolia 测试网上 + ![bind-wallet](./readme/deploy.png "deploy") +2. 提交全部合约文件(ERC20、ERC721、NFTMarket) +3. 提交上架 NFT、购买 NFT 的交易哈希 + 提交上架 NFT: 0xc940613d9297bcef090bb2252ef9e734dffb769d570b1eafc55931ca46d7f7cd + 购买 NFT: 0xf937159ae007f8de3cd163752d66b33a8ad8897926f8e619c9b436c689d864e0 diff --git a/members/Georgiafab/task3/readme/deploy.png b/members/Georgiafab/task3/readme/deploy.png new file mode 100644 index 000000000..33c16f142 Binary files /dev/null and b/members/Georgiafab/task3/readme/deploy.png differ