-
-
Save tolicodes/995b6eb5f6820d8928277411f9bef6b4 to your computer and use it in GitHub Desktop.
OpenGSN
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//SPDX-License-Identifier: Unlicense | |
pragma solidity ^0.8.0; | |
import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; | |
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | |
import "@openzeppelin/contracts/token/common/ERC2981.sol"; | |
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; | |
import "@openzeppelin/contracts/access/Ownable.sol"; | |
import "@opengsn/contracts/src/BaseRelayRecipient.sol"; | |
// This is done so that OpenSea can cover Matic gas costs: https://docs.opensea.io/docs/polygon-basic-integration | |
// we need to figure out how to do this ourselves for the minting step | |
// https://docs.openzeppelin.com/learn/sending-gasless-transactions#what-is-a-meta-tx | |
import "./MetaTransactions.sol"; | |
// We are using OpenGSN to do the transactions (gasless to the customer) | |
contract Collection is Ownable, IERC721, ERC721Enumerable, ERC2981, BaseRelayRecipient, ContextMixin, NativeMetaTransaction { | |
// string functionality | |
using Strings for uint256; | |
using Strings for uint32; | |
event ClaimIdWhitelisted( | |
string claimId, | |
address toAddress, | |
uint32 amount | |
); | |
event ClaimIdMinted( | |
string claimId, | |
address toAddress, | |
uint32 amount | |
); | |
uint256 private _nextTokenId = 0; | |
uint32 private _whitelistedTotal = 0; | |
string private _btURI; | |
// URI for collection metadata | |
string private _cURI; | |
// tracking whitelisting | |
mapping(string => uint32) whitelistedClaimIds; // track whitelist amount for claim | |
mapping(address => uint32) whitelistPerAddress; // track whitelisted amount per address | |
mapping(address => mapping(string => bool)) whitelistedAddressToClaimIdUsed; // ensures no dupes | |
// tracking minting | |
mapping(string => uint32) mintedClaimIds; | |
mapping(address => uint32) mintPerAddress; | |
// 0 means collection is NOT revealed, other numbers meen it is | |
uint256 public startingIndex = 0; | |
// the version of the recepient (the collection contract) | |
// should we same version number as Collection#versionPaymaster | |
string public override versionRecipient = "2.2.3+irelayrecipient"; | |
// max amount of token | |
uint32 public immutable supplyCap; | |
// proves that metadata is valid (ex: like an MD5 hash of the metadata json) | |
string public metadataProofHash; | |
constructor( | |
// Name of collection (ex: Toli Collection) | |
string memory _name, | |
// Max amount of that token (ex: 10000) | |
uint32 _supplyCap, | |
// proves that metadata is valid (ex: like an MD5 hash of the metadata json) | |
string memory _metadataProofHash, | |
// URI for collection metadata (ex: https://xyz.com/metadata/x102129) | |
string memory __cURI, | |
// GSN Forwarder address for gasless txns | |
address _forwarder, | |
// Royalty recipient address | |
address _initialRoyaltyRecipient, | |
// Royalty percentage | |
uint96 _initialFraction | |
) | |
ERC721(_name, "") | |
{ | |
supplyCap = _supplyCap; | |
metadataProofHash = _metadataProofHash; | |
_cURI = __cURI; | |
// | |
// Royalties | |
// | |
if (_initialFraction > 0) { | |
_setDefaultRoyalty(_initialRoyaltyRecipient, _initialFraction); | |
} | |
// | |
// OpenGSN | |
// | |
_setTrustedForwarder(_forwarder); | |
} | |
function contractURI() public view returns (string memory) { | |
return string(abi.encodePacked(_cURI, ".json")); | |
} | |
function whitelistClaimAmount( | |
address[] memory customerAddressesArr, | |
string memory claimId, | |
uint32 amount | |
) external onlyOwner { | |
if (whitelistedClaimIds[claimId] != 0) { | |
require( | |
whitelistedClaimIds[claimId] == amount, | |
string( | |
abi.encodePacked( | |
"collection already whitelisted for ", | |
whitelistedClaimIds[claimId].toString(), | |
" attempted amount of ", | |
amount.toString(), | |
" does not match" | |
) | |
) | |
); | |
} else { | |
require( | |
_whitelistedTotal + amount <= supplyCap, | |
"collection minting cap reached, the amount requested for whitelist would exceed the cap" | |
); | |
whitelistedClaimIds[claimId] = amount; | |
_whitelistedTotal = _whitelistedTotal + amount; | |
} | |
for (uint i = 0; i < customerAddressesArr.length; i++) { | |
address customerAddress = customerAddressesArr[i]; | |
// only whitelist for this address if claim not already whitelisted for this address | |
if (!whitelistedAddressToClaimIdUsed[customerAddress][claimId]) { | |
whitelistedAddressToClaimIdUsed[customerAddress][claimId] = true; | |
emit ClaimIdWhitelisted(claimId, customerAddress, amount); | |
whitelistPerAddress[customerAddress] += amount; | |
} | |
} | |
} | |
function whitelistedClaimIdAmount(string memory claimId) | |
public | |
view | |
returns (uint32) | |
{ | |
return whitelistedClaimIds[claimId]; | |
} | |
function mintedClaimIdAmount(string memory claimId) | |
public | |
view | |
returns (uint32) | |
{ | |
return mintedClaimIds[claimId]; | |
} | |
function whitelistedAmount(address customerAddress) public view returns (uint32) { | |
return whitelistPerAddress[customerAddress]; | |
} | |
function mintedAmount(address customerAddress) public view returns (uint32) { | |
return mintPerAddress[customerAddress]; | |
} | |
function mintableAmount(address customerAddress) public view returns (uint32) { | |
uint32 _mintedAmount = mintedAmount(customerAddress); | |
uint32 _whitelistedAmount = whitelistedAmount(customerAddress); | |
if (_mintedAmount > _whitelistedAmount) { | |
return 0; | |
} else { | |
return whitelistedAmount(customerAddress) - mintedAmount(customerAddress); | |
} | |
} | |
function canMintAmount(address customerAddress, uint32 amount) | |
public | |
view | |
returns (bool) | |
{ | |
return mintableAmount(customerAddress) >= amount; | |
} | |
function mint(uint32 amount, string memory claimId) external { | |
address customerAddress = _msgSender(); | |
require( | |
canMintAmount(customerAddress, amount), | |
"requested mint amount exceeds requesting address's whitelisted amount" | |
); | |
require( | |
totalSupply() + amount <= supplyCap, | |
"collection minting cap reached" | |
); | |
require( | |
whitelistedClaimIds[claimId] != 0, | |
"Collection: claim ID has not been whitelisted" | |
); | |
require( | |
whitelistedClaimIds[claimId] == amount, | |
string( | |
abi.encodePacked( | |
"Collection: incorrect amount for claim ID, received ", | |
amount.toString(), | |
" but expected ", | |
whitelistedClaimIds[claimId].toString() | |
) | |
) | |
); | |
require( | |
mintedClaimIds[claimId] == 0, | |
"Collection: claim ID has already been minted" | |
); | |
// for tracking association to transaction, we emit this event | |
emit ClaimIdMinted(claimId, customerAddress, amount); | |
mintedClaimIds[claimId] = amount; | |
for (uint256 i = 0; i < amount; i++) { | |
_nextTokenId++; // this begins token IDs at 1 | |
mintPerAddress[customerAddress]++; | |
_safeMint(customerAddress, _nextTokenId); | |
} | |
} | |
/** | |
* @dev Finalize starting index | |
*/ | |
function reveal(string memory baseTokenURI) external onlyOwner { | |
require(!isRevealed(), "Collection: already revealed"); | |
_btURI = baseTokenURI; | |
startingIndex = uint256(blockhash(block.number - 1)) % supplyCap; | |
// Prevent default sequence | |
if (startingIndex == 0) { | |
startingIndex = 1; | |
} | |
} | |
/** | |
* @dev Returns true if the metadata starting index has been revealed | |
*/ | |
function isRevealed() public view returns (bool) { | |
return startingIndex > 0; | |
} | |
function walletOfOwner(address _owner) | |
external | |
view | |
returns (uint256[] memory) | |
{ | |
uint256 ownerTokenCount = balanceOf(_owner); | |
uint256[] memory tokenIds = new uint256[](ownerTokenCount); | |
for (uint256 i; i < ownerTokenCount; i++) { | |
tokenIds[i] = tokenOfOwnerByIndex(_owner, i); | |
} | |
return tokenIds; | |
} | |
function tokenURI(uint256 tokenId) | |
public | |
view | |
virtual | |
override | |
returns (string memory) | |
{ | |
require( | |
_exists(tokenId), | |
"ERC721Metadata: URI query for nonexistent token" | |
); | |
if (!isRevealed()) { | |
return | |
string( | |
abi.encodePacked( | |
_cURI, | |
"/", | |
tokenId.toString(), | |
"/placeholder.json" | |
) | |
); | |
} | |
// offset of 1 because token IDs start at 1, not 0 | |
uint256 trueIndex = ((startingIndex + tokenId - 1) % supplyCap) + 1; | |
return | |
string( | |
abi.encodePacked(_btURI, "/", trueIndex.toString(), ".json") | |
); | |
} | |
// | |
// Needed for ERC2981 (Royalties standard) | |
// | |
function updateRoyalty(address recipient, uint96 fraction) external onlyOwner { | |
if (fraction > 0) { | |
_setDefaultRoyalty(recipient, fraction); | |
} else { | |
_deleteDefaultRoyalty(); | |
} | |
_setDefaultRoyalty(recipient, fraction); | |
} | |
/** | |
* @dev See {IERC165-supportsInterface}. | |
*/ | |
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721Enumerable, ERC2981) returns (bool) { | |
return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId); | |
} | |
/** | |
* Override isApprovedForAll to auto-approve OS's proxy contract | |
*/ | |
function isApprovedForAll(address _owner, address _operator) | |
public | |
view | |
override(ERC721, IERC721) | |
returns (bool isOperator) | |
{ | |
// if OpenSea's ERC721 Proxy Address is detected, auto-return true | |
if (_operator == address(0x58807baD0B376efc12F5AD86aAc70E78ed67deaE)) { | |
return true; | |
} | |
// otherwise, use the default ERC721.isApprovedForAll() | |
return ERC721.isApprovedForAll(_owner, _operator); | |
} | |
/** | |
* @dev Withdrawal implemented for safety reasons in case funds are | |
* accidentally sent to the contract | |
*/ | |
function withdraw() external onlyOwner { | |
uint256 balance = address(this).balance; | |
payable(owner()).transfer(balance); | |
} | |
function _msgSender() | |
internal | |
view | |
override(Context, BaseRelayRecipient) | |
returns (address) | |
{ | |
return BaseRelayRecipient._msgSender(); | |
} | |
function _msgData() | |
internal | |
view | |
override(Context, BaseRelayRecipient) | |
returns (bytes memory) | |
{ | |
return BaseRelayRecipient._msgData(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; | |
import { expect } from "chai"; | |
import { ethers } from "hardhat"; | |
// eslint-disable-next-line camelcase, node/no-missing-import | |
import { NovelCollection, NovelCollection__factory } from "../typechain"; | |
const COLLECTION_NAME = "Cats"; | |
const MAX_QUANTITY = 1000; | |
const HASH_PROOF = "0x523abcde"; | |
const CONTRACT_URI = "https://metadata-url.com/my-metadata"; | |
describe("NovelCollection", function () { | |
// eslint-disable-next-line camelcase | |
let NovelCollectionFactory: NovelCollection__factory; | |
let novelCollection: NovelCollection; | |
let owner: SignerWithAddress; | |
// eslint-disable-next-line no-unused-vars | |
let creator: SignerWithAddress; | |
let user1: SignerWithAddress; | |
let user2: SignerWithAddress; | |
let user3: SignerWithAddress; | |
beforeEach(async () => { | |
NovelCollectionFactory = (await ethers.getContractFactory( | |
"NovelCollection", | |
)) as NovelCollection__factory; | |
novelCollection = await NovelCollectionFactory.deploy( | |
COLLECTION_NAME, | |
MAX_QUANTITY, | |
HASH_PROOF, | |
CONTRACT_URI, | |
ethers.constants.AddressZero, | |
ethers.constants.AddressZero, | |
0, | |
); | |
await novelCollection.deployed(); | |
[owner, creator, user1, user2, user3] = await ethers.getSigners(); | |
}); | |
it("sets initialization parameters", async function () { | |
expect(await novelCollection.owner()).to.equal(owner.address); | |
expect(await novelCollection.name()).to.equal(COLLECTION_NAME); | |
expect(await novelCollection.supplyCap()).to.equal(MAX_QUANTITY); | |
expect(await novelCollection.metadataProofHash()).to.equal(HASH_PROOF); | |
}); | |
describe("Token URI", function () { | |
it("returns a placeholder URI before reveal", async function () { | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
1, | |
); | |
await whitelistTx.wait(); | |
const mintTx = await novelCollection | |
.connect(user1) | |
.mint(1, claimId); | |
await mintTx.wait(); | |
expect(await novelCollection.isRevealed()).to.equal(false); | |
expect(await novelCollection.tokenURI(1)).to.contain( | |
"placeholder.json", | |
); | |
}); | |
it("returns actual URI after reveal", async function () { | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
1, | |
); | |
await whitelistTx.wait(); | |
const mintTx = await novelCollection | |
.connect(user1) | |
.mint(1, claimId); | |
await mintTx.wait(); | |
expect(await novelCollection.isRevealed()).to.equal(false); | |
const revealTx = await novelCollection.reveal( | |
"https://realurl.com", | |
); | |
await revealTx.wait(); | |
const startingIndex = await novelCollection.startingIndex(); | |
const tokenIndex = 1; | |
const uri = await novelCollection.tokenURI(tokenIndex); | |
expect(uri).to.include("https://realurl.com/"); | |
expect(uri).to.equal( | |
`https://realurl.com/${ | |
(startingIndex.toNumber() + tokenIndex) % MAX_QUANTITY | |
}.json`, | |
); | |
expect(await novelCollection.isRevealed()).to.equal(true); | |
}); | |
}); | |
describe("Minting and Whitelist", function () { | |
it("allows the whitelisted user to mint", async function () { | |
expect(await novelCollection.balanceOf(user1.address)).to.equal(0); | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
1, | |
); | |
await whitelistTx.wait(); | |
const mintTx = await novelCollection | |
.connect(user1) | |
.mint(1, claimId); | |
await mintTx.wait(); | |
expect(await novelCollection.balanceOf(user1.address)).to.equal(1); | |
}); | |
it("returns whitelist status", async function () { | |
expect( | |
await novelCollection.canMintAmount(user1.address, 1), | |
).to.equal(false); | |
expect( | |
await novelCollection.canMintAmount(user2.address, 1), | |
).to.equal(false); | |
expect( | |
await novelCollection.canMintAmount(user3.address, 1), | |
).to.equal(false); | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
3, | |
); | |
await whitelistTx.wait(); | |
await expect( | |
novelCollection.connect(user1).mint(1, claimId), | |
).to.be.revertedWith( | |
"NovelCollection: incorrect amount for claim ID", | |
); | |
const mintTx = await novelCollection | |
.connect(user1) | |
.mint(3, claimId); | |
await mintTx.wait(); | |
expect( | |
await novelCollection.canMintAmount(user1.address, 3), | |
).to.equal(false); | |
expect( | |
await novelCollection.canMintAmount(user1.address, 0), | |
).to.equal(true); | |
expect( | |
await novelCollection.canMintAmount(user2.address, 1), | |
).to.equal(false); | |
}); | |
it("removes whitelist status after minting", async function () { | |
expect( | |
await novelCollection.canMintAmount(user1.address, 2), | |
).to.equal(false); | |
expect( | |
await novelCollection.canMintAmount(user2.address, 3), | |
).to.equal(false); | |
expect( | |
await novelCollection.canMintAmount(user3.address, 4), | |
).to.equal(false); | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
2, | |
); | |
await whitelistTx.wait(); | |
const mint1Tx = await novelCollection | |
.connect(user1) | |
.mint(2, claimId); | |
await mint1Tx.wait(); | |
expect( | |
await novelCollection.canMintAmount(user1.address, 1), | |
).to.equal(false); | |
}); | |
it("succesful no-op if whitelist for claim ID already done previously", async function () { | |
expect(await novelCollection.balanceOf(user1.address)).to.equal(0); | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
1, | |
); | |
await whitelistTx.wait(); | |
await expect( | |
novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
1, | |
), | |
).to.not.be.reverted; | |
}); | |
it("fails to whitelist if claim ID used with a different amount previously", async function () { | |
expect(await novelCollection.balanceOf(user1.address)).to.equal(0); | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
1, | |
); | |
await whitelistTx.wait(); | |
await expect( | |
novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
2, | |
), | |
).to.be.revertedWith( | |
"collection already whitelisted for 1 attempted amount of 2 does not match", | |
); | |
}); | |
it("returns claimId status", async function () { | |
const claimId = `${Math.random()}`.slice(2); | |
expect( | |
await novelCollection.whitelistedClaimIdAmount(claimId), | |
).to.equal(0); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
3, | |
); | |
await whitelistTx.wait(); | |
expect( | |
await novelCollection.whitelistedClaimIdAmount(claimId), | |
).to.equal(3); | |
}); | |
it("does not allow total cap to be exceeded", async function () { | |
novelCollection = await NovelCollectionFactory.deploy( | |
COLLECTION_NAME, | |
20, | |
HASH_PROOF, | |
CONTRACT_URI, | |
ethers.constants.AddressZero, | |
ethers.constants.AddressZero, | |
0, | |
); | |
await novelCollection.deployed(); | |
const claimId = `${Math.random()}`.slice(2); | |
const whitelistTx1 = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
20, | |
); | |
await whitelistTx1.wait(); | |
await expect( | |
novelCollection.whitelistClaimAmount( | |
[user2.address], | |
`${Math.random()}`.slice(2), | |
1, | |
), | |
).to.be.revertedWith( | |
"collection minting cap reached, the amount requested for whitelist would exceed the cap", | |
); | |
await expect( | |
novelCollection.connect(user1).mint(17, claimId), | |
).to.be.revertedWith( | |
"NovelCollection: incorrect amount for claim ID", | |
); | |
const mint1Tx = await novelCollection | |
.connect(user1) | |
.mint(20, claimId); | |
await mint1Tx.wait(); | |
expect(await novelCollection.balanceOf(user1.address)).to.equal(20); | |
}); | |
it("enforces whitelist limits", async function () { | |
const claimId = `${Math.random()}`.slice(2); | |
novelCollection = await NovelCollectionFactory.deploy( | |
COLLECTION_NAME, | |
20, | |
HASH_PROOF, | |
CONTRACT_URI, | |
ethers.constants.AddressZero, | |
ethers.constants.AddressZero, | |
0, | |
); | |
await novelCollection.deployed(); | |
const whitelistTx = await novelCollection.whitelistClaimAmount( | |
[user1.address], | |
claimId, | |
2, | |
); | |
await whitelistTx.wait(); | |
await expect( | |
novelCollection.connect(user1).mint(3, claimId), | |
).to.be.revertedWith( | |
"requested mint amount exceeds requesting address's whitelisted amount", | |
); | |
await expect( | |
novelCollection.connect(user2).mint(4, claimId), | |
).to.be.revertedWith( | |
"requested mint amount exceeds requesting address's whitelisted amount", | |
); | |
await expect( | |
novelCollection.connect(user3).mint(5, claimId), | |
).to.be.revertedWith( | |
"requested mint amount exceeds requesting address's whitelisted amount", | |
); | |
const mint1Tx = await novelCollection | |
.connect(user1) | |
.mint(2, claimId); | |
await mint1Tx.wait(); | |
expect(await novelCollection.balanceOf(user1.address)).to.equal(2); | |
expect( | |
await novelCollection.canMintAmount(user1.address, 1), | |
).to.equal(false); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { task } from "hardhat/config"; | |
import { verifyCollection } from "./collection/verifyCollection"; | |
task("verify-collection", "Verifies the collection on Polygon Scan") | |
.addParam("address", "path to the JSON for this collection") | |
.setAction(async ({ address }, hre) => { | |
await verifyCollection({ | |
hre, | |
collectionAddress: address, | |
fetchFromFile: true, | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types"; | |
import { AppEnvEnum } from "@novel/shared/utils/env"; | |
import { NovelPaymaster__factory } from "../../typechain/factories/NovelPaymaster__factory"; | |
import { getForwarderAddress, getRelayHubAddress } from "../getConstants"; | |
import { getNovelPaymasterFactory, writePaymasterToFile } from "./getPaymaster"; | |
export type PaymasterConstructorParams = { | |
relayHubAddress?: string; | |
gsnTrustedForwarderAddress?: string; | |
}; | |
export interface IDeployParams extends PaymasterConstructorParams { | |
ethers?: HardhatEthersHelpers; | |
NovelPaymaster?: NovelPaymaster__factory; | |
env: AppEnvEnum; | |
} | |
export interface IDeployPaymasterOutput extends PaymasterConstructorParams { | |
paymasterAddress: string; | |
} | |
// Deploys Paymaster | |
export const deployPaymaster = async ({ | |
env, | |
ethers, | |
// Paymaster funds transactions | |
// ethers interacts with ethereum provider | |
// hardhat makes web3 available without a browser | |
// also has a local blockchain network to test w/o | |
// testnet | |
NovelPaymaster, | |
// Relay hub is starting the transaction (asks the NovelPaymaster if the payment should | |
// go through (preRelayedCall), and then passes on the transaction to the forwarder | |
// The NovelPaymaster holds and hands out the gas needed for the transactions | |
relayHubAddress = getRelayHubAddress(), | |
gsnTrustedForwarderAddress = getForwarderAddress(), | |
}: IDeployParams) => { | |
console.debug("deployPaymaster - Constructor Params", { | |
relayHubAddress, | |
gsnTrustedForwarderAddress, | |
}); | |
if (ethers) { | |
NovelPaymaster = (await getNovelPaymasterFactory( | |
ethers, | |
)) as NovelPaymaster__factory; | |
} else { | |
if (!NovelPaymaster) { | |
throw new Error("ethers or NovelPaymaster factory is required"); | |
} | |
} | |
console.debug("deployPaymaster - Deploying Paymaster"); | |
const paymaster = await NovelPaymaster!.deploy( | |
relayHubAddress, | |
gsnTrustedForwarderAddress, | |
); | |
console.debug("deployPaymaster - Waiting for Paymaster to be Deployed"); | |
await paymaster.deployTransaction.wait(6); // ensures verification will work | |
const paymasterAddress = paymaster.address; | |
const deployOutput = { | |
paymasterAddress, | |
relayHubAddress, | |
gsnTrustedForwarderAddress, | |
}; | |
// if the deploy is successful, we write the output to a file, | |
// so that we don't deploy multiple paymasters | |
// in production, this will be stored in a DB or secrets manager | |
writePaymasterToFile(env, deployOutput); | |
console.debug("deployPaymaster - Paymaster deployed", { | |
paymasterAddress, | |
}); | |
return paymaster; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types"; | |
import { NovelPaymaster__factory } from "../../typechain/factories/NovelPaymaster__factory"; | |
import { getPaymaster } from "./getPaymaster"; | |
import { ethers as ethersUtils } from "ethers"; | |
import { AppEnvEnum } from "@novel/shared/utils/env"; | |
import { getGasParams } from "../getConstants"; | |
interface IFillPaymasterParams { | |
env: AppEnvEnum; | |
ethers: HardhatEthersHelpers; | |
NovelPaymaster?: NovelPaymaster__factory; | |
isTestnet?: boolean; | |
ethToTransfer: string; | |
paymasterAddress?: string; | |
} | |
// Refills paymaster gas | |
export const fillPaymaster = async ({ | |
env, | |
ethers, | |
// How much gas to put in, in eth | |
ethToTransfer, | |
paymasterAddress, | |
isTestnet = !!process.env.TESTNET, | |
}: IFillPaymasterParams) => { | |
const paymaster = await getPaymaster({ | |
env, | |
ethers, | |
paymasterAddress, | |
}); | |
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`; | |
paymasterAddress = paymaster.address; | |
const amountToSendInWei = ethersUtils.utils.parseUnits( | |
ethToTransfer, | |
"ether", | |
); | |
const amountToTransferAsHex = amountToSendInWei.toHexString(); | |
console.debug(`fillPaymaster - sending to ${paymasterType} paymaster`, { | |
paymasterAddress, | |
amountToSendInWei, | |
amountToTransferAsHex, | |
}); | |
// pulls from accounts (usually we have one, this just takes | |
// the last one) | |
const lastSigner = (await ethers.getSigners()).pop(); | |
if (!lastSigner) throw new Error("no signers available"); | |
const transferMoneyTxnParams = { | |
to: paymasterAddress, | |
value: amountToTransferAsHex, | |
// eslint-disable-next-line node/no-unsupported-features/es-syntax | |
...getGasParams(), | |
}; | |
console.debug("fillPaymaster - sending Transaction - transferMoney", { | |
transferMoneyTxnParams, | |
}); | |
const transferMoneyTxn = await lastSigner.sendTransaction( | |
transferMoneyTxnParams, | |
); | |
console.debug("fillPaymaster - Waiting for transferMoneyTxn to complete"); | |
await transferMoneyTxn.wait(); | |
console.debug( | |
`fillPaymaster - transferMoneyTxn sending ${ethToTransfer} MATIC to ${paymasterType} was successful!`, | |
); | |
return true; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { | |
TESTNET, | |
GSN_TRUSTED_FORWARDER_ADDRESS_MUMBAI, | |
GSN_TRUSTED_FORWARDER_ADDRESS_POLYGON, | |
RELAY_HUB_ADDRESS_MUMBAI, | |
RELAY_HUB_ADDRESS_POLYGON, | |
ETH_RPC_URL, | |
ETH_TESTNET_RPC_URL, | |
ETH_WEBSOCKET_URL, | |
ETH_TESTNET_WEBSOCKET_URL, | |
ETH_PRIVATE_KEY, | |
POLYSCAN_API_KEY, | |
} = process.env; | |
export const getForwarderAddress = (): string => { | |
return TESTNET | |
? GSN_TRUSTED_FORWARDER_ADDRESS_MUMBAI! | |
: GSN_TRUSTED_FORWARDER_ADDRESS_POLYGON!; | |
}; | |
export const getRelayHubAddress = (): string => { | |
return TESTNET ? RELAY_HUB_ADDRESS_MUMBAI! : RELAY_HUB_ADDRESS_POLYGON!; | |
}; | |
export const getRpcUrl = (): string => { | |
return TESTNET ? ETH_TESTNET_RPC_URL! : ETH_RPC_URL!; | |
}; | |
export const getWebsocketUrl = (): string => { | |
return TESTNET ? ETH_TESTNET_WEBSOCKET_URL! : ETH_WEBSOCKET_URL!; | |
}; | |
export const getWalletAccounts = (): string[] => { | |
return ETH_PRIVATE_KEY ? [ETH_PRIVATE_KEY] : []; | |
}; | |
export const getPolyscanApiKey = (): string => { | |
return POLYSCAN_API_KEY!; | |
}; | |
export const getGasParams = () => ({ | |
gasLimit: 10e6, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export async function getCurrentGasPricesInGwei() { | |
const response: { | |
safeLow: number; | |
standard: number; | |
fast: number; | |
fastest: number; | |
blockTime: number; | |
blockNumber: number; | |
// eslint-disable-next-line no-undef | |
} = await fetch("https://gasstation-mainnet.matic.network").then( | |
(response) => response.json(), | |
); | |
return response; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { join } from "path"; | |
import { existsSync, readFileSync, writeFileSync } from "fs"; | |
import { NovelPaymaster__factory } from "../../typechain/factories/NovelPaymaster__factory"; | |
import { IDeployPaymasterOutput } from "./deployPaymaster"; | |
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types"; | |
import { AppEnvEnum } from "@novel/shared/utils/env"; | |
const { TESTNET } = process.env; | |
// get the path for paymaster details on local file system | |
// in prod, this will be in a DB or secrets manger (K8S) | |
export const getPaymasterJsonPath = (env: AppEnvEnum) => { | |
const fileName = `paymaster.${env}.${TESTNET ? "mumbai" : "polygon"}.json`; | |
console.log(`loading file ${fileName}`); | |
return join( | |
__dirname, | |
"..", | |
"..", | |
"..", | |
"shared", | |
"paymasterData", | |
fileName, | |
); | |
}; | |
export const getNovelPaymasterFactory = (ethers?: HardhatEthersHelpers) => { | |
if (!ethers) { | |
throw new Error("ethers required to fetch factory from file"); | |
} | |
return ethers!.getContractFactory( | |
"NovelPaymaster", | |
) as Promise<NovelPaymaster__factory>; | |
}; | |
// Gets the paymaster ethers object from json file or passed in address | |
export const getPaymasterFromFile = async ( | |
env: AppEnvEnum, | |
ethers?: HardhatEthersHelpers, | |
) => { | |
const path = getPaymasterJsonPath(env); | |
if (!existsSync(path)) { | |
throw new Error( | |
"Could not find paymaster.json - try running hardhat deploy-paymaster", | |
); | |
} | |
const json = readFileSync(path, "utf8"); | |
const paymasterData = JSON.parse(json); | |
const paymasterAddress = paymasterData.paymasterAddress; | |
// get the paymaster contract ethers object | |
const paymaster = await getPaymasterByAddress({ | |
ethers, | |
paymasterAddress, | |
}); | |
return paymaster; | |
}; | |
interface IGetPaymasterByAddressParams { | |
ethers?: HardhatEthersHelpers; | |
NovelPaymaster?: NovelPaymaster__factory; | |
paymasterAddress: string; | |
} | |
export const getPaymasterByAddress = async ({ | |
ethers, | |
NovelPaymaster, | |
paymasterAddress, | |
}: IGetPaymasterByAddressParams) => { | |
NovelPaymaster = | |
NovelPaymaster || | |
((await getNovelPaymasterFactory(ethers)) as NovelPaymaster__factory); | |
const paymaster = await NovelPaymaster!.attach(paymasterAddress); | |
return paymaster; | |
}; | |
export const writePaymasterToFile = ( | |
env: AppEnvEnum, | |
deployOutput: IDeployPaymasterOutput, | |
): void => { | |
const path = getPaymasterJsonPath(env); | |
writeFileSync( | |
path, | |
JSON.stringify( | |
{ | |
paymasterAddress: deployOutput.paymasterAddress.toLowerCase(), | |
relayHubAddress: deployOutput.relayHubAddress!.toLowerCase(), | |
gsnTrustedForwarderAddress: | |
deployOutput.gsnTrustedForwarderAddress!.toLowerCase(), | |
}, | |
null, | |
2, | |
), | |
); | |
console.debug(`Wrote paymaster JSON to ${path}`); | |
}; | |
interface IGetPaymasterParams { | |
env: AppEnvEnum; | |
ethers?: HardhatEthersHelpers; | |
NovelPaymaster?: NovelPaymaster__factory; | |
paymasterAddress?: string; | |
} | |
export const getPaymaster = async ({ | |
env, | |
ethers, | |
NovelPaymaster, | |
paymasterAddress, | |
}: IGetPaymasterParams) => { | |
let paymaster; | |
if (ethers) { | |
paymaster = await getPaymasterFromFile(env, ethers); | |
} else { | |
if (!paymasterAddress) { | |
throw new Error("ethers or paymasterAddress required"); | |
} | |
paymaster = await getPaymasterByAddress({ | |
NovelPaymaster, | |
paymasterAddress, | |
}); | |
} | |
return paymaster; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types"; | |
import { AppEnvEnum } from "@novel/shared/utils/env"; | |
import { getPaymaster } from "./getPaymaster"; | |
import { ethers as ethersLib } from "ethers"; | |
interface IGetPaymasterBalance { | |
env: AppEnvEnum; | |
ethers?: HardhatEthersHelpers; | |
paymasterAddress?: string; | |
isTestnet?: boolean; | |
} | |
// Get paymaster balance | |
export const getPaymasterBalance = async ({ | |
env, | |
ethers, | |
paymasterAddress, | |
isTestnet = !!process.env.TESTNET, | |
}: IGetPaymasterBalance): Promise<string> => { | |
const paymaster = await getPaymaster({ | |
env, | |
ethers, | |
paymasterAddress, | |
}); | |
paymasterAddress = paymaster.address; | |
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`; | |
console.debug( | |
`getBalance - Checking ${paymasterType} ${paymasterAddress} paymaster balance`, | |
); | |
const balanceInMatic = ethersLib.utils.formatEther( | |
await paymaster.getRelayHubDeposit({ | |
gasLimit: 1e6, | |
}), | |
); | |
console.debug( | |
`getBalance - ${paymasterType} paymaster MATIC Balance:`, | |
balanceInMatic, | |
); | |
return balanceInMatic; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import "../backend/src/env-config"; | |
import "@nomiclabs/hardhat-waffle"; | |
import "@nomiclabs/hardhat-etherscan"; | |
import "@nomiclabs/hardhat-ethers"; | |
import "hardhat-deploy-ethers"; | |
import "@typechain/hardhat"; | |
import "hardhat-gas-reporter"; | |
import "solidity-coverage"; | |
import path from "path"; | |
import { subtask } from "hardhat/config"; | |
import { TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD } from "hardhat/builtin-tasks/task-names"; | |
import { getRpcUrl, getWalletAccounts } from "./scripts/getConstants"; | |
const { POLYSCAN_API_KEY } = process.env; | |
// compilation must be complete before these scripts work | |
if (!process.env.COMPILING) { | |
// eslint-disable-next-line node/no-missing-require | |
require("./scripts/paymaster"); | |
// eslint-disable-next-line node/no-missing-require | |
require("./scripts/collection"); | |
} | |
// yanked straight from here: https://github.com/fvictorio/hardhat-examples/blob/master/custom-solc/hardhat.config.js | |
subtask( | |
TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD, | |
async (args: { solcVersion: string }, hre, runSuper) => { | |
if (args.solcVersion === "0.8.4") { | |
const compilerPath = path.join( | |
__dirname, | |
"soljson-v0.8.4+commit.c7e474f2.js", | |
); | |
return { | |
compilerPath, | |
isSolcJs: true, // if you are using a native compiler, set this to false | |
version: args.solcVersion, | |
// this is used as extra information in the build-info files, but other than | |
// that is not important | |
longVersion: "0.8.4+commit.c7e474f2", | |
}; | |
} | |
// we just use the default subtask if the version is not 0.8.5 | |
return runSuper(); | |
}, | |
); | |
export default { | |
solidity: { | |
version: "0.8.4", // needs to stay fixed, set in Dockerfile | |
settings: { | |
optimizer: { | |
enabled: true, | |
runs: 2000, | |
}, | |
}, | |
}, | |
networks: { | |
rpc: { | |
url: getRpcUrl(), | |
accounts: getWalletAccounts(), | |
}, | |
}, | |
gasReporter: { | |
enabled: process.env.ETH_REPORT_GAS !== undefined, | |
currency: "USD", | |
token: "MATIC", | |
coinmarketcap: process.env.COINMARKETCAP_API_KEY, | |
gasPriceApi: | |
"https://api.polygonscan.com/api?module=proxy&action=eth_gasPrice", | |
}, | |
rpc: { | |
// no limit for deploying | |
// money is no object | |
txfeecap: 0, | |
}, | |
// support for verifying my contract | |
etherscan: { | |
apiKey: POLYSCAN_API_KEY, | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//SPDX-License-Identifier: Unlicense | |
pragma solidity ^0.8.0; | |
// This is done so that OpenSea can cover Matic gas costs: https://docs.opensea.io/docs/polygon-basic-integration | |
// we need to figure out how to do this ourselves for the minting step | |
// https://docs.openzeppelin.com/learn/sending-gasless-transactions#what-is-a-meta-tx | |
/** | |
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/ContextMixin.sol | |
*/ | |
abstract contract ContextMixin { | |
function msgSender() | |
internal | |
view | |
returns (address payable sender) | |
{ | |
if (msg.sender == address(this)) { | |
bytes memory array = msg.data; | |
uint256 index = msg.data.length; | |
assembly { | |
// Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. | |
sender := and( | |
mload(add(array, index)), | |
0xffffffffffffffffffffffffffffffffffffffff | |
) | |
} | |
} else { | |
sender = payable(msg.sender); | |
} | |
return sender; | |
} | |
} | |
/** | |
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/Initializable.sol | |
*/ | |
contract Initializable { | |
bool inited = false; | |
modifier initializer() { | |
require(!inited, "already inited"); | |
_; | |
inited = true; | |
} | |
} | |
/** | |
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/EIP712Base.sol | |
*/ | |
contract EIP712Base is Initializable { | |
struct EIP712Domain { | |
string name; | |
string version; | |
address verifyingContract; | |
bytes32 salt; | |
} | |
string constant public ERC712_VERSION = "1"; | |
bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256( | |
bytes( | |
"EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)" | |
) | |
); | |
bytes32 internal domainSeperator; | |
// supposed to be called once while initializing. | |
// one of the contractsa that inherits this contract follows proxy pattern | |
// so it is not possible to do this in a constructor | |
function _initializeEIP712( | |
string memory name | |
) | |
internal | |
initializer | |
{ | |
_setDomainSeperator(name); | |
} | |
function _setDomainSeperator(string memory name) internal { | |
domainSeperator = keccak256( | |
abi.encode( | |
EIP712_DOMAIN_TYPEHASH, | |
keccak256(bytes(name)), | |
keccak256(bytes(ERC712_VERSION)), | |
address(this), | |
bytes32(getChainId()) | |
) | |
); | |
} | |
function getDomainSeperator() public view returns (bytes32) { | |
return domainSeperator; | |
} | |
function getChainId() public view returns (uint256) { | |
uint256 id; | |
assembly { | |
id := chainid() | |
} | |
return id; | |
} | |
/** | |
* Accept message hash and returns hash message in EIP712 compatible form | |
* So that it can be used to recover signer from signature signed using EIP712 formatted data | |
* https://eips.ethereum.org/EIPS/eip-712 | |
* "\\x19" makes the encoding deterministic | |
* "\\x01" is the version byte to make it compatible to EIP-191 | |
*/ | |
function toTypedMessageHash(bytes32 messageHash) | |
internal | |
view | |
returns (bytes32) | |
{ | |
return | |
keccak256( | |
abi.encodePacked("\x19\x01", getDomainSeperator(), messageHash) | |
); | |
} | |
} | |
/** | |
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/NativeMetaTransaction.sol | |
*/ | |
contract NativeMetaTransaction is EIP712Base { | |
bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256( | |
bytes( | |
"MetaTransaction(uint256 nonce,address from,bytes functionSignature)" | |
) | |
); | |
event MetaTransactionExecuted( | |
address userAddress, | |
address payable relayerAddress, | |
bytes functionSignature | |
); | |
mapping(address => uint256) nonces; | |
/* | |
* Meta transaction structure. | |
* No point of including value field here as if user is doing value transfer then he has the funds to pay for gas | |
* He should call the desired function directly in that case. | |
*/ | |
struct MetaTransaction { | |
uint256 nonce; | |
address from; | |
bytes functionSignature; | |
} | |
function executeMetaTransaction( | |
address userAddress, | |
bytes memory functionSignature, | |
bytes32 sigR, | |
bytes32 sigS, | |
uint8 sigV | |
) public payable returns (bytes memory) { | |
MetaTransaction memory metaTx = MetaTransaction({ | |
nonce: nonces[userAddress], | |
from: userAddress, | |
functionSignature: functionSignature | |
}); | |
require( | |
verify(userAddress, metaTx, sigR, sigS, sigV), | |
"Signer and signature do not match" | |
); | |
// increase nonce for user (to avoid re-use) | |
nonces[userAddress] = nonces[userAddress] + 1; | |
emit MetaTransactionExecuted( | |
userAddress, | |
payable(msg.sender), | |
functionSignature | |
); | |
// Append userAddress and relayer address at the end to extract it from calling context | |
(bool success, bytes memory returnData) = address(this).call( | |
abi.encodePacked(functionSignature, userAddress) | |
); | |
require(success, "Function call not successful"); | |
return returnData; | |
} | |
function hashMetaTransaction(MetaTransaction memory metaTx) | |
internal | |
pure | |
returns (bytes32) | |
{ | |
return | |
keccak256( | |
abi.encode( | |
META_TRANSACTION_TYPEHASH, | |
metaTx.nonce, | |
metaTx.from, | |
keccak256(metaTx.functionSignature) | |
) | |
); | |
} | |
function getNonce(address user) public view returns (uint256 nonce) { | |
nonce = nonces[user]; | |
} | |
function verify( | |
address signer, | |
MetaTransaction memory metaTx, | |
bytes32 sigR, | |
bytes32 sigS, | |
uint8 sigV | |
) internal view returns (bool) { | |
require(signer != address(0), "NativeMetaTransaction: INVALID_SIGNER"); | |
return | |
signer == | |
ecrecover( | |
toTypedMessageHash(hashMetaTransaction(metaTx)), | |
sigV, | |
sigR, | |
sigS | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.8.0; | |
pragma experimental ABIEncoderV2; | |
// SPDX-License-Identifier: MIT OR Apache-2.0 | |
import "@opengsn/contracts/src/BasePaymaster.sol"; | |
import "@opengsn/contracts/src/interfaces/IRelayHub.sol"; | |
import "./Collection.sol"; | |
contract Paymaster is BasePaymaster { | |
constructor(address _relayHubAddress, address _forwarderAddress) { | |
setRelayHub(IRelayHub(_relayHubAddress)); | |
setTrustedForwarder(_forwarderAddress); | |
} | |
/** | |
* Withdrawal implemented for safety reasons in case funds are | |
* accidentally sent to the contract | |
*/ | |
function withdraw() external onlyOwner { | |
uint256 balance = address(this).balance; | |
payable(owner()).transfer(balance); | |
withdrawRelayHubDepositTo( | |
getRelayHubDeposit(), | |
payable(owner()) | |
); | |
} | |
mapping(address => bool) public targetWhitelist; | |
// this is how we mark a contract as one we will pay for | |
function enableContract(address target) external onlyOwner { | |
targetWhitelist[target] = true; | |
} | |
function isEnabledContract(address target) external view returns (bool) { | |
if (targetWhitelist[target]) { | |
return true; | |
} else { | |
return false; | |
} | |
} | |
function preRelayedCall( | |
GsnTypes.RelayRequest calldata relayRequest, | |
bytes calldata signature, | |
bytes calldata approvalData, | |
uint256 maxPossibleGas | |
) external virtual override returns (bytes memory context, bool) { | |
_verifyForwarder(relayRequest); | |
(signature, approvalData, maxPossibleGas); | |
require( | |
targetWhitelist[ | |
relayRequest.request.to | |
], | |
string( | |
abi.encodePacked( | |
"target contract address ", | |
toString(relayRequest.request.to), | |
"is not whitelisted" | |
) | |
) | |
); | |
// Only work on mint function | |
Collection collection = Collection( | |
relayRequest.request.to | |
); | |
bytes4 methodSig = GsnUtils.getMethodSig(relayRequest.request.data); | |
require( | |
methodSig == collection.mint.selector, | |
string( | |
abi.encodePacked( | |
"attempted to call method with signature ", | |
methodSig, | |
" you may only call the mint function" | |
) | |
) | |
); | |
uint32 quantity = extractMintCountFromCall(relayRequest.request.data); | |
require( | |
collection.canMintAmount(relayRequest.request.from, quantity), | |
string( | |
abi.encodePacked( | |
"requested mint amount of ", | |
quantity, | |
" exceeds whitelisted balance" | |
) | |
) | |
); | |
// false as second arg means we don't cover the gas | |
return ("", false); | |
} | |
function postRelayedCall( | |
bytes calldata context, | |
bool success, | |
uint256 gasUseWithoutPost, | |
GsnTypes.RelayData calldata relayData | |
) external virtual override { | |
(context, success, gasUseWithoutPost, relayData); | |
} | |
function versionPaymaster() | |
external | |
view | |
virtual | |
override | |
returns (string memory) | |
{ | |
return "2.2.3+paymaster"; | |
} | |
function extractMintCountFromCall(bytes memory b) | |
internal | |
pure | |
returns (uint32) | |
{ | |
uint32 number; | |
// if this doesn't work then try 6 instead of 4 | |
for (uint8 i = 4; i < 8; i++) { | |
number = number + uint32(uint8(b[i]) * (2**(8 * (8 - (i + 1))))); | |
} | |
return number; | |
} | |
// below is just for logging address in case of error | |
function toString(address account) public pure returns(string memory) { | |
return toString(abi.encodePacked(account)); | |
} | |
function toString(uint256 value) public pure returns(string memory) { | |
return toString(abi.encodePacked(value)); | |
} | |
function toString(bytes32 value) public pure returns(string memory) { | |
return toString(abi.encodePacked(value)); | |
} | |
function toString(bytes memory data) public pure returns(string memory) { | |
bytes memory alphabet = "0123456789abcdef"; | |
bytes memory str = new bytes(2 + data.length * 2); | |
str[0] = "0"; | |
str[1] = "x"; | |
for (uint i = 0; i < data.length; i++) { | |
str[2+i*2] = alphabet[uint(uint8(data[i] >> 4))]; | |
str[3+i*2] = alphabet[uint(uint8(data[i] & 0x0f))]; | |
} | |
return string(str); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; | |
import { expect } from "chai"; | |
import { ethers } from "hardhat"; | |
// eslint-disable-next-line camelcase, node/no-missing-import | |
import { | |
NovelPaymaster, | |
NovelPaymaster__factory, | |
NovelCollection__factory, | |
} from "../typechain"; | |
const COLLECTION_NAME = "Cats"; | |
const MAX_QUANTITY = 1000; | |
const HASH_PROOF = "0x523abcde"; | |
const CONTRACT_URI = "https://metadata-url.com/my-metadata"; | |
describe("NovelPaymaster", function () { | |
// eslint-disable-next-line camelcase | |
let NovelPaymasterFactory: NovelPaymaster__factory; | |
let novelPaymaster: NovelPaymaster; | |
// eslint-disable-next-line no-unused-vars | |
let owner: SignerWithAddress; | |
beforeEach(async () => { | |
NovelPaymasterFactory = (await ethers.getContractFactory( | |
"NovelPaymaster", | |
)) as NovelPaymaster__factory; | |
novelPaymaster = await NovelPaymasterFactory.deploy( | |
ethers.constants.AddressZero, | |
ethers.constants.AddressZero, | |
); | |
await novelPaymaster.deployed(); | |
[owner] = await ethers.getSigners(); | |
}); | |
it("whitelisting in paymaster works", async function () { | |
const NovelCollectionFactory = (await ethers.getContractFactory( | |
"NovelCollection", | |
owner, | |
)) as NovelCollection__factory; | |
const novelCollection = await NovelCollectionFactory.deploy( | |
COLLECTION_NAME, | |
MAX_QUANTITY, | |
HASH_PROOF, | |
CONTRACT_URI, | |
ethers.constants.AddressZero, | |
ethers.constants.AddressZero, | |
0, | |
); | |
await novelCollection.deployed(); | |
const isEnabledBefore = await novelPaymaster.isEnabledContract( | |
novelCollection.address, | |
); | |
expect(isEnabledBefore).to.equal(false); | |
await novelPaymaster.enableContract(novelCollection.address); | |
const isEnabledAfter = await novelPaymaster.isEnabledContract( | |
novelCollection.address, | |
); | |
expect(isEnabledAfter).to.equal(true); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// More info on GSN: https://docs.opengsn.org/#architecture | |
import { HardhatRuntimeEnvironment } from "hardhat/types/runtime"; | |
import { task } from "hardhat/config"; | |
import { deployPaymaster } from "./paymaster/deployPaymaster"; | |
import { getPaymaster } from "./paymaster/getPaymaster"; | |
import { verifyPaymaster } from "./paymaster/verifyPaymaster"; | |
import { fillPaymaster } from "./paymaster/fillPaymaster"; | |
import { withdrawFromPaymaster } from "./paymaster/withdrawFromPaymaster"; | |
import { getPaymasterBalance } from "./paymaster/getPaymasterBalance"; | |
task("deploy-paymaster", "Deploys the paymaster") | |
.addOptionalParam( | |
"verify", | |
"Should we verify the contract after deploying? (boolean)", | |
) | |
.addParam("env", "which environment to use for deploying the paymaster") | |
.setAction(async ({ verify: doVerify = true, env }, hre) => { | |
// check env value | |
if (!Object.values(AppEnvEnum).includes(env)) { | |
throw new Error( | |
`env value of ${env} does not match one of ${Object.values( | |
AppEnvEnum, | |
).join(", ")}`, | |
); | |
} | |
// run the deploy | |
const paymaster = await deployPaymaster({ | |
ethers: hre.ethers, | |
env, | |
}); | |
const paymasterAddress = paymaster.address; | |
// if we specify that we want to verify on Polygon scan, | |
// then do verification | |
if (!doVerify) { | |
return; | |
} | |
try { | |
await verifyPaymaster({ | |
env, | |
hre, | |
paymasterAddress, | |
}); | |
} catch (e) {} | |
}); | |
task( | |
"verify-paymaster", | |
"Verify that a contract is whitelisted on the paymaster", | |
) | |
.addParam("env", "which environment to use for deploying the paymaster") | |
.setAction(async ({ env }, hre) => { | |
// check env value | |
if (!Object.values(AppEnvEnum).includes(env)) { | |
throw new Error( | |
`env value of ${env} does not match one of ${Object.values( | |
AppEnvEnum, | |
).join(", ")}`, | |
); | |
} | |
await verifyPaymaster({ | |
env, | |
hre, | |
}); | |
}); | |
task("fill-paymaster", "Adds native token to paymaster") | |
.addParam( | |
"ethtotransfer", | |
"quantity in eth to add to the paymaster account - fractions allowed", | |
) | |
.addParam("env", "which environment to use for deploying the paymaster") | |
.setAction( | |
async ({ ethtotransfer, env }, hre: HardhatRuntimeEnvironment) => { | |
// check env value | |
if (!Object.values(AppEnvEnum).includes(env)) { | |
throw new Error( | |
`env value of ${env} does not match one of ${Object.values( | |
AppEnvEnum, | |
).join(", ")}`, | |
); | |
} | |
const ethToTransferAsFloat = parseFloat(ethtotransfer); | |
if (isNaN(ethToTransferAsFloat)) { | |
throw new Error("eth must be a number"); | |
} | |
// $.02-ish at current pricing (minimum to run account) | |
if (ethToTransferAsFloat < 0.01) | |
throw new Error("Minimum amount is 0.01 ETH"); | |
await fillPaymaster({ | |
env, | |
ethers: hre.ethers, | |
ethToTransfer: ethtotransfer, | |
}); | |
}, | |
); | |
task("get-paymaster-balance", "Checks paymaster balance") | |
.addParam("env", "which environment to use for deploying the paymaster") | |
.setAction(async ({ env }, hre: HardhatRuntimeEnvironment) => { | |
// check env value | |
if (!Object.values(AppEnvEnum).includes(env)) { | |
throw new Error( | |
`env value of ${env} does not match one of ${Object.values( | |
AppEnvEnum, | |
).join(", ")}`, | |
); | |
} | |
await getPaymasterBalance({ | |
env, | |
ethers: hre.ethers, | |
}); | |
}); | |
task("withdraw-from-paymaster", "Adds native token to paymaster") | |
.addParam("env", "which environment to use for deploying the paymaster") | |
.setAction(async ({ env }, hre: HardhatRuntimeEnvironment) => { | |
// check env value | |
if (!Object.values(AppEnvEnum).includes(env)) { | |
throw new Error( | |
`env value of ${env} does not match one of ${Object.values( | |
AppEnvEnum, | |
).join(", ")}`, | |
); | |
} | |
await withdrawFromPaymaster({ | |
env, | |
ethers: hre.ethers, | |
}); | |
}); | |
task( | |
"check-collection-whitelisted", | |
"Check that a contract is whitelisted on the paymaster", | |
) | |
.addParam("address", "address of the contract") | |
.addParam("env", "which environment to use for deploying the paymaster") | |
.setAction(async ({ address, env }, hre: HardhatRuntimeEnvironment) => { | |
// check env value | |
if (!Object.values(AppEnvEnum).includes(env)) { | |
throw new Error( | |
`env value of ${env} does not match one of ${Object.values( | |
AppEnvEnum, | |
).join(", ")}`, | |
); | |
} | |
const paymaster = await getPaymaster({ | |
env, | |
ethers: hre.ethers, | |
}); | |
const isWhitelisted = await paymaster.isEnabledContract(address); | |
console.log("check-collection-whitelisted", { | |
isWhitelisted, | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Verifies contract with PolygonScan (adds green checkmark) | |
import { AppEnvEnum } from "@novel/shared/utils/env"; | |
import { HardhatRuntimeEnvironment } from "hardhat/types/runtime"; | |
import { getForwarderAddress, getRelayHubAddress } from "../getConstants"; | |
import { getPaymaster } from "./getPaymaster"; | |
interface IVerifyPaymasterParams { | |
env: AppEnvEnum; | |
hre: HardhatRuntimeEnvironment; | |
// the paymaster we want to verify on PolygonScan | |
paymasterAddress?: string; | |
relayHubAddress?: string; | |
gsnTrustedForwarderAddress?: string; | |
isTestnet?: boolean; | |
} | |
export const verifyPaymaster = async ({ | |
env, | |
hre, | |
paymasterAddress, | |
relayHubAddress = getRelayHubAddress(), | |
gsnTrustedForwarderAddress = getForwarderAddress(), | |
isTestnet = !!process.env.TESTNET, | |
}: IVerifyPaymasterParams) => { | |
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`; | |
console.debug( | |
`verifyPaymaster - Verifying ${paymasterType} paymaster at`, | |
paymasterAddress, | |
); | |
const paymaster = await getPaymaster({ | |
env, | |
ethers: hre.ethers, | |
paymasterAddress, | |
}); | |
paymasterAddress = paymaster.address; | |
try { | |
// subtask provided by etherscan (built in) | |
await hre.run("verify:verify", { | |
address: paymasterAddress, | |
constructorArguments: [relayHubAddress, gsnTrustedForwarderAddress], | |
}); | |
console.debug( | |
`verifyPaymaster - Verification of ${paymasterType} paymaster successful`, | |
); | |
} catch (e) { | |
const message = (e as Error).message; | |
// if already verified, that's fine, don't fail | |
// NOTE: annoyingly "similar match" is auto-generated from prior verified matching | |
// byte code (with different constructor arguments) and causes this to fail | |
// therefore it is impossible to achieve an "exact match" | |
// for a contract when there is previously verified matching byte code | |
if (message.includes("Reason: Already Verified")) { | |
console.debug( | |
`verifyPaymaster - Verification successful, ${paymasterType} paymaster already verified`, | |
); | |
} else { | |
console.warn( | |
`Could not verify ${paymasterType} paymaster`, | |
(e as Error).message, | |
); | |
throw e; | |
} | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types"; | |
import { AppEnvEnum } from "@novel/shared/utils/env"; | |
import { getPaymaster } from "./getPaymaster"; | |
interface IWithdrawFromPaymasterParams { | |
env: AppEnvEnum; | |
ethers?: HardhatEthersHelpers; | |
paymasterAddress?: string; | |
isTestnet?: boolean; | |
} | |
// Withdraws from paymaster | |
export const withdrawFromPaymaster = async ({ | |
env, | |
ethers, | |
// Address of the Paymaster | |
// If not specified, fills Paymaster from config file | |
paymasterAddress, | |
isTestnet = !!process.env.TESTNET, | |
}: IWithdrawFromPaymasterParams) => { | |
const paymaster = await getPaymaster({ | |
env, | |
ethers, | |
paymasterAddress, | |
}); | |
paymasterAddress = paymaster.address; | |
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`; | |
console.debug( | |
`withdrawFromPaymaster - withdrawing from ${paymasterType} paymaster`, | |
); | |
const withdrawTxn = await paymaster.withdraw(); | |
console.debug("withdrawFromPaymaster - waiting on withdraw transaction"); | |
await withdrawTxn.wait(); | |
console.debug( | |
`withdrawFromPaymaster - withdraw from ${paymasterType} paymaster was successful!`, | |
); | |
return true; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment