Merge branch 'master' into feat/access-manager
This commit is contained in:
14
test/helpers/account.js
Normal file
14
test/helpers/account.js
Normal file
@ -0,0 +1,14 @@
|
||||
const { web3 } = require('hardhat');
|
||||
const { impersonateAccount, setBalance } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
// Hardhat default balance
|
||||
const DEFAULT_BALANCE = web3.utils.toBN('10000000000000000000000');
|
||||
|
||||
async function impersonate(account, balance = DEFAULT_BALANCE) {
|
||||
await impersonateAccount(account);
|
||||
await setBalance(account, balance);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
impersonate,
|
||||
};
|
||||
22
test/helpers/create.js
Normal file
22
test/helpers/create.js
Normal file
@ -0,0 +1,22 @@
|
||||
const RLP = require('rlp');
|
||||
|
||||
function computeCreateAddress(deployer, nonce) {
|
||||
return web3.utils.toChecksumAddress(web3.utils.sha3(RLP.encode([deployer.address ?? deployer, nonce])).slice(-40));
|
||||
}
|
||||
|
||||
function computeCreate2Address(saltHex, bytecode, deployer) {
|
||||
return web3.utils.toChecksumAddress(
|
||||
web3.utils
|
||||
.sha3(
|
||||
`0x${['ff', deployer.address ?? deployer, saltHex, web3.utils.soliditySha3(bytecode)]
|
||||
.map(x => x.replace(/0x/, ''))
|
||||
.join('')}`,
|
||||
)
|
||||
.slice(-40),
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
computeCreateAddress,
|
||||
computeCreate2Address,
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
function computeCreate2Address(saltHex, bytecode, deployer) {
|
||||
return web3.utils.toChecksumAddress(
|
||||
`0x${web3.utils
|
||||
.sha3(`0x${['ff', deployer, saltHex, web3.utils.soliditySha3(bytecode)].map(x => x.replace(/0x/, '')).join('')}`)
|
||||
.slice(-40)}`,
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
computeCreate2Address,
|
||||
};
|
||||
@ -1,61 +0,0 @@
|
||||
const { promisify } = require('util');
|
||||
|
||||
const BridgeAMBMock = artifacts.require('BridgeAMBMock');
|
||||
const BridgeArbitrumL1Mock = artifacts.require('BridgeArbitrumL1Mock');
|
||||
const BridgeArbitrumL2Mock = artifacts.require('BridgeArbitrumL2Mock');
|
||||
const BridgeOptimismMock = artifacts.require('BridgeOptimismMock');
|
||||
const BridgePolygonChildMock = artifacts.require('BridgePolygonChildMock');
|
||||
|
||||
class BridgeHelper {
|
||||
static async deploy(type) {
|
||||
return new BridgeHelper(await deployBridge(type));
|
||||
}
|
||||
|
||||
constructor(bridge) {
|
||||
this.bridge = bridge;
|
||||
this.address = bridge.address;
|
||||
}
|
||||
|
||||
call(from, target, selector = undefined, args = []) {
|
||||
return this.bridge.relayAs(
|
||||
target.address || target,
|
||||
selector ? target.contract.methods[selector](...args).encodeABI() : '0x',
|
||||
from,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function deployBridge(type = 'Arbitrum-L2') {
|
||||
switch (type) {
|
||||
case 'AMB':
|
||||
return BridgeAMBMock.new();
|
||||
|
||||
case 'Arbitrum-L1':
|
||||
return BridgeArbitrumL1Mock.new();
|
||||
|
||||
case 'Arbitrum-L2': {
|
||||
const instance = await BridgeArbitrumL2Mock.new();
|
||||
const code = await web3.eth.getCode(instance.address);
|
||||
await promisify(web3.currentProvider.send.bind(web3.currentProvider))({
|
||||
jsonrpc: '2.0',
|
||||
method: 'hardhat_setCode',
|
||||
params: ['0x0000000000000000000000000000000000000064', code],
|
||||
id: new Date().getTime(),
|
||||
});
|
||||
return BridgeArbitrumL2Mock.at('0x0000000000000000000000000000000000000064');
|
||||
}
|
||||
|
||||
case 'Optimism':
|
||||
return BridgeOptimismMock.new();
|
||||
|
||||
case 'Polygon-Child':
|
||||
return BridgePolygonChildMock.new();
|
||||
|
||||
default:
|
||||
throw new Error(`CrossChain: ${type} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BridgeHelper,
|
||||
};
|
||||
@ -1,22 +1,41 @@
|
||||
const { config } = require('hardhat');
|
||||
|
||||
const optimizationsEnabled = config.solidity.compilers.some(c => c.settings.optimizer.enabled);
|
||||
const { expect } = require('chai');
|
||||
|
||||
/** Revert handler that supports custom errors. */
|
||||
async function expectRevertCustomError(promise, reason) {
|
||||
try {
|
||||
await promise;
|
||||
expect.fail("Expected promise to throw but it didn't");
|
||||
} catch (revert) {
|
||||
if (reason) {
|
||||
if (optimizationsEnabled) {
|
||||
// Optimizations currently mess with Hardhat's decoding of custom errors
|
||||
expect(revert.message).to.include.oneOf([reason, 'unrecognized return data or custom error']);
|
||||
} else {
|
||||
expect(revert.message).to.include(reason);
|
||||
}
|
||||
}
|
||||
async function expectRevertCustomError(promise, expectedErrorName, args) {
|
||||
if (!Array.isArray(args)) {
|
||||
expect.fail('Expected 3rd array parameter for error arguments');
|
||||
}
|
||||
|
||||
await promise.then(
|
||||
() => expect.fail("Expected promise to throw but it didn't"),
|
||||
({ message }) => {
|
||||
// The revert message for custom errors looks like:
|
||||
// VM Exception while processing transaction:
|
||||
// reverted with custom error 'InvalidAccountNonce("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 0)'
|
||||
|
||||
// Attempt to parse as a custom error
|
||||
const match = message.match(/custom error '(?<name>\w+)\((?<args>.*)\)'/);
|
||||
if (!match) {
|
||||
expect.fail(`Could not parse as custom error. ${message}`);
|
||||
}
|
||||
// Extract the error name and parameters
|
||||
const errorName = match.groups.name;
|
||||
const argMatches = [...match.groups.args.matchAll(/-?\w+/g)];
|
||||
|
||||
// Assert error name
|
||||
expect(errorName).to.be.equal(
|
||||
expectedErrorName,
|
||||
`Unexpected custom error name (with found args: [${argMatches.map(([a]) => a)}])`,
|
||||
);
|
||||
|
||||
// Coerce to string for comparison since `arg` can be either a number or hex.
|
||||
const sanitizedExpected = args.map(arg => arg.toString().toLowerCase());
|
||||
const sanitizedActual = argMatches.map(([arg]) => arg.toString().toLowerCase());
|
||||
|
||||
// Assert argument equality
|
||||
expect(sanitizedActual).to.have.members(sanitizedExpected, `Unexpected ${errorName} arguments`);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
const { BN } = require('@openzeppelin/test-helpers');
|
||||
|
||||
function Enum(...options) {
|
||||
return Object.fromEntries(options.map((key, i) => [key, new BN(i)]));
|
||||
return Object.fromEntries(options.map((key, i) => [key, web3.utils.toBN(i)]));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Enum,
|
||||
ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'),
|
||||
VoteType: Enum('Against', 'For', 'Abstain'),
|
||||
Rounding: Enum('Down', 'Up', 'Zero'),
|
||||
Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'),
|
||||
AccessMode: Enum('Custom', 'Closed', 'Open'),
|
||||
OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'),
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
const { getStorageAt, setStorageAt } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const ImplementationLabel = 'eip1967.proxy.implementation';
|
||||
const AdminLabel = 'eip1967.proxy.admin';
|
||||
const BeaconLabel = 'eip1967.proxy.beacon';
|
||||
@ -7,12 +9,27 @@ function labelToSlot(label) {
|
||||
}
|
||||
|
||||
function getSlot(address, slot) {
|
||||
return web3.eth.getStorageAt(
|
||||
return getStorageAt(
|
||||
web3.utils.isAddress(address) ? address : address.address,
|
||||
web3.utils.isHex(slot) ? slot : labelToSlot(slot),
|
||||
);
|
||||
}
|
||||
|
||||
function setSlot(address, slot, value) {
|
||||
const hexValue = web3.utils.isHex(value) ? value : web3.utils.toHex(value);
|
||||
|
||||
return setStorageAt(
|
||||
web3.utils.isAddress(address) ? address : address.address,
|
||||
web3.utils.isHex(slot) ? slot : labelToSlot(slot),
|
||||
web3.utils.padLeft(hexValue, 64),
|
||||
);
|
||||
}
|
||||
|
||||
async function getAddressInSlot(address, slot) {
|
||||
const slotValue = await getSlot(address, slot);
|
||||
return web3.utils.toChecksumAddress(slotValue.substring(slotValue.length - 40));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ImplementationLabel,
|
||||
AdminLabel,
|
||||
@ -20,5 +37,7 @@ module.exports = {
|
||||
ImplementationSlot: labelToSlot(ImplementationLabel),
|
||||
AdminSlot: labelToSlot(AdminLabel),
|
||||
BeaconSlot: labelToSlot(BeaconLabel),
|
||||
setSlot,
|
||||
getSlot,
|
||||
getAddressInSlot,
|
||||
};
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
const { web3 } = require('hardhat');
|
||||
const { forward } = require('../helpers/time');
|
||||
const { ProposalState } = require('./enums');
|
||||
|
||||
function zip(...args) {
|
||||
return Array(Math.max(...args.map(array => array.length)))
|
||||
@ -14,6 +16,9 @@ function concatOpts(args, opts = null) {
|
||||
return opts ? args.concat(opts) : args;
|
||||
}
|
||||
|
||||
const timelockSalt = (address, descriptionHash) =>
|
||||
'0x' + web3.utils.toBN(address).shln(96).xor(web3.utils.toBN(descriptionHash)).toString(16, 64);
|
||||
|
||||
class GovernorHelper {
|
||||
constructor(governor, mode = 'blocknumber') {
|
||||
this.governor = governor;
|
||||
@ -80,7 +85,7 @@ class GovernorHelper {
|
||||
...concatOpts(proposal.shortProposal, opts),
|
||||
);
|
||||
default:
|
||||
throw new Error(`unsuported visibility "${visibility}"`);
|
||||
throw new Error(`unsupported visibility "${visibility}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,26 +95,17 @@ class GovernorHelper {
|
||||
return vote.signature
|
||||
? // if signature, and either params or reason →
|
||||
vote.params || vote.reason
|
||||
? vote
|
||||
.signature(this.governor, {
|
||||
proposalId: proposal.id,
|
||||
support: vote.support,
|
||||
reason: vote.reason || '',
|
||||
params: vote.params || '',
|
||||
})
|
||||
.then(({ v, r, s }) =>
|
||||
this.governor.castVoteWithReasonAndParamsBySig(
|
||||
...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params || '', v, r, s], opts),
|
||||
? this.sign(vote).then(signature =>
|
||||
this.governor.castVoteWithReasonAndParamsBySig(
|
||||
...concatOpts(
|
||||
[proposal.id, vote.support, vote.voter, vote.reason || '', vote.params || '', signature],
|
||||
opts,
|
||||
),
|
||||
)
|
||||
: vote
|
||||
.signature(this.governor, {
|
||||
proposalId: proposal.id,
|
||||
support: vote.support,
|
||||
})
|
||||
.then(({ v, r, s }) =>
|
||||
this.governor.castVoteBySig(...concatOpts([proposal.id, vote.support, v, r, s], opts)),
|
||||
)
|
||||
),
|
||||
)
|
||||
: this.sign(vote).then(signature =>
|
||||
this.governor.castVoteBySig(...concatOpts([proposal.id, vote.support, vote.voter, signature], opts)),
|
||||
)
|
||||
: vote.params
|
||||
? // otherwise if params
|
||||
this.governor.castVoteWithReasonAndParams(
|
||||
@ -121,6 +117,23 @@ class GovernorHelper {
|
||||
: this.governor.castVote(...concatOpts([proposal.id, vote.support], opts));
|
||||
}
|
||||
|
||||
sign(vote = {}) {
|
||||
return vote.signature(this.governor, this.forgeMessage(vote));
|
||||
}
|
||||
|
||||
forgeMessage(vote = {}) {
|
||||
const proposal = this.currentProposal;
|
||||
|
||||
const message = { proposalId: proposal.id, support: vote.support, voter: vote.voter, nonce: vote.nonce };
|
||||
|
||||
if (vote.params || vote.reason) {
|
||||
message.reason = vote.reason || '';
|
||||
message.params = vote.params || '';
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
async waitForSnapshot(offset = 0) {
|
||||
const proposal = this.currentProposal;
|
||||
const timepoint = await this.governor.proposalSnapshot(proposal.id);
|
||||
@ -196,6 +209,45 @@ class GovernorHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to
|
||||
* the underlying position in the `ProposalState` enum. For example:
|
||||
*
|
||||
* 0x000...10000
|
||||
* ^^^^^^------ ...
|
||||
* ^----- Succeeded
|
||||
* ^---- Defeated
|
||||
* ^--- Canceled
|
||||
* ^-- Active
|
||||
* ^- Pending
|
||||
*/
|
||||
function proposalStatesToBitMap(proposalStates, options = {}) {
|
||||
if (!Array.isArray(proposalStates)) {
|
||||
proposalStates = [proposalStates];
|
||||
}
|
||||
const statesCount = Object.keys(ProposalState).length;
|
||||
let result = 0;
|
||||
|
||||
const uniqueProposalStates = new Set(proposalStates.map(bn => bn.toNumber())); // Remove duplicates
|
||||
for (const state of uniqueProposalStates) {
|
||||
if (state < 0 || state >= statesCount) {
|
||||
expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`);
|
||||
} else {
|
||||
result |= 1 << state;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.inverted) {
|
||||
const mask = 2 ** statesCount - 1;
|
||||
result = result ^ mask;
|
||||
}
|
||||
|
||||
const hex = web3.utils.numberToHex(result);
|
||||
return web3.utils.padLeft(hex, 64);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GovernorHelper,
|
||||
proposalStatesToBitMap,
|
||||
timelockSalt,
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
function mapValues(obj, fn) {
|
||||
return Object.fromEntries([...Object.entries(obj)].map(([k, v]) => [k, fn(v)]));
|
||||
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
11
test/helpers/math.js
Normal file
11
test/helpers/math.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
// sum of integer / bignumber
|
||||
sum: (...args) => args.reduce((acc, n) => acc + n, 0),
|
||||
BNsum: (...args) => args.reduce((acc, n) => acc.add(n), web3.utils.toBN(0)),
|
||||
// min of integer / bignumber
|
||||
min: (...args) => args.slice(1).reduce((x, y) => (x < y ? x : y), args[0]),
|
||||
BNmin: (...args) => args.slice(1).reduce((x, y) => (x.lt(y) ? x : y), args[0]),
|
||||
// max of integer / bignumber
|
||||
max: (...args) => args.slice(1).reduce((x, y) => (x > y ? x : y), args[0]),
|
||||
BNmax: (...args) => args.slice(1).reduce((x, y) => (x.gt(y) ? x : y), args[0]),
|
||||
};
|
||||
Reference in New Issue
Block a user