├── .gitattributes ├── codechecks.yml ├── funding.json ├── test ├── chai-setup.ts ├── utils │ ├── eip712.ts │ └── index.ts ├── SpecificForwarder.test.ts └── UniversalForwarding.test.ts ├── .editorconfig ├── .prettierignore ├── tsconfig.deploy.json ├── solc_0.7 └── ERC2771 │ ├── IERC2771.sol │ ├── IForwarderRegistry.sol │ ├── UsingAppendedCallData.sol │ ├── UsingSpecificForwarder.sol │ └── UsingUniversalForwarding.sol ├── solc_0.8 ├── ERC2771 │ ├── IERC2771.sol │ ├── IForwarderRegistry.sol │ ├── UsingAppendedCallData.sol │ ├── UsingSpecificForwarder.sol │ └── UsingUniversalForwarding.sol ├── Test │ ├── TestSpecificForwarderReceiver.sol │ └── TestUniversalForwardingReceiver.sol ├── UniversalForwarder.sol └── ForwarderRegistry.sol ├── .vscode ├── extensions.json.default ├── launch.json.default └── settings.json.default ├── .prettierrc.js ├── tsconfig.json ├── .mocharc.js ├── .env.example ├── .setup.js ├── .gitignore ├── deploy ├── 001_deploy_forwarder_registry.ts └── 002_deploy_universal_forwarder.ts ├── .github └── workflows │ └── main.yml ├── LICENSE ├── hardhat.config.ts ├── utils └── network.ts ├── package.json ├── export └── deploy │ ├── 001_deploy_forwarder_registry.js │ └── 002_deploy_universal_forwarder.js ├── README.md └── _scripts.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /codechecks.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | - name: eth-gas-reporter/codechecks 3 | settings: 4 | branches: 5 | - main 6 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x9b639cc6061e41a1d12adb39619a2fddb7005074cf965a3a6f4eabc6eea31112" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/chai-setup.ts: -------------------------------------------------------------------------------- 1 | import chaiModule from 'chai'; 2 | import {chaiEthers} from 'chai-ethers'; 3 | chaiModule.use(chaiEthers); 4 | export = chaiModule; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = tab 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | export/ 2 | deployments/ 3 | artifacts/ 4 | cache/ 5 | coverage/ 6 | node_modules/ 7 | package.json 8 | typechain/ 9 | _lib/ 10 | *.json 11 | .yalc 12 | -------------------------------------------------------------------------------- /tsconfig.deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["hardhat.config.ts", "./deploy"], 4 | "compilerOptions": { 5 | "outDir": "export" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /solc_0.7/ERC2771/IERC2771.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | interface IERC2771 { 5 | function isTrustedForwarder(address forwarder) external view returns (bool); 6 | } 7 | -------------------------------------------------------------------------------- /solc_0.8/ERC2771/IERC2771.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IERC2771 { 5 | function isTrustedForwarder(address forwarder) external view returns (bool); 6 | } 7 | -------------------------------------------------------------------------------- /solc_0.7/ERC2771/IForwarderRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | interface IForwarderRegistry { 5 | function isApprovedForwarder(address, address) external view returns (bool); 6 | } 7 | -------------------------------------------------------------------------------- /solc_0.8/ERC2771/IForwarderRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IForwarderRegistry { 5 | function isApprovedForwarder(address, address) external view returns (bool); 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "editorconfig.editorconfig", 5 | "esbenp.prettier-vscode", 6 | "hbenl.vscode-mocha-test-adapter", 7 | "juanblanco.solidity" 8 | ], 9 | "unwantedRecommendations": [] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "hardhat node", 8 | "skipFiles": ["/**"], 9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/hardhat", 10 | "args": ["node"], 11 | "cwd": "${workspaceFolder}" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: true, 3 | singleQuote: true, 4 | trailingComma: 'none', 5 | printWidth: 120, 6 | bracketSpacing: false, 7 | overrides: [ 8 | { 9 | files: '*.sol', 10 | options: { 11 | printWidth: 120, 12 | singleQuote: false, 13 | explicitTypes: 'always', 14 | parser: 'solidity-parse' 15 | } 16 | } 17 | ], 18 | plugins: [require('prettier-plugin-solidity')] 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "downlevelIteration": true, 10 | "outDir": "dist" 11 | }, 12 | "include": [ 13 | "hardhat.config.ts", 14 | "./scripts", 15 | "./deploy", 16 | "./test", 17 | "./utils", 18 | "typechain/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | process.env.TS_NODE_FILES = true; 3 | module.exports = { 4 | 'allow-uncaught': true, 5 | diff: true, 6 | extension: ['ts'], 7 | recursive: true, 8 | reporter: 'spec', 9 | require: ['ts-node/register', 'hardhat/register'], // ['ts-node/register/transpile-only'], (for yarn link ) 10 | slow: 300, 11 | spec: 'test/**/*.test.ts', 12 | timeout: 20000, 13 | ui: 'bdd', 14 | watch: false, 15 | 'watch-files': ['solc_0.8/**/*.sol', 'test/**/*.ts'] 16 | }; 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # network specific node uri : `"ETH_NODE_URI_" + networkName.toUpperCase()` 2 | ETH_NODE_URI_MAINNET=https://eth-mainnet.alchemyapi.io/v2/ 3 | # generic node uri (if no specific found) : 4 | ETH_NODE_URI=https://{{networkName}}.infura.io/v3/ 5 | 6 | # network specific mnemonic : `"MNEMONIC_ " + networkName.toUpperCase()` 7 | MNEMONIC_MAINNET= 8 | # generic mnemonic (if no specific found): 9 | MNEMONIC= 10 | 11 | # coinmarketcap api key for gas report 12 | COINMARKETCAP_API_KEY= 13 | -------------------------------------------------------------------------------- /.setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs-extra'); 3 | function copyFromDefault(p) { 4 | if (!fs.existsSync(p)) { 5 | const defaultFile = `${p}.default`; 6 | if (fs.existsSync(defaultFile)) { 7 | fs.copyFileSync(`${p}.default`, p); 8 | } 9 | } 10 | } 11 | 12 | ['.vscode/settings.json', '.vscode/extensions.json', '.vscode/launch.json'].map(copyFromDefault); 13 | 14 | fs.emptyDirSync('_lib/openzeppelin'); 15 | fs.copySync('node_modules/@openzeppelin', '_lib/openzeppelin', { 16 | recursive: true, 17 | dereference: true 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | artifacts/ 3 | 4 | coverage* 5 | typechain/ 6 | 7 | _lib 8 | 9 | .vscode/* 10 | !.vscode/settings.json.default 11 | !.vscode/launch.json.default 12 | !.vscode/extensions.json.default 13 | 14 | node_modules/ 15 | .env 16 | 17 | .yalc 18 | yalc.lock 19 | 20 | contractsInfo.json 21 | deployments/hardhat 22 | deployments/localhost 23 | 24 | export/* 25 | !export/artifacts 26 | !export/deploy 27 | export/deploy/* 28 | !export/deploy/001_deploy_forwarder_registry.js 29 | !export/deploy/002_deploy_universal_forwarder.js 30 | 31 | private/ 32 | -------------------------------------------------------------------------------- /deploy/001_deploy_forwarder_registry.ts: -------------------------------------------------------------------------------- 1 | import {HardhatRuntimeEnvironment} from 'hardhat/types'; 2 | import {DeployFunction} from 'hardhat-deploy/types'; 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const {deployments, getNamedAccounts} = hre; 6 | const {deploy} = deployments; 7 | 8 | const {deployer} = await getNamedAccounts(); 9 | 10 | await deploy('ForwarderRegistry', { 11 | from: deployer, 12 | log: true, 13 | deterministicDeployment: true 14 | }); 15 | }; 16 | export default func; 17 | func.tags = ['ForwarderRegistry']; 18 | -------------------------------------------------------------------------------- /deploy/002_deploy_universal_forwarder.ts: -------------------------------------------------------------------------------- 1 | import {HardhatRuntimeEnvironment} from 'hardhat/types'; 2 | import {DeployFunction} from 'hardhat-deploy/types'; 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const {deployments, getNamedAccounts} = hre; 6 | const {deploy} = deployments; 7 | 8 | const {deployer} = await getNamedAccounts(); 9 | 10 | await deploy('UniversalForwarder', { 11 | from: deployer, 12 | log: true, 13 | deterministicDeployment: true 14 | }); 15 | }; 16 | export default func; 17 | func.tags = ['UniversalForwarder']; 18 | -------------------------------------------------------------------------------- /.vscode/settings.json.default: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": true 6 | }, 7 | "solidity.linter": "solhint", 8 | "solidity.enableLocalNodeCompiler": false, 9 | "solidity.compileUsingRemoteVersion": "v0.7.6+commit.7338295f", 10 | "solidity.packageDefaultDependenciesContractsDirectory": "", 11 | "solidity.enabledAsYouTypeCompilationErrorCheck": true, 12 | "solidity.validationDelay": 1500, 13 | "solidity.packageDefaultDependenciesDirectory": "node_modules", 14 | "mochaExplorer.env": { 15 | "HARDHAT_CONFIG": "hardhat.config.ts", 16 | "HARDHAT_COMPILE": "true" 17 | }, 18 | "mochaExplorer.require": ["ts-node/register/transpile-only"] 19 | } 20 | -------------------------------------------------------------------------------- /solc_0.7/ERC2771/UsingAppendedCallData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | abstract contract UsingAppendedCallData { 5 | function _lastAppendedDataAsSender() internal pure virtual returns (address payable sender) { 6 | // Copied from openzeppelin : https://github.com/OpenZeppelin/openzeppelin-contracts/blob/9d5f77db9da0604ce0b25148898a94ae2c20d70f/contracts/metatx/ERC2771Context.sol1 7 | // The assembly code is more direct than the Solidity version using `abi.decode`. 8 | // solhint-disable-next-line no-inline-assembly 9 | assembly { 10 | sender := shr(96, calldataload(sub(calldatasize(), 20))) 11 | } 12 | } 13 | 14 | function _msgDataAssuming20BytesAppendedData() internal pure virtual returns (bytes calldata) { 15 | return msg.data[:msg.data.length - 20]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /solc_0.8/ERC2771/UsingAppendedCallData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract UsingAppendedCallData { 5 | function _lastAppendedDataAsSender() internal pure virtual returns (address payable sender) { 6 | // Copied from openzeppelin : https://github.com/OpenZeppelin/openzeppelin-contracts/blob/9d5f77db9da0604ce0b25148898a94ae2c20d70f/contracts/metatx/ERC2771Context.sol1 7 | // The assembly code is more direct than the Solidity version using `abi.decode`. 8 | // solhint-disable-next-line no-inline-assembly 9 | assembly { 10 | sender := shr(96, calldataload(sub(calldatasize(), 20))) 11 | } 12 | } 13 | 14 | function _msgDataAssuming20BytesAppendedData() internal pure virtual returns (bytes calldata) { 15 | return msg.data[:msg.data.length - 20]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /solc_0.8/Test/TestSpecificForwarderReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../ERC2771/UsingSpecificForwarder.sol"; 5 | 6 | contract TestSpecificForwarderReceiver is UsingSpecificForwarder { 7 | mapping(address => uint256) internal _d; 8 | 9 | event Test(address from, string name); 10 | 11 | // solhint-disable-next-line no-empty-blocks 12 | constructor(address forwarder) UsingSpecificForwarder(forwarder) {} 13 | 14 | function doSomething(address from, string calldata name) external payable { 15 | require(_msgSender() == from, "NOT_AUTHORIZED"); 16 | emit Test(from, name); 17 | } 18 | 19 | function test(uint256 d) external { 20 | address sender = _msgSender(); 21 | _d[sender] = d; 22 | } 23 | 24 | function getData(address who) external view returns (uint256) { 25 | return _d[who]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/utils/eip712.ts: -------------------------------------------------------------------------------- 1 | import {EIP712SignerFactory} from '.'; 2 | 3 | export const ForwarderRegistrySignerFactory = new EIP712SignerFactory( 4 | { 5 | name: 'ForwarderRegistry', 6 | chainId: 0 7 | }, 8 | { 9 | ApproveForwarder: [ 10 | { 11 | name: 'signer', 12 | type: 'address' 13 | }, 14 | { 15 | name: 'forwarder', 16 | type: 'address' 17 | }, 18 | { 19 | name: 'approved', 20 | type: 'bool' 21 | }, 22 | { 23 | name: 'nonce', 24 | type: 'uint256' 25 | } 26 | ] 27 | } 28 | ); 29 | 30 | export const UniversalForwarderSignerFactory = new EIP712SignerFactory( 31 | { 32 | name: 'UniversalForwarder', 33 | chainId: 0 34 | }, 35 | { 36 | ApproveForwarderForever: [ 37 | { 38 | name: 'signer', 39 | type: 'address' 40 | }, 41 | { 42 | name: 'forwarder', 43 | type: 'address' 44 | } 45 | ] 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-20.04 6 | strategy: 7 | matrix: 8 | node-version: [15] 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: pnpm/action-setup@v2.2.2 12 | with: 13 | version: 7 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | cache: 'pnpm' 19 | - name: Install dependencies 20 | run: pnpm install 21 | # ------------------------------------------------------------------------- 22 | - name: Formatting 23 | run: pnpm format 24 | - name: Running tests 25 | run: pnpm gas 26 | -------------------------------------------------------------------------------- /solc_0.7/ERC2771/UsingSpecificForwarder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | import "./IERC2771.sol"; 5 | import "./UsingAppendedCallData.sol"; 6 | 7 | abstract contract UsingSpecificForwarder is UsingAppendedCallData, IERC2771 { 8 | address internal immutable _forwarder; 9 | 10 | constructor(address forwarder) { 11 | _forwarder = forwarder; 12 | } 13 | 14 | function isTrustedForwarder(address forwarder) external view virtual override returns (bool) { 15 | return forwarder == _forwarder; 16 | } 17 | 18 | function _msgSender() internal view virtual returns (address payable result) { 19 | if (msg.sender == _forwarder) { 20 | return _lastAppendedDataAsSender(); 21 | } 22 | return msg.sender; 23 | } 24 | 25 | function _msgData() internal view virtual returns (bytes calldata) { 26 | if (msg.sender == _forwarder) { 27 | return _msgDataAssuming20BytesAppendedData(); 28 | } 29 | return msg.data; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /solc_0.8/ERC2771/UsingSpecificForwarder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./IERC2771.sol"; 5 | import "./UsingAppendedCallData.sol"; 6 | 7 | abstract contract UsingSpecificForwarder is UsingAppendedCallData, IERC2771 { 8 | address internal immutable _forwarder; 9 | 10 | constructor(address forwarder) { 11 | _forwarder = forwarder; 12 | } 13 | 14 | function isTrustedForwarder(address forwarder) external view virtual override returns (bool) { 15 | return forwarder == _forwarder; 16 | } 17 | 18 | function _msgSender() internal view virtual returns (address payable result) { 19 | if (msg.sender == _forwarder) { 20 | return _lastAppendedDataAsSender(); 21 | } 22 | return payable(msg.sender); 23 | } 24 | 25 | function _msgData() internal view virtual returns (bytes calldata) { 26 | if (msg.sender == _forwarder) { 27 | return _msgDataAssuming20BytesAppendedData(); 28 | } 29 | return msg.data; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ronan Sandford 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /solc_0.8/Test/TestUniversalForwardingReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../ERC2771/UsingUniversalForwarding.sol"; 5 | 6 | contract TestUniversalForwardingReceiver is UsingUniversalForwarding { 7 | mapping(address => uint256) internal _d; 8 | 9 | event Test(address from, string name); 10 | 11 | // solhint-disable-next-line no-empty-blocks 12 | constructor(IForwarderRegistry forwarderRegistry, address universalForwarder) 13 | UsingUniversalForwarding(forwarderRegistry, universalForwarder) 14 | {} 15 | 16 | function doSomething(address from, string calldata name) external payable { 17 | require(_msgSender() == from, "NOT_AUTHORIZED"); 18 | emit Test(from, name); 19 | } 20 | 21 | receive() external payable { 22 | address sender = _msgSender(); 23 | _d[sender] = 1; 24 | } 25 | 26 | // solhint-disable-next-line no-complex-fallback 27 | fallback() external payable { 28 | address sender = _msgSender(); 29 | _d[sender] = 1; 30 | } 31 | 32 | function test(uint256 d) external { 33 | address sender = _msgSender(); 34 | _d[sender] = d; 35 | } 36 | 37 | function twelve() external { 38 | address sender = _msgSender(); 39 | _d[sender] = 12; 40 | } 41 | 42 | function getData(address who) external view returns (uint256) { 43 | return _d[who]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /solc_0.7/ERC2771/UsingUniversalForwarding.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | import "./UsingAppendedCallData.sol"; 5 | import "./IERC2771.sol"; 6 | import "./IForwarderRegistry.sol"; 7 | 8 | abstract contract UsingUniversalForwarding is UsingAppendedCallData, IERC2771 { 9 | IForwarderRegistry internal immutable _forwarderRegistry; 10 | address internal immutable _universalForwarder; 11 | 12 | constructor(IForwarderRegistry forwarderRegistry, address universalForwarder) { 13 | _universalForwarder = universalForwarder; 14 | _forwarderRegistry = forwarderRegistry; 15 | } 16 | 17 | function isTrustedForwarder(address forwarder) external view virtual override returns (bool) { 18 | return forwarder == _universalForwarder || forwarder == address(_forwarderRegistry); 19 | } 20 | 21 | function _msgSender() internal view virtual returns (address payable) { 22 | address payable msgSender = msg.sender; 23 | address payable sender = _lastAppendedDataAsSender(); 24 | if (msgSender == address(_forwarderRegistry) || msgSender == _universalForwarder) { 25 | // if forwarder use appended data 26 | return sender; 27 | } 28 | 29 | // if msg.sender is neither the registry nor the universal forwarder, 30 | // we have to check the last 20bytes of the call data intepreted as an address 31 | // and check if the msg.sender was registered as forewarder for that address 32 | // we check tx.origin to save gas in case where msg.sender == tx.origin 33 | // solhint-disable-next-line avoid-tx-origin 34 | if (msgSender != tx.origin && _forwarderRegistry.isApprovedForwarder(sender, msgSender)) { 35 | return sender; 36 | } 37 | 38 | return msgSender; 39 | } 40 | 41 | function _msgData() internal view virtual returns (bytes calldata) { 42 | address payable msgSender = msg.sender; 43 | if (msgSender == address(_forwarderRegistry) || msgSender == _universalForwarder) { 44 | // if forwarder use appended data 45 | return _msgDataAssuming20BytesAppendedData(); 46 | } 47 | 48 | // we check tx.origin to save gas in case where msg.sender == tx.origin 49 | // solhint-disable-next-line avoid-tx-origin 50 | if (msgSender != tx.origin && _forwarderRegistry.isApprovedForwarder(_lastAppendedDataAsSender(), msgSender)) { 51 | return _msgDataAssuming20BytesAppendedData(); 52 | } 53 | return msg.data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /solc_0.8/ERC2771/UsingUniversalForwarding.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "./UsingAppendedCallData.sol"; 5 | import "./IERC2771.sol"; 6 | import "./IForwarderRegistry.sol"; 7 | 8 | abstract contract UsingUniversalForwarding is UsingAppendedCallData, IERC2771 { 9 | IForwarderRegistry internal immutable _forwarderRegistry; 10 | address internal immutable _universalForwarder; 11 | 12 | constructor(IForwarderRegistry forwarderRegistry, address universalForwarder) { 13 | _universalForwarder = universalForwarder; 14 | _forwarderRegistry = forwarderRegistry; 15 | } 16 | 17 | function isTrustedForwarder(address forwarder) external view virtual override returns (bool) { 18 | return forwarder == _universalForwarder || forwarder == address(_forwarderRegistry); 19 | } 20 | 21 | function _msgSender() internal view virtual returns (address payable) { 22 | address payable msgSender = payable(msg.sender); 23 | address payable sender = _lastAppendedDataAsSender(); 24 | if (msgSender == address(_forwarderRegistry) || msgSender == _universalForwarder) { 25 | // if forwarder use appended data 26 | return sender; 27 | } 28 | 29 | // if msg.sender is neither the registry nor the universal forwarder, 30 | // we have to check the last 20bytes of the call data intepreted as an address 31 | // and check if the msg.sender was registered as forewarder for that address 32 | // we check tx.origin to save gas in case where msg.sender == tx.origin 33 | // solhint-disable-next-line avoid-tx-origin 34 | if (msgSender != tx.origin && _forwarderRegistry.isApprovedForwarder(sender, msgSender)) { 35 | return sender; 36 | } 37 | 38 | return msgSender; 39 | } 40 | 41 | function _msgData() internal view virtual returns (bytes calldata) { 42 | address payable msgSender = payable(msg.sender); 43 | if (msgSender == address(_forwarderRegistry) || msgSender == _universalForwarder) { 44 | // if forwarder use appended data 45 | return _msgDataAssuming20BytesAppendedData(); 46 | } 47 | 48 | // we check tx.origin to save gas in case where msg.sender == tx.origin 49 | // solhint-disable-next-line avoid-tx-origin 50 | if (msgSender != tx.origin && _forwarderRegistry.isApprovedForwarder(_lastAppendedDataAsSender(), msgSender)) { 51 | return _msgDataAssuming20BytesAppendedData(); 52 | } 53 | return msg.data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/dist/src/signers'; 2 | import {Contract} from 'ethers'; 3 | import {TypedDataDomain, TypedDataField} from '@ethersproject/abstract-signer'; 4 | import {ethers} from 'hardhat'; 5 | 6 | export async function setupUsers( 7 | addresses: string[], 8 | contracts: T 9 | ): Promise<({address: string; signer: SignerWithAddress} & T)[]> { 10 | const users: ({address: string; signer: SignerWithAddress} & T)[] = []; 11 | for (const address of addresses) { 12 | users.push(await setupUser(address, contracts)); 13 | } 14 | return users; 15 | } 16 | 17 | export async function setupUser( 18 | address: string, 19 | contracts: T 20 | ): Promise<{address: string; signer: SignerWithAddress} & T> { 21 | const signer = await ethers.getSigner(address); 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | const user: any = {address, signer}; 24 | for (const key of Object.keys(contracts)) { 25 | user[key] = contracts[key].connect(signer); 26 | } 27 | return user as {address: string; signer: SignerWithAddress} & T; 28 | } 29 | 30 | export class EIP712Signer { 31 | constructor(private domain: TypedDataDomain, private types: Record>) {} 32 | 33 | sign( 34 | user: {signer: SignerWithAddress}, 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | value: Record 37 | ): Promise { 38 | return user.signer._signTypedData(this.domain, this.types, value); 39 | } 40 | } 41 | 42 | export class EIP712SignerFactory { 43 | constructor(private fixedDomain: TypedDataDomain, private types: Record>) {} 44 | 45 | createSigner(domain: TypedDataDomain): { 46 | sign: ( 47 | user: {signer: SignerWithAddress}, 48 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 49 | value: Record 50 | ) => Promise; 51 | } { 52 | const domainToUse = Object.assign(this.fixedDomain, domain); 53 | const types = this.types; 54 | return { 55 | async sign( 56 | user: {signer: SignerWithAddress}, 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | value: Record 59 | ): Promise { 60 | if (domainToUse.chainId === 0) { 61 | domainToUse.chainId = await user.signer.getChainId(); 62 | } 63 | return user.signer._signTypedData(domainToUse, types, value); 64 | } 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import {HardhatUserConfig} from 'hardhat/types'; 3 | import 'hardhat-deploy'; 4 | import '@nomiclabs/hardhat-ethers'; 5 | import 'hardhat-gas-reporter'; 6 | import '@typechain/hardhat'; 7 | import 'solidity-coverage'; 8 | import {node_url, accounts} from './utils/network'; 9 | 10 | const config: HardhatUserConfig = { 11 | solidity: { 12 | version: '0.8.16', 13 | settings: { 14 | optimizer: { 15 | enabled: true, 16 | runs: 999999 17 | } 18 | } 19 | }, 20 | namedAccounts: { 21 | deployer: 0 22 | }, 23 | networks: { 24 | hardhat: { 25 | // process.env.HARDHAT_FORK will specify the network that the fork is made from. 26 | // this line ensure the use of the corresponding accounts 27 | accounts: accounts(process.env.HARDHAT_FORK), 28 | forking: process.env.HARDHAT_FORK 29 | ? { 30 | url: node_url(process.env.HARDHAT_FORK), 31 | blockNumber: process.env.HARDHAT_FORK_NUMBER 32 | ? parseInt(process.env.HARDHAT_FORK_NUMBER) 33 | : undefined 34 | } 35 | : undefined 36 | }, 37 | localhost: { 38 | url: node_url('localhost'), 39 | accounts: accounts() 40 | }, 41 | staging: { 42 | url: node_url('rinkeby'), 43 | accounts: accounts('rinkeby') 44 | }, 45 | production: { 46 | url: node_url('mainnet'), 47 | accounts: accounts('mainnet') 48 | }, 49 | mainnet: { 50 | url: node_url('mainnet'), 51 | accounts: accounts('mainnet') 52 | }, 53 | rinkeby: { 54 | url: node_url('rinkeby'), 55 | accounts: accounts('rinkeby') 56 | }, 57 | kovan: { 58 | url: node_url('kovan'), 59 | accounts: accounts('kovan') 60 | }, 61 | goerli: { 62 | url: node_url('goerli'), 63 | accounts: accounts('goerli') 64 | } 65 | }, 66 | paths: { 67 | sources: 'solc_0.8' 68 | }, 69 | gasReporter: { 70 | currency: 'USD', 71 | gasPrice: 100, 72 | enabled: process.env.REPORT_GAS ? true : false, 73 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 74 | maxMethodDiff: 10 75 | }, 76 | typechain: { 77 | outDir: 'typechain', 78 | target: 'ethers-v5' 79 | }, 80 | mocha: { 81 | timeout: 0 82 | }, 83 | external: process.env.HARDHAT_FORK 84 | ? { 85 | deployments: { 86 | // process.env.HARDHAT_FORK will specify the network that the fork is made from. 87 | // these lines allow it to fetch the deployments from the network being forked from both for node and deploy task 88 | hardhat: ['deployments/' + process.env.HARDHAT_FORK], 89 | localhost: ['deployments/' + process.env.HARDHAT_FORK] 90 | } 91 | } 92 | : undefined 93 | }; 94 | 95 | export default config; 96 | -------------------------------------------------------------------------------- /test/SpecificForwarder.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from './chai-setup'; 2 | import {ethers, deployments, getUnnamedAccounts} from 'hardhat'; 3 | import {ForwarderRegistry, TestSpecificForwarderReceiver__factory} from '../typechain'; 4 | import {setupUsers} from './utils'; 5 | import {ForwarderRegistrySignerFactory} from './utils/eip712'; 6 | 7 | const setup = deployments.createFixture(async () => { 8 | await deployments.fixture(['ForwarderRegistry']); 9 | 10 | const ForwarderRegistry = await ethers.getContract('ForwarderRegistry'); 11 | const TestSpecificForwarderReceiverFactory = ( 12 | await ethers.getContractFactory('TestSpecificForwarderReceiver') 13 | ); 14 | const TestSpecificForwarderReceiver = await TestSpecificForwarderReceiverFactory.deploy(ForwarderRegistry.address); 15 | 16 | const contracts = { 17 | ForwarderRegistry, 18 | TestSpecificForwarderReceiver 19 | }; 20 | 21 | const ForwarderRegistrySigner = ForwarderRegistrySignerFactory.createSigner({ 22 | verifyingContract: contracts.ForwarderRegistry.address 23 | }); 24 | 25 | const users = await setupUsers(await getUnnamedAccounts(), contracts); 26 | 27 | return { 28 | ...contracts, 29 | users, 30 | ForwarderRegistrySigner 31 | }; 32 | }); 33 | 34 | describe('SpecificForwarder', function () { 35 | it('isTrustedForwarder', async function () { 36 | const {TestSpecificForwarderReceiver, ForwarderRegistry} = await setup(); 37 | expect(await TestSpecificForwarderReceiver.isTrustedForwarder(ForwarderRegistry.address)).to.be.equal(true); 38 | }); 39 | it('TestReceiver with msg.sender', async function () { 40 | const {users, TestSpecificForwarderReceiver} = await setup(); 41 | await users[0].TestSpecificForwarderReceiver.test(42); 42 | const value = await TestSpecificForwarderReceiver.callStatic.getData(users[0].address); 43 | expect(value).to.equal(42); 44 | }); 45 | it('TestReceiver with metatx', async function () { 46 | const {users, TestSpecificForwarderReceiver, ForwarderRegistry, ForwarderRegistrySigner} = await setup(); 47 | const {to, data} = await users[0].TestSpecificForwarderReceiver.populateTransaction.test(42); 48 | if (!(to && data)) { 49 | throw new Error(`cannot populate transaction`); 50 | } 51 | const signature = await ForwarderRegistrySigner.sign(users[0], { 52 | signer: users[0].address, 53 | forwarder: users[1].address, 54 | approved: true, 55 | nonce: 0 56 | }); 57 | 58 | const {data: relayerData} = await users[1].ForwarderRegistry.populateTransaction.checkApprovalAndForward( 59 | signature, 60 | false, 61 | to, 62 | data 63 | ); 64 | 65 | await users[1].signer.sendTransaction({ 66 | to: ForwarderRegistry.address, 67 | data: relayerData + users[0].address.slice(2) 68 | }); 69 | 70 | const value = await TestSpecificForwarderReceiver.callStatic.getData(users[0].address); 71 | expect(value).to.equal(42); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /utils/network.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import {HDAccountsUserConfig, HttpNetworkUserConfig, NetworksUserConfig} from 'hardhat/types'; 3 | export function node_url(networkName: string): string { 4 | if (networkName) { 5 | const uri = process.env['ETH_NODE_URI_' + networkName.toUpperCase()]; 6 | if (uri && uri !== '') { 7 | return uri; 8 | } 9 | } 10 | 11 | if (networkName === 'localhost') { 12 | // do not use ETH_NODE_URI 13 | return 'http://localhost:8545'; 14 | } 15 | 16 | let uri = process.env.ETH_NODE_URI; 17 | if (uri) { 18 | uri = uri.replace('{{networkName}}', networkName); 19 | } 20 | if (!uri || uri === '') { 21 | // throw new Error(`environment variable "ETH_NODE_URI" not configured `); 22 | return ''; 23 | } 24 | if (uri.indexOf('{{') >= 0) { 25 | throw new Error(`invalid uri or network not supported by node provider : ${uri}`); 26 | } 27 | return uri; 28 | } 29 | 30 | export function getMnemonic(networkName?: string): string { 31 | if (networkName) { 32 | const mnemonic = process.env['MNEMONIC_' + networkName.toUpperCase()]; 33 | if (mnemonic && mnemonic !== '') { 34 | return mnemonic; 35 | } 36 | } 37 | 38 | const mnemonic = process.env.MNEMONIC; 39 | if (!mnemonic || mnemonic === '') { 40 | return 'test test test test test test test test test test test junk'; 41 | } 42 | return mnemonic; 43 | } 44 | 45 | export function accounts(networkName?: string): {mnemonic: string} { 46 | return {mnemonic: getMnemonic(networkName)}; 47 | } 48 | 49 | export function addForkConfiguration(networks: NetworksUserConfig): NetworksUserConfig { 50 | // While waiting for hardhat PR: https://github.com/nomiclabs/hardhat/pull/1542 51 | if (process.env.HARDHAT_FORK) { 52 | process.env['HARDHAT_DEPLOY_FORK'] = process.env.HARDHAT_FORK; 53 | } 54 | 55 | const currentNetworkName = process.env.HARDHAT_FORK; 56 | let forkURL: string | undefined = currentNetworkName && node_url(currentNetworkName); 57 | let hardhatAccounts: HDAccountsUserConfig | undefined; 58 | if (currentNetworkName && currentNetworkName !== 'hardhat') { 59 | const currentNetwork = networks[currentNetworkName] as HttpNetworkUserConfig; 60 | if (currentNetwork) { 61 | forkURL = currentNetwork.url; 62 | if ( 63 | currentNetwork.accounts && 64 | typeof currentNetwork.accounts === 'object' && 65 | 'mnemonic' in currentNetwork.accounts 66 | ) { 67 | hardhatAccounts = currentNetwork.accounts; 68 | } 69 | } 70 | } 71 | 72 | const newNetworks = { 73 | ...networks, 74 | hardhat: { 75 | ...networks.hardhat, 76 | ...{ 77 | accounts: hardhatAccounts, 78 | forking: forkURL 79 | ? { 80 | url: forkURL, 81 | blockNumber: process.env.HARDHAT_FORK_NUMBER 82 | ? parseInt(process.env.HARDHAT_FORK_NUMBER) 83 | : undefined 84 | } 85 | : undefined, 86 | mining: process.env.MINING_INTERVAL 87 | ? { 88 | auto: false, 89 | interval: process.env.MINING_INTERVAL.split(',').map((v) => parseInt(v)) as [number, number] 90 | } 91 | : undefined 92 | } 93 | } 94 | }; 95 | return newNetworks; 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethereum-universal-forwarder", 3 | "version": "2.0.0-beta", 4 | "description": "Universale Forwarder for Meta Transactions", 5 | "engines": { 6 | "node": ">= 12.18.0" 7 | }, 8 | "repository": "github:wighawag/universal-forwarder", 9 | "author": "wighawag", 10 | "license": "MIT", 11 | "keywords": [ 12 | "ethereum", 13 | "smart-contracts", 14 | "template", 15 | "boilerplate", 16 | "hardhat", 17 | "solidity" 18 | ], 19 | "files": [ 20 | "_lib", 21 | "solc_0.7/", 22 | "solc_0.8/", 23 | "LICENSE", 24 | "README.md", 25 | "export/artifacts/", 26 | "export/deploy/001_deploy_forwarder_registry.js", 27 | "export/deploy/002_deploy_universal_forwarder.js", 28 | "typechain", 29 | "diagram_*" 30 | ], 31 | "devDependencies": { 32 | "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@0.3.0-beta.10", 33 | "@openzeppelin/contracts": "^4.7.3", 34 | "@typechain/ethers-v5": "^10.1.0", 35 | "@typechain/hardhat": "^6.1.3", 36 | "@types/chai": "^4.2.18", 37 | "@types/mocha": "^10.0.0", 38 | "@types/node": "^18.7.23", 39 | "@typescript-eslint/eslint-plugin": "^5.38.1", 40 | "@typescript-eslint/parser": "^5.38.1", 41 | "chai": "^4.2.0", 42 | "chai-ethers": "^0.0.1", 43 | "cross-env": "^7.0.2", 44 | "dotenv": "^16.0.2", 45 | "eslint": "^8.24.0", 46 | "eslint-config-prettier": "^8.3.0", 47 | "ethers": "^5.7.1", 48 | "fs-extra": "^10.0.0", 49 | "hardhat": "^2.11.2", 50 | "hardhat-deploy": "^0.11.15", 51 | "hardhat-gas-reporter": "^1.0.9", 52 | "mocha": "^10.0.0", 53 | "prettier": "^2.3.0", 54 | "prettier-plugin-solidity": "^1.0.0-beta.10", 55 | "solhint": "^3.3.1", 56 | "solhint-plugin-prettier": "^0.0.5", 57 | "solidity-coverage": "^0.8.2", 58 | "ts-generator": "^0.1.1", 59 | "ts-node": "^10.9.1", 60 | "typechain": "^8.1.0", 61 | "typescript": "^4.8.4" 62 | }, 63 | "scripts": { 64 | "prepare": "node ./.setup.js && hardhat typechain", 65 | "lint": "eslint \"**/*.{js,ts}\" && solhint src/**/*.sol", 66 | "lint:fix": "eslint --fix \"**/*.{js,ts}\" && solhint --fix src/**/*.sol", 67 | "format": "prettier --check \"**/*.{ts,js,sol}\"", 68 | "format:fix": "prettier --write \"**/*.{ts,js,sol}\"", 69 | "compile": "hardhat compile", 70 | "export-artifacts": "tsc -p tsconfig.deploy.json && hardhat export-artifacts export/artifacts --include ForwarderRegistry,UniversalForwarder", 71 | "void:deploy": "hardhat deploy", 72 | "test": "cross-env HARDHAT_DEPLOY_FIXTURE=true HARDHAT_COMPILE=true mocha --bail --recursive test", 73 | "gas": "cross-env REPORT_GAS=true hardhat test", 74 | "coverage": "cross-env HARDHAT_DEPLOY_FIXTURE=true hardhat coverage", 75 | "dev": "hardhat node --watch --export contractsInfo.json", 76 | "local:dev": "hardhat --network localhost deploy --watch", 77 | "execute": "node ./_scripts.js run", 78 | "deploy": "node ./_scripts.js deploy", 79 | "export": "node ./_scripts.js export", 80 | "fork:execute": "node ./_scripts.js fork:run", 81 | "fork:deploy": "node ./_scripts.js fork:deploy", 82 | "fork:dev": "node ./_scripts.js fork:dev", 83 | "fork:test": "node ./_scripts.js fork:test" 84 | }, 85 | "dependencies": { 86 | "@ethersproject/abstract-signer": "^5.7.0" 87 | } 88 | } -------------------------------------------------------------------------------- /export/deploy/001_deploy_forwarder_registry.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | var func = function (hre) { 40 | return __awaiter(this, void 0, void 0, function () { 41 | var deployments, getNamedAccounts, deploy, deployer; 42 | return __generator(this, function (_a) { 43 | switch (_a.label) { 44 | case 0: 45 | deployments = hre.deployments, getNamedAccounts = hre.getNamedAccounts; 46 | deploy = deployments.deploy; 47 | return [4 /*yield*/, getNamedAccounts()]; 48 | case 1: 49 | deployer = (_a.sent()).deployer; 50 | return [4 /*yield*/, deploy('ForwarderRegistry', { 51 | from: deployer, 52 | log: true, 53 | deterministicDeployment: true 54 | })]; 55 | case 2: 56 | _a.sent(); 57 | return [2 /*return*/]; 58 | } 59 | }); 60 | }); 61 | }; 62 | exports.default = func; 63 | func.tags = ['ForwarderRegistry']; 64 | -------------------------------------------------------------------------------- /export/deploy/002_deploy_universal_forwarder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | var func = function (hre) { 40 | return __awaiter(this, void 0, void 0, function () { 41 | var deployments, getNamedAccounts, deploy, deployer; 42 | return __generator(this, function (_a) { 43 | switch (_a.label) { 44 | case 0: 45 | deployments = hre.deployments, getNamedAccounts = hre.getNamedAccounts; 46 | deploy = deployments.deploy; 47 | return [4 /*yield*/, getNamedAccounts()]; 48 | case 1: 49 | deployer = (_a.sent()).deployer; 50 | return [4 /*yield*/, deploy('UniversalForwarder', { 51 | from: deployer, 52 | log: true, 53 | deterministicDeployment: true 54 | })]; 55 | case 2: 56 | _a.sent(); 57 | return [2 /*return*/]; 58 | } 59 | }); 60 | }); 61 | }; 62 | exports.default = func; 63 | func.tags = ['UniversalForwarder']; 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # INTRODUCTION 2 | 3 | This repository implements a generic EIP-2771 compliant forwarder that is fully future proof and universal. 4 | 5 | It actually comes with 2 different implementation, the UniversalForwarder and the ForwarderRegistry. 6 | 7 | # UniversalForwarder 8 | 9 | The UniversalForwarder is the most simple and generic forwarder possible. It requires zero storage read or write but approval are for eternity. 10 | 11 | It does that by requiring the caller / relayer to always provide a signature along the call. 12 | 13 | # ForwarderRegistry 14 | 15 | The ForwarderRegistry instead keep a record of approved forwarder for each user. It thus comes at a higher gas cost overall. 16 | 17 | Its advantages is that user are able to revoke approved forwarder if desired. 18 | 19 | # Usage 20 | 21 | The reposiroty is also a npm package [ethereum-universal-forwarder](https://www.npmjs.com/package/ethereum-universal-forwarder) and contains abstract contract you can import in your code to get started with these forwarders. 22 | 23 | If you use [hardhat-deploy](https://github.com/wighawag/hardhat-deploy), it also come with exported deploy script that you can use to have the contract available in your test or specific networks very easily. 24 | 25 | An example repo can be found here : https://github.com/wighawag/test-ethereum-universal-forwarder 26 | 27 | # DEVELOPMENT 28 | 29 | ## INSTALL 30 | 31 | ```bash 32 | yarn 33 | ``` 34 | 35 | ## TEST 36 | 37 | ```bash 38 | yarn test 39 | ``` 40 | 41 | ## SCRIPTS 42 | 43 | Here is the list of npm scripts you can execute: 44 | 45 | Some of them relies on [./\_scripts.js](./_scripts.js) to allow parameterizing it via command line argument (have a look inside if you need modifications) 46 |

47 | 48 | `yarn prepare` 49 | 50 | As a standard lifecycle npm script, it is executed automatically upon install. It generate config file and typechain to get you started with type safe contract interactions 51 |

52 | 53 | `yarn lint`, `yarn lint:fix`, `yarn format` and `yarn format:fix` 54 | 55 | These will lint and format check your code. the `:fix` version will modifiy the files to match the requirement specified in `.eslintrc` and `.prettierrc.` 56 |

57 | 58 | `yarn compile` 59 | 60 | These will compile your contracts 61 |

62 | 63 | `yarn void:deploy` 64 | 65 | This will deploy your contracts on the in-memory hardhat network and exit, leaving no trace. quick way to ensure deployments work as intended without consequences 66 |

67 | 68 | `yarn test [mocha args...]` 69 | 70 | These will execute your tests using mocha. you can pass extra arguments to mocha 71 |

72 | 73 | `yarn coverage` 74 | 75 | These will produce a coverage report in the `coverage/` folder 76 |

77 | 78 | `yarn gas` 79 | 80 | These will produce a gas report for function used in the tests 81 |

82 | 83 | `yarn dev` 84 | 85 | These will run a local hardhat network on `localhost:8545` and deploy your contracts on it. Plus it will watch for any changes and redeploy them. 86 |

87 | 88 | `yarn local:dev` 89 | 90 | This assumes a local node it running on `localhost:8545`. It will deploy your contracts on it. Plus it will watch for any changes and redeploy them. 91 |

92 | 93 | `yarn execute [args...]` 94 | 95 | This will execute the script `` against the specified network 96 |

97 | 98 | `yarn deploy [args...]` 99 | 100 | This will deploy the contract on the specified network. 101 | 102 | Behind the scene it uses `hardhat deploy` command so you can append any argument for it 103 |

104 | 105 | `yarn export ` 106 | 107 | This will export the abi+address of deployed contract to `` 108 |

109 | 110 | `yarn fork:execute [--blockNumber ] [--deploy] [args...]` 111 | 112 | This will execute the script `` against a temporary fork of the specified network 113 | 114 | if `--deploy` is used, deploy scripts will be executed 115 |

116 | 117 | `yarn fork:deploy [--blockNumber ] [args...]` 118 | 119 | This will deploy the contract against a temporary fork of the specified network. 120 | 121 | Behind the scene it uses `hardhat deploy` command so you can append any argument for it 122 |

123 | 124 | `yarn fork:test [--blockNumber ] [mocha args...]` 125 | 126 | This will test the contract against a temporary fork of the specified network. 127 |

128 | 129 | `yarn fork:dev [--blockNumber ] [args...]` 130 | 131 | This will deploy the contract against a fork of the specified network and it will keep running as a node. 132 | 133 | Behind the scene it uses `hardhat node` command so you can append any argument for it 134 | -------------------------------------------------------------------------------- /solc_0.8/UniversalForwarder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../_lib/openzeppelin/contracts/utils/Address.sol"; 5 | import "../_lib/openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | import "./ERC2771/IERC2771.sol"; 7 | import "./ERC2771/UsingAppendedCallData.sol"; 8 | 9 | interface ERC1271 { 10 | function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); 11 | } 12 | 13 | /// @notice Universal Meta Transaction Forwarder 14 | /// It does not perform any extra logic apart from checking if the caller (metatx forwarder) has been approved via signature. 15 | /// Note that forwarder approval are forever. This is to remove the need to read storage. Signature need to be given each time. 16 | /// The overhead (on top of the specific metatx forwarder) is thus just an extra contract load and call + signature check. 17 | contract UniversalForwarder is UsingAppendedCallData, IERC2771 { 18 | using Address for address; 19 | using ECDSA for bytes32; 20 | 21 | bytes4 internal constant ERC1271_MAGICVALUE = 0x1626ba7e; 22 | 23 | bytes32 internal constant EIP712_DOMAIN_NAME = keccak256("UniversalForwarder"); 24 | bytes32 internal constant APPROVAL_TYPEHASH = 25 | keccak256("ApproveForwarderForever(address signer,address forwarder)"); 26 | 27 | uint256 private immutable _deploymentChainId; 28 | bytes32 private immutable _deploymentDomainSeparator; 29 | 30 | constructor() { 31 | uint256 chainId; 32 | //solhint-disable-next-line no-inline-assembly 33 | assembly { 34 | chainId := chainid() 35 | } 36 | _deploymentChainId = chainId; 37 | _deploymentDomainSeparator = _calculateDomainSeparator(chainId); 38 | } 39 | 40 | /// @notice The UniversalForwarder supports every EIP-2771 compliant forwarder. 41 | function isTrustedForwarder(address) external pure override returns (bool) { 42 | return true; 43 | } 44 | 45 | /// @notice Forward the meta transaction by first checking signature if forwarder is approved : no storage involved, approving is forever. 46 | /// @param signature signature by signer for approving forwarder. 47 | /// @param isEIP1271Signature true if the signer is a contract that require authorization via EIP-1271 48 | /// @param target destination of the call (that will receive the meta transaction). 49 | /// @param data the content of the call (the signer address will be appended to it). 50 | function forward( 51 | bytes calldata signature, 52 | bool isEIP1271Signature, 53 | address target, 54 | bytes calldata data 55 | ) external payable { 56 | address signer = _lastAppendedDataAsSender(); 57 | _requireValidSignature(signer, msg.sender, signature, isEIP1271Signature); 58 | target.functionCallWithValue(abi.encodePacked(data, signer), msg.value); 59 | } 60 | 61 | /// @dev Return the DOMAIN_SEPARATOR. 62 | function DOMAIN_SEPARATOR() external view returns (bytes32) { 63 | return _DOMAIN_SEPARATOR(); 64 | } 65 | 66 | // -------------------------------------------------------- INTERNAL -------------------------------------------------------------------- 67 | 68 | /// @dev Return the DOMAIN_SEPARATOR. 69 | function _DOMAIN_SEPARATOR() internal view returns (bytes32) { 70 | uint256 chainId; 71 | //solhint-disable-next-line no-inline-assembly 72 | assembly { 73 | chainId := chainid() 74 | } 75 | 76 | // in case a fork happen, to support the chain that had to change its chainId, we compue the domain operator 77 | return chainId == _deploymentChainId ? _deploymentDomainSeparator : _calculateDomainSeparator(chainId); 78 | } 79 | 80 | /// @dev Calculate the DOMAIN_SEPARATOR. 81 | function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { 82 | return 83 | keccak256( 84 | abi.encode( 85 | keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"), 86 | EIP712_DOMAIN_NAME, 87 | chainId, 88 | address(this) 89 | ) 90 | ); 91 | } 92 | 93 | function _encodeMessage(address signer, address forwarder) internal view returns (bytes memory) { 94 | return 95 | abi.encodePacked( 96 | "\x19\x01", 97 | _DOMAIN_SEPARATOR(), 98 | keccak256(abi.encode(APPROVAL_TYPEHASH, signer, forwarder)) 99 | ); 100 | } 101 | 102 | function _requireValidSignature( 103 | address signer, 104 | address forwarder, 105 | bytes memory signature, 106 | bool isEIP1271Signature 107 | ) internal view { 108 | bytes memory dataToHash = _encodeMessage(signer, forwarder); 109 | if (isEIP1271Signature) { 110 | require( 111 | ERC1271(signer).isValidSignature(keccak256(dataToHash), signature) == ERC1271_MAGICVALUE, 112 | "SIGNATURE_1654_INVALID" 113 | ); 114 | } else { 115 | address actualSigner = keccak256(dataToHash).recover(signature); 116 | require(signer == actualSigner, "SIGNATURE_WRONG_SIGNER"); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/UniversalForwarding.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from './chai-setup'; 2 | import {ethers, deployments, getUnnamedAccounts} from 'hardhat'; 3 | import {ForwarderRegistry, UniversalForwarder, TestUniversalForwardingReceiver__factory} from '../typechain'; 4 | import {setupUsers} from './utils'; 5 | import {ForwarderRegistrySignerFactory, UniversalForwarderSignerFactory} from './utils/eip712'; 6 | 7 | const setup = deployments.createFixture(async () => { 8 | await deployments.fixture(['ForwarderRegistry', 'UniversalForwarder']); 9 | 10 | const ForwarderRegistry = await ethers.getContract('ForwarderRegistry'); 11 | const UniversalForwarder = await ethers.getContract('UniversalForwarder'); 12 | const TestUniversalForwardingReceiverFactory = ( 13 | await ethers.getContractFactory('TestUniversalForwardingReceiver') 14 | ); 15 | const TestUniversalForwardingReceiver = await TestUniversalForwardingReceiverFactory.deploy( 16 | ForwarderRegistry.address, 17 | UniversalForwarder.address 18 | ); 19 | const contracts = { 20 | UniversalForwarder, 21 | ForwarderRegistry, 22 | TestUniversalForwardingReceiver 23 | }; 24 | 25 | const ForwarderRegistrySigner = ForwarderRegistrySignerFactory.createSigner({ 26 | verifyingContract: contracts.ForwarderRegistry.address 27 | }); 28 | 29 | const UniversalForwarderSigner = UniversalForwarderSignerFactory.createSigner({ 30 | verifyingContract: UniversalForwarder.address 31 | }); 32 | 33 | const users = await setupUsers(await getUnnamedAccounts(), contracts); 34 | 35 | return { 36 | ...contracts, 37 | users, 38 | ForwarderRegistrySigner, 39 | UniversalForwarderSigner 40 | }; 41 | }); 42 | 43 | describe('UniversalForwarding', function () { 44 | it('isTrustedForwarder', async function () { 45 | const {ForwarderRegistry} = await setup(); 46 | expect(await ForwarderRegistry.isTrustedForwarder(ForwarderRegistry.address)).to.be.equal(true); 47 | }); 48 | it('TestReceiver with msg.sender', async function () { 49 | const {users, TestUniversalForwardingReceiver} = await setup(); 50 | await users[0].TestUniversalForwardingReceiver.test(42); 51 | const data = await TestUniversalForwardingReceiver.callStatic.getData(users[0].address); 52 | expect(data).to.equal(42); 53 | }); 54 | 55 | it('TestReceiver empty func with msg.sender', async function () { 56 | const {users, TestUniversalForwardingReceiver} = await setup(); 57 | await users[0].TestUniversalForwardingReceiver.twelve(); 58 | const data = await TestUniversalForwardingReceiver.callStatic.getData(users[0].address); 59 | expect(data).to.equal(12); 60 | }); 61 | 62 | it('TestReceiver fallback with msg.sender', async function () { 63 | const {users, TestUniversalForwardingReceiver} = await setup(); 64 | await users[0].signer.sendTransaction({ 65 | to: TestUniversalForwardingReceiver.address 66 | }); 67 | const data = await TestUniversalForwardingReceiver.callStatic.getData(users[0].address); 68 | expect(data).to.equal(1); 69 | }); 70 | 71 | it('ForwarderRegistry metatx', async function () { 72 | const {users, TestUniversalForwardingReceiver, ForwarderRegistry, ForwarderRegistrySigner} = await setup(); 73 | const {to, data} = await users[0].TestUniversalForwardingReceiver.populateTransaction.test(42); 74 | if (!(to && data)) { 75 | throw new Error(`cannot populate transaction`); 76 | } 77 | const signature = await ForwarderRegistrySigner.sign(users[0], { 78 | signer: users[0].address, 79 | forwarder: users[1].address, 80 | approved: true, 81 | nonce: 0 82 | }); 83 | 84 | const {data: relayerData} = await users[1].ForwarderRegistry.populateTransaction.checkApprovalAndForward( 85 | signature, 86 | false, 87 | to, 88 | data 89 | ); 90 | 91 | await users[1].signer.sendTransaction({ 92 | to: ForwarderRegistry.address, 93 | data: relayerData + users[0].address.slice(2) 94 | }); 95 | 96 | const value = await TestUniversalForwardingReceiver.callStatic.getData(users[0].address); 97 | expect(value).to.equal(42); 98 | }); 99 | 100 | it('UniversalForwarder metatx', async function () { 101 | const {users, TestUniversalForwardingReceiver, UniversalForwarder, UniversalForwarderSigner} = await setup(); 102 | const {to, data} = await users[0].TestUniversalForwardingReceiver.populateTransaction.test(42); 103 | if (!(to && data)) { 104 | throw new Error(`cannot populate transaction`); 105 | } 106 | const signature = await UniversalForwarderSigner.sign(users[0], { 107 | signer: users[0].address, 108 | forwarder: users[1].address 109 | }); 110 | 111 | const {data: relayerData} = await users[1].UniversalForwarder.populateTransaction.forward( 112 | signature, 113 | false, 114 | to, 115 | data 116 | ); 117 | 118 | await users[1].signer.sendTransaction({ 119 | to: UniversalForwarder.address, 120 | data: relayerData + users[0].address.slice(2) 121 | }); 122 | 123 | const value = await TestUniversalForwardingReceiver.callStatic.getData(users[0].address); 124 | expect(value).to.equal(42); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /_scripts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | /* eslint-disable no-undef */ 4 | /* eslint-disable @typescript-eslint/no-var-requires */ 5 | const {spawn} = require('child_process'); 6 | const path = require('path'); 7 | require('dotenv').config(); 8 | 9 | const commandlineArgs = process.argv.slice(2); 10 | 11 | function parseArgs(rawArgs, numFixedArgs, expectedOptions) { 12 | const fixedArgs = []; 13 | const options = {}; 14 | const extra = []; 15 | const alreadyCounted = {}; 16 | for (let i = 0; i < rawArgs.length; i++) { 17 | const rawArg = rawArgs[i]; 18 | if (rawArg.startsWith('--')) { 19 | const optionName = rawArg.slice(2); 20 | const optionDetected = expectedOptions[optionName]; 21 | if (!alreadyCounted[optionName] && optionDetected) { 22 | alreadyCounted[optionName] = true; 23 | if (optionDetected === 'boolean') { 24 | options[optionName] = true; 25 | } else { 26 | i++; 27 | options[optionName] = rawArgs[i]; 28 | } 29 | } else { 30 | if (fixedArgs.length < numFixedArgs) { 31 | throw new Error(`expected ${numFixedArgs} fixed args, got only ${fixedArgs.length}`); 32 | } else { 33 | extra.push(rawArg); 34 | } 35 | } 36 | } else { 37 | if (fixedArgs.length < numFixedArgs) { 38 | fixedArgs.push(rawArg); 39 | } else { 40 | for (const opt of Object.keys(expectedOptions)) { 41 | alreadyCounted[opt] = true; 42 | } 43 | extra.push(rawArg); 44 | } 45 | } 46 | } 47 | return {options, extra, fixedArgs}; 48 | } 49 | 50 | function execute(command) { 51 | return new Promise((resolve, reject) => { 52 | const onExit = (error) => { 53 | if (error) { 54 | return reject(error); 55 | } 56 | resolve(); 57 | }; 58 | spawn(command.split(' ')[0], command.split(' ').slice(1), { 59 | stdio: 'inherit', 60 | shell: true 61 | }).on('exit', onExit); 62 | }); 63 | } 64 | 65 | async function performAction(rawArgs) { 66 | const firstArg = rawArgs[0]; 67 | const args = rawArgs.slice(1); 68 | if (firstArg === 'run') { 69 | const {fixedArgs, extra} = parseArgs(args, 2, {}); 70 | let filepath = fixedArgs[1]; 71 | const folder = path.basename(__dirname); 72 | if (filepath.startsWith(folder + '/') || filepath.startsWith(folder + '\\')) { 73 | filepath = filepath.slice(folder.length + 1); 74 | } 75 | await execute( 76 | `cross-env HARDHAT_DEPLOY_LOG=true HARDHAT_NETWORK=${fixedArgs[0]} ts-node --files ${filepath} ${extra.join( 77 | ' ' 78 | )}` 79 | ); 80 | } else if (firstArg === 'deploy') { 81 | const {fixedArgs, extra} = parseArgs(args, 1, {}); 82 | await execute(`hardhat --network ${fixedArgs[0]} deploy --report-gas ${extra.join(' ')}`); 83 | } else if (firstArg === 'verify') { 84 | const {fixedArgs, extra} = parseArgs(args, 1, {}); 85 | const network = fixedArgs[0]; 86 | if (!network) { 87 | console.error(`need to specify the network as first argument`); 88 | return; 89 | } 90 | await execute(`hardhat --network ${network} etherscan-verify ${extra.join(' ')}`); 91 | } else if (firstArg === 'export') { 92 | const {fixedArgs} = parseArgs(args, 2, {}); 93 | await execute(`hardhat --network ${fixedArgs[0]} export --export ${fixedArgs[1]}`); 94 | } else if (firstArg === 'fork:run') { 95 | const {fixedArgs, options, extra} = parseArgs(args, 2, { 96 | deploy: 'boolean', 97 | blockNumber: 'string', 98 | 'no-impersonation': 'boolean' 99 | }); 100 | let filepath = fixedArgs[1]; 101 | const folder = path.basename(__dirname); 102 | if (filepath.startsWith(folder + '/') || filepath.startsWith(folder + '\\')) { 103 | filepath = filepath.slice(folder.length + 1); 104 | } 105 | await execute( 106 | `cross-env ${options.deploy ? 'HARDHAT_DEPLOY_FIXTURE=true' : ''} HARDHAT_DEPLOY_LOG=true HARDHAT_FORK=${ 107 | fixedArgs[0] 108 | } ${options.blockNumber ? `HARDHAT_FORK_NUMBER=${options.blockNumber}` : ''} ${ 109 | options['no-impersonation'] ? `HARDHAT_DEPLOY_NO_IMPERSONATION=true` : '' 110 | } ts-node --files ${filepath} ${extra.join(' ')}` 111 | ); 112 | } else if (firstArg === 'fork:deploy') { 113 | const {fixedArgs, options, extra} = parseArgs(args, 1, { 114 | blockNumber: 'string', 115 | 'no-impersonation': 'boolean' 116 | }); 117 | await execute( 118 | `cross-env HARDHAT_FORK=${fixedArgs[0]} ${ 119 | options.blockNumber ? `HARDHAT_FORK_NUMBER=${options.blockNumber}` : '' 120 | } ${ 121 | options['no-impersonation'] ? `HARDHAT_DEPLOY_NO_IMPERSONATION=true` : '' 122 | } hardhat deploy --report-gas ${extra.join(' ')}` 123 | ); 124 | } else if (firstArg === 'fork:node') { 125 | const {fixedArgs, options, extra} = parseArgs(args, 1, { 126 | blockNumber: 'string', 127 | 'no-impersonation': 'boolean' 128 | }); 129 | await execute( 130 | `cross-env HARDHAT_FORK=${fixedArgs[0]} ${ 131 | options.blockNumber ? `HARDHAT_FORK_NUMBER=${options.blockNumber}` : '' 132 | } ${ 133 | options['no-impersonation'] ? `HARDHAT_DEPLOY_NO_IMPERSONATION=true` : '' 134 | } hardhat node --hostname 0.0.0.0 ${extra.join(' ')}` 135 | ); 136 | } else if (firstArg === 'fork:test') { 137 | const {fixedArgs, options, extra} = parseArgs(args, 1, { 138 | blockNumber: 'string', 139 | 'no-impersonation': 'boolean' 140 | }); 141 | await execute( 142 | `cross-env HARDHAT_FORK=${fixedArgs[0]} ${ 143 | options.blockNumber ? `HARDHAT_FORK_NUMBER=${options.blockNumber}` : '' 144 | } ${ 145 | options['no-impersonation'] ? `HARDHAT_DEPLOY_NO_IMPERSONATION=true` : '' 146 | } HARDHAT_DEPLOY_FIXTURE=true HARDHAT_COMPILE=true mocha --bail --recursive test ${extra.join(' ')}` 147 | ); 148 | } else if (firstArg === 'fork:dev') { 149 | const {fixedArgs, options, extra} = parseArgs(args, 1, { 150 | blockNumber: 'string', 151 | 'no-impersonation': 'boolean' 152 | }); 153 | await execute( 154 | `cross-env HARDHAT_FORK=${fixedArgs[0]} ${ 155 | options.blockNumber ? `HARDHAT_FORK_NUMBER=${options.blockNumber}` : '' 156 | } ${ 157 | options['no-impersonation'] ? `HARDHAT_DEPLOY_NO_IMPERSONATION=true` : '' 158 | } hardhat node --hostname 0.0.0.0 --watch --export contractsInfo.json ${extra.join(' ')}` 159 | ); 160 | } else if (firstArg === 'tenderly:push') { 161 | const {fixedArgs} = parseArgs(args, 1, {}); 162 | await execute(`hardhat --network ${fixedArgs[0]} tenderly:push`); 163 | } 164 | } 165 | 166 | performAction(commandlineArgs); 167 | -------------------------------------------------------------------------------- /solc_0.8/ForwarderRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.16; 3 | 4 | import "../_lib/openzeppelin/contracts/utils/Address.sol"; 5 | import "../_lib/openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | import "./ERC2771/IERC2771.sol"; 7 | import "./ERC2771/UsingAppendedCallData.sol"; 8 | import "./ERC2771/IForwarderRegistry.sol"; 9 | 10 | interface ERC1271 { 11 | function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); 12 | } 13 | 14 | /// @notice Universal Meta Transaction Forwarder Registry. 15 | /// Users can record specific forwarder that will be allowed to forward meta transactions on their behalf. 16 | contract ForwarderRegistry is IForwarderRegistry, UsingAppendedCallData, IERC2771 { 17 | using Address for address; 18 | using ECDSA for bytes32; 19 | 20 | bytes4 internal constant ERC1271_MAGICVALUE = 0x1626ba7e; 21 | 22 | bytes32 internal constant EIP712_DOMAIN_NAME = keccak256("ForwarderRegistry"); 23 | bytes32 internal constant APPROVAL_TYPEHASH = 24 | keccak256("ApproveForwarder(address signer,address forwarder,bool approved,uint256 nonce)"); 25 | 26 | uint256 private immutable _deploymentChainId; 27 | bytes32 private immutable _deploymentDomainSeparator; 28 | 29 | struct Forwarder { 30 | uint248 nonce; 31 | bool approved; 32 | } 33 | mapping(address => mapping(address => Forwarder)) internal _forwarders; 34 | 35 | /// @notice emitted for each Forwarder Approval or Disaproval. 36 | event ForwarderApproved(address indexed signer, address indexed forwarder, bool approved, uint256 nonce); 37 | 38 | constructor() { 39 | uint256 chainId; 40 | //solhint-disable-next-line no-inline-assembly 41 | assembly { 42 | chainId := chainid() 43 | } 44 | _deploymentChainId = chainId; 45 | _deploymentDomainSeparator = _calculateDomainSeparator(chainId); 46 | } 47 | 48 | /// @notice The ForwarderRegistry supports every EIP-2771 compliant forwarder. 49 | function isTrustedForwarder(address) external pure override returns (bool) { 50 | return true; 51 | } 52 | 53 | /// @notice Forward the meta tx (assuming caller has been approved by the signer as forwarder). 54 | /// @param target destination of the call (that will receive the meta transaction). 55 | /// @param data the content of the call (the signer address will be appended to it). 56 | function forward(address target, bytes calldata data) external payable { 57 | address signer = _lastAppendedDataAsSender(); 58 | require(_forwarders[signer][msg.sender].approved, "NOT_AUTHORIZED_FORWARDER"); 59 | target.functionCallWithValue(abi.encodePacked(data, signer), msg.value); 60 | } 61 | 62 | /// @notice return the current nonce for the signer/forwarder pair. 63 | /// @param signer signer who authorize/dauthorize forwarders 64 | /// @param forwarder meta transaction forwarder contract address. 65 | function getNonce(address signer, address forwarder) external view returns (uint256) { 66 | return uint256(_forwarders[signer][forwarder].nonce); 67 | } 68 | 69 | /// @notice return whether a forwarder is approved by a particular signer. 70 | /// @param signer signer who authorized or not the forwarder. 71 | /// @param forwarder meta transaction forwarder contract address. 72 | function isApprovedForwarder(address signer, address forwarder) external view override returns (bool) { 73 | return _forwarders[signer][forwarder].approved; 74 | } 75 | 76 | /// @notice approve a forwarder using EIP-2771 (msg.sender is a forwarder and signer is encoded in the appended data). 77 | /// @param forwarderToChangeApproval address of the forwarder to approve 78 | /// @param approved whether to approve or disapprove (if previously approved) the forwarder. 79 | /// @param signature signature by signer for approving forwarder. 80 | /// @param isEIP1271Signature true if the signer is a contract that require authorization via EIP-1271 81 | function approveForwarder( 82 | address forwarderToChangeApproval, 83 | bool approved, 84 | bytes calldata signature, 85 | bool isEIP1271Signature 86 | ) external { 87 | _approveForwarder( 88 | _lastAppendedDataAsSender(), 89 | forwarderToChangeApproval, 90 | approved, 91 | signature, 92 | isEIP1271Signature 93 | ); 94 | } 95 | 96 | /// @notice approve and forward the meta transaction in one call. 97 | /// @param signature signature by signer for approving forwarder. 98 | /// @param isEIP1271Signature true if the signer is a contract that require authorization via EIP-1271 99 | /// @param target destination of the call (that will receive the meta transaction). 100 | /// @param data the content of the call (the signer address will be appended to it). 101 | function approveAndForward( 102 | bytes calldata signature, 103 | bool isEIP1271Signature, 104 | address target, 105 | bytes calldata data 106 | ) external payable { 107 | address signer = _lastAppendedDataAsSender(); 108 | _approveForwarder(signer, msg.sender, true, signature, isEIP1271Signature); 109 | target.functionCallWithValue(abi.encodePacked(data, signer), msg.value); 110 | } 111 | 112 | /// @notice check approval (but do not record it) and forward the meta transaction in one call. 113 | /// @param signature signature by signer for approving forwarder. 114 | /// @param isEIP1271Signature true if the signer is a contract that require authorization via EIP-1271 115 | /// @param target destination of the call (that will receive the meta transaction). 116 | /// @param data the content of the call (the signer address will be appended to it). 117 | function checkApprovalAndForward( 118 | bytes calldata signature, 119 | bool isEIP1271Signature, 120 | address target, 121 | bytes calldata data 122 | ) external payable { 123 | address signer = _lastAppendedDataAsSender(); 124 | address forwarder = msg.sender; 125 | _requireValidSignature( 126 | signer, 127 | forwarder, 128 | true, 129 | uint256(_forwarders[signer][forwarder].nonce), 130 | signature, 131 | isEIP1271Signature 132 | ); 133 | target.functionCallWithValue(abi.encodePacked(data, signer), msg.value); 134 | } 135 | 136 | /// @dev Return the DOMAIN_SEPARATOR. 137 | function DOMAIN_SEPARATOR() external view returns (bytes32) { 138 | return _DOMAIN_SEPARATOR(); 139 | } 140 | 141 | // -------------------------------------------------------- INTERNAL -------------------------------------------------------------------- 142 | 143 | /// @dev Return the DOMAIN_SEPARATOR. 144 | function _DOMAIN_SEPARATOR() internal view returns (bytes32) { 145 | uint256 chainId; 146 | //solhint-disable-next-line no-inline-assembly 147 | assembly { 148 | chainId := chainid() 149 | } 150 | 151 | // in case a fork happen, to support the chain that had to change its chainId, we compue the domain operator 152 | return chainId == _deploymentChainId ? _deploymentDomainSeparator : _calculateDomainSeparator(chainId); 153 | } 154 | 155 | /// @dev Calculate the DOMAIN_SEPARATOR. 156 | function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { 157 | return 158 | keccak256( 159 | abi.encode( 160 | keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"), 161 | EIP712_DOMAIN_NAME, 162 | chainId, 163 | address(this) 164 | ) 165 | ); 166 | } 167 | 168 | function _encodeMessage( 169 | address signer, 170 | address forwarder, 171 | bool approved, 172 | uint256 nonce 173 | ) internal view returns (bytes memory) { 174 | return 175 | abi.encodePacked( 176 | "\x19\x01", 177 | _DOMAIN_SEPARATOR(), 178 | keccak256(abi.encode(APPROVAL_TYPEHASH, signer, forwarder, approved, nonce)) 179 | ); 180 | } 181 | 182 | function _requireValidSignature( 183 | address signer, 184 | address forwarder, 185 | bool approved, 186 | uint256 nonce, 187 | bytes memory signature, 188 | bool isEIP1271Signature 189 | ) internal view { 190 | bytes memory dataToHash = _encodeMessage(signer, forwarder, approved, nonce); 191 | if (isEIP1271Signature) { 192 | require( 193 | ERC1271(signer).isValidSignature(keccak256(dataToHash), signature) == ERC1271_MAGICVALUE, 194 | "SIGNATURE_1271_INVALID" 195 | ); 196 | } else { 197 | address actualSigner = keccak256(dataToHash).recover(signature); 198 | require(signer == actualSigner, "SIGNATURE_WRONG_SIGNER"); 199 | } 200 | } 201 | 202 | function _approveForwarder( 203 | address signer, 204 | address forwarderToChangeApproval, 205 | bool approved, 206 | bytes memory signature, 207 | bool isEIP1271Signature 208 | ) internal { 209 | Forwarder storage forwarderData = _forwarders[signer][forwarderToChangeApproval]; 210 | uint256 nonce = uint256(forwarderData.nonce); 211 | 212 | _requireValidSignature(signer, forwarderToChangeApproval, approved, nonce, signature, isEIP1271Signature); 213 | 214 | forwarderData.approved = approved; 215 | forwarderData.nonce = uint248(nonce + 1); 216 | emit ForwarderApproved(signer, forwarderToChangeApproval, approved, nonce); 217 | } 218 | } 219 | --------------------------------------------------------------------------------