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 all 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
42 changes: 28 additions & 14 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,36 @@ concurrency:
cancel-in-progress: true

jobs:
lint:
Tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./.devcontainer/install-tools.sh
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
./.devcontainer/install-tools.sh
yarn
env:
SKIP_FOUNDRY: 1
- run: yarn
- run: yarn lint

tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./.devcontainer/install-tools.sh
- name: Lint
run: |
yarn fmt
yarn lint
- name: Run tests
run: |
docker compose up zksync -d
echo "Waiting for zksync to be up..."
while ! nc -z localhost 3050 || ! nc -z localhost 8545; do
echo "zksync is not yet running, waiting..."
sleep 10
done
echo "zksync ports are up! Just wait extra 3 minutes to be sure..."
docker compose logs -f &
LOGS_PID=$!
sleep 180
kill $LOGS_PID
yarn test
env:
SKIP_FOUNDRY: 1
- run: yarn
- run: yarn test
WALLET_PRIVATE_KEY: "0xd293c684d884d56f8d6abd64fc76757d3664904e309a0645baf8522ab6366d9e"
GOVERNANCE_ADDRESS: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049"
WHITELIST_ADMIN_ADDRESS: "0xa61464658AfeAf65CccaaFD3a512b69A83B77618"
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>/**"]
}
]
}

35 changes: 25 additions & 10 deletions contracts/contracts/NODL.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,39 @@ import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC2
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

contract NODL is ERC20, ERC20Burnable, ERC20Capped, AccessControl {
contract NODL is ERC20Burnable, ERC20Capped, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

constructor(address defaultAdmin, address minter) ERC20("Nodle Token", "NODL") ERC20Capped(21000000000 * (10 ** 18)) {
constructor(
address defaultAdmin,
address minter
)
ERC20("Nodle Token", "NODL")
ERC20Capped(21_000_000_000 * (10 ** decimals()))
{
_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin);
_grantRole(MINTER_ROLE, minter);
}

/**
* @dev Returns the number of decimals used to get NODL's user representation.
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view virtual override returns (uint8) {
return 11;
}

function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}

// The following functions are overrides required by Solidity.

function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Capped)
{
super._update(from, to, value);
function _update(
address from,
address to,
uint256 value
) internal override(ERC20, ERC20Capped) {
ERC20Capped._update(from, to, value);
}
}
}
32 changes: 18 additions & 14 deletions contracts/contracts/contentsign/ContentSignNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,40 @@ 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) {
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++;
function safeMint(
address to,
string memory uri
) public onlyRole(MINTER_ROLE) {
uint256 tokenId = nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}

// The following functions are overrides required by Solidity.

function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}

function supportsInterface(bytes4 interfaceId)
function supportsInterface(
bytes4 interfaceId
)
public
view
override(ERC721, ERC721URIStorage, AccessControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
}
35 changes: 21 additions & 14 deletions contracts/contracts/paymasters/BasePaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,18 @@ abstract contract BasePaymaster is IPaymaster, AccessControl {
external
payable
onlyBootloader
returns (bytes4 magic, bytes memory context)
returns (bytes4 magic, bytes memory /* context */)
{
// 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");
revert InvalidPaymasterInput(
"The standard paymaster input must be at least 4 bytes long"
);
}

bytes4 paymasterInputSelector = bytes4(
transaction.paymasterInput[0:4]
);
bytes4 paymasterInputSelector = bytes4(transaction.paymasterInput[0:4]);

// Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit,
// neither paymaster nor account are allowed to access this context variable.
Expand All @@ -65,16 +63,17 @@ abstract contract BasePaymaster is IPaymaster, AccessControl {
} else if (
paymasterInputSelector == IPaymasterFlow.approvalBased.selector
) {
(address token, uint256 amount, bytes memory data) = abi.decode(
transaction.paymasterInput[4:],
(address, uint256, bytes)
);
(address token, uint256 minimalAllowance, bytes memory data) = abi
.decode(
transaction.paymasterInput[4:],
(address, uint256, bytes)
);

_validateAndPayApprovalBasedFlow(
userAddress,
destAddress,
token,
amount,
minimalAllowance,
data,
requiredETH
);
Expand Down Expand Up @@ -113,12 +112,20 @@ abstract contract BasePaymaster is IPaymaster, AccessControl {
bytes32,
ExecutionResult txResult,
uint256 maxRefundedGas
)
external
payable
override
// solhint-disable-next-line no-empty-blocks
) external payable override onlyBootloader {
onlyBootloader
{
// Refunds are not supported yet.
}

function withdraw(address to, uint256 amount) external onlyRole(WITHDRAWER_ROLE) {
function withdraw(
address to,
uint256 amount
) external onlyRole(WITHDRAWER_ROLE) {
(bool success, ) = payable(to).call{value: amount}("");
if (!success) revert FailedToWithdraw();
}
Expand Down
95 changes: 95 additions & 0 deletions contracts/contracts/paymasters/Erc20Paymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

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

contract Erc20Paymaster is BasePaymaster {
using Math for uint256;

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();
error FeeTooHigh(uint256 feePrice, uint256 requiredETH);

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
);

(bool succeeded, uint256 requiredToken) = requiredETH.tryMul(feePrice);
if (!succeeded) {
revert FeeTooHigh(feePrice, requiredETH);
}

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

try
IERC20(token).transferFrom(userAddress, thisAddress, requiredToken)
{
return;
} catch (bytes memory revertReason) {
revert FeeTransferFailed(revertReason);
}
}
}
12 changes: 7 additions & 5 deletions contracts/contracts/paymasters/WhitelistPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ contract WhitelistPaymaster is BasePaymaster {
// role to whitelisted users.
// This allows us to avoid reinventing the wheel and to piggy back on existing code and methods
// that are already audited and tested.
bytes32 public constant WHITELISTED_USER_ROLE = keccak256("WHITELISTED_USER_ROLE");
bytes32 public constant WHITELIST_ADMIN_ROLE = keccak256("WHITELIST_ADMIN_ROLE");
bytes32 public constant WHITELISTED_USER_ROLE =
keccak256("WHITELISTED_USER_ROLE");
bytes32 public constant WHITELIST_ADMIN_ROLE =
keccak256("WHITELIST_ADMIN_ROLE");

mapping(address => bool) public isWhitelistedContract;

Expand Down Expand Up @@ -46,9 +48,9 @@ contract WhitelistPaymaster is BasePaymaster {
return hasRole(WHITELISTED_USER_ROLE, user);
}

function _setContractWhitelist(address[] memory whitelistedContracts)
internal
{
function _setContractWhitelist(
address[] memory whitelistedContracts
) internal {
for (uint256 i = 0; i < whitelistedContracts.length; i++) {
isWhitelistedContract[whitelistedContracts[i]] = true;
}
Expand Down
Loading