Skip to content

Commit

Permalink
Merge pull request #4 from mtiutin/main
Browse files Browse the repository at this point in the history
PureFi Paymaster implementation
  • Loading branch information
uF4No authored Nov 7, 2022
2 parents 0bb3e64 + 0dd5677 commit 4b39449
Show file tree
Hide file tree
Showing 16 changed files with 4,594 additions and 0 deletions.
6 changes: 6 additions & 0 deletions submissions/PureFiPaymaster/About.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Author

Miha Tiutin, CTO @ PureFi
contact: [email protected]
telegram: @mtiutin
ZkSync address: 0x13a8CB7f655162F468B2Bc4CD209c22704C9925A
73 changes: 73 additions & 0 deletions submissions/PureFiPaymaster/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# PureFi custom Paymaster

Original codebase is derived from the "Build a custom paymaster" tutorial from the [zkSync v2 documentation](https://v2-docs.zksync.io/dev/).

The idea of the Paymaster is inspired by the [PureFiContext](https://github.com/purefiprotocol/sdk-solidity/blob/master/contracts/PureFiContext.sol) contract, which is following OpenZeppelin re-entrancy guard contract design approach. Meaning that it is setting context storage variables before target Tx starts, and erases after it finishes.

[PureFiContext](https://github.com/purefiprotocol/sdk-solidity/blob/master/contracts/PureFiContext.sol) itself is the part of the PureFi protocol implementation which delivers AML (Anti-money laundering) verification data into the smart contract, thus allowing smart contract designers and operators take the decision either to accept or to block incoming funds (due to a high risk associated with the address or transaction, for example). PureFi makes use of the so called Rules (identified by RuleID), which associates the identifier (ruleID) with the explisit verification that is
performed on the PureFi Issuer side. This process is typically initiated by the front-end (dApp), then verification is performed and signed package is provided to be used by the dApp to convince Smart contract that required veficication was performed, and it can accept funds. The detailed guide and description can be found [here](https://docs.purefi.io/integrate/products/aml-sdk/interactive-mode)

[PureFiPaymaster](./contracts/PureFiPaymaster.sol) accepts signed packages issued by the PureFi issuer within the Paymaster payload
```
(uint[4] memory data, bytes memory signature) = abi.decode(input, (uint[4], bytes));
```

then decodes and validates this data
```
address issuer = recoverSigner(keccak256(abi.encodePacked(data[0], data[1], data[2], data[3])), signature);
require(hasRole(ISSUER_ROLE, issuer), "PureFiPaymaster: Issuer signature invalid");
require(data[2] + graceTime >= block.timestamp, "PureFiPaymaster: Credentials data expired");
```

then set up the transaction context variables.
```
address contextAddress = address(uint160(_transaction.to));
contextData[contextAddress] = PureFiContext(data[0], data[1], data[2], address(uint160(data[3])), issuer);
```

These variables could be then queried by the target smart contract [FilteredPool.sol](./contracts/example/FilteredPool.sol) to make sure that verification was performed according to the expected rule.
```
function depositTo(
uint256 _amount,
address _to
) external {
//verify sender funds via PureFiContext
(uint256 sessionID, uint256 ruleID, , address verifiedUser, ) = contextHolder.pureFiContextData();
require(ruleID == expectedDepositRuleID, "Invalid ruleID provided");
require(msg.sender == verifiedUser, "Invalid verifiedUser provided");
_deposit(_amount, _to);
}
```
> contextHolder in the code above is actually the PureFiPaymaster contract.
This way the smart contract can be sure that funds and the sender address were verified according to the ruleID expected, and thus, it's safe to accept these funds from the user.

## Deployment and usage

> complete deployment and test requires about 0.03 ETH
create a folder `network_keys` inside the project folder and put a file `secrets.json` into this folder. The structure of the secrets file is the following:
```
{
"mnemonic": "YOUR MNEMONIC HERE",
"infuraApiKey": "<YOUR API KEY HERE>",
"privateKey" : "YOUR MAIN WALLET PK HERE, WITH SOME BALANCE IN ZKSYNC TESTNET"
}
```

Compiling and deployment is performed by the following script:
- `redeploy.sh`

the test is performed by the following command:
- `yarn hardhat deploy-zksync --script use-paymaster-modified.ts`:

## Testing flow
1. ERC20, FilteredPool and PureFiPaymaster are deployed
2. a new wallet (a.k.a. emptyWallet) is generated and obtains 100 ERC20 tokens. No ETH balance exists on this address
3. approval tx is issued from emptyWallet to allow FilteredPool to grap tokens. Tx is processed via PureFiPaymaster which pays for this tx
3. deposit tx is issued from emptyWallet to FilteredPool. ERC20 tokens are transferred from emptyWallet to FilteredPool, totalCap is encreased.
3. withdraw tx is issued from emptyWallet to FilteredPool. ERC20 tokens are transferred from FilteredPool to emptyWallet, totalCap is decreased.

*Important:* deposit and withdraw operations are using different PureFi rules (which is usually the case in real life)
10 changes: 10 additions & 0 deletions submissions/PureFiPaymaster/code/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
artifacts-zk
cache-zk

cache
typechain-types
network_keys
package-lock.json
.DS_store
*.log
73 changes: 73 additions & 0 deletions submissions/PureFiPaymaster/code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# PureFi custom Paymaster

Original codebase is derived from the "Build a custom paymaster" tutorial from the [zkSync v2 documentation](https://v2-docs.zksync.io/dev/).

The idea of the Paymaster is inspired by the [PureFiContext](https://github.com/purefiprotocol/sdk-solidity/blob/master/contracts/PureFiContext.sol) contract, which is following OpenZeppelin re-entrancy guard contract design approach. Meaning that it is setting context storage variables before target Tx starts, and erases after it finishes.

[PureFiContext](https://github.com/purefiprotocol/sdk-solidity/blob/master/contracts/PureFiContext.sol) itself is the part of the PureFi protocol implementation which delivers AML (Anti-money laundering) verification data into the smart contract, thus allowing smart contract designers and operators take the decision either to accept or to block incoming funds (due to a high risk associated with the address or transaction, for example). PureFi makes use of the so called Rules (identified by RuleID), which associates the identifier (ruleID) with the explisit verification that is
performed on the PureFi Issuer side. This process is typically initiated by the front-end (dApp), then verification is performed and signed package is provided to be used by the dApp to convince Smart contract that required veficication was performed, and it can accept funds. The detailed guide and description can be found [here](https://docs.purefi.io/integrate/products/aml-sdk/interactive-mode)

[PureFiPaymaster](./contracts/PureFiPaymaster.sol) accepts signed packages issued by the PureFi issuer within the Paymaster payload
```
(uint[4] memory data, bytes memory signature) = abi.decode(input, (uint[4], bytes));
```

then decodes and validates this data
```
address issuer = recoverSigner(keccak256(abi.encodePacked(data[0], data[1], data[2], data[3])), signature);
require(hasRole(ISSUER_ROLE, issuer), "PureFiPaymaster: Issuer signature invalid");
require(data[2] + graceTime >= block.timestamp, "PureFiPaymaster: Credentials data expired");
```

then set up the transaction context variables.
```
address contextAddress = address(uint160(_transaction.to));
contextData[contextAddress] = PureFiContext(data[0], data[1], data[2], address(uint160(data[3])), issuer);
```

These variables could be then queried by the target smart contract [FilteredPool.sol](./contracts/example/FilteredPool.sol) to make sure that verification was performed according to the expected rule.
```
function depositTo(
uint256 _amount,
address _to
) external {
//verify sender funds via PureFiContext
(uint256 sessionID, uint256 ruleID, , address verifiedUser, ) = contextHolder.pureFiContextData();
require(ruleID == expectedDepositRuleID, "Invalid ruleID provided");
require(msg.sender == verifiedUser, "Invalid verifiedUser provided");
_deposit(_amount, _to);
}
```
> contextHolder in the code above is actually the PureFiPaymaster contract.
This way the smart contract can be sure that funds and the sender address were verified according to the ruleID expected, and thus, it's safe to accept these funds from the user.

## Deployment and usage

> complete deployment and test requires about 0.03 ETH
create a folder `network_keys` inside the project folder and put a file `secrets.json` into this folder. The structure of the secrets file is the following:
```
{
"mnemonic": "YOUR MNEMONIC HERE",
"infuraApiKey": "<YOUR API KEY HERE>",
"privateKey" : "YOUR MAIN WALLET PK HERE, WITH SOME BALANCE IN ZKSYNC TESTNET"
}
```

Compiling and deployment is performed by the following script:
- `redeploy.sh`

the test is performed by the following command:
- `yarn hardhat deploy-zksync --script use-paymaster-modified.ts`:

## Testing flow
1. ERC20, FilteredPool and PureFiPaymaster are deployed
2. a new wallet (a.k.a. emptyWallet) is generated and obtains 100 ERC20 tokens. No ETH balance exists on this address
3. approval tx is issued from emptyWallet to allow FilteredPool to grap tokens. Tx is processed via PureFiPaymaster which pays for this tx
3. deposit tx is issued from emptyWallet to FilteredPool. ERC20 tokens are transferred from emptyWallet to FilteredPool, totalCap is encreased.
3. withdraw tx is issued from emptyWallet to FilteredPool. ERC20 tokens are transferred from FilteredPool to emptyWallet, totalCap is decreased.

*Important:* deposit and withdraw operations are using different PureFi rules (which is usually the case in real life)
26 changes: 26 additions & 0 deletions submissions/PureFiPaymaster/code/contracts/MyERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyERC20 is ERC20 {
uint8 private _decimals;

constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) ERC20(name_, symbol_) {
_decimals = decimals_;
}

function mint(address _to, uint256 _amount) public returns (bool) {
_mint(_to, _amount);
return true;
}

function decimals() public view override returns (uint8) {
return _decimals;
}
}
171 changes: 171 additions & 0 deletions submissions/PureFiPaymaster/code/contracts/PureFiPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import {IPaymaster, ExecutionResult} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol";
import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol";
import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/TransactionHelper.sol";

import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";

import "@openzeppelin/contracts/interfaces/IERC20.sol";

import "./interfaces/IPureFiTxContext.sol";
import "./libraries/SignLib.sol";
import "./libraries/BytesLib.sol";

contract PureFiPaymaster is AccessControl, SignLib, IPaymaster, IPureFiTxContext{

address public pureFiSubscriptionContract;

bytes32 public constant ISSUER_ROLE = 0x0000000000000000000000000000000000000000000000000000000000009999;
// context data
struct PureFiContext{
uint256 sessionID;
uint256 ruleID;
uint256 validUntil;
address sender;
address issuer;
}


uint8 public testMode;
uint256 internal graceTime; //a period verification credentials are considered valid;

mapping (address => PureFiContext) contextData; //context data structure


modifier onlyBootloader() {
require(
msg.sender == BOOTLOADER_FORMAL_ADDRESS,
"PureFiPaymaster: Only bootloader can call this method"
);
// Continure execution if called from the bootloader.
_;
}

constructor(address _admin, address _subscriptionContract) {
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
testMode = 2;
pureFiSubscriptionContract = _subscriptionContract; //this is to validate the PureFi subscription in future.
graceTime = 180;//3 min - default value;
}

function version() external pure returns(uint256){
//xxx.yyy.zzz
return 1000025;
}

function setGracePeriod(uint256 _gracePeriod) external onlyRole(DEFAULT_ADMIN_ROLE){
graceTime = _gracePeriod;
}

function setTestMode(uint8 _testMode) external onlyRole(DEFAULT_ADMIN_ROLE){
testMode = _testMode;
}

function validateAndPayForPaymasterTransaction(
bytes32 _txHash,
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override onlyBootloader returns (bytes memory context) {
require(
_transaction.paymasterInput.length >= 4,
"PureFiPaymaster: The standard paymaster input must be at least 4 bytes long"
);

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

if (paymasterInputSelector == IPaymasterFlow.general.selector) {
//unpack general() data
(bytes memory input) = abi.decode(_transaction.paymasterInput[4:], (bytes));
//unpack embedded data geberated by the PureFi Issuer service

/**
@param data - signed data package from the off-chain verifier
data[0] - verification session ID
data[1] - circuit ID (if required)
data[2] - verification timestamp
data[3] - verified wallet - to be the same as msg.sender
@param signature - Off-chain issuer signature
*/

(uint[4] memory data, bytes memory signature) = abi.decode(input, (uint[4], bytes));

//get issuer address from the signature
address issuer = recoverSigner(keccak256(abi.encodePacked(data[0], data[1], data[2], data[3])), signature);

require(hasRole(ISSUER_ROLE, issuer), "PureFiPaymaster: Issuer signature invalid");
require(data[2] + graceTime >= block.timestamp, "PureFiPaymaster: Credentials data expired");

address contextAddress = address(uint160(_transaction.to));

//saving data locally so that they can be queried by the customer contract

contextData[contextAddress] = PureFiContext(data[0], data[1], data[2], address(uint160(data[3])), issuer);

// Note, that while the minimal amount of ETH needed is tx.ergsPrice * tx.ergsLimit,
// neither paymaster nor account are allowed to access this context variable.
uint256 requiredETH = _transaction.ergsLimit *
_transaction.maxFeePerErg;

// require(msg.value >= requiredETH, "PureFiPaymaster: not enough ETH to pay for tx");

// The bootloader never returns any data, so it can safely be ignored here.
(bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{
value: requiredETH
}("");
require(success, "PureFiPaymaster: Failed to transfer funds to the bootloader");

if(testMode > 7){
// delete contextData[contextAddress]; // THIS DOESN'T WORK
contextData[contextAddress] = PureFiContext(0, 0, 0, address(uint160(0)), address(0)); // THIS DOESN'T WORK EITHER. Don't set testMode > 7
}
}
else {
revert("Unsupported paymaster flow");
}
}

function pureFiContextData() external override view returns (
uint256, //sessionID
uint256, //ruleID
uint256, //validUntil
address, //sender
address //issuer
) {
address _contextAddr = msg.sender;
if(testMode > 7) //don't chage testMode to > 7, the code below will result in "revert" on gasEstimation.
require(contextData[_contextAddr].sessionID > 0, "PureFi: session context is not initialized");
return (contextData[_contextAddr].sessionID, contextData[_contextAddr].ruleID, contextData[_contextAddr].validUntil, contextData[_contextAddr].sender, contextData[_contextAddr].issuer);
}

/**
for testing purposes only
*/
function pureFiContextDataX(address _contextAddr) external view returns (
uint256, //sessionID
uint256, //ruleID
uint256, //validUntil
address, //sender
address //issuer
)
{
return (contextData[_contextAddr].sessionID, contextData[_contextAddr].ruleID, contextData[_contextAddr].validUntil, contextData[_contextAddr].sender, contextData[_contextAddr].issuer);
}

function postOp(
bytes calldata _context,
Transaction calldata _transaction,
bytes32 _txHash,
bytes32 _suggestedSignedHash,
ExecutionResult _txResult,
uint256 _maxRefundedErgs
) external payable onlyBootloader {
//MIHA: no storage writing operations here. results in CALL_EXCEPTION
}

receive() external payable {}
}
Loading

0 comments on commit 4b39449

Please sign in to comment.