initial commit
This commit is contained in:
commit
5902ebcb19
45
.github/workflows/test.yml
vendored
Normal file
45
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FOUNDRY_PROFILE: ci
|
||||
|
||||
jobs:
|
||||
check:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
|
||||
name: Foundry project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Foundry
|
||||
uses: foundry-rs/foundry-toolchain@v1
|
||||
with:
|
||||
version: nightly
|
||||
|
||||
- name: Show Forge version
|
||||
run: |
|
||||
forge --version
|
||||
|
||||
- name: Run Forge fmt
|
||||
run: |
|
||||
forge fmt --check
|
||||
id: fmt
|
||||
|
||||
- name: Run Forge build
|
||||
run: |
|
||||
forge build --sizes
|
||||
id: build
|
||||
|
||||
- name: Run Forge tests
|
||||
run: |
|
||||
forge test -vvv
|
||||
id: test
|
||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# Compiler files
|
||||
cache/
|
||||
out/
|
||||
|
||||
# Ignores development broadcast logs
|
||||
!/broadcast
|
||||
/broadcast/*/31337/
|
||||
/broadcast/**/dry-run/
|
||||
|
||||
# Docs
|
||||
docs/
|
||||
|
||||
# Dotenv file
|
||||
.env
|
||||
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
[submodule "lib/forge-std"]
|
||||
path = lib/forge-std
|
||||
url = https://github.com/foundry-rs/forge-std
|
||||
[submodule "lib/openzeppelin-contracts"]
|
||||
path = lib/openzeppelin-contracts
|
||||
url = https://github.com/openzeppelin/openzeppelin-contracts
|
||||
[submodule "lib/foundry-devops"]
|
||||
path = lib/foundry-devops
|
||||
url = https://github.com/Cyfrin/foundry-devops
|
||||
[submodule "lib/slither"]
|
||||
path = lib/slither
|
||||
url = https://github.com/crytic/slither
|
||||
7
MakeFile
Normal file
7
MakeFile
Normal file
@ -0,0 +1,7 @@
|
||||
-include .env
|
||||
.PHONY: all test clean deploy fund help install snapshot format anvil
|
||||
|
||||
install:
|
||||
@forge install OpenZeppelin/openzeppelin-contracts --no-commit
|
||||
@forge install foundry-rs/forge-std --no-commit
|
||||
@forge install foundry-rs/foundry-devops --no-commit
|
||||
83
README.md
Normal file
83
README.md
Normal file
@ -0,0 +1,83 @@
|
||||
# Pieces Protocol
|
||||
|
||||
# Contest Details
|
||||
|
||||
### Prize Pool
|
||||
|
||||
- High - 100xp
|
||||
- Medium - 20xp
|
||||
- Low - 2xp
|
||||
|
||||
- Starts: January 16, 2025 Noon UTC
|
||||
- Ends: January 23, 2025 Noon UTC
|
||||
|
||||
### Stats
|
||||
|
||||
- nSLOC:
|
||||
- Complexity Score:
|
||||
|
||||
[//]: # "contest-details-open"
|
||||
|
||||
## About the Project
|
||||
|
||||
## Summary
|
||||
|
||||
A new marketplace where users can buy a fraction of an nft and trade with this one, if some user has all the fractions of an nft, can claim the original nft that is locked.
|
||||
|
||||
## Actors
|
||||
|
||||
- Buyer: The purchaser of the nft fraction.
|
||||
- Seller: The seller of the nft fraction
|
||||
|
||||
[//]: # "contest-details-close"
|
||||
|
||||
## Scope (contracts)
|
||||
|
||||
```
|
||||
├── src
|
||||
│ ├── TokenDivider.sol
|
||||
│ └── token
|
||||
│ └── ERC20ToGenerateNftFraccion.sol
|
||||
```
|
||||
|
||||
Compatibilities:
|
||||
|
||||
Blockchains:
|
||||
|
||||
- Ethereum
|
||||
- EVM Equivalent
|
||||
|
||||
Tokens:
|
||||
|
||||
- ETH
|
||||
- ERC721 Standard
|
||||
|
||||
[//]: # "scope-close"
|
||||
[//]: # "getting-started-open"
|
||||
|
||||
## Setup
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/JuanPeVentura/NftFractionMarketplace.git
|
||||
|
||||
make install
|
||||
|
||||
forge build
|
||||
```
|
||||
|
||||
Tests:
|
||||
|
||||
```bash
|
||||
forge test
|
||||
```
|
||||
|
||||
[//]: # "getting-started-close"
|
||||
[//]: # "known-issues-open"
|
||||
|
||||
## Known Issues
|
||||
|
||||
No known issues reported
|
||||
|
||||
[//]: # "known-issues-close"
|
||||
9
foundry.toml
Normal file
9
foundry.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[profile.default]
|
||||
src = "src"
|
||||
out = "out"
|
||||
libs = ["lib"]
|
||||
remappings=['@openzeppelin/contracts=lib/openzeppelin-contracts/contracts']
|
||||
|
||||
fs_permissions = [{ access = "read", path = "./broadcast" }, {access = "read", path = "./Images/"}]
|
||||
|
||||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
||||
17
script/DeployTokenDivider.s.sol
Normal file
17
script/DeployTokenDivider.s.sol
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: SEE LICENSE IN LICENSE
|
||||
pragma solidity ^0.8.18;
|
||||
|
||||
|
||||
import {Script} from 'forge-std/Script.sol';
|
||||
import {TokenDivider} from 'src/TokenDivider.sol';
|
||||
|
||||
contract DeployTokenDivider is Script {
|
||||
function run() external returns(TokenDivider){
|
||||
vm.startBroadcast();
|
||||
TokenDivider tokenDivider = new TokenDivider();
|
||||
vm.stopBroadcast();
|
||||
|
||||
return tokenDivider;
|
||||
}
|
||||
|
||||
}
|
||||
110
script/Interactions.s.sol
Normal file
110
script/Interactions.s.sol
Normal file
@ -0,0 +1,110 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.18;
|
||||
|
||||
|
||||
import {Script, console} from "forge-std/Script.sol";
|
||||
import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol";
|
||||
import {TokenDivider} from "src/TokenDivider.sol";
|
||||
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
|
||||
|
||||
|
||||
contract DivideNft is Script {
|
||||
|
||||
function run(address nftAddress, uint256 tokenId, uint256 amount) public {
|
||||
address mostRecentDelployment = DevOpsTools.get_most_recent_deployment("TokenDivider", block.chainid);
|
||||
divideNft(mostRecentDelployment, nftAddress, tokenId, amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
function divideNft(address mostRecentDeployment, address nftAddress, uint256 tokenId, uint256 amount) public {
|
||||
vm.startBroadcast(msg.sender);
|
||||
IERC721(nftAddress).approve(mostRecentDeployment, tokenId);
|
||||
vm.stopBroadcast();
|
||||
TokenDivider(mostRecentDeployment).divideNft(nftAddress, tokenId, amount);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
contract ClaimNft is Script {
|
||||
|
||||
function run(address nftAddress) public {
|
||||
address mostRecentDelployment = DevOpsTools.get_most_recent_deployment("TokenDivider", block.chainid);
|
||||
claimNft(mostRecentDelployment, nftAddress);
|
||||
}
|
||||
|
||||
|
||||
function claimNft(address mostRecentDeployment, address nftAddress) public {
|
||||
address erc20 = TokenDivider(mostRecentDeployment).getErc20InfoFromNft(nftAddress).erc20Address;
|
||||
uint256 totalErc20MintedAmount = TokenDivider(mostRecentDeployment).getErc20TotalMintedAmount(erc20);
|
||||
vm.startBroadcast(msg.sender);
|
||||
IERC20(erc20).approve(mostRecentDeployment, totalErc20MintedAmount);
|
||||
TokenDivider(mostRecentDeployment).claimNft(nftAddress);
|
||||
vm.stopBroadcast();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
contract TransferErc20 is Script {
|
||||
function run(address nftAddress, address to, uint256 amount) public {
|
||||
address mostRecentDelployment = DevOpsTools.get_most_recent_deployment("TokenDivider", block.chainid);
|
||||
transferErc20(mostRecentDelployment, nftAddress, to, amount);
|
||||
}
|
||||
|
||||
|
||||
function transferErc20(address mostRecentDeployment, address nftAddress, address to, uint256 amount) public {
|
||||
address erc20 = TokenDivider(mostRecentDeployment).getErc20InfoFromNft(nftAddress).erc20Address;
|
||||
|
||||
vm.startBroadcast(msg.sender);
|
||||
IERC20(erc20).approve(mostRecentDeployment, amount);
|
||||
TokenDivider(mostRecentDeployment).transferErcTokens(nftAddress, to, amount);
|
||||
vm.stopBroadcast();
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
contract SellErc20 is Script {
|
||||
function run(address nftAddress, uint256 price, uint256 amount) public {
|
||||
address mostRecentDelployment = DevOpsTools.get_most_recent_deployment("TokenDivider", block.chainid);
|
||||
sellErc20(mostRecentDelployment, nftAddress, price, amount);
|
||||
}
|
||||
|
||||
|
||||
function sellErc20(address mostRecentDeployment, address nftAddress, uint256 price, uint256 amount) public {
|
||||
address erc20 = TokenDivider(mostRecentDeployment).getErc20InfoFromNft(nftAddress).erc20Address;
|
||||
|
||||
vm.startBroadcast(msg.sender);
|
||||
IERC20(erc20).approve(mostRecentDeployment, amount);
|
||||
TokenDivider(mostRecentDeployment).sellErc20(nftAddress, price, amount);
|
||||
vm.stopBroadcast();
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
contract BuyErc20 is Script {
|
||||
function run(uint256 orderIndex, address seller) public {
|
||||
address mostRecentDelployment = DevOpsTools.get_most_recent_deployment("TokenDivider", block.chainid);
|
||||
buyErc20(mostRecentDelployment, orderIndex, seller);
|
||||
}
|
||||
|
||||
|
||||
function buyErc20(address mostRecentDeployment, uint256 orderIndex ,address seller) public {
|
||||
vm.startBroadcast(msg.sender);
|
||||
TokenDivider(mostRecentDeployment).buyOrder{value: TokenDivider(mostRecentDeployment).getOrderPrice(seller, orderIndex) + ((TokenDivider(mostRecentDeployment).getOrderPrice(seller, orderIndex) / 100) / 2)}(orderIndex, seller);
|
||||
vm.stopBroadcast();
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
327
src/TokenDivider.sol
Normal file
327
src/TokenDivider.sol
Normal file
@ -0,0 +1,327 @@
|
||||
// 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) onlyNftOwner(nftAddress, tokenId) onlyNftOwner(nftAddress ,tokenId) external {
|
||||
|
||||
|
||||
if(nftAddress == address(0)) { revert TokenDivider__NftAddressIsZero(); }
|
||||
if(amount == 0) { revert TokenDivider__AmountCantBeZero(); }
|
||||
|
||||
ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(
|
||||
string(abi.encodePacked(ERC721(nftAddress).name(), "Fraccion")),
|
||||
string(abi.encodePacked("F", ERC721(nftAddress).symbol())));
|
||||
|
||||
erc20Contract.mint(address(this), amount);
|
||||
address erc20 = address(erc20Contract);
|
||||
|
||||
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId, "");
|
||||
|
||||
if(IERC721(nftAddress).ownerOf(tokenId) == msg.sender) { revert TokenDivider__NftTransferFailed(); }
|
||||
|
||||
balances[msg.sender][erc20] = amount;
|
||||
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 {
|
||||
|
||||
if(nftAddress == address(0)) {
|
||||
revert TokenDivider__NftAddressIsZero();
|
||||
}
|
||||
|
||||
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
|
||||
|
||||
if(balances[msg.sender][tokenInfo.erc20Address] < erc20ToMintedAmount[tokenInfo.erc20Address]) {
|
||||
revert TokenDivider__NotEnoughErc20Balance();
|
||||
}
|
||||
|
||||
ERC20ToGenerateNftFraccion(tokenInfo.erc20Address).burnFrom(msg.sender, erc20ToMintedAmount[tokenInfo.erc20Address]);
|
||||
|
||||
balances[msg.sender][tokenInfo.erc20Address] = 0;
|
||||
erc20ToMintedAmount[tokenInfo.erc20Address] = 0;
|
||||
|
||||
emit NftClaimed(nftAddress);
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
if(nftPegged == address(0)) {
|
||||
revert TokenDivider__NftAddressIsZero();
|
||||
}
|
||||
|
||||
if( amount == 0) {
|
||||
revert TokenDivider__AmountCantBeZero();
|
||||
}
|
||||
|
||||
ERC20Info memory tokenInfo = nftToErc20Info[nftPegged];
|
||||
if(balances[msg.sender][tokenInfo.erc20Address] < amount) {
|
||||
revert TokenDivider__InsuficientBalance();
|
||||
}
|
||||
|
||||
balances[msg.sender][tokenInfo.erc20Address] -= amount;
|
||||
|
||||
s_userToSellOrders[msg.sender].push(
|
||||
SellOrder({
|
||||
seller: msg.sender,
|
||||
erc20Address: tokenInfo.erc20Address,
|
||||
price: price,
|
||||
amount: amount
|
||||
})
|
||||
);
|
||||
|
||||
emit OrderPublished(amount,msg.sender, nftPegged);
|
||||
|
||||
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 {
|
||||
if(seller == address(0)) {
|
||||
revert TokenDivider__InvalidSeller();
|
||||
}
|
||||
|
||||
|
||||
|
||||
SellOrder memory order = s_userToSellOrders[seller][orderIndex];
|
||||
|
||||
if(msg.value < order.price) {
|
||||
revert TokenDivider__IncorrectEtherAmount();
|
||||
}
|
||||
|
||||
uint256 fee = order.price / 100;
|
||||
uint256 sellerFee = fee / 2;
|
||||
|
||||
|
||||
if(msg.value < order.price + sellerFee) {
|
||||
revert TokenDivider__InsuficientEtherForFees();
|
||||
}
|
||||
|
||||
|
||||
balances[msg.sender][order.erc20Address] += order.amount;
|
||||
|
||||
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
|
||||
|
||||
(bool success, ) = payable(order.seller).call{value: (order.price - sellerFee)}("");
|
||||
|
||||
if(!success) {
|
||||
revert TokenDivider__TransferFailed();
|
||||
}
|
||||
|
||||
(bool taxSuccess, ) = payable(owner()).call{value: fee}("");
|
||||
|
||||
|
||||
if(!taxSuccess) {
|
||||
revert TokenDivider__TransferFailed();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
18
src/token/ERC20ToGenerateNftFraccion.sol
Normal file
18
src/token/ERC20ToGenerateNftFraccion.sol
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.18;
|
||||
|
||||
|
||||
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
|
||||
contract ERC20ToGenerateNftFraccion is ERC20, ERC20Burnable {
|
||||
|
||||
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {
|
||||
|
||||
}
|
||||
|
||||
function mint(address _to, uint256 _amount) public {
|
||||
_mint(_to, _amount);
|
||||
}
|
||||
}
|
||||
49
test/invariant/TokenDividerInvariantTest.t.sol
Normal file
49
test/invariant/TokenDividerInvariantTest.t.sol
Normal file
@ -0,0 +1,49 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.18;
|
||||
|
||||
import {StdInvariant} from 'forge-std/StdInvariant.sol';
|
||||
import {Test} from 'forge-std/Test.sol';
|
||||
import {ERC20Mock} from '@openzeppelin/contracts/mocks/token/ERC20Mock.sol';
|
||||
import {ERC721Mock} from '../mocks/ERC721Mock.sol';
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import {TokenDivider} from 'src/TokenDivider.sol';
|
||||
import {DeployTokenDivider} from 'script/DeployTokenDivider.s.sol';
|
||||
|
||||
|
||||
contract TokenDividerInvariantTest is StdInvariant, Test {
|
||||
DeployTokenDivider deployer;
|
||||
ERC20Mock erc20;
|
||||
ERC721Mock erc721;
|
||||
TokenDivider tokenDivider;
|
||||
|
||||
uint256 public constant AMOUNT = 10e18;
|
||||
function setUp() public{
|
||||
|
||||
deployer = new DeployTokenDivider();
|
||||
tokenDivider = deployer.run();
|
||||
|
||||
erc721 = new ERC721Mock();
|
||||
erc20 = new ERC20Mock();
|
||||
|
||||
|
||||
erc20.mint(address(tokenDivider), AMOUNT);
|
||||
erc721.mint(msg.sender);
|
||||
|
||||
vm.prank(msg.sender);
|
||||
erc721.approve(address(tokenDivider), 0);
|
||||
|
||||
|
||||
targetContract(address(tokenDivider));
|
||||
}
|
||||
|
||||
function invariant__allErc20TokensShouldAlwaysBeEqualToTheTotalSupplyOfThem() public view {
|
||||
uint256 totalERC20Minted= erc20.totalSupply();
|
||||
|
||||
assertEq(totalERC20Minted, AMOUNT);
|
||||
}
|
||||
|
||||
function invariant__gettersShouldNeverRevert() public view {
|
||||
tokenDivider.getBalanceOf(msg.sender, address(erc20));
|
||||
tokenDivider.getErc20TotalMintedAmount(address(erc20));
|
||||
}
|
||||
}
|
||||
16
test/mocks/ERC721Mock.sol
Normal file
16
test/mocks/ERC721Mock.sol
Normal file
@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
contract ERC721Mock is ERC721, Ownable {
|
||||
uint256 private _tokenIdCounter;
|
||||
|
||||
constructor() ERC721("MockToken", "MTK") Ownable(msg.sender) {}
|
||||
|
||||
function mint(address to) public onlyOwner {
|
||||
_safeMint(to, _tokenIdCounter);
|
||||
_tokenIdCounter++;
|
||||
}
|
||||
}
|
||||
154
test/unit/TokenDividerTest.t.sol
Normal file
154
test/unit/TokenDividerTest.t.sol
Normal file
@ -0,0 +1,154 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.18;
|
||||
|
||||
|
||||
import {Test, console} from 'forge-std/Test.sol';
|
||||
import {DeployTokenDivider} from 'script/DeployTokenDivider.s.sol';
|
||||
import {TokenDivider} from 'src/TokenDivider.sol';
|
||||
import {ERC721Mock} from '../mocks/ERC721Mock.sol';
|
||||
import {ERC20Mock} from '@openzeppelin/contracts/mocks/token/ERC20Mock.sol';
|
||||
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
|
||||
|
||||
|
||||
contract TokenDiverTest is Test {
|
||||
DeployTokenDivider deployer;
|
||||
TokenDivider tokenDivider;
|
||||
ERC721Mock erc721Mock;
|
||||
|
||||
address public USER = makeAddr("user");
|
||||
address public USER2 = makeAddr("user2");
|
||||
uint256 constant public STARTING_USER_BALANCE = 10e18;
|
||||
uint256 constant public AMOUNT = 2e18;
|
||||
uint256 constant public TOKEN_ID = 0;
|
||||
|
||||
function setUp() public {
|
||||
deployer = new DeployTokenDivider();
|
||||
tokenDivider = deployer.run();
|
||||
|
||||
erc721Mock = new ERC721Mock();
|
||||
|
||||
erc721Mock.mint(USER);
|
||||
vm.deal(USER2, STARTING_USER_BALANCE);
|
||||
}
|
||||
|
||||
function testDivideNft() public {
|
||||
|
||||
vm.startPrank(USER);
|
||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
||||
|
||||
tokenDivider.divideNft(address(erc721Mock), TOKEN_ID, AMOUNT);
|
||||
vm.stopPrank();
|
||||
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
|
||||
|
||||
console.log("ERC20 Token name is: ", erc20Mock.name());
|
||||
console.log("ERC20 Token symbol is: ", erc20Mock.symbol());
|
||||
assertEq(tokenDivider.getErc20TotalMintedAmount(address(erc20Mock)), AMOUNT);
|
||||
assertEq(erc721Mock.ownerOf(TOKEN_ID), address(tokenDivider));
|
||||
assertEq(tokenDivider.getBalanceOf(USER, address(erc20Mock)), AMOUNT);
|
||||
|
||||
}
|
||||
|
||||
|
||||
modifier nftDivided() {
|
||||
vm.startPrank(USER);
|
||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
||||
tokenDivider.divideNft(address(erc721Mock), TOKEN_ID, AMOUNT);
|
||||
vm.stopPrank();
|
||||
|
||||
_;
|
||||
|
||||
}
|
||||
|
||||
function testDivideNftFailsIsSenderIsNotNftOwner() public {
|
||||
vm.prank(USER);
|
||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
||||
|
||||
vm.startPrank(USER2);
|
||||
vm.expectRevert(TokenDivider.TokenDivider__NotFromNftOwner.selector);
|
||||
tokenDivider.divideNft(address(erc721Mock), TOKEN_ID, AMOUNT);
|
||||
vm.stopPrank();
|
||||
}
|
||||
|
||||
function testTransferErcTokensAndClaimNftFailsIfDontHaveAllTheErc20() public nftDivided {
|
||||
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
|
||||
vm.startPrank(USER);
|
||||
// Arrange
|
||||
|
||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
||||
|
||||
|
||||
// Act / Assert
|
||||
tokenDivider.transferErcTokens(address(erc721Mock),USER2, AMOUNT);
|
||||
|
||||
assertEq(tokenDivider.getBalanceOf(USER2, address(erc20Mock)), AMOUNT);
|
||||
assertEq(tokenDivider.getBalanceOf(USER, address(erc20Mock)), 0);
|
||||
|
||||
vm.expectRevert(TokenDivider.TokenDivider__NotEnoughErc20Balance.selector);
|
||||
|
||||
tokenDivider.claimNft(address(erc721Mock));
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
}
|
||||
|
||||
function testClaimNft() public nftDivided {
|
||||
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
|
||||
vm.startPrank(USER);
|
||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
||||
tokenDivider.claimNft(address(erc721Mock));
|
||||
vm.stopPrank();
|
||||
|
||||
assertEq(erc20Mock.totalSupply(), 0);
|
||||
assertEq(tokenDivider.getBalanceOf(USER, address(erc20Mock)), 0);
|
||||
assertEq(erc721Mock.ownerOf(TOKEN_ID), USER);
|
||||
}
|
||||
|
||||
|
||||
function testSellErc20() public nftDivided{
|
||||
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
|
||||
|
||||
vm.startPrank(USER);
|
||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
||||
tokenDivider.sellErc20(address(erc721Mock), 1e18, AMOUNT);
|
||||
vm.stopPrank();
|
||||
|
||||
assertEq(tokenDivider.getBalanceOf(USER, address(erc20Mock)), 0);
|
||||
assertEq(erc20Mock.balanceOf(address(tokenDivider)), AMOUNT);
|
||||
}
|
||||
|
||||
function testBuyErc20() public nftDivided {
|
||||
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
|
||||
|
||||
uint256 ownerBalanceBefore = address(tokenDivider.owner()).balance;
|
||||
uint256 userBalanceBefore = address(USER).balance;
|
||||
uint256 user2TokenBalanceBefore = tokenDivider.getBalanceOf(USER2, address(erc20Mock));
|
||||
|
||||
vm.startPrank(USER);
|
||||
|
||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
||||
|
||||
tokenDivider.sellErc20(address(erc721Mock), AMOUNT, 1e18); // Creamos una orden de venta por 1 ETH
|
||||
|
||||
uint256 fees = AMOUNT / 100;
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
|
||||
vm.prank(USER2);
|
||||
tokenDivider.buyOrder{value: (3e18)}(0, USER);
|
||||
|
||||
uint256 ownerBalanceAfter = address(tokenDivider.owner()).balance;
|
||||
uint256 userBalanceAfter = address(USER).balance;
|
||||
uint256 user2TokenBalanceAfter = tokenDivider.getBalanceOf(USER2, address(erc20Mock));
|
||||
|
||||
assertEq(user2TokenBalanceAfter - 1e18, user2TokenBalanceBefore);
|
||||
assertEq(ownerBalanceAfter - fees, ownerBalanceBefore);
|
||||
|
||||
if(block.chainid != 31337) {
|
||||
assertEq(userBalanceAfter - AMOUNT + fees / 2, userBalanceBefore);
|
||||
} else {
|
||||
assertEq(user2TokenBalanceAfter, 1e18);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user