Skip to content

Commit

Permalink
Add NFT Marketplace Tests (#1829)
Browse files Browse the repository at this point in the history
* test

* test

* add more setup

* add to path

* githubpath

* seid path

* dapp tests

* try artifacts

* ditch setup step

* deps

* test workflow

* set keyring

* without

* keys add

* add to sh

* nobackend

* no script

* test

* keyring

* no sh

* test

* fix flakiness

* cleanup

* move seid config command

* only if docker

* test without

* if isdocker

* printf mnemonic

* try escape path

* try single quotes

* try modifying execute

* no path

* backend

* try without keyring

* move keyring

* path

* basedir

* try pwd

* config reset

* seid config

* redeclare

* print

* docker path

* dynamic path

* config for all

* full

* lint issue

* backend

* cleanup

* seaport starter

* working locally

* working on all chains

* add readme

* keyring

* stray argument in unit test

* cleanup

* reduce costs

* scripts
  • Loading branch information
mj850 authored Sep 4, 2024
1 parent c58e08e commit 7ebd2c4
Show file tree
Hide file tree
Showing 13 changed files with 599 additions and 127 deletions.
2 changes: 1 addition & 1 deletion contracts/test/CW721toERC721PointerTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("CW721 to ERC721 Pointer", function () {
describe("validation", function(){
it("should not allow a pointer to the pointer", async function(){
try {
await deployErc721PointerForCw721(hre.ethers.provider, pointer, 5)
await deployErc721PointerForCw721(hre.ethers.provider, pointer)
expect.fail(`Expected to be prevented from creating a pointer`);
} catch(e){
expect(e.message).to.include("contract deployment failed");
Expand Down
7 changes: 5 additions & 2 deletions contracts/test/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,11 @@ async function deployErc20PointerNative(provider, name, from=adminKeyName, evmRp
throw new Error("contract deployment failed")
}

async function deployErc721PointerForCw721(provider, cw721Address) {
const command = `seid tx evm register-evm-pointer CW721 ${cw721Address} --from=admin -b block`
async function deployErc721PointerForCw721(provider, cw721Address, from=adminKeyName, evmRpc="") {
let command = `seid tx evm register-evm-pointer CW721 ${cw721Address} --from=${from} -b block`
if (evmRpc) {
command = command + ` --evm-rpc=${evmRpc}`
}
const output = await execute(command);
const txHash = output.replace(/.*0x/, "0x").trim()
let attempt = 0;
Expand Down
48 changes: 48 additions & 0 deletions integration_test/dapp_tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# dApp Tests

This directory contains integration tests that simulate simple use cases on the chain by deploying and running common dApp contracts.
The focus here is mainly on testing common interop scenarios (interactions with associated/unassociated accounts, pointer contracts etc.)
In each test scenario, we deploy the dapp contracts, fund wallets, then go through common end to end scenarios.

## Setup
To run the dapp tests, simply run the script at `/integration_test/dapp_tests/dapp_tests.sh <chain>`

3 chain types are supported, `seilocal`, `devnet` (arctic-1) and `testnet` (atlantic-2). The configs for each chain are stored in `./hardhat.config.js`.

If running on `seilocal`, the script assumes that a local instance of the chain is running by running `/scripts/initialize_local_chain.sh`.
A well funded `admin` account must be available on the local keyring.

If running on the live chains, the tests rely on a `deployer` account, which has to have sufficient funds on the chain the test is running on.
The deployer mnemonic must be stored as an environment variable: DAPP_TESTS_MNEMONIC.
On the test pipelines, the account used is:
- Deployer Sei address: `sei1rtpakm7w9egh0n7xngzm6vrln0szv6yeva6hhn`
- Deployer EVM address: `0x4D952b770C3a0B096e739399B40263D0b516d406`

## Tests

### Uniswap (EVM DEX)
This test deploys a small set of UniswapV3 contracts to the EVM and tests swapping and creation of uniswap pools.
- Test that associated accounts are able to swap erc20 tokens
- Test that associated accounts are able to swap native tokens via pointer
- Test that associated accounts are able to swap cw20 tokens via pointer
- Test that unassociated accounts are able to receive erc20 tokens
- Test that unassociated accounts are able to receive native tokens via pointer
- Unassociated EVM accounts are not able to receive cw20 tokens via pointer
- Test that unassociated accounts can still deploy and supply erc20-erc20pointer liquidity pools.

### Steak (CW Liquid Staking)
This test deploys a set of WASM liquid staking contracts, then tests bonding and unbonding.
- Test that associated accounts are able to bond, then unbond tokens.
- Test that unassociated accounts are able to bond, then unbond tokens.

### NFT Marketplace (EVM NFT Marketplace)
This test deploys a simple NFT Marketplace contract, then tests listing and buying NFTs.
- Test that associated accounts are able to list and buy erc721 tokens
- Test that unassociated accounts are able to list and buy erc721 tokens
- Test that associated accounts are able to buy cw721 tokens via pointers
- Unassociated EVM accounts are currently unable to own or receive cw721 tokens via pointers

### To Be Added
The following is a list of testcases/scenarios that we should add to verify completeness
- CosmWasm DEX tests - test that ERC20 tokens are tradeable via pointer contracts.
- CosmWasm NFT Marketplace tests - test that ERC721 tokens are tradeable via pointer contracts.
2 changes: 1 addition & 1 deletion integration_test/dapp_tests/contracts/MockERC20.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
Expand Down
54 changes: 54 additions & 0 deletions integration_test/dapp_tests/contracts/MockERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract MockERC721 is ERC721, ERC721Enumerable, Ownable {
uint256 private _currentTokenId = 0;

constructor(string memory name, string memory symbol) ERC721(name, symbol) Ownable(msg.sender) {}

function mint(address to) public onlyOwner {
_currentTokenId++;
_mint(to, _currentTokenId);
}

function batchMint(address to, uint256 amount) public onlyOwner {
for (uint256 i = 0; i < amount; i++) {
_currentTokenId++;
_mint(to, _currentTokenId);
}
}

function burn(uint256 tokenId) public {
_burn(tokenId);
}

// The following functions are overrides required by Solidity.

function _update(address to, uint256 tokenId, address auth)
internal
override(ERC721, ERC721Enumerable)
returns (address)
{
return super._update(to, tokenId, auth);
}

function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
163 changes: 163 additions & 0 deletions integration_test/dapp_tests/contracts/NftMarketplace.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";

/*
* @title NftMarketplace
* @auth Patrick Collins
* @notice This contract allows users to list NFTs for sale
* @notice This is the reference
*/
contract NftMarketplace {
error NftMarketplace__PriceNotMet(address nftAddress, uint256 tokenId, uint256 price);
error NftMarketplace__NotListed(address nftAddress, uint256 tokenId);
error NftMarketplace__NoProceeds();
error NftMarketplace__NotOwner();
error NftMarketplace__PriceMustBeAboveZero();
error NftMarketplace__TransferFailed();

event ItemListed(address indexed seller, address indexed nftAddress, uint256 indexed tokenId, uint256 price);
event ItemUpdated(address indexed seller, address indexed nftAddress, uint256 indexed tokenId, uint256 price);
event ItemCanceled(address indexed seller, address indexed nftAddress, uint256 indexed tokenId);
event ItemBought(address indexed buyer, address indexed nftAddress, uint256 indexed tokenId, uint256 price);

mapping(address nftAddress => mapping(uint256 tokenId => Listing)) private s_listings;
mapping(address seller => uint256 proceedAmount) private s_proceeds;

struct Listing {
uint256 price;
address seller;
}

/*//////////////////////////////////////////////////////////////
FUNCTIONS
//////////////////////////////////////////////////////////////*/
/*
* @notice Method for listing NFT
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
* @param price sale price for each item
*/
function listItem(address nftAddress, uint256 tokenId, uint256 price) external {
// Checks
if (price <= 0) {
revert NftMarketplace__PriceMustBeAboveZero();
}

// Effects (Internal)
s_listings[nftAddress][tokenId] = Listing(price, msg.sender);
emit ItemListed(msg.sender, nftAddress, tokenId, price);

// Interactions (External)
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId);
}

/*
* @notice Method for cancelling listing
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
*
* @audit-known seller can front-run a bought NFT and cancel the listing
*/
function cancelListing(address nftAddress, uint256 tokenId) external {
// Checks
if (msg.sender != s_listings[nftAddress][tokenId].seller) {
revert NftMarketplace__NotOwner();
}

// Effects (Internal)
delete s_listings[nftAddress][tokenId];
emit ItemCanceled(msg.sender, nftAddress, tokenId);

// Interactions (External)
IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenId);
}

/*
* @notice Method for buying listing
* @notice The owner of an NFT could unapprove the marketplace,
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
*/
function buyItem(address nftAddress, uint256 tokenId) external payable {
Listing memory listedItem = s_listings[nftAddress][tokenId];
// Checks
if (listedItem.seller == address(0)) {
revert NftMarketplace__NotListed(nftAddress, tokenId);
}
if (msg.value < listedItem.price) {
revert NftMarketplace__PriceNotMet(nftAddress, tokenId, listedItem.price);
}

// Effects (Internal)
s_proceeds[listedItem.seller] += msg.value;
delete s_listings[nftAddress][tokenId];
emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price);

// Interactions (External)
IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenId);
}

/*
* @notice Method for updating listing
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
* @param newPrice Price in Wei of the item
*
* @audit-known seller can front-run a bought NFT and update the listing
*/
function updateListing(address nftAddress, uint256 tokenId, uint256 newPrice) external {
// Checks
if (newPrice <= 0) {
revert NftMarketplace__PriceMustBeAboveZero();
}
if (msg.sender != s_listings[nftAddress][tokenId].seller) {
revert NftMarketplace__NotOwner();
}

// Effects (Internal)
s_listings[nftAddress][tokenId].price = newPrice;
emit ItemUpdated(msg.sender, nftAddress, tokenId, newPrice);
}

/*
* @notice Method for withdrawing proceeds from sales
*
* @audit-known, we should emit an event for withdrawing proceeds
*/
function withdrawProceeds() external {
uint256 proceeds = s_proceeds[msg.sender];
// Checks
if (proceeds <= 0) {
revert NftMarketplace__NoProceeds();
}
// Effects (Internal)
s_proceeds[msg.sender] = 0;

// Interactions (External)
(bool success,) = payable(msg.sender).call{value: proceeds}("");
if (!success) {
revert NftMarketplace__TransferFailed();
}
}

function onERC721Received(address, /*operator*/ address, /*from*/ uint256, /*tokenId*/ bytes calldata /*data*/ )
external
pure
returns (bytes4)
{
return this.onERC721Received.selector;
}

/*//////////////////////////////////////////////////////////////
VIEW/PURE FUNCTIONS
//////////////////////////////////////////////////////////////*/
function getListing(address nftAddress, uint256 tokenId) external view returns (Listing memory) {
return s_listings[nftAddress][tokenId];
}

function getProceeds(address seller) external view returns (uint256) {
return s_proceeds[seller];
}
}
35 changes: 32 additions & 3 deletions integration_test/dapp_tests/dapp_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ fi

set -e

# Build contacts repo first since we rely on that for lib.js
# Define the paths to the test files
uniswap_test="uniswap/uniswapTest.js"
steak_test="steak/SteakTests.js"
nft_test="nftMarketplace/nftMarketplaceTests.js"

# Build contracts repo first since we rely on that for lib.js
cd contracts
npm ci

Expand All @@ -20,5 +25,29 @@ npx hardhat compile
# Set the CONFIG environment variable
export DAPP_TEST_ENV=$1

npx hardhat test --network $1 uniswap/uniswapTest.js
npx hardhat test --network $1 steak/SteakTests.js
# Determine which tests to run
if [ -z "$2" ]; then
tests=("$uniswap_test" "$steak_test" "$nft_test")
else
case $2 in
uniswap)
tests=("$uniswap_test")
;;
steak)
tests=("$steak_test")
;;
nft)
tests=("$nft_test")
;;
*)
echo "Invalid test specified. Please choose either 'uniswap', 'steak', or 'nft'."
exit 1
;;
esac
fi

# Run the selected tests
for test in "${tests[@]}"; do
npx hardhat test --network $1 $test
done

Binary file not shown.
Loading

0 comments on commit 7ebd2c4

Please sign in to comment.