// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20ToGenerateNftFraccion} from "src/token/ERC20ToGenerateNftFraccion.sol"; import {IERC721, ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /** * @title TokenDivider * @author Juan Pedro Ventura Baltian, 14 years old * @notice This contracts was created, with the intention to make a new market of nft franctions * There are a function to divide an nft, then you can sell and buy some fraction of nft, that are basicaly * erc20 tokens, each nft pegged to an nft.There are some validations, to make the platforme the most secure * as possible.This is the first project that i code alone, in blockchain, foundry and solidity. * Thank you so much for read it. */ contract TokenDivider is IERC721Receiver, Ownable { error TokenDivider__NotFromNftOwner(); error TokenDivider__NotEnoughErc20Balance(); error TokenDivider__NftTransferFailed(); error TokenDivider__InsuficientBalance(); error TokenDivider__CantTransferToAddressZero(); error TokenDivider__TransferFailed(); error TokenDivider__NftAddressIsZero(); error TokenDivider__AmountCantBeZero(); error TokenDivider__InvalidSeller(); error TokenDivier__InvalidAmount(); error TokenDivider__IncorrectEtherAmount(); error TokenDivider__InsuficientEtherForFees(); struct ERC20Info { address erc20Address; uint256 tokenId; } struct SellOrder { address seller; address erc20Address; uint256 price; uint256 amount; } /** * @dev balances Relates a user with an amount of a erc20 token, this erc20 tokens is an nft fraction * * @dev nftToErc20Info Relates an nft with the erc20 pegged, and othe data like the erc20 amount, or the tokenId * * @dev s_userToSellOrders Relates a user with an array of sell orders, that each sell order * has a seller, an erc20 that is the token to sell, a price and an amount of erc20 to sell */ mapping(address user => mapping(address erc20Address => uint256 amount)) balances; mapping(address nft => ERC20Info) nftToErc20Info; mapping(address user => SellOrder[] orders) s_userToSellOrders; mapping(address erc20 => address nft) erc20ToNft; mapping(address erc20 => uint256 totalErc20Minted) erc20ToMintedAmount; event NftDivided(address indexed nftAddress, uint256 indexed amountErc20Minted, address indexed erc20Minted); event NftClaimed(address indexed nftAddress); event TokensTransfered(uint256 indexed amount, address indexed erc20Address); event OrderPublished(uint256 indexed amount, address indexed seller, address indexed nftPegged); event OrderSelled(address indexed buyer, uint256 price); /** * * Only the owner of the nft can call a function with this modifier */ modifier onlyNftOwner(address nft, uint256 tokenId) { if (msg.sender != IERC721(nft).ownerOf(tokenId)) { revert TokenDivider__NotFromNftOwner(); } _; } constructor() Ownable(msg.sender) {} /** * @dev Handles the receipt of an ERC721 token. This function is called whenever an ERC721 token is transferred to this contract. */ function onERC721Received( address, /* operator */ address, /* from */ uint256, /* tokenId */ bytes calldata /* data */ ) external pure override returns (bytes4) { // Return this value to confirm the receipt of the NFT return this.onERC721Received.selector; } /** * * @param nftAddress The addres of the nft to divide * @param tokenId The id of the token to divide * @param amount The amount of erc20 tokens to mint for the nft * * @dev in this function, the nft passed as parameter, is locked by transfering it to this contract, then, it gives to the * person calling this function an amount of erc20, beeing like a fraction of this nft. */ function divideNft(address nftAddress, uint256 tokenId, uint256 amount) external onlyNftOwner(nftAddress, tokenId) onlyNftOwner(nftAddress, tokenId) { if (nftAddress == address(0)) revert TokenDivider__NftAddressIsZero(); if (amount == 0) revert TokenDivider__AmountCantBeZero(); // @audit: there is no checking of inExistence tokenId, it might handled on modifier but need to check // @audit-report: dependency has its own check ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion( string(abi.encodePacked(ERC721(nftAddress).name(), "Fraccion")), string(abi.encodePacked("F", ERC721(nftAddress).symbol())) ); // @audit: can we mint again? out of this function? // @audit-finding: low // @audit-report: yes it can be minted again. It should not be able to do so. // @audit-report: but so far this contract only handle the recorded ERC20 transactions erc20Contract.mint(address(this), amount); address erc20 = address(erc20Contract); IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId, ""); // @audit: what if we send the NFT to other address during this period? // @audit-report: you cannot send the NFT to other address, related to unit testing test_failIf_sendNFTDuringDivide if (IERC721(nftAddress).ownerOf(tokenId) == msg.sender) revert TokenDivider__NftTransferFailed(); balances[msg.sender][erc20] = amount; // @audit: what happen if the given nft address already registered in this CA, then another token id register again? // @audit-finding: high (critical) // @audit-report: it overrides the previous divided NFT. The previously ERC20 tokens of divided NFT is no longer usable. nftToErc20Info[nftAddress] = ERC20Info({erc20Address: erc20, tokenId: tokenId}); erc20ToMintedAmount[erc20] = amount; erc20ToNft[erc20] = nftAddress; emit NftDivided(nftAddress, amount, erc20); bool transferSuccess = IERC20(erc20).transfer(msg.sender, amount); if (!transferSuccess) { revert TokenDivider__TransferFailed(); } } /** * * @param nftAddress The address of the nft to claim * * @dev in this function, if you have all the erc20 minted for the nft, you can call this function to claim the nft, * giving to the contract all the erc20 and it will give you back the nft */ function claimNft(address nftAddress) external { // @audit: check with "counterfeit" token // @audit-report: the "counterfeit" or extra minted token is not registered in this contract so its not working. tested test_failIf_claimNftWithCounterfeitNFT if (nftAddress == address(0)) { revert TokenDivider__NftAddressIsZero(); } // @audit: what if the nft address is not stored? // @audit-report: it returns weird error but you can't claim nft with inexistence nft address test_failIf_notExistingNftAddress ERC20Info storage tokenInfo = nftToErc20Info[nftAddress]; if (balances[msg.sender][tokenInfo.erc20Address] < erc20ToMintedAmount[tokenInfo.erc20Address]) { revert TokenDivider__NotEnoughErc20Balance(); } // @audit: what if token already sent to other address through ERC20 send function // @audit-report: there is an error which doesn't procceed the transaction further and it reverts ERC20ToGenerateNftFraccion(tokenInfo.erc20Address).burnFrom( msg.sender, erc20ToMintedAmount[tokenInfo.erc20Address] ); balances[msg.sender][tokenInfo.erc20Address] = 0; erc20ToMintedAmount[tokenInfo.erc20Address] = 0; emit NftClaimed(nftAddress); // @audit: what happen if there is overlaps in same NFT address with different token id? will someone get a different NFT? // @audit-finding: high // @audit-report: you cannot claim the previous NFT with a weird error IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenInfo.tokenId); } /** * * @param nftAddress The nft address pegged to the erc20 * @param to The reciver of the erc20 * @param amount The amount of erc20 to transfer * * @dev you can use this function to transfer nft franctions 100% securily and registered by te contract */ function transferErcTokens(address nftAddress, address to, uint256 amount) external { // @audit: check with "counterfeit" token if (nftAddress == address(0)) { revert TokenDivider__NftAddressIsZero(); } if (to == address(0)) { revert TokenDivider__CantTransferToAddressZero(); } if (amount == 0) { revert TokenDivider__AmountCantBeZero(); } ERC20Info memory tokenInfo = nftToErc20Info[nftAddress]; if (to == address(0)) { revert TokenDivider__CantTransferToAddressZero(); } if (balances[msg.sender][tokenInfo.erc20Address] < amount) { revert TokenDivider__NotEnoughErc20Balance(); } balances[msg.sender][tokenInfo.erc20Address] -= amount; balances[to][tokenInfo.erc20Address] += amount; emit TokensTransfered(amount, tokenInfo.erc20Address); IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, to, amount); } /** * * @param nftPegged The nft address pegged to the tokens to sell * @param price The price of all the tokens to sell * @param amount The amount of tokens to sell * * @dev this function creates a new order, is like publish you assets into a marketplace, where other persons can buy it. * firstly, once you call this function, the amount of tokens that you passed into as a parameter, get blocked, by sending it * to this contract, then a new order is created and published. */ function sellErc20(address nftPegged, uint256 price, uint256 amount) external { // @audit: check with "counterfeit" token if (nftPegged == address(0)) { revert TokenDivider__NftAddressIsZero(); } if (amount == 0) { revert TokenDivider__AmountCantBeZero(); } // @audit: no check for 0 price? ERC20Info memory tokenInfo = nftToErc20Info[nftPegged]; if (balances[msg.sender][tokenInfo.erc20Address] < amount) { revert TokenDivider__InsuficientBalance(); } balances[msg.sender][tokenInfo.erc20Address] -= amount; // token amount reduced from its holder on sell order even tho no one buy it // push sell order to array of sender s_userToSellOrders[msg.sender].push( SellOrder({seller: msg.sender, erc20Address: tokenInfo.erc20Address, price: price, amount: amount}) ); emit OrderPublished(amount, msg.sender, nftPegged); // send erc20 token from owner to this contract // @audit: what if the owner of ERC20 rejected this transaction? IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, address(this), amount); } /** * * @param orderIndex The index of the order in all the orders array of the seller (the seller can have multiple orders active) * @param seller The person who is selling this tokens * * @dev when the buyer call this function, the eth or any token accepted to pay, is sent to the seller * if the transfer executed correctly, then this contract, wich has all the tokens, send the tokens to the msg.sender */ function buyOrder(uint256 orderIndex, address seller) external payable { // @audit: check with "counterfeit" token if (seller == address(0)) { revert TokenDivider__InvalidSeller(); } SellOrder memory order = s_userToSellOrders[seller][orderIndex]; // msg.value can be greater than order price? if (msg.value < order.price) { revert TokenDivider__IncorrectEtherAmount(); } // if price 100, then fee is 1 uint256 fee = order.price / 100; // if price 100, then seller fee is 0.5 uint256 sellerFee = fee / 2; if (msg.value < order.price + sellerFee) { revert TokenDivider__InsuficientEtherForFees(); } // record erc20 amount on storage variable onchain balances[msg.sender][order.erc20Address] += order.amount; // override the current orderIndex with tail, then pop the tail out of array s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1]; s_userToSellOrders[seller].pop(); emit OrderSelled(msg.sender, order.price); // Transfer The Ether // send ether to seller, reduced with seller fee (bool success,) = payable(order.seller).call{value: (order.price - sellerFee)}(""); if (!success) { revert TokenDivider__TransferFailed(); } // send "fee" to "owner"? // @audit: if buyer needs to pay price + seller fee. then seller is given price - seller fee. does fee is guarranted to be paid by buyer? // @audit: who owner? (bool taxSuccess,) = payable(owner()).call{value: fee}(""); if (!taxSuccess) { revert TokenDivider__TransferFailed(); } // send erc20 token to buyer address IERC20(order.erc20Address).transfer(msg.sender, order.amount); } /** * Getters */ function getBalanceOf(address user, address token) public view returns (uint256) { return balances[user][token]; } function getErc20TotalMintedAmount(address erc20) public view returns (uint256) { return erc20ToMintedAmount[erc20]; } function getErc20InfoFromNft(address nft) public view returns (ERC20Info memory) { return nftToErc20Info[nft]; } function getOrderPrice(address seller, uint256 index) public view returns (uint256 price) { price = s_userToSellOrders[seller][index].price; } }