Skip to content

Commit

Permalink
feat: xp nfts contract skeleton (#164)
Browse files Browse the repository at this point in the history
* ZetaXP

* add test

* add test

* Update packages/zevm-app-contracts/test/xp-nft/xp-nft.ts

Co-authored-by: Lucas <[email protected]>

* Update packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol

Co-authored-by: Lucas <[email protected]>

* Update packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol

Co-authored-by: Lucas <[email protected]>

* renames

* rename and add logic

* refactor

* add upgradable

* Update packages/zevm-app-contracts/test/xp-nft/xp-nft.ts

Co-authored-by: Lucas <[email protected]>

* renames and test

---------

Co-authored-by: Lucas <[email protected]>
  • Loading branch information
andresaiello and lucas-janon authored Aug 13, 2024
1 parent f541504 commit b0132b4
Show file tree
Hide file tree
Showing 9 changed files with 1,227 additions and 18 deletions.
10 changes: 10 additions & 0 deletions packages/zevm-app-contracts/contracts/xp-nft/test/xpNFTV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "../xpNFT.sol";

contract ZetaXPV2 is ZetaXP {
function version() public pure override returns (string memory) {
return "2.0.0";
}
}
148 changes: 148 additions & 0 deletions packages/zevm-app-contracts/contracts/xp-nft/xpNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract ZetaXP is ERC721Upgradeable, OwnableUpgradeable {
/* An ECDSA signature. */
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}

struct UpdateData {
address to;
uint256 tokenId;
Signature signature;
uint256 sigTimestamp;
uint256 signedUp;
}

mapping(uint256 => uint256) lastUpdateTimestampByTokenId;
mapping(uint256 => uint256) signedUpByTokenId;

// Base URL for NFT images
string public baseTokenURI;
address public signerAddress;

// Event for New Mint
event NFTMinted(address indexed sender, uint256 indexed tokenId);
// Event for NFT Update
event NFTUpdated(address indexed sender, uint256 indexed tokenId);

error InvalidSigner();
error LengthMismatch();
error TransferNotAllowed();
error OutdatedSignature();

function initialize(
string memory name,
string memory symbol,
string memory baseTokenURI_,
address signerAddress_
) public initializer {
__ERC721_init(name, symbol);
__Ownable_init();
baseTokenURI = baseTokenURI_;
signerAddress = signerAddress_;
}

function version() public pure virtual returns (string memory) {
return "1.0.0";
}

// The following functions are overrides required by Solidity.
function tokenURI(uint256 tokenId) public view override(ERC721Upgradeable) returns (string memory) {
_requireMinted(tokenId);

return string(abi.encodePacked(baseTokenURI, _uint2str(tokenId)));
}

function supportsInterface(bytes4 interfaceId) public view override(ERC721Upgradeable) returns (bool) {
return super.supportsInterface(interfaceId);
}

// Helper function to convert uint to string
function _uint2str(uint _i) internal pure returns (string memory _uintAsString) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (uint8(48 + (_i % 10)));
bstr[k] = bytes1(temp);
_i /= 10;
}
return string(bstr);
}

function _verify(UpdateData memory updateData) private view {
bytes32 payloadHash = _calculateHash(updateData);
bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));

address messageSigner = ecrecover(
messageHash,
updateData.signature.v,
updateData.signature.r,
updateData.signature.s
);

if (signerAddress != messageSigner) revert InvalidSigner();
if (updateData.sigTimestamp <= lastUpdateTimestampByTokenId[updateData.tokenId]) revert OutdatedSignature();
}

// Function to compute the hash of the data and tasks for a token
function _calculateHash(UpdateData memory updateData) private pure returns (bytes32) {
bytes memory encodedData = abi.encode(
updateData.to,
updateData.tokenId,
updateData.sigTimestamp,
updateData.signedUp
);

return keccak256(encodedData);
}

function _updateNFT(UpdateData memory updateData) internal {
_verify(updateData);
lastUpdateTimestampByTokenId[updateData.tokenId] = updateData.sigTimestamp;
signedUpByTokenId[updateData.tokenId] = updateData.signedUp;
}

// External mint function
function mintNFT(UpdateData calldata mintData) external {
_mint(mintData.to, mintData.tokenId);

_updateNFT(mintData);

emit NFTMinted(mintData.to, mintData.tokenId);
}

// External mint function
function updateNFT(UpdateData memory updateData) external {
address owner = ownerOf(updateData.tokenId);
updateData.to = owner;
_updateNFT(updateData);

emit NFTUpdated(owner, updateData.tokenId);
}

// Set the base URI for tokens
function setBaseURI(string calldata _uri) external onlyOwner {
baseTokenURI = _uri;
}

function _transfer(address from, address to, uint256 tokenId) internal override {
revert TransferNotAllowed();
}
}
3 changes: 2 additions & 1 deletion packages/zevm-app-contracts/data/addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"zetaSwap": "0xA8168Dc495Ed61E70f5c1941e2860050AB902cEF",
"zetaSwapBtcInbound": "0x358E2cfC0E16444Ba7D3164Bbeeb6bEA7472c559",
"invitationManager": "0x3649C03C472B698213926543456E9c21081e529d",
"withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E"
"withdrawERC20": "0xa349B9367cc54b47CAb8D09A95836AE8b4D1d84E",
"ZetaXP": "0x80ECE9a08ba893e1B863C0c6B3c098268C146E40"
}
}
}
2 changes: 2 additions & 0 deletions packages/zevm-app-contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "@nomicfoundation/hardhat-verify";
import "@nomiclabs/hardhat-waffle";
import "@typechain/hardhat";
import "@openzeppelin/hardhat-upgrades";
import "hardhat-gas-reporter";
import "solidity-coverage";
import "tsconfig-paths/register";
Expand Down Expand Up @@ -57,6 +58,7 @@ const config: HardhatUserConfig = {
{ version: "0.5.10" /** For create2 factory */ },
{ version: "0.6.6" /** For uniswap v2 */ },
{ version: "0.8.7" },
{ version: "0.8.9" },
],
settings: {
/**
Expand Down
2 changes: 2 additions & 0 deletions packages/zevm-app-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
},
"dependencies": {
"@openzeppelin/contracts": "4.9.3",
"@openzeppelin/contracts-upgradeable": "4.9.3",
"@openzeppelin/hardhat-upgrades": "^1.7.0-rc.0",
"@uniswap/v2-periphery": "1.1.0-beta.0",
"@zetachain/networks": "^4.0.0",
"@zetachain/protocol-contracts": "^4.0.1",
Expand Down
32 changes: 32 additions & 0 deletions packages/zevm-app-contracts/scripts/xp-nft/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { isProtocolNetworkName } from "@zetachain/protocol-contracts";
import { ethers, network, upgrades } from "hardhat";

import { ZetaXP__factory } from "../../typechain-types";
import { saveAddress } from "../address.helpers";

const networkName = network.name;

const ZETA_BASE_URL = "https://api.zetachain.io/nft/";
const signer = "0x1d24d94520B94B26351f6573de5ef9731c48531A";

const deployZetaXP = async () => {
if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name");

const ZetaXPFactory = (await ethers.getContractFactory("ZetaXP")) as ZetaXP__factory;
const zetaXP = await upgrades.deployProxy(ZetaXPFactory, ["ZETA NFT", "ZNFT", ZETA_BASE_URL, signer]);

await zetaXP.deployed();

console.log("ZetaXP deployed to:", zetaXP.address);
saveAddress("ZetaXP", zetaXP.address, networkName);
};

const main = async () => {
if (!isProtocolNetworkName(networkName)) throw new Error("Invalid network name");
await deployZetaXP();
};

main().catch((error) => {
console.error(error);
process.exit(1);
});
38 changes: 38 additions & 0 deletions packages/zevm-app-contracts/test/xp-nft/test.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { ethers } from "hardhat";

export interface Signature {
r: string;
s: string;
v: number;
}

export interface NFT {
signedUp: number;
to: string;
tokenId: number;
}

export interface UpdateParam extends NFT {
sigTimestamp: number;
signature: Signature;
}

export const getSignature = async (
signer: SignerWithAddress,
timestamp: number,
to: string,
tokenId: number,
nft: NFT
) => {
let payload = ethers.utils.defaultAbiCoder.encode(
["address", "uint256", "uint256", "uint256"],
[to, tokenId, timestamp, nft.signedUp]
);

const payloadHash = ethers.utils.keccak256(payload);

// This adds the message prefix
const signature = await signer.signMessage(ethers.utils.arrayify(payloadHash));
return ethers.utils.splitSignature(signature);
};
Loading

0 comments on commit b0132b4

Please sign in to comment.