diff --git a/contracts/test/CW721toERC721PointerTest.js b/contracts/test/CW721toERC721PointerTest.js index 3bfa394f1..999eb8d06 100644 --- a/contracts/test/CW721toERC721PointerTest.js +++ b/contracts/test/CW721toERC721PointerTest.js @@ -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"); diff --git a/contracts/test/lib.js b/contracts/test/lib.js index 3c5fbf633..f69d2dcff 100644 --- a/contracts/test/lib.js +++ b/contracts/test/lib.js @@ -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; diff --git a/integration_test/dapp_tests/README.md b/integration_test/dapp_tests/README.md new file mode 100644 index 000000000..65fa2c439 --- /dev/null +++ b/integration_test/dapp_tests/README.md @@ -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 ` + +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. \ No newline at end of file diff --git a/integration_test/dapp_tests/contracts/MockERC20.sol b/integration_test/dapp_tests/contracts/MockERC20.sol index 58989ad89..e089db9e3 100644 --- a/integration_test/dapp_tests/contracts/MockERC20.sol +++ b/integration_test/dapp_tests/contracts/MockERC20.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/integration_test/dapp_tests/contracts/MockERC721.sol b/integration_test/dapp_tests/contracts/MockERC721.sol new file mode 100644 index 000000000..50ea9b905 --- /dev/null +++ b/integration_test/dapp_tests/contracts/MockERC721.sol @@ -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); + } +} \ No newline at end of file diff --git a/integration_test/dapp_tests/contracts/NftMarketplace.sol b/integration_test/dapp_tests/contracts/NftMarketplace.sol new file mode 100644 index 000000000..a23e6042f --- /dev/null +++ b/integration_test/dapp_tests/contracts/NftMarketplace.sol @@ -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]; + } +} \ No newline at end of file diff --git a/integration_test/dapp_tests/dapp_tests.sh b/integration_test/dapp_tests/dapp_tests.sh index a889b3d2b..0549381e4 100755 --- a/integration_test/dapp_tests/dapp_tests.sh +++ b/integration_test/dapp_tests/dapp_tests.sh @@ -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 @@ -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 + diff --git a/integration_test/dapp_tests/nftMarketplace/cw721_base.wasm b/integration_test/dapp_tests/nftMarketplace/cw721_base.wasm new file mode 100755 index 000000000..09c700df8 Binary files /dev/null and b/integration_test/dapp_tests/nftMarketplace/cw721_base.wasm differ diff --git a/integration_test/dapp_tests/nftMarketplace/nftMarketplaceTests.js b/integration_test/dapp_tests/nftMarketplace/nftMarketplaceTests.js new file mode 100644 index 000000000..3b1fde0df --- /dev/null +++ b/integration_test/dapp_tests/nftMarketplace/nftMarketplaceTests.js @@ -0,0 +1,178 @@ +const { expect } = require("chai"); +const hre = require("hardhat"); + +const {sendFunds, deployEthersContract, estimateAndCall, deployCw721WithPointer, setupAccountWithMnemonic, + mintCw721 +} = require("../utils"); +const { fundAddress, getSeiAddress, execute } = require("../../../contracts/test/lib.js"); +const {evmRpcUrls, chainIds, rpcUrls} = require("../constants"); + +const testChain = process.env.DAPP_TEST_ENV; +console.log("testChain", testChain); +describe("NFT Marketplace", function () { + + let marketplace, deployer, erc721token, erc721PointerToken, cw721Address, originalSeidConfig; + + before(async function () { + const accounts = hre.config.networks[testChain].accounts + const deployerWallet = hre.ethers.Wallet.fromMnemonic(accounts.mnemonic, accounts.path); + deployer = deployerWallet.connect(hre.ethers.provider); + + const seidConfig = await execute('seid config'); + originalSeidConfig = JSON.parse(seidConfig); + + if (testChain === 'seilocal') { + await fundAddress(deployer.address, amount="2000000000000000000000"); + } else { + // Set default seid config to the specified rpc url. + await execute(`seid config chain-id ${chainIds[testChain]}`) + await execute(`seid config node ${rpcUrls[testChain]}`) + } + + await execute(`seid config keyring-backend test`) + + await sendFunds('0.01', deployer.address, deployer) + await setupAccountWithMnemonic("dapptest", accounts.mnemonic, deployer); + + // Deploy MockNFT + const erc721ContractArtifact = await hre.artifacts.readArtifact("MockERC721"); + erc721token = await deployEthersContract("MockERC721", erc721ContractArtifact.abi, erc721ContractArtifact.bytecode, deployer, ["MockERC721", "MKTNFT"]) + + const numNftsToMint = 50 + await estimateAndCall(erc721token, "batchMint", [deployer.address, numNftsToMint]); + + // Deploy CW721 token with ERC721 pointer + const time = Date.now().toString(); + const deployerSeiAddr = await getSeiAddress(deployer.address); + const cw721Details = await deployCw721WithPointer(deployerSeiAddr, deployer, time, evmRpcUrls[testChain]) + erc721PointerToken = cw721Details.pointerContract; + cw721Address = cw721Details.cw721Address; + console.log("CW721 Address", cw721Address); + const numCwNftsToMint = 2; + for (let i = 1; i <= numCwNftsToMint; i++) { + await mintCw721(cw721Address, deployerSeiAddr, i) + } + const cwbal = await erc721PointerToken.balanceOf(deployer.address); + expect(cwbal).to.equal(numCwNftsToMint) + + const nftMarketplaceArtifact = await hre.artifacts.readArtifact("NftMarketplace"); + marketplace = await deployEthersContract("NftMarketplace", nftMarketplaceArtifact.abi, nftMarketplaceArtifact.bytecode, deployer) + }) + + describe("Orders", function () { + async function testNFTMarketplaceOrder(buyer, seller, nftContract, nftId="", expectTransferFail=false) { + let tokenId; + // If nftId is manually supplied (for pointer contract), ensure that deployer owns that token. + if (nftId) { + const nftOwner = await nftContract.ownerOf(nftId); + expect(nftOwner).to.equal(deployer.address); + tokenId = nftId; + } else { + // Refers to the first token owned by the deployer. + tokenId = await nftContract.tokenOfOwnerByIndex(deployer.address, 0); + } + + if (seller.address !== deployer.address) { + if (expectTransferFail) { + // Transfer to unassociated address should fail if seller is not associated. + expect(nftContract.transferFrom(deployer.address, seller.address, tokenId)).to.be.reverted; + + // Associate the seller from here. + await sendFunds("0.01", seller.address, seller); + } + + // Send one NFT to the seller so they can list it. + await estimateAndCall(nftContract, "transferFrom", [deployer.address, seller.address, tokenId]); + + let nftOwner = await nftContract.ownerOf(tokenId); + + // Seller should have the token here + expect(nftOwner).to.equal(seller.address, "NFT should have been transferred to the seller"); + } + + const sellerNftbalance = await nftContract.balanceOf(seller.address); + // Deployer should have at least one token here. + expect(Number(sellerNftbalance)).to.be.greaterThanOrEqual(1, "Seller must have at least 1 NFT remaining") + + // List the NFT on the marketplace contract. + const nftPrice = hre.ethers.utils.parseEther("0.1"); + await estimateAndCall(nftContract.connect(seller), "setApprovalForAll", [marketplace.address, true]) + await estimateAndCall(marketplace.connect(seller), "listItem", [nftContract.address, tokenId, nftPrice]) + + // Confirm that the NFT was listed. + const listing = await marketplace.getListing(nftContract.address, tokenId); + expect(listing.price).to.equal(nftPrice, "Listing price should be correct"); + expect(listing.seller).to.equal(seller.address, "Listing seller should be correct"); + + // Buyer purchases the NFT from the marketplace contract. + if (expectTransferFail) { + // We expect a revert here if the buyer address is not associated, since pointer tokens cant be transferred to buyer. + expect(marketplace.connect(buyer).buyItem(nftContract.address, tokenId, {value: nftPrice})).to.be.reverted; + + // Associate buyer here. + await sendFunds('0.01', buyer.address, buyer); + } + await estimateAndCall(marketplace.connect(buyer), "buyItem", [nftContract.address, tokenId], nftPrice); + + const newSellerNftbalance = await nftContract.balanceOf(seller.address); + expect(Number(newSellerNftbalance)).to.be.lessThan(Number(sellerNftbalance), "NFT should have been transferred from the seller.") + + nftOwner = await nftContract.ownerOf(tokenId); + expect(nftOwner).to.equal(buyer.address, "NFT should have been transferred to the buyer."); + } + + it("Should allow listing and buying erc721 by associated users", async function () { + // Create and fund buyer account + const buyerWallet = hre.ethers.Wallet.createRandom(); + const buyer = buyerWallet.connect(hre.ethers.provider); + await sendFunds("0.5", buyer.address, deployer) + + await testNFTMarketplaceOrder(buyer, deployer, erc721token) + }); + + it("Should allow listing and buying erc721 by unassociated users", async function () { + // Create and fund seller account + const sellerWallet = hre.ethers.Wallet.createRandom(); + const seller = sellerWallet.connect(hre.ethers.provider); + await sendFunds("0.5", seller.address, deployer) + + // Create and fund buyer account + const buyerWallet = hre.ethers.Wallet.createRandom(); + const buyer = buyerWallet.connect(hre.ethers.provider); + await sendFunds("0.5", buyer.address, deployer) + + await testNFTMarketplaceOrder(buyer, seller, erc721token); + }); + + it("Should allow listing and buying erc721 pointer by associated users", async function () { + // Create and fund buyer account + const buyerWallet = hre.ethers.Wallet.createRandom(); + const buyer = buyerWallet.connect(hre.ethers.provider); + await sendFunds("0.5", buyer.address, deployer) + await sendFunds('0.01', buyer.address, buyer) + await testNFTMarketplaceOrder(buyer, deployer, erc721PointerToken, '1'); + }); + + it("Currently does not allow listing or buying erc721 pointer by unassociated users", async function () { + // Create and fund seller account + const sellerWallet = hre.ethers.Wallet.createRandom(); + const seller = sellerWallet.connect(hre.ethers.provider); + await sendFunds("0.5", seller.address, deployer) + + // Create and fund buyer account + const buyerWallet = hre.ethers.Wallet.createRandom(); + const buyer = buyerWallet.connect(hre.ethers.provider); + await sendFunds("0.5", buyer.address, deployer) + + await testNFTMarketplaceOrder(buyer, seller, erc721PointerToken, '2', true); + }); + }) + + after(async function () { + // Set the chain back to regular state + console.log("Resetting") + await execute(`seid config chain-id ${originalSeidConfig["chain-id"]}`) + await execute(`seid config node ${originalSeidConfig["node"]}`) + await execute(`seid config keyring-backend ${originalSeidConfig["keyring-backend"]}`) + }) +}) diff --git a/integration_test/dapp_tests/package-lock.json b/integration_test/dapp_tests/package-lock.json index c1617b5c5..5aa2af6c1 100644 --- a/integration_test/dapp_tests/package-lock.json +++ b/integration_test/dapp_tests/package-lock.json @@ -288,6 +288,23 @@ "typechain": "^8.0.0" } }, + "node_modules/@ethereum-waffle/compiler/node_modules/@typechain/ethers-v5": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-10.2.1.tgz", + "integrity": "sha512-n3tQmCZjRE6IU4h6lqUGiQ1j866n5MTCBJreNEHHVWXa2u9GJTaeYyU1/k+1qLutkyw+sS6VAN+AbeiTqsxd/A==", + "peer": true, + "dependencies": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + }, + "peerDependencies": { + "@ethersproject/abi": "^5.0.0", + "@ethersproject/providers": "^5.0.0", + "ethers": "^5.1.3", + "typechain": "^8.1.1", + "typescript": ">=4.3.0" + } + }, "node_modules/@ethereum-waffle/ens": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@ethereum-waffle/ens/-/ens-4.0.3.tgz", @@ -886,7 +903,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/abstract-provider": "^5.7.0", @@ -941,7 +957,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/basex": "^5.7.0", @@ -971,7 +986,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/address": "^5.7.0", @@ -1057,7 +1071,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/sha2": "^5.7.0" @@ -1267,7 +1280,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -1339,7 +1351,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/bignumber": "^5.7.0", "@ethersproject/constants": "^5.7.0", @@ -1360,7 +1371,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", @@ -1416,7 +1426,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/bytes": "^5.7.0", "@ethersproject/hash": "^5.7.0", @@ -1965,8 +1974,7 @@ "node_modules/@openzeppelin/contracts": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.2.tgz", - "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==", - "license": "MIT" + "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==" }, "node_modules/@openzeppelin/test-helpers": { "version": "0.5.16", @@ -3512,23 +3520,6 @@ "node": ">=4" } }, - "node_modules/@typechain/ethers-v5": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-10.2.1.tgz", - "integrity": "sha512-n3tQmCZjRE6IU4h6lqUGiQ1j866n5MTCBJreNEHHVWXa2u9GJTaeYyU1/k+1qLutkyw+sS6VAN+AbeiTqsxd/A==", - "peer": true, - "dependencies": { - "lodash": "^4.17.15", - "ts-essentials": "^7.0.1" - }, - "peerDependencies": { - "@ethersproject/abi": "^5.0.0", - "@ethersproject/providers": "^5.0.0", - "ethers": "^5.1.3", - "typechain": "^8.1.1", - "typescript": ">=4.3.0" - } - }, "node_modules/@types/abstract-leveldown": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-7.2.5.tgz", @@ -5731,7 +5722,6 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "license": "MIT", "dependencies": { "@ethersproject/abi": "5.7.0", "@ethersproject/abstract-provider": "5.7.0", diff --git a/integration_test/dapp_tests/package.json b/integration_test/dapp_tests/package.json index bd6d8ef52..d58091898 100644 --- a/integration_test/dapp_tests/package.json +++ b/integration_test/dapp_tests/package.json @@ -17,8 +17,8 @@ "ethers": "^5.7.2" }, "devDependencies": { - "it-each": "^0.5.0", "hardhat": "^2.22.6", + "it-each": "^0.5.0", "uuid": "^10.0.0" } } diff --git a/integration_test/dapp_tests/uniswap/uniswapTest.js b/integration_test/dapp_tests/uniswap/uniswapTest.js index 9347abe07..347bb62f0 100755 --- a/integration_test/dapp_tests/uniswap/uniswapTest.js +++ b/integration_test/dapp_tests/uniswap/uniswapTest.js @@ -7,7 +7,7 @@ const { abi: MANAGER_ABI, bytecode: MANAGER_BYTECODE } = require("@uniswap/v3-pe const { abi: SWAP_ROUTER_ABI, bytecode: SWAP_ROUTER_BYTECODE } = require("@uniswap/v3-periphery/artifacts/contracts/SwapRouter.sol/SwapRouter.json"); const {exec} = require("child_process"); const { fundAddress, createTokenFactoryTokenAndMint, deployErc20PointerNative, execute, getSeiAddress, queryWasm, getSeiBalance, isDocker, ABI } = require("../../../contracts/test/lib.js"); -const { deployTokenPool, supplyLiquidity, deployCw20WithPointer, deployEthersContract, sendFunds, pollBalance, setupAccountWithMnemonic } = require("../utils.js") +const { deployTokenPool, supplyLiquidity, deployCw20WithPointer, deployEthersContract, sendFunds, pollBalance, setupAccountWithMnemonic, estimateAndCall } = require("../utils") const { rpcUrls, chainIds, evmRpcUrls} = require("../constants") const { expect } = require("chai"); @@ -53,7 +53,7 @@ describe("Uniswap Test", function () { const userWallet = hre.ethers.Wallet.createRandom(); user = userWallet.connect(hre.ethers.provider); - await sendFunds("5", user.address, deployer) + await sendFunds("1", user.address, deployer) const deployerSeiAddr = await getSeiAddress(deployer.address); @@ -94,7 +94,7 @@ describe("Uniswap Test", function () { // Deploy SwapRouter router = await deployEthersContract("SwapRouter", SWAP_ROUTER_ABI, SWAP_ROUTER_BYTECODE, deployer, deployParams=[factory.address, weth9.address]); - const amountETH = hre.ethers.utils.parseEther("30") + const amountETH = hre.ethers.utils.parseEther("3") // Gets the amount of WETH9 required to instantiate pools by depositing Sei to the contract let gasEstimate = await weth9.estimateGas.deposit({ value: amountETH }) @@ -109,9 +109,9 @@ describe("Uniswap Test", function () { await deployTokenPool(manager, weth9.address, erc20cw20.address) // Add Liquidity to pools - await supplyLiquidity(manager, deployer.address, weth9, token, hre.ethers.utils.parseEther("10"), hre.ethers.utils.parseEther("10")) - await supplyLiquidity(manager, deployer.address, weth9, erc20TokenFactory, hre.ethers.utils.parseEther("10"), hre.ethers.utils.parseEther("10")) - await supplyLiquidity(manager, deployer.address, weth9, erc20cw20, hre.ethers.utils.parseEther("10"), hre.ethers.utils.parseEther("10")) + await supplyLiquidity(manager, deployer.address, weth9, token, hre.ethers.utils.parseEther("1"), hre.ethers.utils.parseEther("1")) + await supplyLiquidity(manager, deployer.address, weth9, erc20TokenFactory, hre.ethers.utils.parseEther("1"), hre.ethers.utils.parseEther("1")) + await supplyLiquidity(manager, deployer.address, weth9, erc20cw20, hre.ethers.utils.parseEther("1"), hre.ethers.utils.parseEther("1")) }) describe("Swaps", function () { @@ -120,20 +120,18 @@ describe("Uniswap Test", function () { const fee = 3000; // Fee tier (0.3%) // Perform a Swap - const amountIn = hre.ethers.utils.parseEther("1"); + const amountIn = hre.ethers.utils.parseEther("0.1"); const amountOutMin = hre.ethers.utils.parseEther("0"); // Minimum amount of MockToken expected - const gasLimit = hre.ethers.utils.hexlify(1000000); // Example gas limit - const gasPrice = await hre.ethers.provider.getGasPrice(); + // const gasLimit = hre.ethers.utils.hexlify(1000000); // Example gas limit + // const gasPrice = await hre.ethers.provider.getGasPrice(); - const deposit = await token1.connect(user).deposit({ value: amountIn, gasLimit, gasPrice }); - await deposit.wait(); + await estimateAndCall(token1.connect(user), "deposit", [], amountIn) const token1balance = await token1.connect(user).balanceOf(user.address); expect(token1balance).to.equal(amountIn.toString(), "token1 balance should be equal to value passed in") - const approval = await token1.connect(user).approve(router.address, amountIn, {gasLimit, gasPrice}); - await approval.wait(); + await estimateAndCall(token1.connect(user), "approve", [router.address, amountIn]) const allowance = await token1.allowance(user.address, router.address); // Change to expect @@ -151,7 +149,7 @@ describe("Uniswap Test", function () { sqrtPriceLimitX96: 0 }, {gasLimit, gasPrice})).to.be.reverted; } else { - const tx = await router.connect(user).exactInputSingle({ + await estimateAndCall(router.connect(user), "exactInputSingle", [{ tokenIn: token1.address, tokenOut: token2.address, fee, @@ -160,12 +158,10 @@ describe("Uniswap Test", function () { amountIn, amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0 - }, {gasLimit, gasPrice}); - - await tx.wait(); + }]) // Check User's MockToken Balance - const balance = BigInt(await token2.balanceOf(user.address)); + const balance = await token2.balanceOf(user.address); // Check that it's more than 0 (no specified amount since there might be slippage) expect(Number(balance)).to.greaterThan(0, "Token2 should have been swapped successfully.") } @@ -181,22 +177,17 @@ describe("Uniswap Test", function () { const fee = 3000; // Fee tier (0.3%) // Perform a Swap - const amountIn = hre.ethers.utils.parseEther("1"); + const amountIn = hre.ethers.utils.parseEther("0.1"); const amountOutMin = hre.ethers.utils.parseEther("0"); // Minimum amount of MockToken expected - let gasPrice = await deployer.getGasPrice(); - let gasLimit = token1.estimateGas.deposit({ value: amountIn }); - const deposit = await token1.deposit({ value: amountIn, gasPrice, gasLimit }); - await deposit.wait(); + await estimateAndCall(token1, "deposit", [], amountIn) const token1balance = await token1.balanceOf(deployer.address); // Check that deployer has amountIn amount of token1 expect(Number(token1balance)).to.greaterThanOrEqual(Number(amountIn), "token1 balance should be received by user") - gasLimit = token1.estimateGas.approve(router.address, amountIn); - const approval = await token1.approve(router.address, amountIn, {gasPrice, gasLimit}); - await approval.wait(); + await estimateAndCall(token1, "approve", [router.address, amountIn]) const allowance = await token1.allowance(deployer.address, router.address); // Check that deployer has approved amountIn amount of token1 to be used by router @@ -212,15 +203,12 @@ describe("Uniswap Test", function () { amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0 } - gasLimit = router.estimateGas.exactInputSingle(txParams); if (expectSwapFail) { - expect(router.exactInputSingle(txParams, {gasPrice, gasLimit})).to.be.reverted; + expect(router.exactInputSingle(txParams)).to.be.reverted; } else { // Perform the swap, with recipient being the unassociated account. - const tx = await router.exactInputSingle(txParams, {gasPrice, gasLimit}); - - await tx.wait(); + await estimateAndCall(router, "exactInputSingle", [txParams]) // Check User's MockToken Balance const balance = await pollBalance(token2, unassocUser.address, function(bal) {return bal === 0}); @@ -281,7 +269,7 @@ describe("Uniswap Test", function () { const unassocUser = unassocUserWallet.connect(hre.ethers.provider); // Fund the user account. Creating pools is a expensive operation so we supply more funds here for gas. - await sendFunds("5", unassocUser.address, deployer) + await sendFunds("0.5", unassocUser.address, deployer) await deployTokenPool(manager.connect(unassocUser), erc20TokenFactory.address, token.address) }) @@ -291,18 +279,15 @@ describe("Uniswap Test", function () { const unassocUser = unassocUserWallet.connect(hre.ethers.provider); // Fund the user account - await sendFunds("2", unassocUser.address, deployer) + await sendFunds("0.5", unassocUser.address, deployer) const erc20TokenFactoryAmount = "100000" - let gasPrice = deployer.getGasPrice(); - let gasLimit = erc20TokenFactory.estimateGas.transfer(unassocUser.address, erc20TokenFactoryAmount); - const tx = await erc20TokenFactory.transfer(unassocUser.address, erc20TokenFactoryAmount, {gasPrice, gasLimit}); - await tx.wait(); + + await estimateAndCall(erc20TokenFactory, "transfer", [unassocUser.address, erc20TokenFactoryAmount]) const mockTokenAmount = "100000" - gasLimit = token.estimateGas.transfer(unassocUser.address, mockTokenAmount); - const tx2 = await token.transfer(unassocUser.address, mockTokenAmount, {gasPrice, gasLimit}); - await tx2.wait(); + await estimateAndCall(token, "transfer", [unassocUser.address, mockTokenAmount]) + const managerConnected = manager.connect(unassocUser); const erc20TokenFactoryConnected = erc20TokenFactory.connect(unassocUser); const mockTokenConnected = token.connect(unassocUser); diff --git a/integration_test/dapp_tests/utils.js b/integration_test/dapp_tests/utils.js index 31a22a7f9..9c66a92a0 100644 --- a/integration_test/dapp_tests/utils.js +++ b/integration_test/dapp_tests/utils.js @@ -1,31 +1,15 @@ const {v4: uuidv4} = require("uuid"); const hre = require("hardhat"); -const { ABI, deployErc20PointerForCw20, getSeiAddress, deployWasm, execute, delay, isDocker } = require("../../contracts/test/lib.js"); +const { ABI, deployErc20PointerForCw20, deployErc721PointerForCw721, getSeiAddress, deployWasm, execute, delay, isDocker } = require("../../contracts/test/lib.js"); const path = require('path') async function deployTokenPool(managerContract, firstTokenAddr, secondTokenAddr, swapRatio=1, fee=3000) { const sqrtPriceX96 = BigInt(Math.sqrt(swapRatio) * (2 ** 96)); // Initial price (1:1) - const gasPrice = await hre.ethers.provider.getGasPrice(); const [token0, token1] = tokenOrder(firstTokenAddr, secondTokenAddr); - let gasLimit = await managerContract.estimateGas.createAndInitializePoolIfNecessary( - token0.address, - token1.address, - fee, - sqrtPriceX96, - ); - - gasLimit = gasLimit.mul(12).div(10) + await estimateAndCall(managerContract, "createAndInitializePoolIfNecessary", [token0.address, token1.address, fee, sqrtPriceX96]) // token0 addr must be < token1 addr - const poolTx = await managerContract.createAndInitializePoolIfNecessary( - token0.address, - token1.address, - fee, - sqrtPriceX96, - {gasLimit, gasPrice} - ); - await poolTx.wait(); console.log("Pool created and initialized"); } @@ -34,45 +18,18 @@ async function supplyLiquidity(managerContract, recipientAddr, firstTokenContrac // Define the amount of tokens to be approved and added as liquidity console.log("Supplying liquidity to pool") const [token0, token1] = tokenOrder(firstTokenContract.address, secondTokenContract.address, firstTokenAmt, secondTokenAmt); - const gasPrice = await hre.ethers.provider.getGasPrice(); - - let gasLimit = await firstTokenContract.estimateGas.approve(managerContract.address, firstTokenAmt); - gasLimit = gasLimit.mul(12).div(10) // Approve the NonfungiblePositionManager to spend the specified amount of firstToken - const approveFirstTokenTx = await firstTokenContract.approve(managerContract.address, firstTokenAmt, {gasLimit, gasPrice}); - await approveFirstTokenTx.wait(); + await estimateAndCall(firstTokenContract, "approve", [managerContract.address, firstTokenAmt]); let allowance = await firstTokenContract.allowance(recipientAddr, managerContract.address); let balance = await firstTokenContract.balanceOf(recipientAddr); - gasLimit = await secondTokenContract.estimateGas.approve(managerContract.address, secondTokenAmt); - gasLimit = gasLimit.mul(12).div(10) // Approve the NonfungiblePositionManager to spend the specified amount of secondToken - const approveSecondTokenTx = await secondTokenContract.approve(managerContract.address, secondTokenAmt, {gasLimit, gasPrice}); - await approveSecondTokenTx.wait(); - - allowance = await secondTokenContract.allowance(recipientAddr, managerContract.address); - balance = await secondTokenContract.balanceOf(recipientAddr); - - gasLimit = await managerContract.estimateGas.mint({ - token0: token0.address, - token1: token1.address, - fee: 3000, // Fee tier (0.3%) - tickLower: -887220, - tickUpper: 887220, - amount0Desired: token0.amount, - amount1Desired: token1.amount, - amount0Min: 0, - amount1Min: 0, - recipient: recipientAddr, - deadline: Math.floor(Date.now() / 1000) + 60 * 10, // 10 minutes from now - }) - - gasLimit = gasLimit.mul(12).div(10) + await estimateAndCall(secondTokenContract, "approve", [managerContract.address, secondTokenAmt]) // Add liquidity to the pool - const liquidityTx = await managerContract.mint({ + await estimateAndCall(managerContract, "mint", [{ token0: token0.address, token1: token1.address, fee: 3000, // Fee tier (0.3%) @@ -84,9 +41,8 @@ async function supplyLiquidity(managerContract, recipientAddr, firstTokenContrac amount1Min: 0, recipient: recipientAddr, deadline: Math.floor(Date.now() / 1000) + 60 * 10, // 10 minutes from now - }, {gasLimit, gasPrice}); + }]); - await liquidityTx.wait(); console.log("Liquidity added"); } @@ -122,6 +78,19 @@ async function deployCw20WithPointer(deployerSeiAddr, signer, time, evmRpc="") { return {"pointerContract": pointerContract, "cw20Address": cw20Address} } +async function deployCw721WithPointer(deployerSeiAddr, signer, time, evmRpc="") { + const CW721_BASE_PATH = (await isDocker()) ? '../integration_test/dapp_tests/nftMarketplace/cw721_base.wasm' : path.resolve(__dirname, '../dapp_tests/nftMarketplace/cw721_base.wasm') + const cw721Address = await deployWasm(CW721_BASE_PATH, deployerSeiAddr, "cw721", { + "name": `testCw721${time}`, + "symbol": "TESTNFT", + "minter": deployerSeiAddr, + "withdraw_address": deployerSeiAddr, + }, deployerSeiAddr); + const pointerAddr = await deployErc721PointerForCw721(hre.ethers.provider, cw721Address, deployerSeiAddr, evmRpc); + const pointerContract = new hre.ethers.Contract(pointerAddr, ABI.ERC721, signer); + return {"pointerContract": pointerContract, "cw721Address": cw721Address} +} + async function deployEthersContract(name, abi, bytecode, deployer, deployParams=[]) { const contract = new hre.ethers.ContractFactory(abi, bytecode, deployer); const deployTx = contract.getDeployTransaction(...deployParams); @@ -141,6 +110,12 @@ async function doesTokenFactoryDenomExist(denom) { } async function sendFunds(amountSei, recipient, signer) { + + const bal = await signer.getBalance(); + if (bal.lt(hre.ethers.utils.parseEther(amountSei))) { + throw new Error(`Signer has insufficient balance. Want ${hre.ethers.utils.parseEther(amountSei)}, has ${bal}`); + } + const gasLimit = await signer.estimateGas({ to: recipient, value: hre.ethers.utils.parseEther(amountSei) @@ -159,6 +134,50 @@ async function sendFunds(amountSei, recipient, signer) { await fundUser.wait(); } +async function estimateAndCall(contract, method, args=[], value=0) { + let gasLimit; + try { + if (value) { + gasLimit = await contract.estimateGas[method](...args, {value: value}); + } else { + gasLimit = await contract.estimateGas[method](...args); + } + } catch (error) { + if (error.data) { + console.error("Transaction revert reason:", hre.ethers.utils.toUtf8String(error.data)); + } else { + console.error("Error fulfilling order:", error); + } + } + const gasPrice = await contract.signer.getGasPrice(); + let output; + if (value) { + output = await contract[method](...args, {gasPrice, gasLimit, value}) + } else { + output = await contract[method](...args, {gasPrice, gasLimit}) + } + await output.wait(); + return output; +} + +const mintCw721 = async (contractAddress, address, id) => { + const msg = { + mint: { + token_id: `${id}`, + owner: `${address}`, + token_uri:"" + }, + }; + const jsonString = JSON.stringify(msg).replace(/"/g, '\\"'); + const command = `seid tx wasm execute ${contractAddress} "${jsonString}" --from=${address} --gas=500000 --gas-prices=0.1usei --broadcast-mode=block -y --output=json`; + const output = await execute(command); + const response = JSON.parse(output); + if (response.code !== 0) { + throw new Error(response.raw_log); + } + return response; +}; + async function pollBalance(erc20Contract, address, criteria, maxAttempts=3) { let bal = 0; let attempt = 1; @@ -355,14 +374,17 @@ module.exports = { harvest, queryTokenBalance, addAccount, + estimateAndCall, addDeployerAccount, setupAccountWithMnemonic, transferTokens, deployTokenPool, supplyLiquidity, deployCw20WithPointer, + deployCw721WithPointer, deployEthersContract, doesTokenFactoryDenomExist, pollBalance, - sendFunds + sendFunds, + mintCw721 }; \ No newline at end of file