Add utilities for CrossChain messaging (#3183)

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
This commit is contained in:
Hadrien Croubois
2022-03-30 16:41:04 +02:00
committed by GitHub
parent 02fcc75bb7
commit 668a648bc6
36 changed files with 1477 additions and 73 deletions

View File

@ -0,0 +1,59 @@
const { expectRevert } = require('@openzeppelin/test-helpers');
const { BridgeHelper } = require('../helpers/crosschain');
const {
shouldBehaveLikeAccessControl,
} = require('./AccessControl.behavior.js');
const crossChainRoleAlias = (role) => web3.utils.leftPad(
web3.utils.toHex(web3.utils.toBN(role).xor(web3.utils.toBN(web3.utils.soliditySha3('CROSSCHAIN_ALIAS')))),
64,
);
const AccessControlCrossChainMock = artifacts.require('AccessControlCrossChainMock');
const ROLE = web3.utils.soliditySha3('ROLE');
contract('AccessControl', function (accounts) {
before(async function () {
this.bridge = await BridgeHelper.deploy();
});
beforeEach(async function () {
this.accessControl = await AccessControlCrossChainMock.new({ from: accounts[0] });
});
shouldBehaveLikeAccessControl('AccessControl', ...accounts);
describe('CrossChain enabled', function () {
beforeEach(async function () {
await this.accessControl.grantRole(ROLE, accounts[0], { from: accounts[0] });
await this.accessControl.grantRole(crossChainRoleAlias(ROLE), accounts[1], { from: accounts[0] });
});
it('check alliassing', async function () {
expect(await this.accessControl.crossChainRoleAlias(ROLE)).to.be.bignumber.equal(crossChainRoleAlias(ROLE));
});
it('Crosschain calls not authorized to non-aliased addresses', async function () {
await expectRevert(
this.bridge.call(
accounts[0],
this.accessControl,
'senderProtected',
[ ROLE ],
),
`AccessControl: account ${accounts[0].toLowerCase()} is missing role ${crossChainRoleAlias(ROLE)}`,
);
});
it('Crosschain calls not authorized to non-aliased addresses', async function () {
await this.bridge.call(
accounts[1],
this.accessControl,
'senderProtected',
[ ROLE ],
);
});
});
});

View File

@ -0,0 +1,83 @@
const { BridgeHelper } = require('../helpers/crosschain');
const { expectRevertCustomError } = require('../helpers/customError');
function randomAddress () {
return web3.utils.toChecksumAddress(web3.utils.randomHex(20));
}
const CrossChainEnabledAMBMock = artifacts.require('CrossChainEnabledAMBMock');
const CrossChainEnabledArbitrumL1Mock = artifacts.require('CrossChainEnabledArbitrumL1Mock');
const CrossChainEnabledArbitrumL2Mock = artifacts.require('CrossChainEnabledArbitrumL2Mock');
const CrossChainEnabledOptimismMock = artifacts.require('CrossChainEnabledOptimismMock');
const CrossChainEnabledPolygonChildMock = artifacts.require('CrossChainEnabledPolygonChildMock');
function shouldBehaveLikeReceiver (sender = randomAddress()) {
it('should reject same-chain calls', async function () {
await expectRevertCustomError(
this.receiver.crossChainRestricted(),
'NotCrossChainCall()',
);
});
it('should restrict to cross-chain call from a invalid sender', async function () {
await expectRevertCustomError(
this.bridge.call(sender, this.receiver, 'crossChainOwnerRestricted()'),
`InvalidCrossChainSender("${sender}", "${await this.receiver.owner()}")`,
);
});
it('should grant access to cross-chain call from the owner', async function () {
await this.bridge.call(
await this.receiver.owner(),
this.receiver,
'crossChainOwnerRestricted()',
);
});
}
contract('CrossChainEnabled', function () {
describe('AMB', function () {
beforeEach(async function () {
this.bridge = await BridgeHelper.deploy('AMB');
this.receiver = await CrossChainEnabledAMBMock.new(this.bridge.address);
});
shouldBehaveLikeReceiver();
});
describe('Arbitrum-L1', function () {
beforeEach(async function () {
this.bridge = await BridgeHelper.deploy('Arbitrum-L1');
this.receiver = await CrossChainEnabledArbitrumL1Mock.new(this.bridge.address);
});
shouldBehaveLikeReceiver();
});
describe('Arbitrum-L2', function () {
beforeEach(async function () {
this.bridge = await BridgeHelper.deploy('Arbitrum-L2');
this.receiver = await CrossChainEnabledArbitrumL2Mock.new(this.bridge.address);
});
shouldBehaveLikeReceiver();
});
describe('Optimism', function () {
beforeEach(async function () {
this.bridge = await BridgeHelper.deploy('Optimism');
this.receiver = await CrossChainEnabledOptimismMock.new(this.bridge.address);
});
shouldBehaveLikeReceiver();
});
describe('Polygon-Child', function () {
beforeEach(async function () {
this.bridge = await BridgeHelper.deploy('Polygon-Child');
this.receiver = await CrossChainEnabledPolygonChildMock.new(this.bridge.address);
});
shouldBehaveLikeReceiver();
});
});

View File

@ -0,0 +1,63 @@
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,
};

View File

@ -0,0 +1,24 @@
const { config } = require('hardhat');
const optimizationsEnabled = config.solidity.compilers.some(c => c.settings.optimizer.enabled);
/** 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);
}
}
}
};
module.exports = {
expectRevertCustomError,
};

View File

@ -1,5 +1,5 @@
const { expectEvent } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { expectRevertCustomError } = require('../../helpers/customError');
const Bytes32DequeMock = artifacts.require('Bytes32DequeMock');
@ -10,18 +10,6 @@ async function getContent (deque) {
return values;
}
/** Revert handler that supports custom errors. */
async function expectRevert (promise, reason) {
try {
await promise;
expect.fail('Expected promise to throw but it didn\'t');
} catch (error) {
if (reason) {
expect(error.message).to.include(reason);
}
}
}
contract('DoubleEndedQueue', function (accounts) {
const bytesA = '0xdeadbeef'.padEnd(66, '0');
const bytesB = '0x0123456789'.padEnd(66, '0');
@ -39,10 +27,10 @@ contract('DoubleEndedQueue', function (accounts) {
});
it('reverts on accesses', async function () {
await expectRevert(this.deque.popBack(), 'Empty()');
await expectRevert(this.deque.popFront(), 'Empty()');
await expectRevert(this.deque.back(), 'Empty()');
await expectRevert(this.deque.front(), 'Empty()');
await expectRevertCustomError(this.deque.popBack(), 'Empty()');
await expectRevertCustomError(this.deque.popFront(), 'Empty()');
await expectRevertCustomError(this.deque.back(), 'Empty()');
await expectRevertCustomError(this.deque.front(), 'Empty()');
});
});
@ -63,7 +51,7 @@ contract('DoubleEndedQueue', function (accounts) {
});
it('out of bounds access', async function () {
await expectRevert(this.deque.at(this.content.length), 'OutOfBounds()');
await expectRevertCustomError(this.deque.at(this.content.length), 'OutOfBounds()');
});
describe('push', function () {