diff --git a/package.json b/package.json index ef080e30..7359cef7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "@ethersproject/abstract-signer": "5.7.0", "@ethersproject/properties": "5.7.0", "@ethersproject/providers": "5.7.2", + "@fireblocks/fireblocks-web3-provider": "^1.2.4", + "@fireblocks/hardhat-fireblocks": "^1.2.2", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-etherscan": "^3.1.7", "@nomiclabs/hardhat-waffle": "^2.0.3", @@ -62,8 +64,6 @@ "ethereum-waffle": "^3.4.4", "ethernal": "^2.0.2", "ethers": "^5.7.2", - "fireblocks-defi-sdk": "1.2.15", - "fireblocks-sdk": "^4.0.0", "fs-extra": "^10.1.0", "google-artifactregistry-auth": "^3.1.0", "handlebars": "^4.7.7", @@ -116,9 +116,5 @@ "snapshot:production": "FOUNDRY_PROFILE=production forge snapshot --snap .gas-snapshot-production", "lint:diff": "npx eslint --cache --cache-location ./.eslintcache -c .eslintrc.js $(git diff --relative --name-only --diff-filter=d $(git merge-base HEAD origin/master) -- \"*.ts\" \"*.js\" \"*.env\" \"*.toml\")", "artifactregistry-login": "npx google-artifactregistry-auth" - }, - "dependencies": { - "@fireblocks/fireblocks-web3-provider": "^1.2.4", - "@fireblocks/hardhat-fireblocks": "^1.2.2" } } diff --git a/plugins/fireblocks/fireblocks-signer.ts b/plugins/fireblocks/fireblocks-signer.ts new file mode 100644 index 00000000..a54456af --- /dev/null +++ b/plugins/fireblocks/fireblocks-signer.ts @@ -0,0 +1,145 @@ +/* eslint-disable @typescript-eslint/dot-notation -- frequent FireblocksWeb3Provider private member access */ +import type { + Signer, + TypedDataDomain, + TypedDataField, +} from '@ethersproject/abstract-signer'; +import type { + TransactionRequest, + TransactionResponse, +} from '@ethersproject/providers'; +import { _TypedDataEncoder, toUtf8Bytes } from 'ethers/lib/utils'; +import type { Deferrable } from '@ethersproject/properties'; +import type { Bytes } from '@ethersproject/bytes'; +import type { BigNumber } from 'ethers'; +import { ethers } from 'ethers'; +import { FireblocksSigner as HardhatFireblocksSigner } from '@fireblocks/hardhat-fireblocks/dist/src/provider'; +import type { + FireblocksProviderConfig, + FireblocksWeb3Provider, +} from '@fireblocks/fireblocks-web3-provider'; +import type { EIP1193Provider } from 'hardhat/types/provider'; + +export class FireblocksSigner + extends HardhatFireblocksSigner + implements Signer +{ + _isSigner = true; + + provider?: ethers.providers.Provider | undefined; + + private _jsonRpcSigner: ethers.providers.JsonRpcSigner; + + private _ethersWeb3Provider: ethers.providers.Web3Provider; + + private _defaultNote: string | undefined; + + constructor( + provider: EIP1193Provider, + fireblocksConfig: FireblocksProviderConfig + ) { + super(provider, fireblocksConfig); + this._ethersWeb3Provider = new ethers.providers.Web3Provider( + this['_fireblocksWeb3Provider'] as FireblocksWeb3Provider + ); + this._defaultNote = fireblocksConfig.note; + this._jsonRpcSigner = this._ethersWeb3Provider.getSigner(); + } + + connect(provider: ethers.providers.Provider): Signer { + return this._jsonRpcSigner.connect(provider); + } + + async getAddress(): Promise { + return this._jsonRpcSigner.getAddress(); + } + + setNote(memo: string): void { + (this['_fireblocksWeb3Provider'] as FireblocksWeb3Provider)['note'] = memo; + } + + restoreDefaultNote(): void { + (this['_fireblocksWeb3Provider'] as FireblocksWeb3Provider)['note'] = + this._defaultNote; + } + + getBalance( + blockTag?: ethers.providers.BlockTag | undefined + ): Promise { + return this._jsonRpcSigner.getBalance(blockTag); + } + + getTransactionCount( + blockTag?: ethers.providers.BlockTag | undefined + ): Promise { + return this._jsonRpcSigner.getTransactionCount(blockTag); + } + + estimateGas(transaction: Deferrable): Promise { + return this._jsonRpcSigner.estimateGas(transaction); + } + + call( + transaction: Deferrable, + blockTag?: ethers.providers.BlockTag | undefined + ): Promise { + return this._jsonRpcSigner.call(transaction, blockTag); + } + + sendTransaction( + transaction: Deferrable + ): Promise { + return this.sendTransaction(transaction); + } + + getChainId(): Promise { + return this._jsonRpcSigner.getChainId(); + } + + getGasPrice(): Promise { + return this._jsonRpcSigner.getGasPrice(); + } + + getFeeData(): Promise { + return this._jsonRpcSigner.getFeeData(); + } + + resolveName(name: string): Promise { + return this._jsonRpcSigner.resolveName(name); + } + + checkTransaction( + transaction: Deferrable + ): Deferrable { + return this._jsonRpcSigner.checkTransaction(transaction); + } + + populateTransaction( + transaction: Deferrable + ): Promise { + return this._jsonRpcSigner.populateTransaction(transaction); + } + + _checkProvider(operation?: string | undefined): void { + return this._jsonRpcSigner._checkProvider(operation); + } + + async signMessage(message: Bytes | string): Promise { + const data = typeof message === 'string' ? toUtf8Bytes(message) : message; + return this._jsonRpcSigner.signMessage(data); + } + + signTransaction( + _transaction: Deferrable + ): Promise { + throw new Error('signing transactions is unsupported by JsonRpcSigner'); + } + + async _signTypedData( + domain: TypedDataDomain, + types: Record, + value: Record + ): Promise { + return this._jsonRpcSigner._signTypedData(domain, types, value); + } +} diff --git a/plugins/fireblocks/index.ts b/plugins/fireblocks/index.ts new file mode 100644 index 00000000..126aeced --- /dev/null +++ b/plugins/fireblocks/index.ts @@ -0,0 +1,74 @@ +/* eslint-disable no-param-reassign -- hre and config are intended to be configured via assignment in this file */ +import '@nomiclabs/hardhat-ethers'; + +import { extendConfig, extendEnvironment } from 'hardhat/config'; +import { BackwardsCompatibilityProviderAdapter } from 'hardhat/internal/core/providers/backwards-compatibility'; +import { + AutomaticGasPriceProvider, + AutomaticGasProvider, +} from 'hardhat/internal/core/providers/gas-providers'; +import { HttpProvider } from 'hardhat/internal/core/providers/http'; +import type { + EIP1193Provider, + HardhatConfig, + HardhatUserConfig, + HttpNetworkUserConfig, +} from 'hardhat/types'; + +import './type-extensions'; +import { version as SDK_VERSION } from '@fireblocks/hardhat-fireblocks/package.json'; + +import { FireblocksSigner } from './fireblocks-signer'; + +extendConfig( + (config: HardhatConfig, userConfig: Readonly) => { + const userNetworks = userConfig.networks; + if (userNetworks === undefined) { + return; + } + for (const networkName in userNetworks) { + const network = userNetworks[networkName]! as HttpNetworkUserConfig; + if (network.fireblocks !== undefined) { + if ( + networkName === 'hardhat' || + (network.url || '').includes('localhost') || + (network.url || '').includes('127.0.0.1') + ) { + throw new Error('Fireblocks is only supported for public networks.'); + } + (config.networks[networkName] as HttpNetworkUserConfig).fireblocks = { + note: 'Created by Nori custom Fireblocks Hardhat Plugin', + logTransactionStatusChanges: true, + ...network.fireblocks, + rpcUrl: network.url, + userAgent: `hardhat-fireblocks/${SDK_VERSION}`, + }; + } + } + } +); + +extendEnvironment((hre) => { + if ((hre.network.config as HttpNetworkUserConfig).fireblocks != undefined) { + const httpNetConfig = hre.network.config as HttpNetworkUserConfig; + const eip1193Provider = new HttpProvider( + httpNetConfig.url!, + hre.network.name, + httpNetConfig.httpHeaders, + httpNetConfig.timeout + ); + let wrappedProvider: EIP1193Provider; + wrappedProvider = new FireblocksSigner( + eip1193Provider, + (hre.network.config as HttpNetworkUserConfig).fireblocks! + ); + wrappedProvider = new AutomaticGasProvider( + wrappedProvider, + hre.network.config.gasMultiplier + ); + wrappedProvider = new AutomaticGasPriceProvider(wrappedProvider); + hre.network.provider = new BackwardsCompatibilityProviderAdapter( + wrappedProvider + ); + } +}); diff --git a/plugins/fireblocks/type-extensions.ts b/plugins/fireblocks/type-extensions.ts new file mode 100644 index 00000000..86905378 --- /dev/null +++ b/plugins/fireblocks/type-extensions.ts @@ -0,0 +1,27 @@ +import 'hardhat/types/config'; +import 'hardhat/types/runtime'; + +import type { FireblocksProviderConfig } from '@fireblocks/fireblocks-web3-provider'; + +import type { FireblocksSigner } from './fireblocks-signer'; + +declare module 'hardhat/types/config' { + interface HttpNetworkUserConfig { + fireblocks?: FireblocksProviderConfig; + } + interface HttpNetworkConfig { + fireblocks?: FireblocksProviderConfig; + } + interface HardhatConfig { + fireblocks: FireblocksProviderConfig; + } +} + +declare module 'hardhat/types/runtime' { + interface HardhatRuntimeEnvironment { + fireblocks: { + getSigners: () => Promise; + getSigner: (index: number) => Promise; + }; + } +} diff --git a/plugins/index.ts b/plugins/index.ts index 445220ad..80c36439 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -1,4 +1,5 @@ /* eslint-disable no-param-reassign -- hre is intended to be configured via assignment in this file */ + import 'tsconfig-paths/register'; import '@nomiclabs/hardhat-waffle'; import '@openzeppelin/hardhat-defender'; @@ -21,7 +22,8 @@ import { lazyFunction, lazyObject } from 'hardhat/plugins'; import type { FactoryOptions } from '@nomiclabs/hardhat-ethers/types'; import type { HardhatNetworkHDAccountsConfig } from 'hardhat/types'; import { Wallet } from 'ethers'; -import type { FireblocksWeb3Provider } from '@fireblocks/fireblocks-web3-provider'; +import type { FireblocksSigner } from 'plugins/fireblocks/fireblocks-signer'; +import { fireblocks } from 'hardhat'; import { Eip2612Signer } from '@/signers/eip-26126'; import { namedAccountIndices, namedAccounts } from '@/config/accounts'; @@ -94,7 +96,7 @@ const deployOrUpgradeProxy = async < // '0x582a885C03A0104Dc3053FAA8486c178e51E48Db' // ); - const [signer] = await hre.getSigners(); + const [signer]: Signer[] = await hre.getSigners(); hre.trace( `deployOrUpgrade: ${contractName} from address ${await signer.getAddress()}` @@ -119,15 +121,19 @@ const deployOrUpgradeProxy = async < // ) ) { hre.trace('Deploying proxy and instance', contractName); - const fireblocksSigner = signer as unknown as FireblocksWeb3Provider; - // eslint-disable-next-line - fireblocksSigner['note'] = `Deploy proxy and instance for ${contractName}`; + const fireblocksSigner = signer as FireblocksSigner; + if (typeof fireblocksSigner.setNote === 'function') { + fireblocksSigner.setNote(`Deploy proxy and instance for ${contractName}`); + } contract = await hre.upgrades.deployProxy( contractFactory, args, options ); await contract.deployed(); + if (typeof fireblocksSigner.restoreDefaultNote === 'function') { + fireblocksSigner.restoreDefaultNote(); + } hre.log( 'Deployed proxy and instance', contractName, @@ -145,9 +151,12 @@ const deployOrUpgradeProxy = async < const existingImplementationAddress = await hre.upgrades.erc1967.getImplementationAddress(maybeProxyAddress); hre.trace('Existing implementation at:', existingImplementationAddress); - const fireblocksSigner = signer as unknown as FireblocksWeb3Provider; - // eslint-disable-next-line - fireblocksSigner['note'] = `Upgrade contract instance for ${contractName}`; + const fireblocksSigner = signer as FireblocksSigner; + if (typeof fireblocksSigner.setNote === 'function') { + fireblocksSigner.setNote( + `Upgrade contract instance for ${contractName}` + ); + } const deployment = await hre.deployments.get(contractName); const artifact = await hre.deployments.getArtifact(contractName); if (deployment.bytecode === artifact.bytecode) { @@ -177,6 +186,9 @@ const deployOrUpgradeProxy = async < await contract.deployed(); hre.trace('...successful deployment transaction', contractName); } + if (typeof fireblocksSigner.restoreDefaultNote === 'function') { + fireblocksSigner.restoreDefaultNote(); + } } catch (error) { hre.log(`Failed to upgrade ${contractName} with error:`, error); throw new Error(`Failed to upgrade ${contractName} with error: ${error}`); @@ -197,7 +209,7 @@ const deployNonUpgradeable = async < args: unknown[]; options?: FactoryOptions; }): Promise> => { - const [signer]: Signer[] = await hre.getSigners(); + const [signer] = await hre.getSigners(); hre.log( `deployNonUpgradeable: ${contractName} from address ${await signer.getAddress()}` ); @@ -205,13 +217,17 @@ const deployNonUpgradeable = async < contractName, { ...options, signer } ); - const fireblocksSigner = signer as unknown as FireblocksWeb3Provider; - // eslint-disable-next-line - fireblocksSigner['note'] = `Deploy ${contractName}`; + const fireblocksSigner = signer as FireblocksSigner; + if (typeof fireblocksSigner.setNote === 'function') { + fireblocksSigner.setNote(`Deploy ${contractName}`); + } const contract = (await contractFactory.deploy( ...args )) as InstanceOfContract; hre.log('Deployed non upgradeable contract', contractName, contract.address); + if (typeof fireblocksSigner.restoreDefaultNote === 'function') { + fireblocksSigner.restoreDefaultNote(); + } return contract; }; @@ -228,7 +244,7 @@ extendEnvironment((hre) => { if (hre.network.config.live) { if (hre.config.fireblocks === undefined) { throw new Error( - 'Fireblocks config is required for live networks. Please set FIREBLOCKS_API_KEY and FIREBLOCKS_SECRET_KEY_PATH in your environment.' + 'Fireblocks config is required for live networks. Please set FIREBLOCKS_API_KEY and FIREBLOCKS_SECRET_KEY_PATH and FIREBLOCKS_VAULT_ID in your environment.' ); } if (Boolean(hre.config.fireblocks.apiKey)) { diff --git a/tasks/index.ts b/tasks/index.ts index 99c0433d..e84b0b16 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -28,6 +28,7 @@ import { GET_MIGRATE_CERTIFICATES_TASK } from './migrate-certificates'; import { GET_LIST_MIGRATED_REMOVALS_TASK } from './list-remaining-migrated-removals'; import { TASK as FORCE_UPGRADE_TASK } from './force-ugrade'; import { TASK as SIGN_MESSAGE_TASK } from './sign-message'; +import { TASK as TEST_SIGN_TYPED_TASK } from './test-sign-typed'; export interface Task { run: ActionType< @@ -86,4 +87,5 @@ export const TASKS = { }, [FORCE_UPGRADE_TASK.name]: { ...FORCE_UPGRADE_TASK }, [SIGN_MESSAGE_TASK.name]: { ...SIGN_MESSAGE_TASK }, + [TEST_SIGN_TYPED_TASK.name]: { ...TEST_SIGN_TYPED_TASK }, } as const; diff --git a/tasks/test-sign-typed.ts b/tasks/test-sign-typed.ts new file mode 100644 index 00000000..ef6b37d0 --- /dev/null +++ b/tasks/test-sign-typed.ts @@ -0,0 +1,55 @@ +import { task } from 'hardhat/config'; + +import type { FireblocksSigner } from '../plugins/fireblocks/fireblocks-signer'; + +export interface TestSignTypesTaskParameters {} + +export const types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], +}; + +export const TASK = { + name: 'test-sign-typed', + description: 'Test signing typed data', + run: async ( + taskArguments: TestSignTypesTaskParameters, + hre: CustomHardHatRuntimeEnvironment + ): Promise => { + const [signer] = (await hre.getSigners()) as FireblocksSigner[]; + const domain = { + name: 'NORI', + version: '1', + chainId: hre.network.config.chainId, + verifyingContract: '0xB3fe45C08137dD6adACb2918D899e0C0dBB036C8', + }; + const owner = '0x0F032F48fD4b38eA605F438922CA19FA79d0e6A7'; + const spender = '0xcdcb43cb7b668f0c1ca04fe4b60da7f8c62be393'; + const value = { + owner, + spender, + value: ethers.BigNumber.from('46425588600000000000000'), // ethers.utils.parseEther('100'), + nonce: ethers.BigNumber.from('0'), + deadline: 1_664_492_773, + }; + const signature = await signer._signTypedData(domain, types, value); + const verified = ethers.utils.verifyTypedData( + domain, + types, + value, + signature + ); + if (verified == (await signer.getAddress())) { + console.log(`Verified`); + } else { + console.log(`Verification failed`); + } + }, +} as const; + +task(TASK.name, TASK.description, TASK.run); diff --git a/tasks/vesting.ts b/tasks/vesting.ts index 60119560..e8ba297f 100644 --- a/tasks/vesting.ts +++ b/tasks/vesting.ts @@ -12,7 +12,8 @@ import type { Signer } from '@ethersproject/abstract-signer'; import type { CSVParseParam } from 'csvtojson/v2/Parameters'; import { isAddress, getAddress } from 'ethers/lib/utils'; import moment from 'moment'; -import type { FireblocksWeb3Provider } from '@fireblocks/fireblocks-web3-provider'; + +import type { FireblocksSigner } from '../plugins/fireblocks/fireblocks-signer'; import type { BridgedPolygonNORI, LockedNORI } from '@/typechain-types'; import { getOctokit } from '@/tasks/utils/github'; @@ -844,17 +845,19 @@ const CREATE_SUBTASK = { { name: 'deadline', type: 'uint256' }, ], }; - const fireblocksSigner = - bpNori.signer as unknown as FireblocksWeb3Provider; + const fireblocksSigner = bpNori.signer as FireblocksSigner; const latestBlock = await fireblocksSigner.provider?.getBlock('latest'); // TODO error handling on undefined latest block - const deadline = latestBlock.timestamp + 3600; // one hour into the future + const deadline = latestBlock!.timestamp + 3600; // one hour into the future const owner = await fireblocksSigner.getAddress(); const name = await bpNori.name(); const nonce = await bpNori.nonces(owner); const chainId = await fireblocksSigner.getChainId(); - // eslint-disable-next-line - fireblocksSigner['note'] = `Permit BridgedPolygonNORI Spend by LockedNORI: ${memo || ''}`; + if (typeof fireblocksSigner.setNote === 'function') { + fireblocksSigner.setNote( + `Permit BridgedPolygonNORI Spend by LockedNORI: ${memo || ''}` + ); + } const signature = await fireblocksSigner._signTypedData( { name, @@ -889,10 +892,8 @@ const CREATE_SUBTASK = { ); } } else { - if (typeof fireblocksSigner.setNextTransactionMemo === 'function') { - fireblocksSigner.setNextTransactionMemo( - `Vesting Create: ${memo || ''}` - ); + if (typeof fireblocksSigner.setNote === 'function') { + fireblocksSigner.setNote(`Vesting Create: ${memo || ''}`); } const batchCreateGrantsTx = await lNori.batchCreateGrants( amounts, @@ -902,6 +903,9 @@ const CREATE_SUBTASK = { r, s ); + if (typeof fireblocksSigner.restoreDefaultNote === 'function') { + fireblocksSigner.restoreDefaultNote(); + } const result = await batchCreateGrantsTx.wait(); hre.log( chalk.bold.bgWhiteBright.black( @@ -993,11 +997,10 @@ const REVOKE_SUBTASK = { ); } } else { - const fireblocksSigner = - lNori.signer as unknown as FireblocksWeb3Provider; - // eslint-disable-next-line - fireblocksSigner['note'] = - `Vesting Revoke: ${memo || ''}`; + const fireblocksSigner = lNori.signer as FireblocksSigner; + if (typeof fireblocksSigner.setNote === 'function') { + fireblocksSigner.setNote(`Vesting Revoke: ${memo || ''}`); + } const batchRevokeUnvestedTokenAmountsTx = await lNori.batchRevokeUnvestedTokenAmounts( fromAccounts, @@ -1005,6 +1008,9 @@ const REVOKE_SUBTASK = { atTimes, amounts ); + if (typeof fireblocksSigner.restoreDefaultNote === 'function') { + fireblocksSigner.restoreDefaultNote(); + } const result = await batchRevokeUnvestedTokenAmountsTx.wait(); hre.log( chalk.bold.bgWhiteBright.black( diff --git a/types/global.d.ts b/types/global.d.ts index 2471a4c4..a5bba1b7 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -11,7 +11,7 @@ import type { ContractFactory, ethers as defaultEthers, } from 'ethers'; -import type { Signer, TypedDataSigner } from '@ethersproject/abstract-signer'; +import type { Signer } from '@ethersproject/abstract-signer'; import type { DeployProxyOptions } from '@openzeppelin/hardhat-upgrades/src/utils'; import type { FactoryOptions, @@ -28,6 +28,8 @@ import type { } from 'hardhat-deploy/dist/types'; import type { HardhatUserConfig } from 'hardhat/types'; import type { Deployment } from 'hardhat-deploy/types'; +import type { FireblocksProviderConfig } from '@fireblocks/fireblocks-web3-provider'; +import type { FireblocksSigner } from '@plugins/fireblocks/fireblocks-signer'; import type { debug } from '../utils/debug'; import type { TASKS } from '../tasks';