Skip to content

Commit 7ebd2c4

Browse files
authored
Add NFT Marketplace Tests (#1829)
* 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
1 parent c58e08e commit 7ebd2c4

File tree

13 files changed

+599
-127
lines changed

13 files changed

+599
-127
lines changed

contracts/test/CW721toERC721PointerTest.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe("CW721 to ERC721 Pointer", function () {
2727
describe("validation", function(){
2828
it("should not allow a pointer to the pointer", async function(){
2929
try {
30-
await deployErc721PointerForCw721(hre.ethers.provider, pointer, 5)
30+
await deployErc721PointerForCw721(hre.ethers.provider, pointer)
3131
expect.fail(`Expected to be prevented from creating a pointer`);
3232
} catch(e){
3333
expect(e.message).to.include("contract deployment failed");

contracts/test/lib.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,11 @@ async function deployErc20PointerNative(provider, name, from=adminKeyName, evmRp
252252
throw new Error("contract deployment failed")
253253
}
254254

255-
async function deployErc721PointerForCw721(provider, cw721Address) {
256-
const command = `seid tx evm register-evm-pointer CW721 ${cw721Address} --from=admin -b block`
255+
async function deployErc721PointerForCw721(provider, cw721Address, from=adminKeyName, evmRpc="") {
256+
let command = `seid tx evm register-evm-pointer CW721 ${cw721Address} --from=${from} -b block`
257+
if (evmRpc) {
258+
command = command + ` --evm-rpc=${evmRpc}`
259+
}
257260
const output = await execute(command);
258261
const txHash = output.replace(/.*0x/, "0x").trim()
259262
let attempt = 0;

integration_test/dapp_tests/README.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# dApp Tests
2+
3+
This directory contains integration tests that simulate simple use cases on the chain by deploying and running common dApp contracts.
4+
The focus here is mainly on testing common interop scenarios (interactions with associated/unassociated accounts, pointer contracts etc.)
5+
In each test scenario, we deploy the dapp contracts, fund wallets, then go through common end to end scenarios.
6+
7+
## Setup
8+
To run the dapp tests, simply run the script at `/integration_test/dapp_tests/dapp_tests.sh <chain>`
9+
10+
3 chain types are supported, `seilocal`, `devnet` (arctic-1) and `testnet` (atlantic-2). The configs for each chain are stored in `./hardhat.config.js`.
11+
12+
If running on `seilocal`, the script assumes that a local instance of the chain is running by running `/scripts/initialize_local_chain.sh`.
13+
A well funded `admin` account must be available on the local keyring.
14+
15+
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.
16+
The deployer mnemonic must be stored as an environment variable: DAPP_TESTS_MNEMONIC.
17+
On the test pipelines, the account used is:
18+
- Deployer Sei address: `sei1rtpakm7w9egh0n7xngzm6vrln0szv6yeva6hhn`
19+
- Deployer EVM address: `0x4D952b770C3a0B096e739399B40263D0b516d406`
20+
21+
## Tests
22+
23+
### Uniswap (EVM DEX)
24+
This test deploys a small set of UniswapV3 contracts to the EVM and tests swapping and creation of uniswap pools.
25+
- Test that associated accounts are able to swap erc20 tokens
26+
- Test that associated accounts are able to swap native tokens via pointer
27+
- Test that associated accounts are able to swap cw20 tokens via pointer
28+
- Test that unassociated accounts are able to receive erc20 tokens
29+
- Test that unassociated accounts are able to receive native tokens via pointer
30+
- Unassociated EVM accounts are not able to receive cw20 tokens via pointer
31+
- Test that unassociated accounts can still deploy and supply erc20-erc20pointer liquidity pools.
32+
33+
### Steak (CW Liquid Staking)
34+
This test deploys a set of WASM liquid staking contracts, then tests bonding and unbonding.
35+
- Test that associated accounts are able to bond, then unbond tokens.
36+
- Test that unassociated accounts are able to bond, then unbond tokens.
37+
38+
### NFT Marketplace (EVM NFT Marketplace)
39+
This test deploys a simple NFT Marketplace contract, then tests listing and buying NFTs.
40+
- Test that associated accounts are able to list and buy erc721 tokens
41+
- Test that unassociated accounts are able to list and buy erc721 tokens
42+
- Test that associated accounts are able to buy cw721 tokens via pointers
43+
- Unassociated EVM accounts are currently unable to own or receive cw721 tokens via pointers
44+
45+
### To Be Added
46+
The following is a list of testcases/scenarios that we should add to verify completeness
47+
- CosmWasm DEX tests - test that ERC20 tokens are tradeable via pointer contracts.
48+
- CosmWasm NFT Marketplace tests - test that ERC721 tokens are tradeable via pointer contracts.

integration_test/dapp_tests/contracts/MockERC20.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-License-Identifier: MIT
1+
// SPDX-License-Identifier: MIT
22
pragma solidity ^0.8.20;
33

44
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
5+
import "@openzeppelin/contracts/access/Ownable.sol";
6+
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
7+
8+
contract MockERC721 is ERC721, ERC721Enumerable, Ownable {
9+
uint256 private _currentTokenId = 0;
10+
11+
constructor(string memory name, string memory symbol) ERC721(name, symbol) Ownable(msg.sender) {}
12+
13+
function mint(address to) public onlyOwner {
14+
_currentTokenId++;
15+
_mint(to, _currentTokenId);
16+
}
17+
18+
function batchMint(address to, uint256 amount) public onlyOwner {
19+
for (uint256 i = 0; i < amount; i++) {
20+
_currentTokenId++;
21+
_mint(to, _currentTokenId);
22+
}
23+
}
24+
25+
function burn(uint256 tokenId) public {
26+
_burn(tokenId);
27+
}
28+
29+
// The following functions are overrides required by Solidity.
30+
31+
function _update(address to, uint256 tokenId, address auth)
32+
internal
33+
override(ERC721, ERC721Enumerable)
34+
returns (address)
35+
{
36+
return super._update(to, tokenId, auth);
37+
}
38+
39+
function _increaseBalance(address account, uint128 value)
40+
internal
41+
override(ERC721, ERC721Enumerable)
42+
{
43+
super._increaseBalance(account, value);
44+
}
45+
46+
function supportsInterface(bytes4 interfaceId)
47+
public
48+
view
49+
override(ERC721, ERC721Enumerable)
50+
returns (bool)
51+
{
52+
return super.supportsInterface(interfaceId);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.20;
3+
4+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
5+
6+
/*
7+
* @title NftMarketplace
8+
* @auth Patrick Collins
9+
* @notice This contract allows users to list NFTs for sale
10+
* @notice This is the reference
11+
*/
12+
contract NftMarketplace {
13+
error NftMarketplace__PriceNotMet(address nftAddress, uint256 tokenId, uint256 price);
14+
error NftMarketplace__NotListed(address nftAddress, uint256 tokenId);
15+
error NftMarketplace__NoProceeds();
16+
error NftMarketplace__NotOwner();
17+
error NftMarketplace__PriceMustBeAboveZero();
18+
error NftMarketplace__TransferFailed();
19+
20+
event ItemListed(address indexed seller, address indexed nftAddress, uint256 indexed tokenId, uint256 price);
21+
event ItemUpdated(address indexed seller, address indexed nftAddress, uint256 indexed tokenId, uint256 price);
22+
event ItemCanceled(address indexed seller, address indexed nftAddress, uint256 indexed tokenId);
23+
event ItemBought(address indexed buyer, address indexed nftAddress, uint256 indexed tokenId, uint256 price);
24+
25+
mapping(address nftAddress => mapping(uint256 tokenId => Listing)) private s_listings;
26+
mapping(address seller => uint256 proceedAmount) private s_proceeds;
27+
28+
struct Listing {
29+
uint256 price;
30+
address seller;
31+
}
32+
33+
/*//////////////////////////////////////////////////////////////
34+
FUNCTIONS
35+
//////////////////////////////////////////////////////////////*/
36+
/*
37+
* @notice Method for listing NFT
38+
* @param nftAddress Address of NFT contract
39+
* @param tokenId Token ID of NFT
40+
* @param price sale price for each item
41+
*/
42+
function listItem(address nftAddress, uint256 tokenId, uint256 price) external {
43+
// Checks
44+
if (price <= 0) {
45+
revert NftMarketplace__PriceMustBeAboveZero();
46+
}
47+
48+
// Effects (Internal)
49+
s_listings[nftAddress][tokenId] = Listing(price, msg.sender);
50+
emit ItemListed(msg.sender, nftAddress, tokenId, price);
51+
52+
// Interactions (External)
53+
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId);
54+
}
55+
56+
/*
57+
* @notice Method for cancelling listing
58+
* @param nftAddress Address of NFT contract
59+
* @param tokenId Token ID of NFT
60+
*
61+
* @audit-known seller can front-run a bought NFT and cancel the listing
62+
*/
63+
function cancelListing(address nftAddress, uint256 tokenId) external {
64+
// Checks
65+
if (msg.sender != s_listings[nftAddress][tokenId].seller) {
66+
revert NftMarketplace__NotOwner();
67+
}
68+
69+
// Effects (Internal)
70+
delete s_listings[nftAddress][tokenId];
71+
emit ItemCanceled(msg.sender, nftAddress, tokenId);
72+
73+
// Interactions (External)
74+
IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenId);
75+
}
76+
77+
/*
78+
* @notice Method for buying listing
79+
* @notice The owner of an NFT could unapprove the marketplace,
80+
* @param nftAddress Address of NFT contract
81+
* @param tokenId Token ID of NFT
82+
*/
83+
function buyItem(address nftAddress, uint256 tokenId) external payable {
84+
Listing memory listedItem = s_listings[nftAddress][tokenId];
85+
// Checks
86+
if (listedItem.seller == address(0)) {
87+
revert NftMarketplace__NotListed(nftAddress, tokenId);
88+
}
89+
if (msg.value < listedItem.price) {
90+
revert NftMarketplace__PriceNotMet(nftAddress, tokenId, listedItem.price);
91+
}
92+
93+
// Effects (Internal)
94+
s_proceeds[listedItem.seller] += msg.value;
95+
delete s_listings[nftAddress][tokenId];
96+
emit ItemBought(msg.sender, nftAddress, tokenId, listedItem.price);
97+
98+
// Interactions (External)
99+
IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenId);
100+
}
101+
102+
/*
103+
* @notice Method for updating listing
104+
* @param nftAddress Address of NFT contract
105+
* @param tokenId Token ID of NFT
106+
* @param newPrice Price in Wei of the item
107+
*
108+
* @audit-known seller can front-run a bought NFT and update the listing
109+
*/
110+
function updateListing(address nftAddress, uint256 tokenId, uint256 newPrice) external {
111+
// Checks
112+
if (newPrice <= 0) {
113+
revert NftMarketplace__PriceMustBeAboveZero();
114+
}
115+
if (msg.sender != s_listings[nftAddress][tokenId].seller) {
116+
revert NftMarketplace__NotOwner();
117+
}
118+
119+
// Effects (Internal)
120+
s_listings[nftAddress][tokenId].price = newPrice;
121+
emit ItemUpdated(msg.sender, nftAddress, tokenId, newPrice);
122+
}
123+
124+
/*
125+
* @notice Method for withdrawing proceeds from sales
126+
*
127+
* @audit-known, we should emit an event for withdrawing proceeds
128+
*/
129+
function withdrawProceeds() external {
130+
uint256 proceeds = s_proceeds[msg.sender];
131+
// Checks
132+
if (proceeds <= 0) {
133+
revert NftMarketplace__NoProceeds();
134+
}
135+
// Effects (Internal)
136+
s_proceeds[msg.sender] = 0;
137+
138+
// Interactions (External)
139+
(bool success,) = payable(msg.sender).call{value: proceeds}("");
140+
if (!success) {
141+
revert NftMarketplace__TransferFailed();
142+
}
143+
}
144+
145+
function onERC721Received(address, /*operator*/ address, /*from*/ uint256, /*tokenId*/ bytes calldata /*data*/ )
146+
external
147+
pure
148+
returns (bytes4)
149+
{
150+
return this.onERC721Received.selector;
151+
}
152+
153+
/*//////////////////////////////////////////////////////////////
154+
VIEW/PURE FUNCTIONS
155+
//////////////////////////////////////////////////////////////*/
156+
function getListing(address nftAddress, uint256 tokenId) external view returns (Listing memory) {
157+
return s_listings[nftAddress][tokenId];
158+
}
159+
160+
function getProceeds(address seller) external view returns (uint256) {
161+
return s_proceeds[seller];
162+
}
163+
}

integration_test/dapp_tests/dapp_tests.sh

+32-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ fi
88

99
set -e
1010

11-
# Build contacts repo first since we rely on that for lib.js
11+
# Define the paths to the test files
12+
uniswap_test="uniswap/uniswapTest.js"
13+
steak_test="steak/SteakTests.js"
14+
nft_test="nftMarketplace/nftMarketplaceTests.js"
15+
16+
# Build contracts repo first since we rely on that for lib.js
1217
cd contracts
1318
npm ci
1419

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

23-
npx hardhat test --network $1 uniswap/uniswapTest.js
24-
npx hardhat test --network $1 steak/SteakTests.js
28+
# Determine which tests to run
29+
if [ -z "$2" ]; then
30+
tests=("$uniswap_test" "$steak_test" "$nft_test")
31+
else
32+
case $2 in
33+
uniswap)
34+
tests=("$uniswap_test")
35+
;;
36+
steak)
37+
tests=("$steak_test")
38+
;;
39+
nft)
40+
tests=("$nft_test")
41+
;;
42+
*)
43+
echo "Invalid test specified. Please choose either 'uniswap', 'steak', or 'nft'."
44+
exit 1
45+
;;
46+
esac
47+
fi
48+
49+
# Run the selected tests
50+
for test in "${tests[@]}"; do
51+
npx hardhat test --network $1 $test
52+
done
53+
Binary file not shown.

0 commit comments

Comments
 (0)