Skip to content

Commit

Permalink
Merge pull request #17 from protokol/feat/nftyhalloween
Browse files Browse the repository at this point in the history
feat: nfty halloween contract implementation with tests, deployment and instructions
  • Loading branch information
kristjank authored Oct 22, 2021
2 parents cdcee3c + 0ec1945 commit 6997ab2
Show file tree
Hide file tree
Showing 12 changed files with 1,023 additions and 488 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ PINATA_API_SECRET='3333'
NFT_CONTRACT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
IPFS_FOLDER_CID='CIDID'
ETHERSCAN_API_KEY='YOUR_API-KEY'

MNEMONIC=some-mnemonic

NFTY_PASS_BASE_URL=www.placeholder.com/

NFTY_HALLOWEEN_BASE_URL=www.placeholder2.com/
NFTY_HALLOWEEN_NFTY_PASS_ADDRESS=0xF78aaEE20f2a464DeAdC6E815D8fea50A9e8Cd52
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,4 @@ lint/outputs/
lint/tmp/
# lint/reports/

node_modules
node_modules
abi
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,92 @@ Check `package.json` scripts for more options.
Use `.env.example` file and adapt it to you values and settings.

Have Fun!

## Nfty Village contract development instructions

To build on top of Nfty Contracts we will have to deploy them. We will do this on
Rinkeby testnet and then we will verify the contract so it will be possible to interact
with it on Etherscan.

The environment file example is name `.env.example`, you can check what variables are
needed there or you can just follow this guide in which I will tell you what to setup.

### Installation

```sh
npm i -g hardhat-shorthand
npm install
hh compile
```

To execute tests
```sh
hh test
```

### General Setup

#### 1. RPC provider
I recommend you to use [Alchemy API](https://www.alchemy.com/)

```dosini
RINKEBY_RPC_URL=https://eth-rinkeby.alchemyapi.io/v2/<API-KEY>
```

#### 2. Mnemonic
You can export you mnemonic from MetaMask

```dosini
MNEMONIC=your-mnemonic-passphrase
```

#### 3. Etherscan API
Setup [Etherscan API](https://etherscan.io/) key so you will be able to verify your contract, this will enable
you to interact with smart contract via Etherscan.

```dosini
ETHERSCAN_API_KEY=<YOUR-KEY>
```

### Deploying Nfty Pass

To deploy Nfty Pass we will set default base uri which the contract will have

```dosini
NFTY_PASS_BASE_URL=www.placeholder.com/
```

#### Deployment

First argument is Contract address that was deployed and the second one is base url
that was used in deployment.
```sh
hh deploy --tags pass --network rinkeby

hh verify <CONTRACT-ADDRESS> "www.placeholder.com/" --network rinkeby
```

The ABI json file which is needed by the GUI to interact with smart contract is located
at `./abi/contracts/NftyPass/NftyPass.json`

### Deploying Nfty Halloween set
To deploy Halloween set we will have set NftyPass Contract Address and base url

```dosini
NFTY_HALLOWEEN_BASE_URL=www.placeholder2.com/
NFTY_HALLOWEEN_NFTY_PASS_ADDRESS=0xbe715eBA71324CE2277144D09aFe678c881B6615
```

First argument is contract address.

Second argument is `NFTY_HALLOWEEN_BASE_URL`

Third argument is `NFTY_HALLOWEEN_NFTY_PASS_ADDRESS`
```sh
hh deploy --tags halloween --network rinkeby

hh verify <CONTRACT-ADDRESS> "www.placeholder2.com/" "0xbe715eBA71324CE2277144D09aFe678c881B6615" --network rinkeby
```

ABI json file is located at `./abi/contracts/NftyHalloween/NftyHalloween.json`

175 changes: 175 additions & 0 deletions contracts/ERC721CustomEnumerable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

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

/**
* @dev This implements an optional extension of {ERC721} defined in the EIP that adds
* enumerability of all the token ids in the contract as well as all token ids owned by each
* account.
*/
abstract contract ERC721CustomEnumerable is ERC721 {
// Mapping from owner to list of owned token IDs
mapping(address => mapping(uint256 => uint256)) private _ownedTokens;

// Mapping from token ID to index of the owner tokens list
mapping(uint256 => uint256) private _ownedTokensIndex;

// Array with all token ids, used for enumeration
uint256[] private _allTokens;

// Mapping from token id to position in the allTokens array
mapping(uint256 => uint256) private _allTokensIndex;

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return
interfaceId == type(ERC721CustomEnumerable).interfaceId ||
super.supportsInterface(interfaceId);
}

/**
* @dev See {IERC721Enumerable-tokenOfOwnerByIndex}.
*/
function tokenOfOwnerByIndex(address owner, uint256 index)
public
view
virtual
returns (uint256)
{
require(
index < ERC721.balanceOf(owner),
"ERC721CustomEnumerable: owner index out of bounds"
);
return _ownedTokens[owner][index];
}

/**
* @dev See {IERC721Enumerable-tokenByIndex}.
*/
function tokenByIndex(uint256 index) public view virtual returns (uint256) {
require(
index < _allTokens.length,
"ERC721CustomEnumerable: global index out of bounds"
);
return _allTokens[index];
}

/**
* @dev Hook that is called before any token transfer. This includes minting
* and burning.
*
* Calling conditions:
*
* - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
* transferred to `to`.
* - When `from` is zero, `tokenId` will be minted for `to`.
* - When `to` is zero, ``from``'s `tokenId` will be burned.
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId);

if (from == address(0)) {
_addTokenToAllTokensEnumeration(tokenId);
} else if (from != to) {
_removeTokenFromOwnerEnumeration(from, tokenId);
}
if (to == address(0)) {
_removeTokenFromAllTokensEnumeration(tokenId);
} else if (to != from) {
_addTokenToOwnerEnumeration(to, tokenId);
}
}

/**
* @dev Private function to add a token to this extension's ownership-tracking data structures.
* @param to address representing the new owner of the given token ID
* @param tokenId uint256 ID of the token to be added to the tokens list of the given address
*/
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
uint256 length = ERC721.balanceOf(to);
_ownedTokens[to][length] = tokenId;
_ownedTokensIndex[tokenId] = length;
}

/**
* @dev Private function to add a token to this extension's token tracking data structures.
* @param tokenId uint256 ID of the token to be added to the tokens list
*/
function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
_allTokensIndex[tokenId] = _allTokens.length;
_allTokens.push(tokenId);
}

/**
* @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
* while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
* gas optimizations e.g. when performing a transfer operation (avoiding double writes).
* This has O(1) time complexity, but alters the order of the _ownedTokens array.
* @param from address representing the previous owner of the given token ID
* @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
*/
function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId)
private
{
// To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
// then delete the last slot (swap and pop).

uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
uint256 tokenIndex = _ownedTokensIndex[tokenId];

// When the token to delete is the last token, the swap operation is unnecessary
if (tokenIndex != lastTokenIndex) {
uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

_ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
_ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
}

// This also deletes the contents at the last position of the array
delete _ownedTokensIndex[tokenId];
delete _ownedTokens[from][lastTokenIndex];
}

/**
* @dev Private function to remove a token from this extension's token tracking data structures.
* This has O(1) time complexity, but alters the order of the _allTokens array.
* @param tokenId uint256 ID of the token to be removed from the tokens list
*/
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
// To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
// then delete the last slot (swap and pop).

uint256 lastTokenIndex = _allTokens.length - 1;
uint256 tokenIndex = _allTokensIndex[tokenId];

// When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
// rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
// an 'if' statement (like in _removeTokenFromOwnerEnumeration)
uint256 lastTokenId = _allTokens[lastTokenIndex];

_allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
_allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

// This also deletes the contents at the last position of the array
delete _allTokensIndex[tokenId];
_allTokens.pop();
}
}
82 changes: 82 additions & 0 deletions contracts/NftyHalloween.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@1001-digital/erc721-extensions/contracts/RandomlyAssigned.sol";

import "./ERC721CustomEnumerable.sol";

contract NftyHalloween is
ERC721,
Pausable,
Ownable,
RandomlyAssigned,
ERC721CustomEnumerable
{
uint256 public constant MAX_TOKENS = 9000;
string private nftyBaseURI = "";
IERC721 public nftyPass;

mapping(uint256 => address) private claimed;

constructor(
string memory _nftyBaseURI,
address _nftyPass
) ERC721("NftyHalloween", "NFTYH")
RandomlyAssigned(MAX_TOKENS, 0)
{
nftyBaseURI = _nftyBaseURI;
nftyPass = IERC721(_nftyPass);
}

function mint(uint256 pass) external whenNotPaused {
require(nftyPass.ownerOf(pass) == msg.sender, "Pass not owned by sender");
require(claimed[pass] == address(0), "Pass already used");

claimed[pass] = msg.sender;
uint256 next = nextToken();
_safeMint(msg.sender, next);
}

function setBaseURI(string memory baseURI) external onlyOwner {
nftyBaseURI = baseURI;
}

function pause() external onlyOwner {
_pause();
}

function unpause() external onlyOwner {
_unpause();
}

function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal override(ERC721, ERC721CustomEnumerable) {
super._beforeTokenTransfer(from, to, tokenId);
}

function claimedPass(uint256 pass) public view returns (address) {
address claimedAddress = claimed[pass];
require(claimedAddress != address(0), "Pass not claimed");

return claimedAddress;
}

function _baseURI() internal view virtual override returns (string memory) {
return nftyBaseURI;
}

function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721CustomEnumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
1 change: 0 additions & 1 deletion contracts/NftyPass.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
Expand Down
6 changes: 4 additions & 2 deletions deploy/01_Deploy.ts → deploy/01_Deploy_Pass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
accounts[0]
)) as NftyPass__factory;

nftTokenContract = await tokenFactory.deploy("www.placeholder.com/");
nftTokenContract = await tokenFactory.deploy(
process.env.NFTY_PASS_BASE_URL || "www.placeholder.com/"
);

console.log(
`The address the Contract WILL have once mined: ${nftTokenContract.address}`
Expand All @@ -37,4 +39,4 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
};
export default func;
func.id = "deploy";
func.tags = ["local"];
func.tags = ["pass"];
Loading

0 comments on commit 6997ab2

Please sign in to comment.