Skip to content

Commit

Permalink
Merge branch 'main' into feat/the-graph
Browse files Browse the repository at this point in the history
  • Loading branch information
idea404 authored Sep 15, 2023
2 parents d89d0e4 + 6b286be commit 0de78a4
Show file tree
Hide file tree
Showing 35 changed files with 14,097 additions and 0 deletions.
3 changes: 3 additions & 0 deletions cspell-zksync.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,6 @@ ISTN
Zerion
pepe
PEPE
Arweave
Streamr
TLDR

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WALLET_PRIVATE_KEY=
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 RedStone Finance

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Example dApp - Stable price NFT marketplace

This repo is designed to show how to build a dApp that uses [RedStone oracles](https://redstone.finance/) on [zkSync](https://zksync.io/).

The repo contains an implementation of an NFT marketplace dApp with so-called "stable" price. It means that sellers can create sell orders (offers), specifying the price amount in USD. But buyers are able to pay with native coins, the required amount of which is calculated dynamically at the moment of the order execution. Repo lacks few crucial parts which will demonstrate how to integrate RedStone oracles and deploy dApp on zkSync Era Testnet.

## 🧑‍💻 Implementation

We use [hardhat](https://hardhat.org/), version prepared for working on [zkSync](https://github.com/matter-labs/hardhat-zksync), and [ethers.js](https://docs.ethers.io/v5/) for deployment scripts and contract tests. Frontend is implemented in [React](https://reactjs.org/).

### Code structure

```bash
├── contracts # Solidity contracts
│ ├── ExampleNFT.sol # Example ERC721 contract
│ ├── Marketplace.sol # Simple NFT marketplace contract
│ ├── StableMarketplace.sol # NFT marketplace contract with stable price
│ └── ...
├── public # Folder with public html files and images for React app
├── deploy # Contract deployment script
├── src # React app source code
│ ├── components
│ │ ├── App.tsx # Main React component
│ ├── core
│ │ ├── blockchain.ts # TS module responsible for interaction with blockchain and contracts
│ ├── config/ # Folder with contract ABIs and deployed contract addresses
│ └── ...
├── test # Contract tests
└── ...
```

### Contracts

#### ExampleNFT.sol

`ExampleNFT` is a simple ERC721 contract with automated sequential token id assignment

```js
function mint() external {
_mint(msg.sender, nextTokenId);
nextTokenId++;
}
```

This contract extends `ERC721Enumerable` implementation created by the `@openzeppelin` team, which adds view functions for listing all tokens and tokens owned by a user.

#### Marketplace.sol

`Marketplace` is an NFT marketplace contract, which allows to post sell orders for any NFT token that follows [EIP-721 non-fungible token standard](https://eips.ethereum.org/EIPS/eip-721). It has the following functions:

```js

// Created a new sell order
// This function requires approval for transfer on the specified NFT token
function postSellOrder(address nftContractAddress, uint256 tokenId, uint256 price) external {}

// Only order creator can call this function
function cancelOrder(uint256 orderId) external {}

// Allows to get info about all orders (including canceled, and executed ones)
function getAllOrders() public view returns (SellOrder[] memory) {}

// Returns expected price in ETH for the given order
function getPrice(uint256 orderId) public view returns (uint256) {}

// Requires sending at least the minimal amount of ETH
function buy(uint256 orderId) external payable {}

```

The implementation is quite straightforward, so we won't describe it here. You can check the full contract code in the [contracts/Marketplace.sol.](contracts/Marketplace.sol)

#### StableMarketplace.sol

`StableMarketplace` is the marketplace contract with the stable price support. It extends the `Marketplace.sol` implementation and only overrides its `_getPriceFromOrder` function.
This contract will integrate RedStone oracles functionalities and will be described later.

### Frontend

You can check the code of the React app in the `src` folder. We tried to simplify it as much as possible and leave only the core marketplace functions.

The main UI logic is located in the `App.tsx` file, and the contract interaction logic is in the `blockchain.ts` file.

If you take a look into the `blockchain.ts` file code, you'll notice that each contract call that needs to process RedStone data is made on a contract instance, that was wrapped by [@redstone-finance/evm-connector](https://www.npmjs.com/package/@redstone-finance/evm-connector).

### Tests

We've used hardhat test framework to contract tests. All the tests are located in the [test](test/) folder.

## 🌎 Useful links

- [Repo with examples](https://github.com/redstone-finance/redstone-evm-examples)
- [RedStone Documentation](https://docs.redstone.finance/)
- [RedStone Price Feeds](https://docs.redstone.finance/docs/smart-contract-devs/price-feeds)
- [Data from any URL](https://docs.redstone.finance/docs/smart-contract-devs/custom-urls)
- [NFT Data Feeds](https://docs.redstone.finance/docs/smart-contract-devs/nft-data-feeds)
- [Randomness](https://docs.redstone.finance/docs/smart-contract-devs/randomness)

## 🙋‍♂️ Need help?

Please feel free to contact the RedStone team [on Discord](https://redstone.finance/discord) if you have any questions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract ExampleNFT is ERC721Enumerable {
uint256 private nextTokenId = 1;

constructor() ERC721("ExampleNFT", "ENFT") {}

function mint() external {
_mint(msg.sender, nextTokenId);
nextTokenId++;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

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

contract Marketplace {
enum OrderStatus {
ACTIVE,
CANCELED,
EXECUTED
}

struct SellOrder {
address nftContractAddress;
uint256 tokenId;
address creator;
uint256 price;
OrderStatus status;
}

SellOrder[] private sellOrders;

function postSellOrder(
address nftContractAddress,
uint256 tokenId,
uint256 price
) external {
// Check if tokenId is owned by tx sender
IERC721 nftContract = IERC721(nftContractAddress);
require(nftContract.ownerOf(tokenId) == msg.sender);

// Transfer NFT token to the contract address
// Sender needs to approve the transfer before posting sell order
nftContract.transferFrom(msg.sender, address(this), tokenId);

// Save order in the sellOrders mapping
sellOrders.push(
SellOrder(
nftContractAddress,
tokenId,
msg.sender,
price,
OrderStatus.ACTIVE
)
);
}

function cancelOrder(uint256 orderId) external {
SellOrder storage order = sellOrders[orderId];

// Only order creator can cancel the order
require(order.creator == msg.sender);

// Transfer NFT back to order creator
IERC721 nftContract = IERC721(order.nftContractAddress);
nftContract.transferFrom(address(this), msg.sender, order.tokenId);

// Update order status
order.status = OrderStatus.CANCELED;
}

function buy(uint256 orderId) external payable {
// Order must exist and be in the active state
SellOrder storage order = sellOrders[orderId];
require(order.status == OrderStatus.ACTIVE);

// Check transfered ETH value
uint256 expectedEthAmount = _getPriceFromOrder(order);
require(expectedEthAmount <= msg.value);

// Transfer NFT to buyer
IERC721 nftContract = IERC721(order.nftContractAddress);
nftContract.transferFrom(address(this), msg.sender, order.tokenId);

// Mark order as executed
order.status = OrderStatus.EXECUTED;
}

function _getPriceFromOrder(
SellOrder memory order
) internal view virtual returns (uint256) {
return order.price;
}

// Getters for the UI
function getPrice(uint256 orderId) public view returns (uint256) {
SellOrder storage order = sellOrders[orderId];
return _getPriceFromOrder(order);
}

function getAllOrders() public view returns (SellOrder[] memory) {
return sellOrders;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./Marketplace.sol";

/*
StableMarketplace contract should extend MainDemoConsumerBase contract
For being able to use redstone oracles data, more inf:
https://docs.redstone.finance/docs/smart-contract-devs/get-started/redstone-core#1-adjust-your-smart-contracts
*/
contract StableMarketplace is Marketplace {
/*
`_getPriceFromOrder` function should uses the `getOracleNumericValueFromTxMsg` function,
which fetches signed data from tx calldata and verifies its signature
*/
function _getPriceFromOrder(
SellOrder memory order
) internal view override returns (uint256) {
// TO IMPLEMENT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import hre from "hardhat";
import fs from "fs";
import { ethers } from "ethers";
import { Wallet } from "zksync-web3";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import dotenv from "dotenv";

dotenv.config();

// load wallet private key from env file
const PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY || "";

if (!PRIVATE_KEY)
throw "⛔️ Private key not detected! Add it to the .env file!";


export default async function (hre: HardhatRuntimeEnvironment) {
const wallet = new Wallet(PRIVATE_KEY);
const deployer = new Deployer(hre, wallet);

const nftContract = await deployContract("ExampleNFT", deployer);
const marketplaceContract = await deployContract("StableMarketplace", deployer);

// Update JSON file with addresses
updateAddressesFile({
nft: nftContract.address,
marketplace: marketplaceContract.address,
});
}

export const deployContract = async (contractName: string, deployer: Deployer) => {
console.log(`Running deploy script for the PriceChecker contract`);

// const artifact = await deployer.loadArtifact("PriceChecker");
const artifact = await deployer.loadArtifact(contractName);

// Estimate contract deployment fee
const deploymentFee = await deployer.estimateDeployFee(artifact, []);

// ⚠️ OPTIONAL: You can skip this block if your account already has funds in L2
// Deposit funds to L2
// const depositHandle = await deployer.zkWallet.deposit({
// to: deployer.zkWallet.address,
// token: utils.ETH_ADDRESS,
// amount: deploymentFee.mul(2),
// });
// // Wait until the deposit is processed on zkSync
// await depositHandle.wait();

// Deploy this contract. The returned object will be of a `Contract` type, similarly to ones in `ethers`.
// `greeting` is an argument for contract constructor.
const parsedFee = ethers.utils.formatEther(deploymentFee.toString());
console.log(`The deployment is estimated to cost ${parsedFee} ETH`);

const contract = await deployer.deploy(artifact);

// Show the contract info.
const contractAddress = contract.address;
console.log(`${artifact.contractName} was deployed to ${contractAddress}`);
return contract;
}

function updateAddressesFile(addresses: { nft: string; marketplace: string }) {
const addressesFilePath = `./src/config/${hre.network.name}-addresses.json`;
console.log(`Saving addresses to ${addressesFilePath}`);
fs.writeFileSync(
addressesFilePath,
JSON.stringify(addresses, null, 2) + "\n"
);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HardhatUserConfig } from "hardhat/config";

import "@matterlabs/hardhat-zksync-deploy";
import "@matterlabs/hardhat-zksync-solc";
import "@matterlabs/hardhat-zksync-verify";
import "@matterlabs/hardhat-zksync-chai-matchers"

// dynamically changes endpoints for local tests
const zkSyncTestnet =
process.env.NODE_ENV == "test"
? {
url: "http://localhost:3050",
ethNetwork: "http://localhost:8545",
zksync: true,
}
: {
url: "https://zksync2-testnet.zksync.dev",
ethNetwork: "goerli",
zksync: true,
// contract verification endpoint
verifyURL:
"https://zksync2-testnet-explorer.zksync.dev/contract_verification",
};

const config: HardhatUserConfig = {
zksolc: {
version: "latest",
settings: {},
},
defaultNetwork: "zkSyncTestnet",
networks: {
hardhat: {
zksync: false,
},
zkSyncTestnet,
},
solidity: {
version: "0.8.17",
},
};

export default config;
Loading

0 comments on commit 0de78a4

Please sign in to comment.