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

feat: add erc20 paymaster #6

Merged
merged 46 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3a4404f
feat: add erc20 paymaster
aliXsed Jan 25, 2024
e74cd34
chore: remove unused imports
aliXsed Jan 25, 2024
f2f8194
chore: fix whitespacing
aliXsed Jan 25, 2024
666fc35
fix: remove granting role to admin as it's done in base
aliXsed Jan 25, 2024
b74cd7a
Merge branch 'main' into aliX/nodl-paymaster
aliXsed Jan 25, 2024
1716276
fix: format and warnings
aliXsed Jan 25, 2024
597987c
Merge branch 'main' into aliX/nodl-paymaster
aliXsed Jan 26, 2024
bcfb8ba
test: add nonce option to deploy contract and add a Erc20Paymaster test
aliXsed Jan 26, 2024
b01c715
feat: revoke price oracle role for nodl-paymaster plus more tests
aliXsed Jan 30, 2024
cfa1bfa
Merge branch 'main' into aliX/nodl-paymaster
aliXsed Jan 30, 2024
a2c04d2
test(Erc20Paymaster): fix "Non Admin cannot grant or revoke roles"
aliXsed Jan 30, 2024
dc90979
test: random user can mint NFT using nodl paymaster
aliXsed Jan 30, 2024
c003edd
test: ensure transaction published in block + tidy up
aliXsed Jan 31, 2024
eec6dd7
test: use local node for testing, setup eth provider correctly
aliXsed Feb 6, 2024
3ac3c1a
feat: enable reading next token id from ContentSignNfT plus checking …
aliXsed Feb 7, 2024
4e5392f
test: get gas price from provider for calcs to match paymaster
aliXsed Feb 8, 2024
9f3ae8f
test: remove approval for paymaster
aliXsed Feb 8, 2024
7bde422
test: remove tranferring eth to user wallet
aliXsed Feb 8, 2024
3a819e5
feat: Make mint and burn fail if cap is exceeded
aliXsed Feb 9, 2024
b1e915d
test: fix all tests to wait for tx inclusion and explicitly specify g…
aliXsed Feb 9, 2024
ac0a005
test(Erc20Paymaster): fix reading fee price freshly from contract
aliXsed Feb 9, 2024
a2277fa
Update contracts/test/paymasters/Erc20Paymaster.test.ts
aliXsed Feb 11, 2024
d694959
feat(NODL): override decimal function
aliXsed Feb 11, 2024
4bce915
feat(Erc20Paymaster): revert on too high fee explicitly
aliXsed Feb 12, 2024
0936b67
test(Erc20Paymaster): check paymaster failures
aliXsed Feb 12, 2024
f0d89e7
test(Erc20Paymaster): fix fee too high test
aliXsed Feb 12, 2024
c599075
test(Erc20Paymaster): fix "Transaction fails if fee is too high"
aliXsed Feb 12, 2024
ce82446
test(Erc20Paymaster): remove explicit nonce management
aliXsed Feb 12, 2024
30b8dc7
test: remove explicit nonce management
aliXsed Feb 12, 2024
ab29a50
chore: enforce format check
aliXsed Feb 12, 2024
1372ae5
test: separate getRandomWallet from getWallet
aliXsed Feb 13, 2024
1a32efc
Merge branch 'main' into aliX/nodl-paymaster
aliXsed Feb 13, 2024
8dc6599
chore: fix format
aliXsed Feb 13, 2024
339c9a4
chore: start zksync in detached mode
aliXsed Feb 13, 2024
0c56484
chore: add extra delay for service to be fully up
aliXsed Feb 13, 2024
c87ff9c
chore: try checking port to run test
aliXsed Feb 13, 2024
5044924
chore: check port 8545 alongside 3050
aliXsed Feb 13, 2024
50defc6
chore: extra wait
aliXsed Feb 13, 2024
f481207
chore: use different detach mode for docker
aliXsed Feb 13, 2024
e6f1d60
chore: print docker logs while waiting
aliXsed Feb 13, 2024
8bbfe91
ci: fix service in the background
aliXsed Feb 13, 2024
88726b7
ci: alternative approach
aliXsed Feb 13, 2024
57fdaa4
ci: another attempt
aliXsed Feb 13, 2024
445ce34
ci: fix env
aliXsed Feb 13, 2024
15ce634
ci: extra wait
aliXsed Feb 13, 2024
bbf7bde
ci: kill better
aliXsed Feb 13, 2024
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
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Hardhat Test",
"program": "${workspaceFolder}/contracts/node_modules/.bin/hardhat",
"args": ["test", "--network", "localNode"],
"cwd": "${workspaceFolder}/contracts",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
}
]
}

6 changes: 2 additions & 4 deletions contracts/contracts/contentsign/ContentSignNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,19 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
/// configurable URL
contract ContentSignNFT is ERC721, ERC721URIStorage, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
uint256 private _nextTokenId;
uint256 public nextTokenId;

constructor(string memory name, string memory symbol, address admin) ERC721(name, symbol) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(MINTER_ROLE, admin);
}

function safeMint(address to, string memory uri) public onlyRole(MINTER_ROLE) {
uint256 tokenId = _nextTokenId++;
uint256 tokenId = nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}

// The following functions are overrides required by Solidity.

function tokenURI(uint256 tokenId)
public
view
Expand Down
6 changes: 2 additions & 4 deletions contracts/contracts/paymasters/BasePaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ abstract contract BasePaymaster is IPaymaster, AccessControl {
{
// By default we consider the transaction as accepted.
magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
// By default no context will be returned unless the paymaster flow requires a post transaction call.
context = new bytes(0);

if (transaction.paymasterInput.length < 4) {
revert InvalidPaymasterInput("The standard paymaster input must be at least 4 bytes long");
Expand All @@ -65,7 +63,7 @@ abstract contract BasePaymaster is IPaymaster, AccessControl {
} else if (
paymasterInputSelector == IPaymasterFlow.approvalBased.selector
) {
(address token, uint256 amount, bytes memory data) = abi.decode(
(address token, uint256 minimalAllowance, bytes memory data) = abi.decode(
transaction.paymasterInput[4:],
(address, uint256, bytes)
);
Expand All @@ -74,7 +72,7 @@ abstract contract BasePaymaster is IPaymaster, AccessControl {
userAddress,
destAddress,
token,
amount,
minimalAllowance,
data,
requiredETH
);
Expand Down
78 changes: 78 additions & 0 deletions contracts/contracts/paymasters/Erc20Paymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

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

contract Erc20Paymaster is BasePaymaster {
bytes32 public constant PRICE_ORACLE_ROLE = keccak256("PRICE_ORACLE_ROLE");

uint256 public feePrice;
address public allowedToken;

error AllowanceNotEnough(uint256 provided, uint256 required);
error FeeTransferFailed(bytes reason);
error TokenNotAllowed();

constructor(address admin, address priceOracle, address erc20, uint256 initialFeePrice) BasePaymaster(admin, admin) {
_grantRole(PRICE_ORACLE_ROLE, priceOracle);
allowedToken = erc20;
feePrice = initialFeePrice;
}

function grantPriceOracleRole(address oracle) public onlyRole(DEFAULT_ADMIN_ROLE) {
_grantRole(PRICE_ORACLE_ROLE, oracle);
}

function revokePriceOracleRole(address oracle) public onlyRole(DEFAULT_ADMIN_ROLE) {
_revokeRole(PRICE_ORACLE_ROLE, oracle);
}

function updateFeePrice(uint256 newFeePrice) public onlyRole(PRICE_ORACLE_ROLE) {
feePrice = newFeePrice;
}

function _validateAndPayGeneralFlow(
address /* from */,
address /* to */,
uint256 /* requiredETH */
) internal pure override {
revert PaymasterFlowNotSupported();
}

function _validateAndPayApprovalBasedFlow(
address userAddress,
address /* destAddress */,
address token,
uint256 amount,
bytes memory /* data */,
uint256 requiredETH
) internal override {

if (token != allowedToken) {
revert TokenNotAllowed();
}

address thisAddress = address(this);

uint256 providedAllowance = IERC20(token).allowance(
userAddress,
thisAddress
);

uint256 requiredToken = requiredETH * feePrice;

if (providedAllowance < requiredToken) {
revert AllowanceNotEnough(providedAllowance, requiredToken);
}

try
IERC20(token).transferFrom(userAddress, thisAddress, requiredToken)
{
return;
} catch (bytes memory revertReason) {
revert FeeTransferFailed(revertReason);
}
}
}
9 changes: 7 additions & 2 deletions contracts/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import { deployContract, getGovernance, getWhitelistAdmin } from "./utils";
import { execSync } from "child_process";

export default async function() {
await deployContract("NODL", [getGovernance(), getGovernance()]);
const nodl = await deployContract("NODL", [getGovernance(), getGovernance()]);
const nodlAddress = await nodl.getAddress();

const nft = await deployContract("ContentSignNFT", ["Click", "CLK", getWhitelistAdmin()]);
const nftAddress = await nft.getAddress();
await deployContract("WhitelistPaymaster", [getGovernance(), getWhitelistAdmin(), [await nft.getAddress()]]);
await deployContract("WhitelistPaymaster", [getGovernance(), getWhitelistAdmin(), [nftAddress]]);

const initialFeePrice = 1; // Means 1 nodl per 1 wei
const priceOracle = getWhitelistAdmin(); // For now we assume that the whitelist admin is the same as the price oracle for NODL paymaster
const paymasterContract = await deployContract("Erc20Paymaster", [getGovernance(), priceOracle, nodlAddress, initialFeePrice]);

// used for docker compose setup so we can deploy a The Graph indexer on the NFT contract
execSync(`echo "${nftAddress}" > .nft-contract-address`);
Expand Down
43 changes: 25 additions & 18 deletions contracts/deploy/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Provider, Wallet } from "zksync-ethers";
import * as hre from "hardhat";
import { ethers } from "ethers";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import dotenv from "dotenv";
import { BigNumberish, formatEther } from "ethers";

import "@matterlabs/hardhat-zksync-node/dist/type-extensions";
import "@matterlabs/hardhat-zksync-verify/dist/src/type-extensions";
Expand All @@ -13,25 +13,19 @@ dotenv.config();
export const getProvider = () => {
const rpcUrl = hre.network.config.url;
if (!rpcUrl) throw `⛔️ RPC URL wasn't found in "${hre.network.name}"! Please add a "url" field to the network config in hardhat.config.ts`;
return new Provider(rpcUrl);;
}

// Initialize zkSync Provider
const provider = new Provider(rpcUrl);

return provider;
export const getEthProvider = () => {
return hre.network.config.ethNetwork ?? ethers.getDefaultProvider(hre.network.config.ethNetwork);
}

export const getWallet = (privateKey?: string) => {
if (!privateKey) {
// Get wallet private key from .env file
if (!process.env.WALLET_PRIVATE_KEY) throw "⛔️ Wallet private key wasn't found in .env file!";
const randomWallet = Wallet.createRandom();
return new Wallet(randomWallet.privateKey, getProvider(), getEthProvider());
}

const provider = getProvider();

// Initialize zkSync Wallet
const wallet = new Wallet(privateKey ?? process.env.WALLET_PRIVATE_KEY!, provider);

return wallet;
return new Wallet(privateKey ?? process.env.WALLET_PRIVATE_KEY!, getProvider(), getEthProvider());
aliXsed marked this conversation as resolved.
Show resolved Hide resolved
}

export const getGovernance = () => {
Expand All @@ -47,7 +41,7 @@ export const getWhitelistAdmin = () => {
export const verifyEnoughBalance = async (wallet: Wallet, amount: bigint) => {
// Check if the wallet has enough balance
const balance = await wallet.getBalance();
if (balance < amount) throw `⛔️ Wallet balance is too low! Required ${formatEther(amount)} ETH, but current ${wallet.address} balance is ${formatEther(balance)} ETH`;
if (balance < amount) throw `⛔️ Wallet balance is too low! Required ${ethers.formatEther(amount)} ETH, but current ${wallet.address} balance is ${ethers.formatEther(balance)} ETH`;
}

/**
Expand Down Expand Up @@ -86,7 +80,7 @@ type DeployContractOptions = {
*/
skipChecks?: boolean
}
export const deployContract = async (contractArtifactName: string, constructorArguments?: any[], options?: DeployContractOptions) => {
export const deployContract = async (contractArtifactName: string, constructorArguments?: any[], options?: DeployContractOptions, nonce?: number) => {
const log = (message: string) => {
if (!options?.silent) console.log(message);
}
Expand All @@ -107,13 +101,13 @@ export const deployContract = async (contractArtifactName: string, constructorAr
if (!options?.skipChecks) {
const deploymentFee = await deployer.estimateDeployFee(artifact, constructorArguments || []);

log(`Estimated total deployment cost: ${formatEther(deploymentFee)} ETH`)
log(`Estimated total deployment cost: ${ethers.formatEther(deploymentFee)} ETH`)

await verifyEnoughBalance(wallet, deploymentFee);
}

// Deploy the contract to zkSync but behind a transparent proxy
const contract = await deployer.deploy(artifact, constructorArguments);
const contract = await deployer.deploy(artifact, constructorArguments, { nonce });
await contract.waitForDeployment();
const contractAddress = await contract.getAddress();

Expand All @@ -139,6 +133,19 @@ export const deployContract = async (contractArtifactName: string, constructorAr
return contract;
}

/**
* Get a deployed contract given its name and address connected to the provided wallet
* @param name
* @param address
* @param wallet
* @returns the specified contract
*/
export function getContract(name: string, address: string, wallet: Wallet) {
const artifact = hre.artifacts.readArtifactSync(name);
return new ethers.Contract(address, artifact.abi, wallet);
}


/**
* Rich wallets can be used for testing purposes.
* Available on zkSync In-memory node and Dockerized node.
Expand Down
12 changes: 6 additions & 6 deletions contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "@matterlabs/hardhat-zksync-deploy";
import "@matterlabs/hardhat-zksync-verify";

const config: HardhatUserConfig = {
defaultNetwork: "dockerizedNode",
defaultNetwork: "localNode",
networks: {
zkSyncSepoliaTestnet: {
url: "https://sepolia.era.zksync.dev",
Expand All @@ -27,7 +27,7 @@ const config: HardhatUserConfig = {
zksync: true,
verifyURL: "https://zksync2-testnet-explorer.zksync.dev/contract_verification",
},
dockerizedNode: {
localNode: {
url: "http://localhost:3050",
ethNetwork: "http://localhost:8545",
zksync: true,
Expand All @@ -41,10 +41,7 @@ const config: HardhatUserConfig = {
url: "http://zksync:3050",
ethNetwork: "http://geth:8545",
zksync: true,
},
hardhat: {
zksync: true,
},
}
},
zksolc: {
version: "latest",
Expand All @@ -56,6 +53,9 @@ const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
},
mocha: {
timeout: 120000 // Timeout in milliseconds
}
};

export default config;
4 changes: 2 additions & 2 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"deploy": "hardhat deploy-zksync --script deploy.ts",
"compile": "hardhat compile",
"clean": "hardhat clean",
"test": "hardhat test --network hardhat",
"mint": "hardhat run scripts/mint.ts --network dockerizedNode",
"test": "hardhat test --network localNode",
"mint": "hardhat run scripts/mint.ts --network localNode",
"lint": "solhint \"contracts/**/*.sol\""
},
"devDependencies": {
Expand Down
5 changes: 3 additions & 2 deletions contracts/test/paymasters/BasePaymaster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai';
import { Contract, Provider, Wallet, utils } from "zksync-ethers";
import * as ethers from "ethers";
import { setupEnv } from './helpers';
import { deployContract } from '../../deploy/utils';
import { deployContract, getProvider } from '../../deploy/utils';

describe("BasePaymaster", function () {
let paymaster: Contract;
Expand All @@ -23,7 +23,8 @@ describe("BasePaymaster", function () {
withdrawerWallet = result.withdrawerWallet;
sponsorWallet = result.sponsorWallet;
userWallet = result.userWallet;
provider = result.provider;

provider = getProvider();

// using the admin or sponsor wallet to deploy seem to have us run into
// a nonce management bug in zksync-ethers
Expand Down
Loading
Loading