Compare commits
No commits in common. "audit" and "main" have entirely different histories.
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
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -12,6 +12,3 @@ docs/
|
|||||||
|
|
||||||
# Dotenv file
|
# Dotenv file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Vim buffer file
|
|
||||||
*.swp
|
|
||||||
|
|||||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -3,7 +3,7 @@
|
|||||||
url = https://github.com/foundry-rs/forge-std
|
url = https://github.com/foundry-rs/forge-std
|
||||||
[submodule "lib/openzeppelin-contracts"]
|
[submodule "lib/openzeppelin-contracts"]
|
||||||
path = lib/openzeppelin-contracts
|
path = lib/openzeppelin-contracts
|
||||||
url = https://github.com/OpenZeppelin/openzeppelin-contracts
|
url = https://github.com/openzeppelin/openzeppelin-contracts
|
||||||
[submodule "lib/foundry-devops"]
|
[submodule "lib/foundry-devops"]
|
||||||
path = lib/foundry-devops
|
path = lib/foundry-devops
|
||||||
url = https://github.com/Cyfrin/foundry-devops
|
url = https://github.com/Cyfrin/foundry-devops
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
Subproject commit b93cf4bc34ff214c099dc970b153f85ade8c9f66
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Subproject commit 90b6dde61322b916020d396817023602f4ef96ed
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527
|
|
||||||
@ -1,344 +1,327 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
pragma solidity ^0.8.18;
|
pragma solidity ^0.8.18;
|
||||||
|
|
||||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
import {ERC20ToGenerateNftFraccion} from "src/token/ERC20ToGenerateNftFraccion.sol";
|
import {ERC20ToGenerateNftFraccion} from "src/token/ERC20ToGenerateNftFraccion.sol";
|
||||||
import {IERC721, ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
import {IERC721, ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||||
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
||||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title TokenDivider
|
* @title TokenDivider
|
||||||
* @author Juan Pedro Ventura Baltian, 14 years old
|
* @author Juan Pedro Ventura Baltian, 14 years old
|
||||||
* @notice This contracts was created, with the intention to make a new market of nft franctions
|
* @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
|
* 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
|
* 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.
|
* as possible.This is the first project that i code alone, in blockchain, foundry and solidity.
|
||||||
* Thank you so much for read it.
|
* Thank you so much for read it.
|
||||||
*/
|
*/
|
||||||
contract TokenDivider is IERC721Receiver, Ownable {
|
|
||||||
error TokenDivider__NotFromNftOwner();
|
|
||||||
error TokenDivider__NotEnoughErc20Balance();
|
contract TokenDivider is IERC721Receiver,Ownable {
|
||||||
error TokenDivider__NftTransferFailed();
|
|
||||||
error TokenDivider__InsuficientBalance();
|
error TokenDivider__NotFromNftOwner();
|
||||||
error TokenDivider__CantTransferToAddressZero();
|
error TokenDivider__NotEnoughErc20Balance();
|
||||||
error TokenDivider__TransferFailed();
|
error TokenDivider__NftTransferFailed();
|
||||||
error TokenDivider__NftAddressIsZero();
|
error TokenDivider__InsuficientBalance();
|
||||||
error TokenDivider__AmountCantBeZero();
|
error TokenDivider__CantTransferToAddressZero();
|
||||||
error TokenDivider__InvalidSeller();
|
error TokenDivider__TransferFailed();
|
||||||
error TokenDivier__InvalidAmount();
|
error TokenDivider__NftAddressIsZero();
|
||||||
error TokenDivider__IncorrectEtherAmount();
|
error TokenDivider__AmountCantBeZero();
|
||||||
error TokenDivider__InsuficientEtherForFees();
|
error TokenDivider__InvalidSeller();
|
||||||
|
error TokenDivier__InvalidAmount();
|
||||||
struct ERC20Info {
|
error TokenDivider__IncorrectEtherAmount();
|
||||||
address erc20Address;
|
error TokenDivider__InsuficientEtherForFees();
|
||||||
uint256 tokenId;
|
|
||||||
}
|
struct ERC20Info {
|
||||||
|
address erc20Address;
|
||||||
struct SellOrder {
|
uint256 tokenId;
|
||||||
address seller;
|
}
|
||||||
address erc20Address;
|
|
||||||
uint256 price;
|
struct SellOrder {
|
||||||
uint256 amount;
|
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
|
* @dev balances Relates a user with an amount of a erc20 token, this erc20 tokens is an nft fraction
|
||||||
* has a seller, an erc20 that is the token to sell, a price and an amount of erc20 to sell
|
|
||||||
*/
|
@dev nftToErc20Info Relates an nft with the erc20 pegged, and othe data like the erc20 amount, or the tokenId
|
||||||
mapping(address user => mapping(address erc20Address => uint256 amount)) balances;
|
|
||||||
mapping(address nft => ERC20Info) nftToErc20Info;
|
@dev s_userToSellOrders Relates a user with an array of sell orders, that each sell order
|
||||||
mapping(address user => SellOrder[] orders) s_userToSellOrders;
|
has a seller, an erc20 that is the token to sell, a price and an amount of erc20 to sell
|
||||||
mapping(address erc20 => address nft) erc20ToNft;
|
|
||||||
mapping(address erc20 => uint256 totalErc20Minted) erc20ToMintedAmount;
|
*/
|
||||||
|
mapping(address user => mapping(address erc20Address => uint256 amount)) balances;
|
||||||
event NftDivided(address indexed nftAddress, uint256 indexed amountErc20Minted, address indexed erc20Minted);
|
mapping(address nft => ERC20Info) nftToErc20Info;
|
||||||
event NftClaimed(address indexed nftAddress);
|
mapping(address user => SellOrder[] orders) s_userToSellOrders;
|
||||||
event TokensTransfered(uint256 indexed amount, address indexed erc20Address);
|
mapping(address erc20 => address nft) erc20ToNft;
|
||||||
event OrderPublished(uint256 indexed amount, address indexed seller, address indexed nftPegged);
|
mapping(address erc20 => uint256 totalErc20Minted) erc20ToMintedAmount;
|
||||||
event OrderSelled(address indexed buyer, uint256 price);
|
|
||||||
|
event NftDivided(address indexed nftAddress, uint256 indexed amountErc20Minted, address indexed erc20Minted);
|
||||||
/**
|
event NftClaimed(address indexed nftAddress);
|
||||||
*
|
event TokensTransfered(uint256 indexed amount, address indexed erc20Address);
|
||||||
* Only the owner of the nft can call a function with this modifier
|
event OrderPublished(uint256 indexed amount, address indexed seller, address indexed nftPegged);
|
||||||
*/
|
event OrderSelled(address indexed buyer, uint256 price);
|
||||||
modifier onlyNftOwner(address nft, uint256 tokenId) {
|
|
||||||
if (msg.sender != IERC721(nft).ownerOf(tokenId)) {
|
|
||||||
revert TokenDivider__NotFromNftOwner();
|
/**
|
||||||
}
|
*
|
||||||
_;
|
* Only the owner of the nft can call a function with this modifier
|
||||||
}
|
*/
|
||||||
|
modifier onlyNftOwner(address nft, uint256 tokenId) {
|
||||||
constructor() Ownable(msg.sender) {}
|
if(msg.sender != IERC721(nft).ownerOf(tokenId)) {
|
||||||
|
revert TokenDivider__NotFromNftOwner();
|
||||||
/**
|
}
|
||||||
* @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 */
|
constructor() Ownable(msg.sender) {}
|
||||||
address, /* from */
|
|
||||||
uint256, /* tokenId */
|
|
||||||
bytes calldata /* data */
|
|
||||||
) external pure override returns (bytes4) {
|
/**
|
||||||
// Return this value to confirm the receipt of the NFT
|
* @dev Handles the receipt of an ERC721 token. This function is called whenever an ERC721 token is transferred to this contract.
|
||||||
return this.onERC721Received.selector;
|
*/
|
||||||
}
|
function onERC721Received(
|
||||||
|
address /* operator */,
|
||||||
/**
|
address /* from */,
|
||||||
*
|
uint256 /* tokenId */,
|
||||||
* @param nftAddress The addres of the nft to divide
|
bytes calldata /* data */
|
||||||
* @param tokenId The id of the token to divide
|
) external pure override returns (bytes4) {
|
||||||
* @param amount The amount of erc20 tokens to mint for the nft
|
// Return this value to confirm the receipt of the NFT
|
||||||
*
|
return this.onERC721Received.selector;
|
||||||
* @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
|
* @param nftAddress The addres of the nft to divide
|
||||||
onlyNftOwner(nftAddress, tokenId)
|
* @param tokenId The id of the token to divide
|
||||||
onlyNftOwner(nftAddress, tokenId)
|
* @param amount The amount of erc20 tokens to mint for the nft
|
||||||
{
|
*
|
||||||
if (nftAddress == address(0)) revert TokenDivider__NftAddressIsZero();
|
* @dev in this function, the nft passed as parameter, is locked by transfering it to this contract, then, it gives to the
|
||||||
if (amount == 0) revert TokenDivider__AmountCantBeZero();
|
* person calling this function an amount of erc20, beeing like a fraction of this nft.
|
||||||
// @audit: there is no checking of inExistence tokenId, it might handled on modifier but need to check
|
*/
|
||||||
// @audit-report: dependency has its own check
|
|
||||||
|
function divideNft(address nftAddress, uint256 tokenId, uint256 amount) onlyNftOwner(nftAddress, tokenId) onlyNftOwner(nftAddress ,tokenId) external {
|
||||||
ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(
|
|
||||||
string(abi.encodePacked(ERC721(nftAddress).name(), "Fraccion")),
|
|
||||||
string(abi.encodePacked("F", ERC721(nftAddress).symbol()))
|
if(nftAddress == address(0)) { revert TokenDivider__NftAddressIsZero(); }
|
||||||
);
|
if(amount == 0) { revert TokenDivider__AmountCantBeZero(); }
|
||||||
|
|
||||||
// @audit: can we mint again? out of this function?
|
ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(
|
||||||
// @audit-finding: low
|
string(abi.encodePacked(ERC721(nftAddress).name(), "Fraccion")),
|
||||||
// @audit-report: yes it can be minted again. It should not be able to do so.
|
string(abi.encodePacked("F", ERC721(nftAddress).symbol())));
|
||||||
// @audit-report: but so far this contract only handle the recorded ERC20 transactions
|
|
||||||
erc20Contract.mint(address(this), amount);
|
erc20Contract.mint(address(this), amount);
|
||||||
address erc20 = address(erc20Contract);
|
address erc20 = address(erc20Contract);
|
||||||
|
|
||||||
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId, "");
|
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId, "");
|
||||||
|
|
||||||
// @audit: what if we send the NFT to other address during this period?
|
if(IERC721(nftAddress).ownerOf(tokenId) == msg.sender) { revert TokenDivider__NftTransferFailed(); }
|
||||||
// @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;
|
||||||
|
nftToErc20Info[nftAddress] = ERC20Info({erc20Address: erc20, tokenId: tokenId});
|
||||||
balances[msg.sender][erc20] = amount;
|
erc20ToMintedAmount[erc20] = amount;
|
||||||
// @audit: what happen if the given nft address already registered in this CA, then another token id register again?
|
erc20ToNft[erc20] = nftAddress;
|
||||||
// @audit-finding: high (critical)
|
|
||||||
// @audit-report: it overrides the previous divided NFT. The previously ERC20 tokens of divided NFT is no longer usable.
|
emit NftDivided(nftAddress, amount, erc20);
|
||||||
nftToErc20Info[nftAddress] = ERC20Info({erc20Address: erc20, tokenId: tokenId});
|
|
||||||
erc20ToMintedAmount[erc20] = amount;
|
bool transferSuccess = IERC20(erc20).transfer(msg.sender, amount);
|
||||||
erc20ToNft[erc20] = nftAddress;
|
if(!transferSuccess) {
|
||||||
|
revert TokenDivider__TransferFailed();
|
||||||
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
|
||||||
*
|
*/
|
||||||
* @param nftAddress The address of the nft to claim
|
|
||||||
*
|
function claimNft(address nftAddress) external {
|
||||||
* @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
|
if(nftAddress == address(0)) {
|
||||||
*/
|
revert TokenDivider__NftAddressIsZero();
|
||||||
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
|
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
|
||||||
if (nftAddress == address(0)) {
|
|
||||||
revert TokenDivider__NftAddressIsZero();
|
if(balances[msg.sender][tokenInfo.erc20Address] < erc20ToMintedAmount[tokenInfo.erc20Address]) {
|
||||||
}
|
revert TokenDivider__NotEnoughErc20Balance();
|
||||||
|
}
|
||||||
// @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
|
ERC20ToGenerateNftFraccion(tokenInfo.erc20Address).burnFrom(msg.sender, erc20ToMintedAmount[tokenInfo.erc20Address]);
|
||||||
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
|
|
||||||
|
balances[msg.sender][tokenInfo.erc20Address] = 0;
|
||||||
if (balances[msg.sender][tokenInfo.erc20Address] < erc20ToMintedAmount[tokenInfo.erc20Address]) {
|
erc20ToMintedAmount[tokenInfo.erc20Address] = 0;
|
||||||
revert TokenDivider__NotEnoughErc20Balance();
|
|
||||||
}
|
emit NftClaimed(nftAddress);
|
||||||
|
|
||||||
// @audit: what if token already sent to other address through ERC20 send function
|
IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenInfo.tokenId);
|
||||||
// @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;
|
* @param nftAddress The nft address pegged to the erc20
|
||||||
erc20ToMintedAmount[tokenInfo.erc20Address] = 0;
|
* @param to The reciver of the erc20
|
||||||
|
* @param amount The amount of erc20 to transfer
|
||||||
emit NftClaimed(nftAddress);
|
*
|
||||||
|
* @dev you can use this function to transfer nft franctions 100% securily and registered by te contract
|
||||||
// @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
|
function transferErcTokens(address nftAddress,address to, uint256 amount) external {
|
||||||
IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenInfo.tokenId);
|
|
||||||
}
|
if(nftAddress == address(0)) {
|
||||||
|
revert TokenDivider__NftAddressIsZero();
|
||||||
/**
|
}
|
||||||
*
|
|
||||||
* @param nftAddress The nft address pegged to the erc20
|
if(to == address(0)) {
|
||||||
* @param to The reciver of the erc20
|
revert TokenDivider__CantTransferToAddressZero();
|
||||||
* @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
|
if(amount == 0) {
|
||||||
*/
|
revert TokenDivider__AmountCantBeZero();
|
||||||
function transferErcTokens(address nftAddress, address to, uint256 amount) external {
|
}
|
||||||
// @audit: check with "counterfeit" token
|
|
||||||
if (nftAddress == address(0)) {
|
ERC20Info memory tokenInfo = nftToErc20Info[nftAddress];
|
||||||
revert TokenDivider__NftAddressIsZero();
|
|
||||||
}
|
if(to == address(0)) {
|
||||||
|
revert TokenDivider__CantTransferToAddressZero();
|
||||||
if (to == address(0)) {
|
}
|
||||||
revert TokenDivider__CantTransferToAddressZero();
|
if(balances[msg.sender][tokenInfo.erc20Address] < amount) {
|
||||||
}
|
revert TokenDivider__NotEnoughErc20Balance();
|
||||||
|
}
|
||||||
if (amount == 0) {
|
|
||||||
revert TokenDivider__AmountCantBeZero();
|
balances[msg.sender][tokenInfo.erc20Address] -= amount;
|
||||||
}
|
balances[to][tokenInfo.erc20Address] += amount;
|
||||||
|
|
||||||
ERC20Info memory tokenInfo = nftToErc20Info[nftAddress];
|
emit TokensTransfered(amount, tokenInfo.erc20Address);
|
||||||
|
|
||||||
if (to == address(0)) {
|
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender,to, amount);
|
||||||
revert TokenDivider__CantTransferToAddressZero();
|
}
|
||||||
}
|
|
||||||
if (balances[msg.sender][tokenInfo.erc20Address] < amount) {
|
/**
|
||||||
revert TokenDivider__NotEnoughErc20Balance();
|
*
|
||||||
}
|
* @param nftPegged The nft address pegged to the tokens to sell
|
||||||
|
* @param price The price of all the tokens to sell
|
||||||
balances[msg.sender][tokenInfo.erc20Address] -= amount;
|
* @param amount The amount of tokens to sell
|
||||||
balances[to][tokenInfo.erc20Address] += amount;
|
*
|
||||||
|
* @dev this function creates a new order, is like publish you assets into a marketplace, where other persons can buy it.
|
||||||
emit TokensTransfered(amount, tokenInfo.erc20Address);
|
* 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.
|
||||||
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, to, amount);
|
*/
|
||||||
}
|
|
||||||
|
function sellErc20(address nftPegged, uint256 price,uint256 amount) external {
|
||||||
/**
|
if(nftPegged == address(0)) {
|
||||||
*
|
revert TokenDivider__NftAddressIsZero();
|
||||||
* @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
|
if( amount == 0) {
|
||||||
*
|
revert TokenDivider__AmountCantBeZero();
|
||||||
* @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.
|
ERC20Info memory tokenInfo = nftToErc20Info[nftPegged];
|
||||||
*/
|
if(balances[msg.sender][tokenInfo.erc20Address] < amount) {
|
||||||
function sellErc20(address nftPegged, uint256 price, uint256 amount) external {
|
revert TokenDivider__InsuficientBalance();
|
||||||
// @audit: check with "counterfeit" token
|
}
|
||||||
if (nftPegged == address(0)) {
|
|
||||||
revert TokenDivider__NftAddressIsZero();
|
balances[msg.sender][tokenInfo.erc20Address] -= amount;
|
||||||
}
|
|
||||||
|
s_userToSellOrders[msg.sender].push(
|
||||||
if (amount == 0) {
|
SellOrder({
|
||||||
revert TokenDivider__AmountCantBeZero();
|
seller: msg.sender,
|
||||||
}
|
erc20Address: tokenInfo.erc20Address,
|
||||||
// @audit: no check for 0 price?
|
price: price,
|
||||||
// @audit-report: no check but it successfully transfer the given amount without transfering the given ether
|
amount: amount
|
||||||
|
})
|
||||||
ERC20Info memory tokenInfo = nftToErc20Info[nftPegged];
|
);
|
||||||
if (balances[msg.sender][tokenInfo.erc20Address] < amount) {
|
|
||||||
revert TokenDivider__InsuficientBalance();
|
emit OrderPublished(amount,msg.sender, nftPegged);
|
||||||
}
|
|
||||||
|
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender,address(this), amount);
|
||||||
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})
|
*
|
||||||
);
|
* @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
|
||||||
emit OrderPublished(amount, msg.sender, nftPegged);
|
*
|
||||||
|
* @dev when the buyer call this function, the eth or any token accepted to pay, is sent to the seller
|
||||||
// send erc20 token from owner to this contract
|
* if the transfer executed correctly, then this contract, wich has all the tokens, send the tokens to the msg.sender
|
||||||
// @audit: what if the owner of ERC20 rejected this transaction?
|
*/
|
||||||
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, address(this), amount);
|
|
||||||
}
|
function buyOrder(uint256 orderIndex, address seller) external payable {
|
||||||
|
if(seller == address(0)) {
|
||||||
/**
|
revert TokenDivider__InvalidSeller();
|
||||||
*
|
}
|
||||||
* @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
|
SellOrder memory order = s_userToSellOrders[seller][orderIndex];
|
||||||
* if the transfer executed correctly, then this contract, wich has all the tokens, send the tokens to the msg.sender
|
|
||||||
*/
|
if(msg.value < order.price) {
|
||||||
function buyOrder(uint256 orderIndex, address seller) external payable {
|
revert TokenDivider__IncorrectEtherAmount();
|
||||||
// @audit: check with "counterfeit" token
|
}
|
||||||
if (seller == address(0)) {
|
|
||||||
revert TokenDivider__InvalidSeller();
|
uint256 fee = order.price / 100;
|
||||||
}
|
uint256 sellerFee = fee / 2;
|
||||||
|
|
||||||
SellOrder memory order = s_userToSellOrders[seller][orderIndex];
|
|
||||||
|
if(msg.value < order.price + sellerFee) {
|
||||||
// msg.value can be greater than order price?
|
revert TokenDivider__InsuficientEtherForFees();
|
||||||
if (msg.value < order.price) {
|
}
|
||||||
revert TokenDivider__IncorrectEtherAmount();
|
|
||||||
}
|
|
||||||
|
balances[msg.sender][order.erc20Address] += order.amount;
|
||||||
// if price 100, then fee is 1
|
|
||||||
uint256 fee = order.price / 100;
|
s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1];
|
||||||
// if price 100, then seller fee is 0.5
|
s_userToSellOrders[seller].pop();
|
||||||
uint256 sellerFee = fee / 2;
|
|
||||||
|
emit OrderSelled(msg.sender, order.price);
|
||||||
if (msg.value < order.price + sellerFee) {
|
|
||||||
revert TokenDivider__InsuficientEtherForFees();
|
// Transfer The Ether
|
||||||
}
|
|
||||||
|
(bool success, ) = payable(order.seller).call{value: (order.price - sellerFee)}("");
|
||||||
// record erc20 amount on storage variable onchain
|
|
||||||
balances[msg.sender][order.erc20Address] += order.amount;
|
if(!success) {
|
||||||
|
revert TokenDivider__TransferFailed();
|
||||||
// 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();
|
(bool taxSuccess, ) = payable(owner()).call{value: fee}("");
|
||||||
|
|
||||||
emit OrderSelled(msg.sender, order.price);
|
|
||||||
|
if(!taxSuccess) {
|
||||||
// Transfer The Ether
|
revert TokenDivider__TransferFailed();
|
||||||
|
}
|
||||||
// send ether to seller, reduced with seller fee
|
|
||||||
(bool success,) = payable(order.seller).call{value: (order.price - sellerFee)}("");
|
IERC20(order.erc20Address).transfer(msg.sender, order.amount);
|
||||||
|
|
||||||
if (!success) {
|
}
|
||||||
revert TokenDivider__TransferFailed();
|
|
||||||
}
|
/** Getters */
|
||||||
|
|
||||||
// send "fee" to "owner"?
|
function getBalanceOf(address user, address token) public view returns(uint256) {
|
||||||
// @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?
|
return balances[user][token];
|
||||||
// @audit-report: yes it does. if the price is 0, then there is no value increase
|
}
|
||||||
// @audit: who owner?
|
|
||||||
// @audit-report: the deployer
|
function getErc20TotalMintedAmount(address erc20) public view returns(uint256) {
|
||||||
(bool taxSuccess,) = payable(owner()).call{value: fee}("");
|
return erc20ToMintedAmount[erc20];
|
||||||
|
}
|
||||||
if (!taxSuccess) {
|
|
||||||
revert TokenDivider__TransferFailed();
|
function getErc20InfoFromNft(address nft) public view returns(ERC20Info memory) {
|
||||||
}
|
return nftToErc20Info[nft];
|
||||||
|
}
|
||||||
// send erc20 token to buyer address
|
|
||||||
IERC20(order.erc20Address).transfer(msg.sender, order.amount);
|
function getOrderPrice(address seller, uint256 index) public view returns(uint256 price) {
|
||||||
}
|
price = s_userToSellOrders[seller][index].price;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,293 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
pragma solidity ^0.8.18;
|
|
||||||
|
|
||||||
import {Test} 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';
|
|
||||||
|
|
||||||
contract TokenDividerClaimTest 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);
|
|
||||||
|
|
||||||
// divide the ERC721 and mint ERC20
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
TOKEN_ID,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_assertSetUp() public {
|
|
||||||
erc721Mock.mint(USER);
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if NFT already moved to tokenDivider
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if USER has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_claimNftSuccessfully() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
|
||||||
tokenDivider.claimNft(address(erc721Mock));
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
address(USER),
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getErc20TotalMintedAmount(
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(USER)
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
erc20Mock.balanceOf(
|
|
||||||
address(USER)
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_failIf_claimNftWithCounterfeitNFT() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
erc20Mock.mint(address(USER2), AMOUNT);
|
|
||||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
|
||||||
vm.expectRevert(
|
|
||||||
TokenDivider.TokenDivider__NotEnoughErc20Balance.selector
|
|
||||||
);
|
|
||||||
tokenDivider.claimNft(address(erc721Mock));
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_failIf_notExistingNftAddress() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
|
||||||
tokenDivider.claimNft(address(1));
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
address(USER),
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getErc20TotalMintedAmount(
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(USER)
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
erc20Mock.balanceOf(
|
|
||||||
address(USER)
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_failIf_ERC20SentToOtherOnClaim() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
erc20Mock.balanceOf(
|
|
||||||
address(USER)
|
|
||||||
),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
|
||||||
erc20Mock.transfer(
|
|
||||||
address(USER2),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
address(USER),
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
address(USER2),
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), AMOUNT);
|
|
||||||
vm.expectRevert();
|
|
||||||
tokenDivider.claimNft(address(erc721Mock));
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_failIf_claimOverlappingNFTWithDifferentId() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// mint ERC721 with new TOKEN_ID
|
|
||||||
erc721Mock.mint(USER2);
|
|
||||||
uint256 newTokenId = TOKEN_ID + 1;
|
|
||||||
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
erc721Mock.approve(address(tokenDivider), newTokenId);
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
newTokenId,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
ERC20Mock erc20MockWithNewTokenId = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if NFT already moved to tokenDivider
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(newTokenId),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
// assert if USER2 has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
USER2,
|
|
||||||
address(erc20MockWithNewTokenId)
|
|
||||||
),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
|
|
||||||
// USER2 claim NFT
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
erc20MockWithNewTokenId.approve(
|
|
||||||
address(tokenDivider),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
tokenDivider.claimNft(address(erc721Mock));
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(newTokenId),
|
|
||||||
address(USER2)
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert ERC20 address before USER claim NFT
|
|
||||||
// @audit-report: it returns different erc20 address
|
|
||||||
// @dev: feel free to uncomment to see the error
|
|
||||||
/*
|
|
||||||
assertEq(
|
|
||||||
address(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
),
|
|
||||||
address(erc20Mock)
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// USER claim NFT
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
erc20MockWithNewTokenId.approve(
|
|
||||||
address(tokenDivider),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
// @dev: it weirdly returns ERC721InsufficientApproval
|
|
||||||
// despite its already approved in above.
|
|
||||||
// maybe because of different ERC20 address?
|
|
||||||
// @dev: changing the ERC20 address and the USER prank
|
|
||||||
// doesn't help
|
|
||||||
/*
|
|
||||||
tokenDivider.claimNft(address(erc721Mock));
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(USER)
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,279 +0,0 @@
|
|||||||
// 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 TokenDividerDivideTest 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); // @auditor: user has the NFT
|
|
||||||
vm.deal(USER2, STARTING_USER_BALANCE); // @auditor: user2 got 10 ether
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_assertSetUp() public view {
|
|
||||||
// USER has the ERC721 NFT of TOKEN_ID
|
|
||||||
assertTrue(address(erc721Mock.ownerOf(TOKEN_ID)) == address(USER));
|
|
||||||
|
|
||||||
// USER2 has no ERC721 NFT of TOKEN_ID
|
|
||||||
assertTrue(address(erc721Mock.ownerOf(TOKEN_ID)) != address(USER2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_divideNftSuccessfully() public {
|
|
||||||
vm.startPrank(USER);
|
|
||||||
|
|
||||||
// approve the NFT to receive the NFT in USER
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
TOKEN_ID,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if NFT already moved to tokenDivider
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
// assert if USER has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_failIf_sendNFTDuringDivide() public {
|
|
||||||
vm.startPrank(USER);
|
|
||||||
|
|
||||||
// approve the NFT to receive the NFT in USER
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
|
|
||||||
// send the NFT to USER2
|
|
||||||
erc721Mock.safeTransferFrom(
|
|
||||||
address(USER),
|
|
||||||
address(USER2),
|
|
||||||
TOKEN_ID
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.expectRevert(
|
|
||||||
TokenDivider.TokenDivider__NotFromNftOwner.selector
|
|
||||||
);
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
TOKEN_ID,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_sendNFTBeforeDivide() public {
|
|
||||||
vm.startPrank(USER);
|
|
||||||
|
|
||||||
// send the NFT to USER2
|
|
||||||
erc721Mock.safeTransferFrom(
|
|
||||||
address(USER),
|
|
||||||
address(USER2),
|
|
||||||
TOKEN_ID
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
|
|
||||||
// approve the NFT to receive the NFT in USER
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
TOKEN_ID,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if USER2 has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER2, address(erc20Mock)),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_failIf_nonExistenceNftTokenId() public {
|
|
||||||
vm.startPrank(USER);
|
|
||||||
|
|
||||||
// approve the NFT to receive the NFT in USER
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
|
|
||||||
vm.expectRevert();
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
404,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_failWhen_reMintTheDividedERC20Token() public {
|
|
||||||
vm.startPrank(USER);
|
|
||||||
|
|
||||||
// approve the NFT to receive the NFT in USER
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
TOKEN_ID,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if USER has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
erc20Mock.mint(address(USER2), AMOUNT);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
USER,
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
USER2,
|
|
||||||
address(erc20Mock)
|
|
||||||
),
|
|
||||||
erc20Mock.balanceOf(address(USER2))
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
erc20Mock.totalSupply(),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_divideSameNFTAddressWithDifferentTokenId() public {
|
|
||||||
vm.startPrank(USER);
|
|
||||||
|
|
||||||
// approve the NFT to receive the NFT in USER
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
TOKEN_ID,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if NFT already moved to tokenDivider
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
// assert if USER has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 counterfeitTokenId = TOKEN_ID + 1;
|
|
||||||
erc721Mock.mint(USER2);
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(counterfeitTokenId),
|
|
||||||
address(USER2)
|
|
||||||
);
|
|
||||||
|
|
||||||
// USER2 divide the counterfeit
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
|
|
||||||
// approve the NFT to receive the NFT in USER2
|
|
||||||
erc721Mock.approve(
|
|
||||||
address(tokenDivider),
|
|
||||||
counterfeitTokenId
|
|
||||||
);
|
|
||||||
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
counterfeitTokenId,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
// now both of users (USER and USER2) has the same amount of registered ERC20 tokens despite it refer to different NFT Token Id
|
|
||||||
|
|
||||||
ERC20Mock erc20MockCounterfeit = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address
|
|
||||||
);
|
|
||||||
// assert if NFT already moved to tokenDivider
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(counterfeitTokenId),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if previous NFT is still being hold by contract
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if USER2 has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
USER2,
|
|
||||||
address(erc20MockCounterfeit)
|
|
||||||
),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
// assert if USER has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(
|
|
||||||
USER,
|
|
||||||
address(erc20MockCounterfeit)
|
|
||||||
),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,237 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
pragma solidity ^0.8.18;
|
|
||||||
|
|
||||||
import {Test} 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';
|
|
||||||
|
|
||||||
contract TokenDividerSellTest 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);
|
|
||||||
|
|
||||||
// divide the ERC721 and mint ERC20
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
|
|
||||||
tokenDivider.divideNft(
|
|
||||||
address(erc721Mock),
|
|
||||||
TOKEN_ID,
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_assertSetUp() public {
|
|
||||||
erc721Mock.mint(USER);
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if NFT already moved to tokenDivider
|
|
||||||
assertEq(
|
|
||||||
erc721Mock.ownerOf(TOKEN_ID),
|
|
||||||
address(tokenDivider)
|
|
||||||
);
|
|
||||||
|
|
||||||
// assert if USER has the ERC20 token
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_sellErc20() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 sellAmount = 1;
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), sellAmount);
|
|
||||||
tokenDivider.sellErc20(
|
|
||||||
address(erc721Mock),
|
|
||||||
1,
|
|
||||||
sellAmount
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT - 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_sellErc20WithZeroPrice() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 sellAmount = 1e17;
|
|
||||||
uint256 sellPrice = 0;
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), sellAmount);
|
|
||||||
tokenDivider.sellErc20(
|
|
||||||
address(erc721Mock),
|
|
||||||
sellPrice,
|
|
||||||
sellAmount
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT - sellAmount
|
|
||||||
);
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
sellPrice,
|
|
||||||
tokenDivider.getOrderPrice(
|
|
||||||
address(USER),
|
|
||||||
0 // index of order price
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_sellAndBuyErc20() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 sellAmount = 1e17;
|
|
||||||
uint256 sellPrice = 1 ether;
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), sellAmount);
|
|
||||||
tokenDivider.sellErc20(
|
|
||||||
address(erc721Mock),
|
|
||||||
sellPrice,
|
|
||||||
sellAmount
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT - sellAmount
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 buyPrice = sellPrice + ((sellPrice / 100) / 2);
|
|
||||||
vm.deal(
|
|
||||||
address(USER2),
|
|
||||||
buyPrice
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 contractInitialBalance = address(
|
|
||||||
tokenDivider.owner()
|
|
||||||
).balance;
|
|
||||||
assertEq(
|
|
||||||
address(USER).balance,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
tokenDivider.buyOrder{value: buyPrice}(
|
|
||||||
0,
|
|
||||||
address(USER)
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
erc20Mock.balanceOf(address(USER2)),
|
|
||||||
sellAmount
|
|
||||||
);
|
|
||||||
uint256 sellerFee = buyPrice - sellPrice;
|
|
||||||
assertEq(
|
|
||||||
address(
|
|
||||||
tokenDivider.owner()
|
|
||||||
).balance - contractInitialBalance,
|
|
||||||
sellerFee * 2
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
address(USER).balance,
|
|
||||||
sellPrice - sellerFee
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function test_sellAndBuyErc20With0Price() public {
|
|
||||||
ERC20Mock erc20Mock = ERC20Mock(
|
|
||||||
tokenDivider.getErc20InfoFromNft(
|
|
||||||
address(erc721Mock)
|
|
||||||
).erc20Address
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 sellAmount = 1e17;
|
|
||||||
uint256 sellPrice = 0 ether;
|
|
||||||
vm.startPrank(USER);
|
|
||||||
erc20Mock.approve(address(tokenDivider), sellAmount);
|
|
||||||
tokenDivider.sellErc20(
|
|
||||||
address(erc721Mock),
|
|
||||||
sellPrice,
|
|
||||||
sellAmount
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
tokenDivider.getBalanceOf(USER, address(erc20Mock)),
|
|
||||||
AMOUNT - sellAmount
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 buyPrice = sellPrice + ((sellPrice / 100) / 2);
|
|
||||||
vm.deal(
|
|
||||||
address(USER2),
|
|
||||||
buyPrice
|
|
||||||
);
|
|
||||||
|
|
||||||
uint256 contractInitialBalance = address(
|
|
||||||
tokenDivider.owner()
|
|
||||||
).balance;
|
|
||||||
assertEq(
|
|
||||||
address(USER).balance,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
vm.startPrank(USER2);
|
|
||||||
tokenDivider.buyOrder{value: buyPrice}(
|
|
||||||
0,
|
|
||||||
address(USER)
|
|
||||||
);
|
|
||||||
vm.stopPrank();
|
|
||||||
|
|
||||||
assertEq(
|
|
||||||
erc20Mock.balanceOf(address(USER2)),
|
|
||||||
sellAmount
|
|
||||||
);
|
|
||||||
uint256 sellerFee = buyPrice - sellPrice;
|
|
||||||
assertEq(
|
|
||||||
address(
|
|
||||||
tokenDivider.owner()
|
|
||||||
).balance - contractInitialBalance,
|
|
||||||
sellerFee * 2
|
|
||||||
);
|
|
||||||
assertEq(
|
|
||||||
address(USER).balance,
|
|
||||||
sellPrice - sellerFee
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -27,8 +27,8 @@ contract TokenDiverTest is Test {
|
|||||||
|
|
||||||
erc721Mock = new ERC721Mock();
|
erc721Mock = new ERC721Mock();
|
||||||
|
|
||||||
erc721Mock.mint(USER); // @auditor: user has the NFT
|
erc721Mock.mint(USER);
|
||||||
vm.deal(USER2, STARTING_USER_BALANCE); // @auditor: user2 got 10 ether
|
vm.deal(USER2, STARTING_USER_BALANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDivideNft() public {
|
function testDivideNft() public {
|
||||||
@ -151,4 +151,4 @@ contract TokenDiverTest is Test {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user