UNPKG

11.8 kBPlain TextView Raw
1import type { ethers } from "ethers";
2import type { SignerWithAddress } from "../signers";
3import type { FactoryOptions, Libraries } from "../types";
4
5import { NomicLabsHardhatPluginError } from "hardhat/plugins";
6import {
7 Artifact,
8 HardhatRuntimeEnvironment,
9 NetworkConfig,
10} from "hardhat/types";
11
12interface Link {
13 sourceName: string;
14 libraryName: string;
15 address: string;
16}
17
18const pluginName = "hardhat-ethers";
19
20function isArtifact(artifact: any): artifact is Artifact {
21 const {
22 contractName,
23 sourceName,
24 abi,
25 bytecode,
26 deployedBytecode,
27 linkReferences,
28 deployedLinkReferences,
29 } = artifact;
30
31 return (
32 typeof contractName === "string" &&
33 typeof sourceName === "string" &&
34 Array.isArray(abi) &&
35 typeof bytecode === "string" &&
36 typeof deployedBytecode === "string" &&
37 linkReferences !== undefined &&
38 deployedLinkReferences !== undefined
39 );
40}
41
42export async function getSigners(
43 hre: HardhatRuntimeEnvironment
44): Promise<SignerWithAddress[]> {
45 const accounts = await hre.ethers.provider.listAccounts();
46
47 const signersWithAddress = await Promise.all(
48 accounts.map((account) => getSigner(hre, account))
49 );
50
51 return signersWithAddress;
52}
53
54export async function getSigner(
55 hre: HardhatRuntimeEnvironment,
56 address: string
57): Promise<SignerWithAddress> {
58 const { SignerWithAddress: SignerWithAddressImpl } = await import(
59 "../signers"
60 );
61
62 const signer = hre.ethers.provider.getSigner(address);
63
64 const signerWithAddress = await SignerWithAddressImpl.create(signer);
65
66 return signerWithAddress;
67}
68
69export function getContractFactory(
70 hre: HardhatRuntimeEnvironment,
71 name: string,
72 signerOrOptions?: ethers.Signer | FactoryOptions
73): Promise<ethers.ContractFactory>;
74
75export function getContractFactory(
76 hre: HardhatRuntimeEnvironment,
77 abi: any[],
78 bytecode: ethers.utils.BytesLike,
79 signer?: ethers.Signer
80): Promise<ethers.ContractFactory>;
81
82export async function getContractFactory(
83 hre: HardhatRuntimeEnvironment,
84 nameOrAbi: string | any[],
85 bytecodeOrFactoryOptions?:
86 | (ethers.Signer | FactoryOptions)
87 | ethers.utils.BytesLike,
88 signer?: ethers.Signer
89) {
90 if (typeof nameOrAbi === "string") {
91 const artifact = await hre.artifacts.readArtifact(nameOrAbi);
92
93 return getContractFactoryFromArtifact(
94 hre,
95 artifact,
96 bytecodeOrFactoryOptions as ethers.Signer | FactoryOptions | undefined
97 );
98 }
99
100 return getContractFactoryByAbiAndBytecode(
101 hre,
102 nameOrAbi,
103 bytecodeOrFactoryOptions as ethers.utils.BytesLike,
104 signer
105 );
106}
107
108function isFactoryOptions(
109 signerOrOptions?: ethers.Signer | FactoryOptions
110): signerOrOptions is FactoryOptions {
111 const { Signer } = require("ethers") as typeof ethers;
112 if (signerOrOptions === undefined || signerOrOptions instanceof Signer) {
113 return false;
114 }
115
116 return true;
117}
118
119export async function getContractFactoryFromArtifact(
120 hre: HardhatRuntimeEnvironment,
121 artifact: Artifact,
122 signerOrOptions?: ethers.Signer | FactoryOptions
123) {
124 let libraries: Libraries = {};
125 let signer: ethers.Signer | undefined;
126
127 if (!isArtifact(artifact)) {
128 throw new NomicLabsHardhatPluginError(
129 pluginName,
130 `You are trying to create a contract factory from an artifact, but you have not passed a valid artifact parameter.`
131 );
132 }
133
134 if (isFactoryOptions(signerOrOptions)) {
135 signer = signerOrOptions.signer;
136 libraries = signerOrOptions.libraries ?? {};
137 } else {
138 signer = signerOrOptions;
139 }
140
141 if (artifact.bytecode === "0x") {
142 throw new NomicLabsHardhatPluginError(
143 pluginName,
144 `You are trying to create a contract factory for the contract ${artifact.contractName}, which is abstract and can't be deployed.
145If you want to call a contract using ${artifact.contractName} as its interface use the "getContractAt" function instead.`
146 );
147 }
148
149 const linkedBytecode = await collectLibrariesAndLink(artifact, libraries);
150
151 return getContractFactoryByAbiAndBytecode(
152 hre,
153 artifact.abi,
154 linkedBytecode,
155 signer
156 );
157}
158
159async function collectLibrariesAndLink(
160 artifact: Artifact,
161 libraries: Libraries
162) {
163 const { utils } = require("ethers") as typeof ethers;
164
165 const neededLibraries: Array<{
166 sourceName: string;
167 libName: string;
168 }> = [];
169 for (const [sourceName, sourceLibraries] of Object.entries(
170 artifact.linkReferences
171 )) {
172 for (const libName of Object.keys(sourceLibraries)) {
173 neededLibraries.push({ sourceName, libName });
174 }
175 }
176
177 const linksToApply: Map<string, Link> = new Map();
178 for (const [linkedLibraryName, linkedLibraryAddress] of Object.entries(
179 libraries
180 )) {
181 if (!utils.isAddress(linkedLibraryAddress)) {
182 throw new NomicLabsHardhatPluginError(
183 pluginName,
184 `You tried to link the contract ${artifact.contractName} with the library ${linkedLibraryName}, but provided this invalid address: ${linkedLibraryAddress}`
185 );
186 }
187
188 const matchingNeededLibraries = neededLibraries.filter((lib) => {
189 return (
190 lib.libName === linkedLibraryName ||
191 `${lib.sourceName}:${lib.libName}` === linkedLibraryName
192 );
193 });
194
195 if (matchingNeededLibraries.length === 0) {
196 let detailedMessage: string;
197 if (neededLibraries.length > 0) {
198 const libraryFQNames = neededLibraries
199 .map((lib) => `${lib.sourceName}:${lib.libName}`)
200 .map((x) => `* ${x}`)
201 .join("\n");
202 detailedMessage = `The libraries needed are:
203${libraryFQNames}`;
204 } else {
205 detailedMessage = "This contract doesn't need linking any libraries.";
206 }
207 throw new NomicLabsHardhatPluginError(
208 pluginName,
209 `You tried to link the contract ${artifact.contractName} with ${linkedLibraryName}, which is not one of its libraries.
210${detailedMessage}`
211 );
212 }
213
214 if (matchingNeededLibraries.length > 1) {
215 const matchingNeededLibrariesFQNs = matchingNeededLibraries
216 .map(({ sourceName, libName }) => `${sourceName}:${libName}`)
217 .map((x) => `* ${x}`)
218 .join("\n");
219 throw new NomicLabsHardhatPluginError(
220 pluginName,
221 `The library name ${linkedLibraryName} is ambiguous for the contract ${artifact.contractName}.
222It may resolve to one of the following libraries:
223${matchingNeededLibrariesFQNs}
224
225To fix this, choose one of these fully qualified library names and replace where appropriate.`
226 );
227 }
228
229 const [neededLibrary] = matchingNeededLibraries;
230
231 const neededLibraryFQN = `${neededLibrary.sourceName}:${neededLibrary.libName}`;
232
233 // The only way for this library to be already mapped is
234 // for it to be given twice in the libraries user input:
235 // once as a library name and another as a fully qualified library name.
236 if (linksToApply.has(neededLibraryFQN)) {
237 throw new NomicLabsHardhatPluginError(
238 pluginName,
239 `The library names ${neededLibrary.libName} and ${neededLibraryFQN} refer to the same library and were given as two separate library links.
240Remove one of them and review your library links before proceeding.`
241 );
242 }
243
244 linksToApply.set(neededLibraryFQN, {
245 sourceName: neededLibrary.sourceName,
246 libraryName: neededLibrary.libName,
247 address: linkedLibraryAddress,
248 });
249 }
250
251 if (linksToApply.size < neededLibraries.length) {
252 const missingLibraries = neededLibraries
253 .map((lib) => `${lib.sourceName}:${lib.libName}`)
254 .filter((libFQName) => !linksToApply.has(libFQName))
255 .map((x) => `* ${x}`)
256 .join("\n");
257
258 throw new NomicLabsHardhatPluginError(
259 pluginName,
260 `The contract ${artifact.contractName} is missing links for the following libraries:
261${missingLibraries}
262
263Learn more about linking contracts at https://hardhat.org/plugins/nomiclabs-hardhat-ethers.html#library-linking
264`
265 );
266 }
267
268 return linkBytecode(artifact, [...linksToApply.values()]);
269}
270
271async function getContractFactoryByAbiAndBytecode(
272 hre: HardhatRuntimeEnvironment,
273 abi: any[],
274 bytecode: ethers.utils.BytesLike,
275 signer?: ethers.Signer
276) {
277 const { ContractFactory } = require("ethers") as typeof ethers;
278
279 if (signer === undefined) {
280 const signers = await hre.ethers.getSigners();
281 signer = signers[0];
282 }
283
284 const abiWithAddedGas = addGasToAbiMethodsIfNecessary(
285 hre.network.config,
286 abi
287 );
288
289 return new ContractFactory(abiWithAddedGas, bytecode, signer);
290}
291
292export async function getContractAt(
293 hre: HardhatRuntimeEnvironment,
294 nameOrAbi: string | any[],
295 address: string,
296 signer?: ethers.Signer
297) {
298 if (typeof nameOrAbi === "string") {
299 const artifact = await hre.artifacts.readArtifact(nameOrAbi);
300
301 return getContractAtFromArtifact(hre, artifact, address, signer);
302 }
303
304 const { Contract } = require("ethers") as typeof ethers;
305
306 if (signer === undefined) {
307 const signers = await hre.ethers.getSigners();
308 signer = signers[0];
309 }
310
311 // If there's no signer, we want to put the provider for the selected network here.
312 // This allows read only operations on the contract interface.
313 const signerOrProvider: ethers.Signer | ethers.providers.Provider =
314 signer !== undefined ? signer : hre.ethers.provider;
315
316 const abiWithAddedGas = addGasToAbiMethodsIfNecessary(
317 hre.network.config,
318 nameOrAbi
319 );
320
321 return new Contract(address, abiWithAddedGas, signerOrProvider);
322}
323
324export async function getContractAtFromArtifact(
325 hre: HardhatRuntimeEnvironment,
326 artifact: Artifact,
327 address: string,
328 signer?: ethers.Signer
329) {
330 if (!isArtifact(artifact)) {
331 throw new NomicLabsHardhatPluginError(
332 pluginName,
333 `You are trying to create a contract by artifact, but you have not passed a valid artifact parameter.`
334 );
335 }
336
337 const factory = await getContractFactoryByAbiAndBytecode(
338 hre,
339 artifact.abi,
340 "0x",
341 signer
342 );
343
344 let contract = factory.attach(address);
345 // If there's no signer, we connect the contract instance to the provider for the selected network.
346 if (contract.provider === null) {
347 contract = contract.connect(hre.ethers.provider);
348 }
349
350 return contract;
351}
352
353// This helper adds a `gas` field to the ABI function elements if the network
354// is set up to use a fixed amount of gas.
355// This is done so that ethers doesn't automatically estimate gas limits on
356// every call.
357function addGasToAbiMethodsIfNecessary(
358 networkConfig: NetworkConfig,
359 abi: any[]
360): any[] {
361 const { BigNumber } = require("ethers") as typeof ethers;
362
363 if (networkConfig.gas === "auto" || networkConfig.gas === undefined) {
364 return abi;
365 }
366
367 // ethers adds 21000 to whatever the abi `gas` field has. This may lead to
368 // OOG errors, as people may set the default gas to the same value as the
369 // block gas limit, especially on Hardhat Network.
370 // To avoid this, we substract 21000.
371 // HOTFIX: We substract 1M for now. See: https://github.com/ethers-io/ethers.js/issues/1058#issuecomment-703175279
372 const gasLimit = BigNumber.from(networkConfig.gas).sub(1000000).toHexString();
373
374 const modifiedAbi: any[] = [];
375
376 for (const abiElement of abi) {
377 if (abiElement.type !== "function") {
378 modifiedAbi.push(abiElement);
379 continue;
380 }
381
382 modifiedAbi.push({
383 ...abiElement,
384 gas: gasLimit,
385 });
386 }
387
388 return modifiedAbi;
389}
390
391function linkBytecode(artifact: Artifact, libraries: Link[]): string {
392 let bytecode = artifact.bytecode;
393
394 // TODO: measure performance impact
395 for (const { sourceName, libraryName, address } of libraries) {
396 const linkReferences = artifact.linkReferences[sourceName][libraryName];
397 for (const { start, length } of linkReferences) {
398 bytecode =
399 bytecode.substr(0, 2 + start * 2) +
400 address.substr(2) +
401 bytecode.substr(2 + (start + length) * 2);
402 }
403 }
404
405 return bytecode;
406}