Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Hyperboard contract #1068

Open
wants to merge 32 commits into
base: develop-contracts
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f6d8d84
feat(db): update allowlist query (#1057)
bitbeckers Aug 29, 2023
04cf3b2
wip: NFT contract
Abhimanyu121 Sep 13, 2023
d48d5f3
WIP: NFT contract - Fixed counter errors
Abhimanyu121 Sep 13, 2023
d8a918c
forge install: safe-contracts
Abhimanyu121 Sep 13, 2023
19594ed
WIP: Addeding 6551 support
Abhimanyu121 Sep 14, 2023
9a6d8f6
WIP: test case helpers
Abhimanyu121 Sep 16, 2023
5dbf643
forge install: openzeppelin-contracts
Abhimanyu121 Sep 16, 2023
fcb2cd2
WIP: test contract
Abhimanyu121 Sep 16, 2023
a1a20c6
Initial draft for Hyperboard NFT
Abhimanyu121 Sep 17, 2023
674bee8
Merge branch 'main' into develop
Abhimanyu121 Sep 17, 2023
cd81b8e
Added events and added way to updated base uri
Abhimanyu121 Sep 18, 2023
cca0911
Updated hyperboard contract with the new logic
Abhimanyu121 Sep 24, 2023
87b7827
Updated comments
Abhimanyu121 Sep 24, 2023
5fc4cb0
removed unused import
Abhimanyu121 Sep 24, 2023
300f3a0
WIP:Wallet impl
Abhimanyu121 Sep 24, 2023
0f19644
forge install: safe-contracts
Abhimanyu121 Sep 24, 2023
6f3e95e
WIP:wallet IMPL
Abhimanyu121 Sep 24, 2023
a83a7cc
WIP:Wallet IMPL
Abhimanyu121 Sep 24, 2023
2b5ed01
forge install: safe-contracts
Abhimanyu121 Sep 24, 2023
ce9db2a
WIP: wallet impl
Abhimanyu121 Sep 24, 2023
dce74cd
WIP: wallet impl
Abhimanyu121 Sep 25, 2023
d236b97
Added subgraph for tracking hyperboard
Abhimanyu121 Sep 26, 2023
a08e0a2
Updated hyperboards contract and fixed testcases
Abhimanyu121 Oct 7, 2023
a50b90b
Added deploy scripts and code cleanup
Abhimanyu121 Oct 8, 2023
d25368b
removed unused safe contracts
Abhimanyu121 Oct 8, 2023
29a2eaa
removed test logs
Abhimanyu121 Oct 8, 2023
433233d
fix(graph): update matchstick dependency version
bitbeckers Oct 10, 2023
35b33f3
Minor fixes and corrections
Abhimanyu121 Oct 15, 2023
8670086
chore: update hyperboard deployment script
0xartem Oct 15, 2023
b79636b
Merge pull request #1 from 0xartem/develop
Abhimanyu121 Oct 16, 2023
afe36de
Updated start block and config
Abhimanyu121 Oct 16, 2023
3c19045
Merge branch 'develop' of https://github.com/Abhimanyu121/hypercerts …
Abhimanyu121 Oct 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .gitmodules
Empty file.
3 changes: 3 additions & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ test = "test/foundry"
[profile.ci]
fuzz = { runs = 1024 }
verbosity = 1

[rpc_endpoints]
goerli = "https://rpc.ankr.com/eth_goerli"
1 change: 1 addition & 0 deletions contracts/remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ murky/=lib/murky/src/
oz-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
oz-contracts/=lib/murky/lib/openzeppelin-contracts/
prb-test/=lib/prb-test/src/
safe-contracts/=lib/safe-contracts/contracts
272 changes: 272 additions & 0 deletions contracts/src/hyperboards/HyperboardNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import { IERC20 } from "oz-contracts/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "oz-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import { ERC721 } from "oz-contracts/contracts/token/ERC721/ERC721.sol";
import { ERC721URIStorage } from "oz-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import { ERC721Enumerable } from "oz-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

import { HypercertMinter } from "../HypercertMinter.sol";

import { EIP712 } from "oz-contracts/contracts/utils/cryptography/draft-EIP712.sol";
import { ECDSA } from "oz-contracts/contracts/utils/cryptography/ECDSA.sol";

import { Strings } from "oz-contracts/contracts/utils/Strings.sol";

import { Ownable } from "oz-contracts/contracts/access/Ownable.sol";
import { CountersUpgradeable } from "oz-upgradeable/utils/CountersUpgradeable.sol";

import { Errors } from "../libs/Errors.sol";

/// @title A hyperboard NFT
/// @author Abhimanyu Shekhawat
/// @notice This is an NFT for representing various hyperboards
contract Hyperboard is ERC721, ERC721URIStorage, ERC721Enumerable, Ownable, EIP712 {
using SafeERC20 for IERC20;
string public subgraphEndpoint;
Copy link
Contributor

@bitbeckers bitbeckers Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is used for the dynamic NFT we specced out, correct? I think this setup is tricky because a change in the subgraph endpoints would result in a breaking change. To compare, an ipfs:// URL can be mapped to a gateway, but subgraphs don't follow these kind of standards I believe.

One solution could be a central registry for the IPFS uri so that the rendered page and the subgraph URL can be governed on a protocol level.

nvm, the contract is the central registry. Still, maybe we can find a way around something like the Graph.

What do you think?

P.S. this is something beyond PoC scope, but we should be aware of the implications.

string public baseUri;
HypercertMinter public hypercertMinter;

mapping(uint256 => uint256[]) private _allowListedCertsMapping;
mapping(uint256 => uint256[]) public consentBasedCertsMapping;

using CountersUpgradeable for CountersUpgradeable.Counter;
CountersUpgradeable.Counter private _counter;

/// @notice Emitted when new token is minted
/// @param to Address thats recieving NFT.
/// @param tokenId tokenId of the new mint.
/// @param metadata of hyperboard.
event Mint(address indexed to, uint256 indexed tokenId, string metadata);

/// @notice Event is emitted when contract gets consent from a hypercert.
/// @param tokenId Token Id of hyperboard.
/// @param claimId ClaimId of a hypercert.
/// @param owner owner of hypercert.
event GotConsent(uint256 indexed tokenId, uint256 indexed claimId, address owner);

/// @notice Emitted when ERC20 tokens are withdrawn.
/// @param to address where tokens were sent to.
/// @param tokenAddress Address of token withdrawn.
/// @param amount amount of tokems withdrawn.
event WithdrawToken(address indexed to, address indexed tokenAddress, uint256 indexed amount);

/// @notice Emitted when subgraph endpoint is updated.
/// @param endpoint updated Subgraph endpoint.
event SubgraphUpdated(string endpoint);

/// @notice Emitted when base uri is updated.
/// @param baseUri Updated baseuri.
event BaseUriUpdated(string baseUri);

/// @notice Emitted when hypercert address is updated
/// @param hypercertMinter semifungible token.
event HypercertMinterUpdated(HypercertMinter indexed hypercertMinter);

/// @param name_ name of NFT.
/// @param hypercertMinter_ hypercertToken address
/// @param symbol_ NFT symbol
/// @param subgraphEndpoint_ updateable subgraph endpoint
/// @param baseUri_ base ipfs uri for NFT page.
constructor(
HypercertMinter hypercertMinter_,
string memory name_,
string memory symbol_,
string memory subgraphEndpoint_,
string memory baseUri_,
string memory version
) ERC721(name_, symbol_) EIP712(name_, version) {
if (address(hypercertMinter_) == address(0)) revert Errors.ZeroAddress();
hypercertMinter = hypercertMinter_;
emit HypercertMinterUpdated(hypercertMinter);

subgraphEndpoint = subgraphEndpoint_;
emit SubgraphUpdated(subgraphEndpoint);

baseUri = baseUri_;
emit BaseUriUpdated(baseUri);
}

/// @notice Mints a new Hyperboard.
/// @param to The address to which the Hyperboard NFT will be minted.
/// @param allowlistedClaimIds_ Claim IDs corresponding to allowlisted certificates.
/// @return tokenId The ID of the minted NFT.
function mint(
address to,
uint256[] memory allowlistedClaimIds_,
string memory metadata_
) external returns (uint256 tokenId) {
if (to == address(0)) revert Errors.ZeroAddress();

tokenId = _counter.current();
_mint(to, tokenId);
_setTokenURI(tokenId, metadata_);
_setAllowlist(tokenId, allowlistedClaimIds_);

_counter.increment();
emit Mint(to, tokenId, metadata_);

return tokenId;
}

/// @notice gives consent from a hypercert to be part of Hyperboard
/// @param tokenId_ tokenId of a hyperboard.
/// @param claimId_ ClaimId of a hypercert.
function consentForHyperboard(uint256 tokenId_, uint256 claimId_) external {
Abhimanyu121 marked this conversation as resolved.
Show resolved Hide resolved
_consentForHyperboard(tokenId_, claimId_, msg.sender);
}

/// @notice gives consent from a hypercert to be part of Hyperboard using signatures of the owner
/// @param tokenId_ tokenId of a hyperboard.
/// @param claimId_ ClaimId of a hypercert.
/// @param signer Address of owner of hypercert.
/// @param r r of signature of digest for consent.
/// @param s s of signature of digest for consent.
/// @param v v of signature of digest for consent.
function consentForHyperboardWithSignature(
uint256 tokenId_,
uint256 claimId_,
address signer,
bytes32 r,
bytes32 s,
uint8 v
) external {
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(keccak256("ConsentForHyperBoard(uint256 tokenId_,uint256 claimId_)"), tokenId_, claimId_)
)
);
address recoveredSigner = ECDSA.recover(digest, v, r, s);
if (recoveredSigner != signer) revert Errors.InvalidSigner();
_consentForHyperboard(tokenId_, claimId_, signer);
}

/// @notice gives digest that needs to be signed by owner to give consenst using signature.
/// @param tokenId_ tokenId of a hyperboard.
/// @param claimId_ ClaimId of a hypercert.
function getDigestForConsent(uint256 tokenId_, uint256 claimId_) external returns (bytes32) {
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(keccak256("ConsentForHyperBoard(uint256 tokenId_,uint256 claimId_)"), tokenId_, claimId_)
)
);
return digest;
}

/// @notice internal function that actually gives consent.
/// @param tokenId_ tokenId of a hyperboard.
/// @param claimId_ ClaimId of a hypercert.
/// @param owner_ address of owner of hypercert.
function _consentForHyperboard(uint256 tokenId_, uint256 claimId_, address owner_) internal {
if (hypercertMinter.ownerOf(claimId_) != owner_) revert Errors.NotApprovedOrOwner();
consentBasedCertsMapping[tokenId_].push(claimId_);
emit GotConsent(tokenId_, claimId_, msg.sender);
}

/// @notice Updates the allowlisted certificates for an Hyperboard NFT.
/// @param tokenId_ The ID of the NFT.
/// @param allowlistedClaimIds_ Updated claim IDs corresponding to allowlisted certificates.
/// @param metadata_ Updated metadata, to reflect new claimIds in the board.
function updateHyperboad(
uint256 tokenId_,
uint256[] memory allowlistedClaimIds_,
string memory metadata_
) external {
if (ownerOf(tokenId_) != msg.sender) revert Errors.NotApprovedOrOwner();
_setTokenURI(tokenId_, metadata_);
_setAllowlist(tokenId_, allowlistedClaimIds_);
}

/// @notice Gets the allowlisted certificates for an NFT.
/// @param tokenId The ID of the NFT.
/// @return allowlistedCerts The array of allowlisted certificate addresses.
function getAllowListedCerts(uint256 tokenId) external view returns (uint256[] memory) {
return _allowListedCertsMapping[tokenId];
}

/// @dev Get URI of token, i.e. URL of NFT webpage
/// @param tokenId id of the token to get URI for.
function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return
string.concat(
baseUri,
"?tokenId=",
Strings.toString(tokenId),
"&subgraph=",
subgraphEndpoint,
"&tokenURI=",
ERC721URIStorage.tokenURI(tokenId)
);
}

/// @dev checks if this contract supports specific interface.
/// @param interfaceId interface ID that you want to check the contract against.
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
return super.supportsInterface(interfaceId);
}

/// @notice Sets the subgraph endpoint for the hyperboards.
/// @param endpoint_ The new subgraph endpoint.
function setSubgraphEndpoint(string memory endpoint_) external onlyOwner {
subgraphEndpoint = endpoint_;
emit SubgraphUpdated(subgraphEndpoint);
}

/// @notice update the base URI
/// @param baseUri_ The new 6551 registry address.
function setBaseUri(string memory baseUri_) external onlyOwner {
baseUri = baseUri_;
emit BaseUriUpdated(baseUri);
}

/// @notice update the hypercert contract
/// @param hypercertMinter_ The new minter address.
function setHypercertContract(HypercertMinter hypercertMinter_) external onlyOwner {
hypercertMinter_ = hypercertMinter;
emit HypercertMinterUpdated(hypercertMinter_);
}

/// @notice Withdraws accidentally transferred ERC20 tokens from the contract to a specified account.
/// @param token The ERC20 token contract address.
/// @param amount The amount of tokens to withdraw.
/// @param account The recipient account address.
function withdrawErc20(IERC20 token, uint256 amount, address account) external onlyOwner {
token.safeTransfer(account, amount);
emit WithdrawToken(account, address(token), amount);
}

/// @notice Withdraws accidentally transferred Ether from the contract to a specified account.
/// @param amount The amount of Ether to withdraw.
/// @param account The recipient account address.
function withdrawEther(uint256 amount, address payable account) external onlyOwner {
(bool sent, ) = account.call{ value: amount }("");
if (!sent) revert Errors.FailedToSendToken();
emit WithdrawToken(account, address(0), amount);
}

/// @dev Sets the allowlisted certificates and their corresponding claim IDs for an NFT.
/// @param tokenId The ID of the NFT.
/// @param allowlistedClaimIds_ Claim IDs corresponding to allowlisted certificates.
/// @dev This function is used internally to set the allowlist for a specific NFT.
function _setAllowlist(uint256 tokenId, uint256[] memory allowlistedClaimIds_) internal {
_allowListedCertsMapping[tokenId] = allowlistedClaimIds_;
}

/// @dev any condtion can be put into this to be checked before transefering tokens.
/// @param from which accounts token needs to be tranferred.
/// @param to who will be the recipient of token.
/// @param tokenId token Id of token being transferred.
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal override(ERC721, ERC721Enumerable) {
super._beforeTokenTransfer(from, to, tokenId);
}

/// @dev internal function to burn the token
/// @param tokenId Id of token that needs to be burned.
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
}
3 changes: 3 additions & 0 deletions contracts/src/libs/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ library Errors {
error NotApprovedOrOwner();
error TransfersNotAllowed();
error TypeMismatch();
error ZeroAddress();
error FailedToSendToken();
error InvalidSigner();
}
55 changes: 55 additions & 0 deletions contracts/tasks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,58 @@ task("deploy-trader", "Deploy HypercertTrader and verify")
}
}
});

task("deploy-hyperboard", "Deploy Hyperboard and verify")
.addParam("hypercerts", "Hypercerts smart contract address")
.addParam("name", "Hyperboards NFT name", "Hyperboard")
.addParam("symbol", "Hyperboards NFT symbol", "HBRD")
.addParam("subgraph", "Subgraph endpoint")
.addParam("baseUri", "Base NFT metadata URI")
.addOptionalParam("output", "write the details of the deployment to this file if this is set")
.setAction(async ({ hypercerts, name, symbol, subgraph, baseUri, output }, { ethers, upgrades }) => {
const version = "1";
const hyperboard = await ethers.deployContract("Hyperboard", [
hypercerts,
name,
symbol,
subgraph,
baseUri,
version,
]);
const contract = await hyperboard.deployed();
console.log(`hyperboard is deployed to address: ${hyperboard.address}`);

// If the `deploymentFile` option is set then write the deployed address to
// a json object on disk. This is intended to be deliberate with how we
// output the contract address and other contract information.
if (output) {
const txReceipt = await contract.provider.getTransactionReceipt(hyperboard.deployTransaction.hash);
await writeFile(
output,
JSON.stringify({
address: hyperboard.address,
blockNumber: txReceipt.blockNumber,
}),
"utf-8",
);
}

if (hre.network.name !== "hardhat" && hre.network.name !== "localhost") {
try {
const code = await hyperboard.instance?.provider.getCode(hyperboard.address);
if (code === "0x") {
console.log(`${hyperboard.name} contract deployment has not completed. waiting to verify...`);
await hyperboard.instance?.deployed();
}
await hre.run("verify:verify", {
address: hyperboard.address,
constructorArguments: [hypercerts, name, symbol, subgraph, baseUri, version],
});
} catch ({ message }) {
if ((message as string).includes("Reason: Already Verified")) {
console.log("Reason: Already Verified");
}
console.error(message);
}
}
});
Loading