Reimplement GovernanceRouter as an AbacusApp (#178)
parent
6e7fbd76fe
commit
3796248374
@ -1,557 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
pragma experimental ABIEncoderV2; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {Outbox} from "../Outbox.sol"; |
||||
import {Version0} from "../Version0.sol"; |
||||
import {XAppConnectionManager, TypeCasts} from "../XAppConnectionManager.sol"; |
||||
import {IMessageRecipient} from "../../interfaces/IMessageRecipient.sol"; |
||||
import {GovernanceMessage} from "./GovernanceMessage.sol"; |
||||
// ============ External Imports ============ |
||||
import {Initializable} from "@openzeppelin/contracts/proxy/Initializable.sol"; |
||||
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; |
||||
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
|
||||
contract GovernanceRouter is Version0, Initializable, IMessageRecipient { |
||||
// ============ Libraries ============ |
||||
|
||||
using SafeMath for uint256; |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
using GovernanceMessage for bytes29; |
||||
|
||||
// ============ Immutables ============ |
||||
|
||||
uint32 public immutable localDomain; |
||||
// number of seconds before recovery can be activated |
||||
uint256 public immutable recoveryTimelock; |
||||
|
||||
// ============ Public Storage ============ |
||||
|
||||
// timestamp when recovery timelock expires; 0 if timelock has not been initiated |
||||
uint256 public recoveryActiveAt; |
||||
// the address of the recovery manager multisig |
||||
address public recoveryManager; |
||||
// the local entity empowered to call governance functions, set to 0x0 on non-Governor chains |
||||
address public governor; |
||||
// domain of Governor chain -- for accepting incoming messages from Governor |
||||
uint32 public governorDomain; |
||||
// xAppConnectionManager contract which stores Inbox addresses |
||||
XAppConnectionManager public xAppConnectionManager; |
||||
// domain -> remote GovernanceRouter contract address |
||||
mapping(uint32 => bytes32) public routers; |
||||
// array of all domains with registered GovernanceRouter |
||||
uint32[] public domains; |
||||
|
||||
// ============ Upgrade Gap ============ |
||||
|
||||
// gap for upgrade safety |
||||
uint256[43] private __GAP; |
||||
|
||||
// ============ Events ============ |
||||
|
||||
/** |
||||
* @notice Emitted a remote GovernanceRouter address is added, removed, or changed |
||||
* @param domain the domain of the remote Router |
||||
* @param previousRouter the previously registered router; 0 if router is being added |
||||
* @param newRouter the new registered router; 0 if router is being removed |
||||
*/ |
||||
event SetRouter( |
||||
uint32 indexed domain, |
||||
bytes32 previousRouter, |
||||
bytes32 newRouter |
||||
); |
||||
|
||||
/** |
||||
* @notice Emitted when the Governor role is transferred |
||||
* @param previousGovernorDomain the domain of the previous Governor |
||||
* @param newGovernorDomain the domain of the new Governor |
||||
* @param previousGovernor the address of the previous Governor; 0 if the governor was remote |
||||
* @param newGovernor the address of the new Governor; 0 if the governor is remote |
||||
*/ |
||||
event TransferGovernor( |
||||
uint32 previousGovernorDomain, |
||||
uint32 newGovernorDomain, |
||||
address indexed previousGovernor, |
||||
address indexed newGovernor |
||||
); |
||||
|
||||
/** |
||||
* @notice Emitted when the RecoveryManager role is transferred |
||||
* @param previousRecoveryManager the address of the previous RecoveryManager |
||||
* @param newRecoveryManager the address of the new RecoveryManager |
||||
*/ |
||||
event TransferRecoveryManager( |
||||
address indexed previousRecoveryManager, |
||||
address indexed newRecoveryManager |
||||
); |
||||
|
||||
/** |
||||
* @notice Emitted when recovery state is initiated by the RecoveryManager |
||||
* @param recoveryManager the address of the current RecoveryManager who initiated the transition |
||||
* @param recoveryActiveAt the block at which recovery state will be active |
||||
*/ |
||||
event InitiateRecovery( |
||||
address indexed recoveryManager, |
||||
uint256 recoveryActiveAt |
||||
); |
||||
|
||||
/** |
||||
* @notice Emitted when recovery state is exited by the RecoveryManager |
||||
* @param recoveryManager the address of the current RecoveryManager who initiated the transition |
||||
*/ |
||||
event ExitRecovery(address recoveryManager); |
||||
|
||||
modifier typeAssert(bytes29 _view, GovernanceMessage.Types _type) { |
||||
_view.assertType(uint40(_type)); |
||||
_; |
||||
} |
||||
|
||||
// ============ Modifiers ============ |
||||
|
||||
modifier onlyInbox() { |
||||
require(xAppConnectionManager.isInbox(msg.sender), "!inbox"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyGovernorRouter(uint32 _domain, bytes32 _address) { |
||||
require(_isGovernorRouter(_domain, _address), "!governorRouter"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyGovernor() { |
||||
require(msg.sender == governor, "! called by governor"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyRecoveryManager() { |
||||
require(msg.sender == recoveryManager, "! called by recovery manager"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyInRecovery() { |
||||
require(inRecovery(), "! in recovery"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyNotInRecovery() { |
||||
require(!inRecovery(), "in recovery"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyGovernorOrRecoveryManager() { |
||||
if (!inRecovery()) { |
||||
require(msg.sender == governor, "! called by governor"); |
||||
} else { |
||||
require( |
||||
msg.sender == recoveryManager, |
||||
"! called by recovery manager" |
||||
); |
||||
} |
||||
_; |
||||
} |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor(uint32 _localDomain, uint256 _recoveryTimelock) { |
||||
localDomain = _localDomain; |
||||
recoveryTimelock = _recoveryTimelock; |
||||
} |
||||
|
||||
// ============ Initializer ============ |
||||
|
||||
function initialize( |
||||
address _xAppConnectionManager, |
||||
address _recoveryManager |
||||
) public initializer { |
||||
// initialize governor |
||||
address _governorAddr = msg.sender; |
||||
bool _isLocalGovernor = true; |
||||
_transferGovernor(localDomain, _governorAddr, _isLocalGovernor); |
||||
// initialize recovery manager |
||||
recoveryManager = _recoveryManager; |
||||
// initialize XAppConnectionManager |
||||
setXAppConnectionManager(_xAppConnectionManager); |
||||
require( |
||||
xAppConnectionManager.localDomain() == localDomain, |
||||
"XAppConnectionManager bad domain" |
||||
); |
||||
} |
||||
|
||||
// ============ External Functions ============ |
||||
|
||||
/** |
||||
* @notice Handle Abacus messages |
||||
* For all non-Governor chains to handle messages |
||||
* sent from the Governor chain via Abacus. |
||||
* Governor chain should never receive messages, |
||||
* because non-Governor chains are not able to send them |
||||
* @param _origin The domain (of the Governor Router) |
||||
* @param _sender The message sender (must be the Governor Router) |
||||
* @param _message The message |
||||
*/ |
||||
function handle( |
||||
uint32 _origin, |
||||
bytes32 _sender, |
||||
bytes memory _message |
||||
) external override onlyInbox onlyGovernorRouter(_origin, _sender) { |
||||
bytes29 _msg = _message.ref(0); |
||||
if (_msg.isValidCall()) { |
||||
_handleCall(_msg.tryAsCall()); |
||||
} else if (_msg.isValidTransferGovernor()) { |
||||
_handleTransferGovernor(_msg.tryAsTransferGovernor()); |
||||
} else if (_msg.isValidSetRouter()) { |
||||
_handleSetRouter(_msg.tryAsSetRouter()); |
||||
} else { |
||||
require(false, "!valid message type"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Dispatch calls locally |
||||
* @param _calls The calls |
||||
*/ |
||||
function callLocal(GovernanceMessage.Call[] calldata _calls) |
||||
external |
||||
onlyGovernorOrRecoveryManager |
||||
{ |
||||
for (uint256 i = 0; i < _calls.length; i++) { |
||||
_dispatchCall(_calls[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Dispatch calls on a remote chain via the remote GovernanceRouter |
||||
* @param _destination The domain of the remote chain |
||||
* @param _calls The calls |
||||
*/ |
||||
function callRemote( |
||||
uint32 _destination, |
||||
GovernanceMessage.Call[] calldata _calls |
||||
) external onlyGovernor onlyNotInRecovery { |
||||
// ensure that destination chain has enrolled router |
||||
bytes32 _router = _mustHaveRouter(_destination); |
||||
// format call message |
||||
bytes memory _msg = GovernanceMessage.formatCalls(_calls); |
||||
// dispatch call message using Abacus |
||||
Outbox(xAppConnectionManager.outbox()).dispatch( |
||||
_destination, |
||||
_router, |
||||
_msg |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* @notice Transfer governorship |
||||
* @param _newDomain The domain of the new governor |
||||
* @param _newGovernor The address of the new governor |
||||
*/ |
||||
function transferGovernor(uint32 _newDomain, address _newGovernor) |
||||
external |
||||
onlyGovernor |
||||
onlyNotInRecovery |
||||
{ |
||||
bool _isLocalGovernor = _isLocalDomain(_newDomain); |
||||
// transfer the governor locally |
||||
_transferGovernor(_newDomain, _newGovernor, _isLocalGovernor); |
||||
// if the governor domain is local, we only need to change the governor address locally |
||||
// no need to message remote routers; they should already have the same domain set and governor = bytes32(0) |
||||
if (_isLocalGovernor) { |
||||
return; |
||||
} |
||||
// format transfer governor message |
||||
bytes memory _transferGovernorMessage = GovernanceMessage |
||||
.formatTransferGovernor( |
||||
_newDomain, |
||||
TypeCasts.addressToBytes32(_newGovernor) |
||||
); |
||||
// send transfer governor message to all remote routers |
||||
// note: this assumes that the Router is on the global GovernorDomain; |
||||
// this causes a process error when relinquishing governorship |
||||
// on a newly deployed domain which is not the GovernorDomain |
||||
_sendToAllRemoteRouters(_transferGovernorMessage); |
||||
} |
||||
|
||||
/** |
||||
* @notice Transfer recovery manager role |
||||
* @dev callable by the recoveryManager at any time to transfer the role |
||||
* @param _newRecoveryManager The address of the new recovery manager |
||||
*/ |
||||
function transferRecoveryManager(address _newRecoveryManager) |
||||
external |
||||
onlyRecoveryManager |
||||
{ |
||||
emit TransferRecoveryManager(recoveryManager, _newRecoveryManager); |
||||
recoveryManager = _newRecoveryManager; |
||||
} |
||||
|
||||
/** |
||||
* @notice Set the router address for a given domain and |
||||
* dispatch the change to all remote routers |
||||
* @param _domain The domain |
||||
* @param _router The address of the new router |
||||
*/ |
||||
function setRouter(uint32 _domain, bytes32 _router) |
||||
external |
||||
onlyGovernor |
||||
onlyNotInRecovery |
||||
{ |
||||
// set the router locally |
||||
_setRouter(_domain, _router); |
||||
// format message to set the router on all remote routers |
||||
bytes memory _setRouterMessage = GovernanceMessage.formatSetRouter( |
||||
_domain, |
||||
_router |
||||
); |
||||
|
||||
_sendToAllRemoteRouters(_setRouterMessage); |
||||
} |
||||
|
||||
/** |
||||
* @notice Set the router address *locally only* |
||||
* for the deployer to setup the router mapping locally |
||||
* before transferring governorship to the "true" governor |
||||
* @dev External helper for contract setup |
||||
* @param _domain The domain |
||||
* @param _router The new router |
||||
*/ |
||||
function setRouterLocal(uint32 _domain, bytes32 _router) |
||||
external |
||||
onlyGovernorOrRecoveryManager |
||||
{ |
||||
// set the router locally |
||||
_setRouter(_domain, _router); |
||||
} |
||||
|
||||
/** |
||||
* @notice Set the address of the XAppConnectionManager |
||||
* @dev Domain/address validation helper |
||||
* @param _xAppConnectionManager The address of the new xAppConnectionManager |
||||
*/ |
||||
function setXAppConnectionManager(address _xAppConnectionManager) |
||||
public |
||||
onlyGovernorOrRecoveryManager |
||||
{ |
||||
xAppConnectionManager = XAppConnectionManager(_xAppConnectionManager); |
||||
} |
||||
|
||||
/** |
||||
* @notice Initiate the recovery timelock |
||||
* @dev callable by the recovery manager |
||||
*/ |
||||
function initiateRecoveryTimelock() |
||||
external |
||||
onlyNotInRecovery |
||||
onlyRecoveryManager |
||||
{ |
||||
require(recoveryActiveAt == 0, "recovery already initiated"); |
||||
// set the time that recovery will be active |
||||
recoveryActiveAt = block.timestamp.add(recoveryTimelock); |
||||
emit InitiateRecovery(recoveryManager, recoveryActiveAt); |
||||
} |
||||
|
||||
/** |
||||
* @notice Exit recovery mode |
||||
* @dev callable by the recovery manager to end recovery mode |
||||
*/ |
||||
function exitRecovery() external onlyRecoveryManager { |
||||
require(recoveryActiveAt != 0, "recovery not initiated"); |
||||
delete recoveryActiveAt; |
||||
emit ExitRecovery(recoveryManager); |
||||
} |
||||
|
||||
// ============ Public Functions ============ |
||||
|
||||
/** |
||||
* @notice Check if the contract is in recovery mode currently |
||||
* @return TRUE iff the contract is actively in recovery mode currently |
||||
*/ |
||||
function inRecovery() public view returns (bool) { |
||||
uint256 _recoveryActiveAt = recoveryActiveAt; |
||||
bool _recoveryInitiated = _recoveryActiveAt != 0; |
||||
bool _recoveryActive = _recoveryActiveAt <= block.timestamp; |
||||
return _recoveryInitiated && _recoveryActive; |
||||
} |
||||
|
||||
// ============ Internal Functions ============ |
||||
|
||||
/** |
||||
* @notice Handle message dispatching calls locally |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleCall(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, GovernanceMessage.Types.Call) |
||||
{ |
||||
GovernanceMessage.Call[] memory _calls = _msg.getCalls(); |
||||
for (uint256 i = 0; i < _calls.length; i++) { |
||||
_dispatchCall(_calls[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message transferring governorship to a new Governor |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleTransferGovernor(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, GovernanceMessage.Types.TransferGovernor) |
||||
{ |
||||
uint32 _newDomain = _msg.domain(); |
||||
address _newGovernor = TypeCasts.bytes32ToAddress(_msg.governor()); |
||||
bool _isLocalGovernor = _isLocalDomain(_newDomain); |
||||
_transferGovernor(_newDomain, _newGovernor, _isLocalGovernor); |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message setting the router address for a given domain |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleSetRouter(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, GovernanceMessage.Types.SetRouter) |
||||
{ |
||||
uint32 _domain = _msg.domain(); |
||||
bytes32 _router = _msg.router(); |
||||
_setRouter(_domain, _router); |
||||
} |
||||
|
||||
/** |
||||
* @notice Dispatch message to all remote routers |
||||
* @param _msg The message |
||||
*/ |
||||
function _sendToAllRemoteRouters(bytes memory _msg) internal { |
||||
Outbox _outbox = Outbox(xAppConnectionManager.outbox()); |
||||
|
||||
for (uint256 i = 0; i < domains.length; i++) { |
||||
if (domains[i] != uint32(0)) { |
||||
_outbox.dispatch(domains[i], routers[domains[i]], _msg); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Dispatch call locally |
||||
* @param _call The call |
||||
* @return _ret |
||||
*/ |
||||
function _dispatchCall(GovernanceMessage.Call memory _call) |
||||
internal |
||||
returns (bytes memory _ret) |
||||
{ |
||||
address _toContract = TypeCasts.bytes32ToAddress(_call.to); |
||||
// attempt to dispatch using low-level call |
||||
bool _success; |
||||
(_success, _ret) = _toContract.call(_call.data); |
||||
// revert if the call failed |
||||
require(_success, "call failed"); |
||||
} |
||||
|
||||
/** |
||||
* @notice Transfer governorship within this contract's state |
||||
* @param _newDomain The domain of the new governor |
||||
* @param _newGovernor The address of the new governor |
||||
* @param _isLocalGovernor True if the newDomain is the localDomain |
||||
*/ |
||||
function _transferGovernor( |
||||
uint32 _newDomain, |
||||
address _newGovernor, |
||||
bool _isLocalGovernor |
||||
) internal { |
||||
// require that the governor domain has a valid router |
||||
if (!_isLocalGovernor) { |
||||
_mustHaveRouter(_newDomain); |
||||
} |
||||
// Governor is 0x0 unless the governor is local |
||||
address _newGov = _isLocalGovernor ? _newGovernor : address(0); |
||||
// emit event before updating state variables |
||||
emit TransferGovernor(governorDomain, _newDomain, governor, _newGov); |
||||
// update state |
||||
governorDomain = _newDomain; |
||||
governor = _newGov; |
||||
} |
||||
|
||||
/** |
||||
* @notice Set the router for a given domain |
||||
* @param _domain The domain |
||||
* @param _newRouter The new router |
||||
*/ |
||||
function _setRouter(uint32 _domain, bytes32 _newRouter) internal { |
||||
bytes32 _previousRouter = routers[_domain]; |
||||
// emit event at beginning in case return after remove |
||||
emit SetRouter(_domain, _previousRouter, _newRouter); |
||||
// if the router is being removed, remove the domain |
||||
if (_newRouter == bytes32(0)) { |
||||
_removeDomain(_domain); |
||||
return; |
||||
} |
||||
// if the router is being added, add the domain |
||||
if (_previousRouter == bytes32(0)) { |
||||
_addDomain(_domain); |
||||
} |
||||
// update state with new router |
||||
routers[_domain] = _newRouter; |
||||
} |
||||
|
||||
/** |
||||
* @notice Add a domain that has a router |
||||
* @param _domain The domain |
||||
*/ |
||||
function _addDomain(uint32 _domain) internal { |
||||
domains.push(_domain); |
||||
} |
||||
|
||||
/** |
||||
* @notice Remove a domain and its associated router |
||||
* @param _domain The domain |
||||
*/ |
||||
function _removeDomain(uint32 _domain) internal { |
||||
delete routers[_domain]; |
||||
// find the index of the domain to remove & delete it from domains[] |
||||
for (uint256 i = 0; i < domains.length; i++) { |
||||
if (domains[i] == _domain) { |
||||
delete domains[i]; |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Determine if a given domain and address is the Governor Router |
||||
* @param _domain The domain |
||||
* @param _address The address of the domain's router |
||||
* @return _ret True if the given domain/address is the |
||||
* Governor Router. |
||||
*/ |
||||
function _isGovernorRouter(uint32 _domain, bytes32 _address) |
||||
internal |
||||
view |
||||
returns (bool) |
||||
{ |
||||
return _domain == governorDomain && _address == routers[_domain]; |
||||
} |
||||
|
||||
/** |
||||
* @notice Determine if a given domain is the local domain |
||||
* @param _domain The domain |
||||
* @return _ret - True if the given domain is the local domain |
||||
*/ |
||||
function _isLocalDomain(uint32 _domain) internal view returns (bool) { |
||||
return _domain == localDomain; |
||||
} |
||||
|
||||
/** |
||||
* @notice Require that a domain has a router and returns the router |
||||
* @param _domain The domain |
||||
* @return _router - The domain's router |
||||
*/ |
||||
function _mustHaveRouter(uint32 _domain) |
||||
internal |
||||
view |
||||
returns (bytes32 _router) |
||||
{ |
||||
_router = routers[_domain]; |
||||
require(_router != bytes32(0), "!router"); |
||||
} |
||||
} |
@ -1,31 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
pragma experimental ABIEncoderV2; |
||||
|
||||
import "../governance/GovernanceRouter.sol"; |
||||
import {TypeCasts} from "../XAppConnectionManager.sol"; |
||||
|
||||
contract TestGovernanceRouter is GovernanceRouter { |
||||
constructor(uint32 _localDomain, uint256 _recoveryTimelock) |
||||
GovernanceRouter(_localDomain, 50) |
||||
{} // solhint-disable-line no-empty-blocks |
||||
|
||||
function testSetRouter(uint32 _domain, bytes32 _router) external { |
||||
_setRouter(_domain, _router); // set the router locally |
||||
|
||||
bytes memory _setRouterMessage = GovernanceMessage.formatSetRouter( |
||||
_domain, |
||||
_router |
||||
); |
||||
|
||||
_sendToAllRemoteRouters(_setRouterMessage); |
||||
} |
||||
|
||||
function containsDomain(uint32 _domain) external view returns (bool) { |
||||
for (uint256 i = 0; i < domains.length; i++) { |
||||
if (domains[i] == _domain) return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
@ -1,390 +0,0 @@ |
||||
import { ethers, abacus } from 'hardhat'; |
||||
import { expect } from 'chai'; |
||||
|
||||
import { formatCall, formatAbacusMessage } from './utils'; |
||||
import { increaseTimestampBy, UpgradeTestHelpers } from '../utils'; |
||||
import { Validator } from '../lib/core'; |
||||
import { Address, Signer } from '../lib/types'; |
||||
import { AbacusDeployment } from '../lib/AbacusDeployment'; |
||||
import { GovernanceDeployment } from '../lib/GovernanceDeployment'; |
||||
import { |
||||
MysteryMathV2__factory, |
||||
TestInbox, |
||||
TestInbox__factory, |
||||
TestRecipient__factory, |
||||
TestGovernanceRouter, |
||||
Inbox, |
||||
Outbox, |
||||
} from '../../typechain'; |
||||
|
||||
const helpers = require('../../../../vectors/proof.json'); |
||||
|
||||
const governorDomain = 1000; |
||||
const nonGovernorDomain = 2000; |
||||
const thirdDomain = 3000; |
||||
const domains = [governorDomain, nonGovernorDomain, thirdDomain]; |
||||
const processGas = 850000; |
||||
const reserveGas = 15000; |
||||
const nullRoot = '0x' + '00'.repeat(32); |
||||
|
||||
/* |
||||
* Deploy the full Abacus suite on two chains |
||||
*/ |
||||
describe('GovernanceRouter', async () => { |
||||
let abacusDeployment: AbacusDeployment; |
||||
let governanceDeployment: GovernanceDeployment; |
||||
let signer: Signer, |
||||
secondSigner: Signer, |
||||
thirdRouter: Signer, |
||||
firstGovernor: Address, |
||||
secondGovernor: Address, |
||||
governorRouter: TestGovernanceRouter, |
||||
nonGovernorRouter: TestGovernanceRouter, |
||||
governorOutbox: Outbox, |
||||
governorInboxOnNonGovernorChain: TestInbox, |
||||
nonGovernorInboxOnGovernorChain: TestInbox, |
||||
validator: Validator; |
||||
|
||||
async function expectGovernor( |
||||
governanceRouter: TestGovernanceRouter, |
||||
expectedGovernorDomain: number, |
||||
expectedGovernor: Address, |
||||
) { |
||||
expect(await governanceRouter.governorDomain()).to.equal( |
||||
expectedGovernorDomain, |
||||
); |
||||
expect(await governanceRouter.governor()).to.equal(expectedGovernor); |
||||
} |
||||
|
||||
before(async () => { |
||||
[thirdRouter, signer, secondSigner] = await ethers.getSigners(); |
||||
validator = await Validator.fromSigner(signer, governorDomain); |
||||
}); |
||||
|
||||
beforeEach(async () => { |
||||
abacusDeployment = await AbacusDeployment.fromDomains(domains, signer); |
||||
governanceDeployment = await GovernanceDeployment.fromAbacusDeployment( |
||||
abacusDeployment, |
||||
signer, |
||||
); |
||||
|
||||
firstGovernor = await signer.getAddress(); |
||||
secondGovernor = await secondSigner.getAddress(); |
||||
|
||||
governorRouter = governanceDeployment.router(governorDomain); |
||||
nonGovernorRouter = governanceDeployment.router(nonGovernorDomain); |
||||
|
||||
governorInboxOnNonGovernorChain = abacusDeployment.inbox( |
||||
nonGovernorDomain, |
||||
governorDomain, |
||||
); |
||||
nonGovernorInboxOnGovernorChain = abacusDeployment.inbox( |
||||
governorDomain, |
||||
nonGovernorDomain, |
||||
); |
||||
|
||||
governorOutbox = abacusDeployment.outbox(governorDomain); |
||||
}); |
||||
|
||||
// NB: must be first test for message proof
|
||||
it('Sends cross-chain message to upgrade contract', async () => { |
||||
const upgradeUtils = new UpgradeTestHelpers(); |
||||
|
||||
// get upgradeBeaconController
|
||||
const ubc = abacusDeployment.ubc(nonGovernorDomain); |
||||
// Transfer ownership of the UBC to governance.
|
||||
await ubc.transferOwnership(nonGovernorRouter.address); |
||||
const mysteryMath = await upgradeUtils.deployMysteryMathUpgradeSetup( |
||||
signer, |
||||
ubc, |
||||
); |
||||
|
||||
// expect results before upgrade
|
||||
await upgradeUtils.expectMysteryMathV1(mysteryMath.proxy); |
||||
|
||||
// Deploy Implementation 2
|
||||
const factory2 = new MysteryMathV2__factory(signer); |
||||
const implementation2 = await factory2.deploy(); |
||||
|
||||
// Format abacus call message
|
||||
const call = await formatCall(ubc, 'upgrade', [ |
||||
mysteryMath.beacon.address, |
||||
implementation2.address, |
||||
]); |
||||
|
||||
// dispatch call on local governorRouter
|
||||
let tx = await governorRouter.callRemote(nonGovernorDomain, [call]); |
||||
|
||||
await abacusDeployment.processMessagesFromDomain(governorDomain); |
||||
// test implementation was upgraded
|
||||
await upgradeUtils.expectMysteryMathV2(mysteryMath.proxy); |
||||
}); |
||||
|
||||
it('Rejects message from unenrolled inbox', async () => { |
||||
const inboxFactory = new TestInbox__factory(signer); |
||||
const unenrolledInbox = await inboxFactory.deploy( |
||||
nonGovernorDomain, |
||||
processGas, |
||||
reserveGas, |
||||
); |
||||
// The ValdiatorManager is unused in this test, but needs to be a contract.
|
||||
await unenrolledInbox.initialize( |
||||
thirdDomain, |
||||
unenrolledInbox.address, |
||||
nullRoot, |
||||
0, |
||||
); |
||||
|
||||
// Create TransferGovernor message
|
||||
const transferGovernorMessage = abacus.governance.formatTransferGovernor( |
||||
thirdDomain, |
||||
abacus.ethersAddressToBytes32(secondGovernor), |
||||
); |
||||
|
||||
const abacusMessage = await formatAbacusMessage( |
||||
unenrolledInbox, |
||||
governorRouter, |
||||
nonGovernorRouter, |
||||
transferGovernorMessage, |
||||
); |
||||
|
||||
// Expect inbox processing to fail when nonGovernorRouter reverts in handle
|
||||
let success = await unenrolledInbox.callStatic.testProcess(abacusMessage); |
||||
expect(success).to.be.false; |
||||
}); |
||||
|
||||
it('Rejects message not from governor router', async () => { |
||||
// Create TransferGovernor message
|
||||
const transferGovernorMessage = abacus.governance.formatTransferGovernor( |
||||
nonGovernorDomain, |
||||
abacus.ethersAddressToBytes32(nonGovernorRouter.address), |
||||
); |
||||
|
||||
const abacusMessage = await formatAbacusMessage( |
||||
governorInboxOnNonGovernorChain, |
||||
nonGovernorRouter, |
||||
governorRouter, |
||||
transferGovernorMessage, |
||||
); |
||||
|
||||
// Set message status to MessageStatus.Proven
|
||||
await nonGovernorInboxOnGovernorChain.setMessageProven(abacusMessage); |
||||
|
||||
// Expect inbox processing to fail when nonGovernorRouter reverts in handle
|
||||
let success = await nonGovernorInboxOnGovernorChain.callStatic.testProcess( |
||||
abacusMessage, |
||||
); |
||||
expect(success).to.be.false; |
||||
}); |
||||
|
||||
it('Accepts a valid transfer governor message', async () => { |
||||
// Enroll router for new domain (in real setting this would
|
||||
// be executed with an Abacus message sent to the nonGovernorRouter)
|
||||
await nonGovernorRouter.testSetRouter( |
||||
thirdDomain, |
||||
abacus.ethersAddressToBytes32(thirdRouter.address), |
||||
); |
||||
|
||||
// Create TransferGovernor message
|
||||
const transferGovernorMessage = abacus.governance.formatTransferGovernor( |
||||
thirdDomain, |
||||
abacus.ethersAddressToBytes32(thirdRouter.address), |
||||
); |
||||
|
||||
const abacusMessage = await formatAbacusMessage( |
||||
governorInboxOnNonGovernorChain, |
||||
governorRouter, |
||||
nonGovernorRouter, |
||||
transferGovernorMessage, |
||||
); |
||||
|
||||
// Expect successful tx on static call
|
||||
let success = await governorInboxOnNonGovernorChain.callStatic.process( |
||||
abacusMessage, |
||||
); |
||||
expect(success).to.be.true; |
||||
|
||||
await governorInboxOnNonGovernorChain.process(abacusMessage); |
||||
await expectGovernor( |
||||
nonGovernorRouter, |
||||
thirdDomain, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
}); |
||||
|
||||
it('Accepts valid set router message', async () => { |
||||
// Create address for router to enroll and domain for router
|
||||
const [router] = await ethers.getSigners(); |
||||
|
||||
// Create SetRouter message
|
||||
const setRouterMessage = abacus.governance.formatSetRouter( |
||||
thirdDomain, |
||||
abacus.ethersAddressToBytes32(router.address), |
||||
); |
||||
|
||||
const abacusMessage = await formatAbacusMessage( |
||||
governorInboxOnNonGovernorChain, |
||||
governorRouter, |
||||
nonGovernorRouter, |
||||
setRouterMessage, |
||||
); |
||||
|
||||
// Expect successful tx
|
||||
let success = await governorInboxOnNonGovernorChain.callStatic.process( |
||||
abacusMessage, |
||||
); |
||||
expect(success).to.be.true; |
||||
|
||||
// Expect new router to be registered for domain and for new domain to be
|
||||
// in domains array
|
||||
await governorInboxOnNonGovernorChain.process(abacusMessage); |
||||
expect(await nonGovernorRouter.routers(thirdDomain)).to.equal( |
||||
abacus.ethersAddressToBytes32(router.address), |
||||
); |
||||
expect(await nonGovernorRouter.containsDomain(thirdDomain)).to.be.true; |
||||
}); |
||||
|
||||
it('Accepts valid call messages', async () => { |
||||
// const TestRecipient = await abacus.deployImplementation('TestRecipient');
|
||||
const testRecipientFactory = new TestRecipient__factory(signer); |
||||
const testRecipient = await testRecipientFactory.deploy(); |
||||
|
||||
// Format abacus call message
|
||||
const arg = 'String!'; |
||||
const call = await formatCall(testRecipient, 'receiveString', [arg]); |
||||
|
||||
// Create Call message to test recipient that calls receiveString
|
||||
const callMessage = abacus.governance.formatCalls([call, call]); |
||||
|
||||
const abacusMessage = await formatAbacusMessage( |
||||
governorInboxOnNonGovernorChain, |
||||
governorRouter, |
||||
nonGovernorRouter, |
||||
callMessage, |
||||
); |
||||
|
||||
// Expect successful tx
|
||||
let success = await governorInboxOnNonGovernorChain.callStatic.testProcess( |
||||
abacusMessage, |
||||
); |
||||
|
||||
expect(success).to.be.true; |
||||
}); |
||||
|
||||
it('Transfers governorship', async () => { |
||||
// Transfer governor on current governor chain
|
||||
|
||||
// Governor HAS NOT been transferred on original governor domain
|
||||
await expectGovernor(governorRouter, governorDomain, firstGovernor); |
||||
// Governor HAS NOT been transferred on original non-governor domain
|
||||
await expectGovernor( |
||||
nonGovernorRouter, |
||||
governorDomain, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
|
||||
// transfer governorship to nonGovernorRouter
|
||||
await governorRouter.transferGovernor(nonGovernorDomain, secondGovernor); |
||||
|
||||
// Governor HAS been transferred on original governor domain
|
||||
await expectGovernor( |
||||
governorRouter, |
||||
nonGovernorDomain, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
// Governor HAS NOT been transferred on original non-governor domain
|
||||
await expectGovernor( |
||||
nonGovernorRouter, |
||||
governorDomain, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
|
||||
const transferGovernorMessage = abacus.governance.formatTransferGovernor( |
||||
nonGovernorDomain, |
||||
abacus.ethersAddressToBytes32(secondGovernor), |
||||
); |
||||
|
||||
const abacusMessage = await formatAbacusMessage( |
||||
governorInboxOnNonGovernorChain, |
||||
governorRouter, |
||||
nonGovernorRouter, |
||||
transferGovernorMessage, |
||||
); |
||||
|
||||
// Process transfer governor message on Inbox
|
||||
await governorInboxOnNonGovernorChain.process(abacusMessage); |
||||
|
||||
// Governor HAS been transferred on original governor domain
|
||||
await expectGovernor( |
||||
governorRouter, |
||||
nonGovernorDomain, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
// Governor HAS been transferred on original non-governor domain
|
||||
await expectGovernor(nonGovernorRouter, nonGovernorDomain, secondGovernor); |
||||
}); |
||||
|
||||
it('Upgrades using GovernanceRouter call', async () => { |
||||
const upgradeUtils = new UpgradeTestHelpers(); |
||||
|
||||
// get upgradeBeaconController
|
||||
const ubc = abacusDeployment.ubc(governorDomain); |
||||
// Transfer ownership of the UBC to governance.
|
||||
await ubc.transferOwnership(governorRouter.address); |
||||
const mysteryMath = await upgradeUtils.deployMysteryMathUpgradeSetup( |
||||
signer, |
||||
ubc, |
||||
); |
||||
|
||||
// expect results before upgrade
|
||||
await upgradeUtils.expectMysteryMathV1(mysteryMath.proxy); |
||||
|
||||
// Deploy Implementation 2
|
||||
const v2Factory = new MysteryMathV2__factory(signer); |
||||
const implementation = await v2Factory.deploy(); |
||||
|
||||
// Format abacus call message
|
||||
const call = await formatCall(ubc, 'upgrade', [ |
||||
mysteryMath.beacon.address, |
||||
implementation.address, |
||||
]); |
||||
|
||||
// dispatch call on local governorRouter
|
||||
await expect(governorRouter.callLocal([call])).to.emit( |
||||
ubc, |
||||
'BeaconUpgraded', |
||||
); |
||||
|
||||
// test implementation was upgraded
|
||||
await upgradeUtils.expectMysteryMathV2(mysteryMath.proxy); |
||||
}); |
||||
|
||||
it('Calls ValidatorManager to set the validator for a domain', async () => { |
||||
const [newValidator] = await ethers.getSigners(); |
||||
const validatorManager = abacusDeployment.validatorManager(governorDomain); |
||||
await validatorManager.transferOwnership(governorRouter.address); |
||||
|
||||
// check current Validator address on Outbox
|
||||
let currentValidatorAddr = await validatorManager.validators( |
||||
governorDomain, |
||||
); |
||||
expect(currentValidatorAddr).to.equal( |
||||
await abacusDeployment.validator(governorDomain).signer.getAddress(), |
||||
); |
||||
|
||||
// format abacus call message
|
||||
const call = await formatCall(validatorManager, 'setValidator', [ |
||||
governorDomain, |
||||
newValidator.address, |
||||
]); |
||||
|
||||
await expect(governorRouter.callLocal([call])).to.emit( |
||||
validatorManager, |
||||
'NewValidator', |
||||
); |
||||
|
||||
// check for new validator
|
||||
currentValidatorAddr = await validatorManager.validators(governorDomain); |
||||
expect(currentValidatorAddr).to.equal(newValidator.address); |
||||
}); |
||||
}); |
@ -1,138 +0,0 @@ |
||||
import { abacus, ethers } from 'hardhat'; |
||||
import { expect } from 'chai'; |
||||
|
||||
import * as utils from './utils'; |
||||
import { Validator, MessageStatus } from '../lib/core'; |
||||
import { Signer, BytesArray } from '../lib/types'; |
||||
import { TestRecipient__factory, TestInbox } from '../../typechain'; |
||||
import { AbacusDeployment } from '../lib/AbacusDeployment'; |
||||
import { GovernanceDeployment } from '../lib/GovernanceDeployment'; |
||||
|
||||
const proveAndProcessTestCases = require('../../../../vectors/proveAndProcess.json'); |
||||
|
||||
const domains = [1000, 2000]; |
||||
const localDomain = domains[0]; |
||||
const remoteDomain = domains[1]; |
||||
|
||||
/* |
||||
* Deploy the full Abacus suite on two chains |
||||
* dispatch messages to Outbox |
||||
* checkpoint on Outbox |
||||
* Sign and relay checkpoints to Inbox |
||||
* TODO prove and process messages on Inbox |
||||
*/ |
||||
describe('SimpleCrossChainMessage', async () => { |
||||
let abacusDeployment: AbacusDeployment; |
||||
let governanceDeployment: GovernanceDeployment; |
||||
let randomSigner: Signer, validator: Validator; |
||||
|
||||
before(async () => { |
||||
[randomSigner] = await ethers.getSigners(); |
||||
validator = await Validator.fromSigner(randomSigner, localDomain); |
||||
abacusDeployment = await AbacusDeployment.fromDomains( |
||||
domains, |
||||
randomSigner, |
||||
); |
||||
governanceDeployment = await GovernanceDeployment.fromAbacusDeployment( |
||||
abacusDeployment, |
||||
randomSigner, |
||||
); |
||||
}); |
||||
|
||||
it('All Outboxs have correct initial state', async () => { |
||||
const nullRoot = '0x' + '00'.repeat(32); |
||||
|
||||
const governorOutbox = abacusDeployment.outbox(localDomain); |
||||
|
||||
let [root, index] = await governorOutbox.latestCheckpoint(); |
||||
expect(root).to.equal(nullRoot); |
||||
expect(index).to.equal(0); |
||||
|
||||
const nonGovernorOutbox = abacusDeployment.outbox(remoteDomain); |
||||
|
||||
[root, index] = await nonGovernorOutbox.latestCheckpoint(); |
||||
expect(root).to.equal(nullRoot); |
||||
expect(index).to.equal(0); |
||||
}); |
||||
|
||||
it('Origin Outbox accepts valid messages', async () => { |
||||
const messages = ['message'].map((message) => |
||||
utils.formatMessage(message, remoteDomain, randomSigner.address), |
||||
); |
||||
await utils.dispatchMessages( |
||||
abacusDeployment.outbox(localDomain), |
||||
messages, |
||||
); |
||||
}); |
||||
|
||||
it('Destination Inbox accepts a checkpoint', async () => { |
||||
const outbox = abacusDeployment.outbox(localDomain); |
||||
const inbox = abacusDeployment.inbox(remoteDomain, localDomain); |
||||
await utils.checkpoint(outbox, inbox, validator); |
||||
}); |
||||
|
||||
it('Origin Outbox accepts batched messages', async () => { |
||||
const messages = ['message1', 'message2', 'message3'].map((message) => |
||||
utils.formatMessage(message, remoteDomain, randomSigner.address), |
||||
); |
||||
await utils.dispatchMessages( |
||||
abacusDeployment.outbox(localDomain), |
||||
messages, |
||||
); |
||||
}); |
||||
|
||||
it('Destination Inbox Accepts a second checkpoint', async () => { |
||||
const outbox = abacusDeployment.outbox(localDomain); |
||||
const inbox = abacusDeployment.inbox(remoteDomain, localDomain); |
||||
await utils.checkpoint(outbox, inbox, validator); |
||||
}); |
||||
|
||||
it('Proves and processes a message on Inbox', async () => { |
||||
// get governance routers
|
||||
const governorRouter = governanceDeployment.router(localDomain); |
||||
const nonGovernorRouter = governanceDeployment.router(remoteDomain); |
||||
|
||||
const inbox = abacusDeployment.inbox(remoteDomain, localDomain); |
||||
const testRecipientFactory = new TestRecipient__factory(randomSigner); |
||||
const TestRecipient = await testRecipientFactory.deploy(); |
||||
|
||||
// ensure `processed` has an initial value of false
|
||||
expect(await TestRecipient.processed()).to.be.false; |
||||
|
||||
// create Call message to test recipient that calls `processCall`
|
||||
const arg = true; |
||||
const call = await utils.formatCall(TestRecipient, 'processCall', [arg]); |
||||
const callMessage = abacus.governance.formatCalls([call]); |
||||
|
||||
// Create Abacus message that is sent from the governor domain and governor
|
||||
// to the nonGovernorRouter on the nonGovernorDomain
|
||||
const nonce = 0; |
||||
const abacusMessage = abacus.formatMessage( |
||||
1000, |
||||
governorRouter.address, |
||||
nonce, |
||||
2000, |
||||
nonGovernorRouter.address, |
||||
callMessage, |
||||
); |
||||
|
||||
// get merkle proof
|
||||
const { path, index } = proveAndProcessTestCases[0]; |
||||
const messageHash = abacus.messageHash(abacusMessage); |
||||
|
||||
// set root
|
||||
const proofRoot = await inbox.testBranchRoot( |
||||
messageHash, |
||||
path as BytesArray, |
||||
index, |
||||
); |
||||
await inbox.setCheckpoint(proofRoot, 1); |
||||
|
||||
// prove and process message
|
||||
await inbox.proveAndProcess(abacusMessage, path as BytesArray, index); |
||||
|
||||
// expect call to have been processed
|
||||
expect(await TestRecipient.processed()).to.be.true; |
||||
expect(await inbox.messages(messageHash)).to.equal(MessageStatus.PROCESSED); |
||||
}); |
||||
}); |
@ -1,164 +0,0 @@ |
||||
import { expect } from 'chai'; |
||||
import { ethers, abacus } from 'hardhat'; |
||||
import * as types from 'ethers'; |
||||
|
||||
import { Validator } from '../lib/core'; |
||||
import { CallData, Address } from '../lib/types'; |
||||
import { |
||||
Inbox, |
||||
TestInbox, |
||||
Outbox, |
||||
TestGovernanceRouter, |
||||
} from '../../typechain'; |
||||
|
||||
type MessageDetails = { |
||||
message: string; |
||||
destinationDomain: number; |
||||
recipientAddress: Address; |
||||
}; |
||||
|
||||
/* |
||||
* Dispatch a message from the specified Outbox contract. |
||||
* |
||||
* @param messageDetails - Message type containing |
||||
* the message string, |
||||
* the destination domain to which the message will be sent, |
||||
* the recipient address on the destination domain to which the message will be dispatched |
||||
*/ |
||||
export async function dispatchMessage( |
||||
outbox: Outbox, |
||||
messageDetails: MessageDetails, |
||||
) { |
||||
const { message, destinationDomain, recipientAddress } = messageDetails; |
||||
|
||||
// Send message with random signer address as msg.sender
|
||||
await outbox.dispatch( |
||||
destinationDomain, |
||||
abacus.ethersAddressToBytes32(recipientAddress), |
||||
ethers.utils.formatBytes32String(message), |
||||
); |
||||
} |
||||
|
||||
/* |
||||
* Dispatch a set of messages to the specified Outbox contract. |
||||
* |
||||
* @param messages - Message[] |
||||
*/ |
||||
export async function dispatchMessages( |
||||
outbox: Outbox, |
||||
messages: MessageDetails[], |
||||
) { |
||||
for (let message of messages) { |
||||
await dispatchMessage(outbox, message); |
||||
} |
||||
} |
||||
|
||||
/* |
||||
* Checkpoints a Outbox, signs that checkpoint, and checkpoints the Inbox |
||||
* |
||||
* @param outbox - The Outbox contract |
||||
* @param inbox - The Inbox contract |
||||
* @param validator - The Validator |
||||
*/ |
||||
export async function checkpoint( |
||||
outbox: Outbox, |
||||
inbox: Inbox, |
||||
validator: Validator, |
||||
) { |
||||
await outbox.checkpoint(); |
||||
const [root, index] = await outbox.latestCheckpoint(); |
||||
const { signature } = await validator.signCheckpoint(root, index.toNumber()); |
||||
await inbox.checkpoint(root, index, signature); |
||||
const checkpointedIndex = await inbox.checkpoints(root); |
||||
expect(checkpointedIndex).to.equal(index); |
||||
} |
||||
|
||||
/* |
||||
* Format into a Message type |
||||
* |
||||
* @param messageString - string for the body of the message |
||||
* @param messageDestinationDomain - domain where the message will be sent |
||||
* @param messageRecipient - recipient of the message on the destination domain |
||||
* |
||||
* @return message - Message type |
||||
*/ |
||||
export function formatMessage( |
||||
message: string, |
||||
destinationDomain: number, |
||||
recipientAddress: Address, |
||||
): MessageDetails { |
||||
return { |
||||
message, |
||||
destinationDomain, |
||||
recipientAddress, |
||||
}; |
||||
} |
||||
|
||||
export async function formatAbacusMessage( |
||||
inbox: TestInbox, |
||||
governorRouter: TestGovernanceRouter, |
||||
destinationRouter: TestGovernanceRouter, |
||||
message: string, |
||||
): Promise<string> { |
||||
const nonce = 0; |
||||
const governorDomain = await governorRouter.localDomain(); |
||||
const destinationDomain = await destinationRouter.localDomain(); |
||||
|
||||
// Create Abacus message that is sent from the governor domain and governor
|
||||
// to the nonGovernorRouter on the nonGovernorDomain
|
||||
const abacusMessage = abacus.formatMessage( |
||||
governorDomain, |
||||
governorRouter.address, |
||||
nonce, |
||||
destinationDomain, |
||||
destinationRouter.address, |
||||
message, |
||||
); |
||||
|
||||
// Set message status to MessageStatus.Proven
|
||||
await inbox.setMessageProven(abacusMessage); |
||||
|
||||
return abacusMessage; |
||||
} |
||||
|
||||
export async function formatCall( |
||||
destinationContract: types.Contract, |
||||
functionStr: string, |
||||
functionArgs: any[], |
||||
): Promise<CallData> { |
||||
// Set up data for call message
|
||||
const callFunc = destinationContract.interface.getFunction(functionStr); |
||||
const callDataEncoded = destinationContract.interface.encodeFunctionData( |
||||
callFunc, |
||||
functionArgs, |
||||
); |
||||
|
||||
return { |
||||
to: abacus.ethersAddressToBytes32(destinationContract.address), |
||||
data: callDataEncoded, |
||||
}; |
||||
} |
||||
|
||||
// Send a transaction from the specified signer
|
||||
export async function sendFromSigner( |
||||
signer: types.Signer, |
||||
contract: types.Contract, |
||||
functionName: string, |
||||
args: any[], |
||||
) { |
||||
const data = encodeData(contract, functionName, args); |
||||
|
||||
return signer.sendTransaction({ |
||||
to: contract.address, |
||||
data, |
||||
}); |
||||
} |
||||
|
||||
function encodeData( |
||||
contract: types.Contract, |
||||
functionName: string, |
||||
args: any[], |
||||
): string { |
||||
const func = contract.interface.getFunction(functionName); |
||||
return contract.interface.encodeFunctionData(func, args); |
||||
} |
@ -0,0 +1,4 @@ |
||||
export * as types from './lib/types'; |
||||
export * as utils from './lib/utils'; |
||||
export * as core from './lib/core'; |
||||
export { AbacusDeployment } from './lib/AbacusDeployment'; |
@ -1,9 +0,0 @@ |
||||
import '@nomiclabs/hardhat-waffle'; |
||||
import { extendEnvironment } from 'hardhat/config'; |
||||
|
||||
import { abacus } from './core'; |
||||
|
||||
// HardhatRuntimeEnvironment
|
||||
extendEnvironment((hre) => { |
||||
hre.abacus = abacus; |
||||
}); |
@ -0,0 +1,385 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
pragma experimental ABIEncoderV2; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {Router} from "../Router.sol"; |
||||
import {GovernanceMessage} from "./GovernanceMessage.sol"; |
||||
// ============ External Imports ============ |
||||
import {Version0} from "@abacus-network/abacus-sol/contracts/Version0.sol"; |
||||
import {TypeCasts} from "@abacus-network/abacus-sol/contracts/XAppConnectionManager.sol"; |
||||
import {Initializable} from "@openzeppelin/contracts/proxy/Initializable.sol"; |
||||
import {SafeMath} from "@openzeppelin/contracts/math/SafeMath.sol"; |
||||
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
||||
|
||||
/** |
||||
* @dev GovernanceRouter has two modes of operation, normal and recovery. |
||||
* During normal mode, `owner()` returns the `governor`, giving it permission |
||||
* to call `onlyOwner` functions. |
||||
* During recovery mode, `owner()` returns the `_owner`, giving it permission |
||||
* to call `onlyOwner` functions. |
||||
*/ |
||||
contract GovernanceRouter is Version0, Router { |
||||
// ============ Libraries ============ |
||||
|
||||
using SafeMath for uint256; |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
using GovernanceMessage for bytes29; |
||||
|
||||
// ============ Immutables ============ |
||||
|
||||
// number of seconds before recovery can be activated |
||||
uint256 public immutable recoveryTimelock; |
||||
|
||||
// ============ Public Storage ============ |
||||
|
||||
// timestamp when recovery timelock expires; 0 if timelock has not been initiated |
||||
uint256 public recoveryActiveAt; |
||||
// the local entity empowered to call governance functions during normal |
||||
// operation, typically set to 0x0 on all chains but one |
||||
address public governor; |
||||
|
||||
// ============ Upgrade Gap ============ |
||||
|
||||
// gap for upgrade safety |
||||
uint256[48] private __GAP; |
||||
|
||||
// ============ Events ============ |
||||
|
||||
/** |
||||
* @notice Emitted when the Governor role is set |
||||
* @param governor the address of the new Governor |
||||
*/ |
||||
event SetGovernor(address indexed governor); |
||||
|
||||
/** |
||||
* @notice Emitted when recovery state is initiated by the Owner |
||||
* @param owner the address of the current owner who initiated the transition |
||||
* @param recoveryActiveAt the block at which recovery state will be active |
||||
*/ |
||||
event InitiateRecovery(address indexed owner, uint256 recoveryActiveAt); |
||||
|
||||
/** |
||||
* @notice Emitted when recovery state is exited by the Owner |
||||
* @param owner the address of the current Owner who initiated the transition |
||||
*/ |
||||
event ExitRecovery(address owner); |
||||
|
||||
// ============ Modifiers ============ |
||||
|
||||
modifier typeAssert(bytes29 _view, GovernanceMessage.Types _type) { |
||||
_view.assertType(uint40(_type)); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyInRecovery() { |
||||
require(inRecovery(), "!recovery"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyNotInRecovery() { |
||||
require(!inRecovery(), "recovery"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyGovernor() { |
||||
require(msg.sender == governor, "!governor"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyRecoveryManager() { |
||||
require(msg.sender == recoveryManager(), "!recoveryManager"); |
||||
_; |
||||
} |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor(uint256 _recoveryTimelock) { |
||||
recoveryTimelock = _recoveryTimelock; |
||||
} |
||||
|
||||
// ============ Initializer ============ |
||||
|
||||
function initialize(address _xAppConnectionManager) public initializer { |
||||
__XAppConnectionClient_initialize(_xAppConnectionManager); |
||||
governor = msg.sender; |
||||
} |
||||
|
||||
// ============ External Functions ============ |
||||
|
||||
/** |
||||
* @notice Handle Abacus messages |
||||
* For all non-Governor chains to handle messages |
||||
* sent from the Governor chain via Abacus. |
||||
* Governor chain should never receive messages, |
||||
* because non-Governor chains are not able to send them |
||||
* @param _origin The domain (of the Governor Router) |
||||
* @param _sender The message sender (must be the Governor Router) |
||||
* @param _message The message |
||||
*/ |
||||
function handle( |
||||
uint32 _origin, |
||||
bytes32 _sender, |
||||
bytes memory _message |
||||
) external override onlyInbox onlyRemoteRouter(_origin, _sender) { |
||||
bytes29 _msg = _message.ref(0); |
||||
if (_msg.isValidCall()) { |
||||
_handleCall(_msg.tryAsCall()); |
||||
} else if (_msg.isValidSetGovernor()) { |
||||
_handleSetGovernor(_msg.tryAsSetGovernor()); |
||||
} else if (_msg.isValidEnrollRemoteRouter()) { |
||||
_handleEnrollRemoteRouter(_msg.tryAsEnrollRemoteRouter()); |
||||
} else if (_msg.isValidSetXAppConnectionManager()) { |
||||
_handleSetXAppConnectionManager( |
||||
_msg.tryAsSetXAppConnectionManager() |
||||
); |
||||
} else { |
||||
require(false, "!valid message type"); |
||||
} |
||||
} |
||||
|
||||
// ============ External Local Functions ============ |
||||
|
||||
/** |
||||
* @notice Make local calls. |
||||
* @param _calls The calls |
||||
*/ |
||||
function call(GovernanceMessage.Call[] calldata _calls) external onlyOwner { |
||||
for (uint256 i = 0; i < _calls.length; i++) { |
||||
_makeCall(_calls[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Sets the governor of the router. |
||||
* @param _governor The address of the new governor |
||||
*/ |
||||
function setGovernor(address _governor) external onlyOwner { |
||||
_setGovernor(_governor); |
||||
} |
||||
|
||||
/** |
||||
* @notice Initiate the recovery timelock |
||||
* @dev callable by the recovery manager iff not in recovery |
||||
*/ |
||||
function initiateRecoveryTimelock() |
||||
external |
||||
onlyNotInRecovery |
||||
onlyRecoveryManager |
||||
{ |
||||
require(recoveryActiveAt == 0, "recovery already initiated"); |
||||
// set the time that recovery will be active |
||||
recoveryActiveAt = block.timestamp.add(recoveryTimelock); |
||||
emit InitiateRecovery(recoveryManager(), recoveryActiveAt); |
||||
} |
||||
|
||||
/** |
||||
* @notice Exit recovery mode |
||||
* @dev callable by the recovery manager iff in recovery |
||||
*/ |
||||
function exitRecovery() external onlyInRecovery onlyRecoveryManager { |
||||
delete recoveryActiveAt; |
||||
emit ExitRecovery(recoveryManager()); |
||||
} |
||||
|
||||
// ============ External Remote Functions ============ |
||||
|
||||
/** |
||||
* @notice Dispatch calls on a remote chain via the remote GovernanceRouter |
||||
* @param _destination The domain of the remote chain |
||||
* @param _calls The calls |
||||
*/ |
||||
function callRemote( |
||||
uint32 _destination, |
||||
GovernanceMessage.Call[] calldata _calls |
||||
) external onlyGovernor onlyNotInRecovery { |
||||
bytes memory _msg = GovernanceMessage.formatCalls(_calls); |
||||
_dispatchToRemoteRouter(_destination, _msg); |
||||
} |
||||
|
||||
/** |
||||
* @notice Enroll a remote router on a remote router. |
||||
* @param _destination The domain of the enroller |
||||
* @param _domain The domain of the enrollee |
||||
* @param _router The address of the enrollee |
||||
*/ |
||||
function enrollRemoteRouterRemote( |
||||
uint32 _destination, |
||||
uint32 _domain, |
||||
bytes32 _router |
||||
) external onlyGovernor onlyNotInRecovery { |
||||
bytes memory _msg = GovernanceMessage.formatEnrollRemoteRouter( |
||||
_domain, |
||||
_router |
||||
); |
||||
_dispatchToRemoteRouter(_destination, _msg); |
||||
} |
||||
|
||||
/** |
||||
* @notice Sets the xAppConnectionManager of a remote router. |
||||
* @param _destination The domain of router on which to set the xAppConnectionManager |
||||
* @param _xAppConnectionManager The address of the xAppConnectionManager contract |
||||
*/ |
||||
function setXAppConnectionManagerRemote( |
||||
uint32 _destination, |
||||
address _xAppConnectionManager |
||||
) external onlyGovernor onlyNotInRecovery { |
||||
bytes memory _msg = GovernanceMessage.formatSetXAppConnectionManager( |
||||
TypeCasts.addressToBytes32(_xAppConnectionManager) |
||||
); |
||||
_dispatchToRemoteRouter(_destination, _msg); |
||||
} |
||||
|
||||
/** |
||||
* @notice Sets the governor of a remote router. |
||||
* @param _destination The domain of router on which to set the governor |
||||
* @param _governor The address of the new governor |
||||
*/ |
||||
function setGovernorRemote(uint32 _destination, address _governor) |
||||
external |
||||
onlyGovernor |
||||
onlyNotInRecovery |
||||
{ |
||||
bytes memory _msg = GovernanceMessage.formatSetGovernor( |
||||
TypeCasts.addressToBytes32(_governor) |
||||
); |
||||
_dispatchToRemoteRouter(_destination, _msg); |
||||
} |
||||
|
||||
// ============ Public Functions ============ |
||||
|
||||
/** |
||||
* @notice Transfers the recovery manager to a new address. |
||||
* @dev Callable by the governor when not in recovery mode or the |
||||
* recoveryManager at any time. |
||||
* @param _recoveryManager The address of the new recovery manager |
||||
*/ |
||||
function transferOwnership(address _recoveryManager) |
||||
public |
||||
virtual |
||||
override |
||||
{ |
||||
// If we are not in recovery, temporarily enter recovery so that the |
||||
// recoveryManager can call transferOwnership. |
||||
if (msg.sender == recoveryManager() && !inRecovery()) { |
||||
uint256 _recoveryActiveAt = recoveryActiveAt; |
||||
recoveryActiveAt = 1; |
||||
OwnableUpgradeable.transferOwnership(_recoveryManager); |
||||
recoveryActiveAt = _recoveryActiveAt; |
||||
} else { |
||||
OwnableUpgradeable.transferOwnership(_recoveryManager); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the address of the current owner. |
||||
* @dev When not in recovery mode, the governor owns the contract. |
||||
*/ |
||||
function owner() public view virtual override returns (address) { |
||||
return inRecovery() ? recoveryManager() : governor; |
||||
} |
||||
|
||||
/** |
||||
* @notice Returns the address of the recovery manager. |
||||
* @dev Exposing via this funciton is necessary because we overload |
||||
* `owner()` in order to make `onlyOwner()` work as intended, and because |
||||
* `OwnableUpgradeable` does not expose the private `_owner`. |
||||
*/ |
||||
function recoveryManager() public view returns (address) { |
||||
return OwnableUpgradeable.owner(); |
||||
} |
||||
|
||||
/** |
||||
* @notice Check if the contract is in recovery mode currently |
||||
* @return TRUE iff the contract is actively in recovery mode currently |
||||
*/ |
||||
function inRecovery() public view returns (bool) { |
||||
uint256 _recoveryActiveAt = recoveryActiveAt; |
||||
bool _recoveryInitiated = _recoveryActiveAt != 0; |
||||
bool _recoveryActive = _recoveryActiveAt <= block.timestamp; |
||||
return _recoveryInitiated && _recoveryActive; |
||||
} |
||||
|
||||
// ============ Internal Functions ============ |
||||
|
||||
/** |
||||
* @notice Handle message dispatching calls locally |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleCall(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, GovernanceMessage.Types.Call) |
||||
{ |
||||
GovernanceMessage.Call[] memory _calls = _msg.getCalls(); |
||||
for (uint256 i = 0; i < _calls.length; i++) { |
||||
_makeCall(_calls[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message transferring governorship to a new Governor |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleSetGovernor(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, GovernanceMessage.Types.SetGovernor) |
||||
{ |
||||
address _governor = TypeCasts.bytes32ToAddress(_msg.governor()); |
||||
_setGovernor(_governor); |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message setting the router address for a given domain |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleEnrollRemoteRouter(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, GovernanceMessage.Types.EnrollRemoteRouter) |
||||
{ |
||||
uint32 _domain = _msg.domain(); |
||||
bytes32 _router = _msg.router(); |
||||
_enrollRemoteRouter(_domain, _router); |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message setting the xAppConnectionManager address |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleSetXAppConnectionManager(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, GovernanceMessage.Types.SetXAppConnectionManager) |
||||
{ |
||||
address _xAppConnectionManager = TypeCasts.bytes32ToAddress( |
||||
_msg.xAppConnectionManager() |
||||
); |
||||
_setXAppConnectionManager(_xAppConnectionManager); |
||||
} |
||||
|
||||
/** |
||||
* @notice Call local contract. |
||||
* @param _call The call |
||||
* @return _ret |
||||
*/ |
||||
function _makeCall(GovernanceMessage.Call memory _call) |
||||
internal |
||||
returns (bytes memory _ret) |
||||
{ |
||||
address _to = TypeCasts.bytes32ToAddress(_call.to); |
||||
// attempt to dispatch using low-level call |
||||
bool _success; |
||||
(_success, _ret) = _to.call(_call.data); |
||||
// revert if the call failed |
||||
require(_success, "call failed"); |
||||
} |
||||
|
||||
/** |
||||
* @notice Set the governor. |
||||
* @param _governor The address of the new governor |
||||
*/ |
||||
function _setGovernor(address _governor) internal { |
||||
governor = _governor; |
||||
emit SetGovernor(_governor); |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
contract TestSet { |
||||
uint256 private x; |
||||
|
||||
function set(uint256 _x) external { |
||||
x = _x; |
||||
} |
||||
|
||||
function get() external view returns (uint256) { |
||||
return x; |
||||
} |
||||
} |
@ -1,5 +1,5 @@ |
||||
import { ethers } from 'hardhat'; |
||||
import { TestEncoding__factory } from '../typechain'; |
||||
import { TestEncoding__factory } from '../../typechain'; |
||||
|
||||
describe('Encoding', async () => { |
||||
it('encodes', async () => { |
@ -1,5 +1,5 @@ |
||||
import { BigNumber, BigNumberish, ethers } from 'ethers'; |
||||
import { BridgeToken } from '../../typechain'; |
||||
import { BridgeToken } from '../../../typechain'; |
||||
|
||||
const PERMIT_TYPEHASH = ethers.utils.keccak256( |
||||
ethers.utils.toUtf8Bytes( |
@ -0,0 +1,49 @@ |
||||
import { BytesLike } from 'ethers'; |
||||
|
||||
export enum BridgeMessageTypes { |
||||
INVALID = 0, |
||||
TOKEN_ID, |
||||
MESSAGE, |
||||
TRANSFER, |
||||
DETAILS, |
||||
REQUEST_DETAILS, |
||||
} |
||||
|
||||
/********* TOKEN BRIDGE *********/ |
||||
|
||||
export type MessageLen = { |
||||
identifier: number; |
||||
tokenId: number; |
||||
transfer: number; |
||||
details: number; |
||||
requestDetails: number; |
||||
}; |
||||
|
||||
export type Action = DetailsAction | TransferAction | RequestDetailsAction; |
||||
|
||||
export type Message = { |
||||
tokenId: TokenIdentifier; |
||||
action: Action; |
||||
}; |
||||
|
||||
export type TransferAction = { |
||||
type: BridgeMessageTypes.TRANSFER; |
||||
recipient: BytesLike; |
||||
amount: number | BytesLike; |
||||
}; |
||||
|
||||
export type DetailsAction = { |
||||
type: BridgeMessageTypes.DETAILS; |
||||
name: string; |
||||
symbol: string; |
||||
decimals: number; |
||||
}; |
||||
|
||||
export type RequestDetailsAction = { |
||||
type: BridgeMessageTypes.REQUEST_DETAILS; |
||||
}; |
||||
|
||||
export type TokenIdentifier = { |
||||
domain: string | number; |
||||
id: BytesLike; |
||||
}; |
@ -0,0 +1,482 @@ |
||||
import { ethers } from 'hardhat'; |
||||
import { expect } from 'chai'; |
||||
|
||||
import { |
||||
types, |
||||
utils, |
||||
core, |
||||
AbacusDeployment, |
||||
} from '@abacus-network/abacus-sol/test'; |
||||
import { |
||||
formatSetGovernor, |
||||
formatCall, |
||||
increaseTimestampBy, |
||||
} from './lib/utils'; |
||||
import { GovernanceDeployment } from './lib/GovernanceDeployment'; |
||||
import { |
||||
TestSet, |
||||
TestSet__factory, |
||||
GovernanceRouter, |
||||
GovernanceRouter__factory, |
||||
} from '../../typechain'; |
||||
|
||||
const recoveryTimelock = 60 * 60 * 24 * 7; |
||||
const localDomain = 1000; |
||||
const remoteDomain = 2000; |
||||
const testDomain = 3000; |
||||
const domains = [localDomain, remoteDomain]; |
||||
const ONLY_OWNER_REVERT_MESSAGE = 'Ownable: caller is not the owner'; |
||||
|
||||
describe('GovernanceRouter', async () => { |
||||
let governor: types.Signer, |
||||
recoveryManager: types.Signer, |
||||
router: GovernanceRouter, |
||||
remote: GovernanceRouter, |
||||
testSet: TestSet, |
||||
abacus: AbacusDeployment, |
||||
governance: GovernanceDeployment; |
||||
|
||||
before(async () => { |
||||
[governor, recoveryManager] = await ethers.getSigners(); |
||||
|
||||
const testSetFactory = new TestSet__factory(governor); |
||||
testSet = await testSetFactory.deploy(); |
||||
abacus = await AbacusDeployment.fromDomains(domains, governor); |
||||
}); |
||||
|
||||
beforeEach(async () => { |
||||
governance = await GovernanceDeployment.fromAbacusDeployment( |
||||
abacus, |
||||
governor, |
||||
recoveryManager, |
||||
); |
||||
router = governance.router(localDomain); |
||||
remote = governance.router(remoteDomain); |
||||
}); |
||||
|
||||
it('Cannot be initialized twice', async () => { |
||||
await expect( |
||||
router.initialize(ethers.constants.AddressZero), |
||||
).to.be.revertedWith('Initializable: contract is already initialized'); |
||||
}); |
||||
|
||||
it('accepts message from enrolled inbox and router', async () => { |
||||
expect(await router.governor()).to.not.equal(ethers.constants.AddressZero); |
||||
const message = formatSetGovernor(ethers.constants.AddressZero); |
||||
// Create a fake abacus message coming from the remote governance router.
|
||||
const fakeMessage = core.formatMessage( |
||||
remoteDomain, |
||||
remote.address, |
||||
0, // nonce is ignored
|
||||
localDomain, |
||||
router.address, |
||||
message, |
||||
); |
||||
|
||||
const inbox = abacus.inbox(localDomain, remoteDomain); |
||||
await inbox.setMessageProven(fakeMessage); |
||||
// Expect inbox processing to fail when reverting in handle
|
||||
await inbox.testProcess(fakeMessage); |
||||
expect(await router.governor()).to.equal(ethers.constants.AddressZero); |
||||
}); |
||||
|
||||
it('rejects message from unenrolled inbox', async () => { |
||||
const message = formatSetGovernor(ethers.constants.AddressZero); |
||||
await expect( |
||||
router.handle( |
||||
remoteDomain, |
||||
utils.addressToBytes32(remote.address), |
||||
message, |
||||
), |
||||
).to.be.revertedWith('!inbox'); |
||||
}); |
||||
|
||||
it('rejects message from unenrolled router', async () => { |
||||
const message = formatSetGovernor(ethers.constants.AddressZero); |
||||
// Create a fake abacus message coming from the remote governance router.
|
||||
const fakeMessage = core.formatMessage( |
||||
remoteDomain, |
||||
ethers.constants.AddressZero, |
||||
0, // nonce is ignored
|
||||
localDomain, |
||||
router.address, |
||||
message, |
||||
); |
||||
|
||||
const inbox = abacus.inbox(localDomain, remoteDomain); |
||||
await inbox.setMessageProven(fakeMessage); |
||||
// Expect inbox processing to fail when reverting in handle
|
||||
let success = await inbox.callStatic.testProcess(fakeMessage); |
||||
expect(success).to.be.false; |
||||
}); |
||||
|
||||
describe('when not in recovery mode', async () => { |
||||
it('governor is the owner', async () => { |
||||
expect(await router.owner()).to.equal(governor.address); |
||||
}); |
||||
|
||||
it('governor can set local recovery manager', async () => { |
||||
expect(await router.recoveryManager()).to.equal(recoveryManager.address); |
||||
await router.transferOwnership(router.address); |
||||
expect(await router.recoveryManager()).to.equal(router.address); |
||||
expect(await router.recoveryActiveAt()).to.equal(0); |
||||
}); |
||||
|
||||
it('governor can make local calls', async () => { |
||||
const value = 12; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await router.call([call]); |
||||
expect(await testSet.get()).to.equal(value); |
||||
}); |
||||
|
||||
it('governor can set local governor', async () => { |
||||
expect(await router.governor()).to.equal(governor.address); |
||||
await router.setGovernor(ethers.constants.AddressZero); |
||||
expect(await router.governor()).to.equal(ethers.constants.AddressZero); |
||||
}); |
||||
|
||||
it('governor can set local xAppConnectionManager', async () => { |
||||
expect(await router.xAppConnectionManager()).to.equal( |
||||
abacus.connectionManager(localDomain).address, |
||||
); |
||||
await router.setXAppConnectionManager(ethers.constants.AddressZero); |
||||
expect(await router.xAppConnectionManager()).to.equal( |
||||
ethers.constants.AddressZero, |
||||
); |
||||
}); |
||||
|
||||
it('governor can enroll local remote router', async () => { |
||||
expect(await router.routers(testDomain)).to.equal( |
||||
ethers.constants.HashZero, |
||||
); |
||||
const newRouter = utils.addressToBytes32(router.address); |
||||
await router.enrollRemoteRouter(testDomain, newRouter); |
||||
expect(await router.routers(testDomain)).to.equal(newRouter); |
||||
}); |
||||
|
||||
it('governor can make remote calls', async () => { |
||||
const value = 13; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await router.callRemote(domains[1], [call]); |
||||
await abacus.processMessages(); |
||||
expect(await testSet.get()).to.equal(value); |
||||
}); |
||||
|
||||
it('governor can set remote governor', async () => { |
||||
const newGovernor = governor.address; |
||||
expect(await remote.governor()).to.not.equal(newGovernor); |
||||
await router.setGovernorRemote(remoteDomain, newGovernor); |
||||
await abacus.processMessages(); |
||||
expect(await remote.governor()).to.equal(newGovernor); |
||||
}); |
||||
|
||||
it('governor can set remote xAppConnectionManager', async () => { |
||||
const newConnectionManager = ethers.constants.AddressZero; |
||||
expect(await remote.xAppConnectionManager()).to.not.equal( |
||||
newConnectionManager, |
||||
); |
||||
await router.setXAppConnectionManagerRemote( |
||||
remoteDomain, |
||||
newConnectionManager, |
||||
); |
||||
await abacus.processMessages(); |
||||
expect(await remote.xAppConnectionManager()).to.equal( |
||||
newConnectionManager, |
||||
); |
||||
}); |
||||
|
||||
it('governor can enroll remote remote router', async () => { |
||||
expect(await remote.routers(testDomain)).to.equal( |
||||
ethers.constants.HashZero, |
||||
); |
||||
const newRouter = utils.addressToBytes32(router.address); |
||||
await router.enrollRemoteRouterRemote( |
||||
remoteDomain, |
||||
testDomain, |
||||
newRouter, |
||||
); |
||||
await abacus.processMessages(); |
||||
expect(await remote.routers(testDomain)).to.equal(newRouter); |
||||
}); |
||||
|
||||
it('governor cannot initiate recovery', async () => { |
||||
await expect(router.initiateRecoveryTimelock()).to.be.revertedWith( |
||||
'!recoveryManager', |
||||
); |
||||
}); |
||||
|
||||
it('governor cannot exit recovery', async () => { |
||||
await expect(router.exitRecovery()).to.be.revertedWith('!recovery'); |
||||
}); |
||||
|
||||
it('recoveryManager can set local recovery manager', async () => { |
||||
expect(await router.recoveryManager()).to.equal(recoveryManager.address); |
||||
await router.connect(recoveryManager).transferOwnership(router.address); |
||||
expect(await router.recoveryManager()).to.equal(router.address); |
||||
expect(await router.recoveryActiveAt()).to.equal(0); |
||||
}); |
||||
|
||||
it('recovery manager cannot make local calls', async () => { |
||||
const value = 12; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await expect( |
||||
router.connect(recoveryManager).call([call]), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('recovery manager cannot set local governor', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setGovernor(ethers.constants.AddressZero), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('recovery manager cannot set local xAppConnectionManager', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setXAppConnectionManager(router.address), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('recovery manager cannot enroll local remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.enrollRemoteRouter( |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('recovery manager cannot make remote calls', async () => { |
||||
const value = 13; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await expect( |
||||
router.connect(recoveryManager).callRemote(domains[1], [call]), |
||||
).to.be.revertedWith('!governor'); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote governor', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setGovernorRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!governor'); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote xAppConnectionManager', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setXAppConnectionManagerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!governor'); |
||||
}); |
||||
|
||||
it('recovery manager cannot enroll remote remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.enrollRemoteRouterRemote( |
||||
testDomain, |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith('!governor'); |
||||
}); |
||||
|
||||
it('recovery manager can initiate recovery', async () => { |
||||
await expect( |
||||
router.connect(recoveryManager).initiateRecoveryTimelock(), |
||||
).to.emit(router, 'InitiateRecovery'); |
||||
}); |
||||
|
||||
it('recovery manager cannot exit recovery', async () => { |
||||
await expect( |
||||
router.connect(recoveryManager).exitRecovery(), |
||||
).to.be.revertedWith('!recovery'); |
||||
}); |
||||
}); |
||||
|
||||
describe('when in recovery mode', async () => { |
||||
beforeEach(async () => { |
||||
router = router.connect(recoveryManager); |
||||
await router.initiateRecoveryTimelock(); |
||||
expect(await router.inRecovery()).to.be.false; |
||||
await increaseTimestampBy(ethers.provider, recoveryTimelock); |
||||
expect(await router.inRecovery()).to.be.true; |
||||
}); |
||||
|
||||
it('recovery manager is the owner', async () => { |
||||
expect(await router.owner()).to.equal(recoveryManager.address); |
||||
}); |
||||
|
||||
it('recovery manager can set local recovery manager', async () => { |
||||
const recoveryActiveAt = await router.recoveryActiveAt(); |
||||
expect(await router.recoveryManager()).to.equal(recoveryManager.address); |
||||
await router.transferOwnership(router.address); |
||||
expect(await router.recoveryManager()).to.equal(router.address); |
||||
expect(await router.recoveryActiveAt()).to.equal(recoveryActiveAt); |
||||
}); |
||||
|
||||
it('recovery manager can make local calls', async () => { |
||||
const value = 12; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await router.call([call]); |
||||
expect(await testSet.get()).to.equal(value); |
||||
}); |
||||
|
||||
it('recovery manager can set local governor', async () => { |
||||
expect(await router.governor()).to.equal(governor.address); |
||||
await router.setGovernor(ethers.constants.AddressZero); |
||||
expect(await router.governor()).to.equal(ethers.constants.AddressZero); |
||||
}); |
||||
|
||||
it('recovery manager can set local xAppConnectionManager', async () => { |
||||
expect(await router.xAppConnectionManager()).to.equal( |
||||
abacus.connectionManager(localDomain).address, |
||||
); |
||||
await router.setXAppConnectionManager(ethers.constants.AddressZero); |
||||
expect(await router.xAppConnectionManager()).to.equal( |
||||
ethers.constants.AddressZero, |
||||
); |
||||
}); |
||||
|
||||
it('recovery manager can enroll local remote router', async () => { |
||||
expect(await router.routers(testDomain)).to.equal( |
||||
ethers.constants.HashZero, |
||||
); |
||||
const newRouter = utils.addressToBytes32(router.address); |
||||
await router.enrollRemoteRouter(testDomain, newRouter); |
||||
expect(await router.routers(testDomain)).to.equal(newRouter); |
||||
}); |
||||
|
||||
it('recovery manager cannot make remote calls', async () => { |
||||
const value = 13; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await expect(router.callRemote(domains[1], [call])).to.be.revertedWith( |
||||
'!governor', |
||||
); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote governor', async () => { |
||||
await expect( |
||||
router.setGovernorRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!governor'); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote xAppConnectionManager', async () => { |
||||
await expect( |
||||
router.setXAppConnectionManagerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!governor'); |
||||
}); |
||||
|
||||
it('recovery manager cannot enroll remote remote router', async () => { |
||||
await expect( |
||||
router.enrollRemoteRouterRemote( |
||||
remoteDomain, |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith('!governor'); |
||||
}); |
||||
|
||||
it('recovery manager cannot initiate recovery', async () => { |
||||
await expect(router.initiateRecoveryTimelock()).to.be.revertedWith( |
||||
'recovery', |
||||
); |
||||
}); |
||||
|
||||
it('recovery manager can exit recovery ', async () => { |
||||
await expect(router.exitRecovery()).to.emit(router, 'ExitRecovery'); |
||||
expect(await router.inRecovery()).to.be.false; |
||||
}); |
||||
|
||||
it('governor cannot make local calls', async () => { |
||||
const value = 12; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await expect(router.connect(governor).call([call])).to.be.revertedWith( |
||||
ONLY_OWNER_REVERT_MESSAGE, |
||||
); |
||||
}); |
||||
|
||||
it('governor cannot set local governor', async () => { |
||||
await expect( |
||||
router.connect(governor).setGovernor(ethers.constants.AddressZero), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('governor cannot set local recovery manager', async () => { |
||||
await expect( |
||||
router.connect(governor).transferOwnership(router.address), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('governor cannot set local xAppConnectionManager', async () => { |
||||
await expect( |
||||
router.connect(governor).setXAppConnectionManager(router.address), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('governor cannot enroll local remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(governor) |
||||
.enrollRemoteRouter( |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('governor cannot make remote calls', async () => { |
||||
const value = 13; |
||||
const call = await formatCall(testSet, 'set', [value]); |
||||
await expect( |
||||
router.connect(governor).callRemote(domains[1], [call]), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('governor cannot set remote governor', async () => { |
||||
await expect( |
||||
router |
||||
.connect(governor) |
||||
.setGovernorRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('governor cannot set remote xAppConnectionManager', async () => { |
||||
await expect( |
||||
router |
||||
.connect(governor) |
||||
.setXAppConnectionManagerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('governor cannot enroll remote remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(governor) |
||||
.enrollRemoteRouterRemote( |
||||
remoteDomain, |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('governor cannot initiate recovery', async () => { |
||||
await expect( |
||||
router.connect(governor).initiateRecoveryTimelock(), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('governor cannot exit recovery', async () => { |
||||
await expect(router.connect(governor).exitRecovery()).to.be.revertedWith( |
||||
'!recoveryManager', |
||||
); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,93 @@ |
||||
import { ethers } from 'ethers'; |
||||
import { types, utils } from '@abacus-network/abacus-sol/test'; |
||||
|
||||
export enum GovernanceMessage { |
||||
CALL = 1, |
||||
SETGOVERNOR = 2, |
||||
ENROLLREMOTEROUTER = 3, |
||||
SETXAPPCONNECTIONMANAGER = 5, |
||||
} |
||||
|
||||
export function formatSetGovernor(address: types.Address): string { |
||||
return ethers.utils.solidityPack( |
||||
['bytes1', 'bytes32'], |
||||
[GovernanceMessage.SETGOVERNOR, utils.addressToBytes32(address)], |
||||
); |
||||
} |
||||
|
||||
export function formatSetXAppConnectionManager(address: types.Address): string { |
||||
return ethers.utils.solidityPack( |
||||
['bytes1', 'bytes32'], |
||||
[ |
||||
GovernanceMessage.SETXAPPCONNECTIONMANAGER, |
||||
utils.addressToBytes32(address), |
||||
], |
||||
); |
||||
} |
||||
|
||||
export function formatEnrollRemoteRouter( |
||||
domain: types.Domain, |
||||
address: types.Address, |
||||
): string { |
||||
return ethers.utils.solidityPack( |
||||
['bytes1', 'uint32', 'bytes32'], |
||||
[ |
||||
GovernanceMessage.ENROLLREMOTEROUTER, |
||||
domain, |
||||
utils.addressToBytes32(address), |
||||
], |
||||
); |
||||
} |
||||
|
||||
export function formatCalls(callsData: types.CallData[]): string { |
||||
let callBody = '0x'; |
||||
const numCalls = callsData.length; |
||||
|
||||
for (let i = 0; i < numCalls; i++) { |
||||
const { to, data } = callsData[i]; |
||||
const dataLen = utils.getHexStringByteLength(data); |
||||
|
||||
if (!to || !data) { |
||||
throw new Error(`Missing data in Call ${i + 1}: \n ${callsData[i]}`); |
||||
} |
||||
|
||||
let hexBytes = ethers.utils.solidityPack( |
||||
['bytes32', 'uint256', 'bytes'], |
||||
[to, dataLen, data], |
||||
); |
||||
|
||||
// remove 0x before appending
|
||||
callBody += hexBytes.slice(2); |
||||
} |
||||
|
||||
return ethers.utils.solidityPack( |
||||
['bytes1', 'bytes1', 'bytes'], |
||||
[GovernanceMessage.CALL, numCalls, callBody], |
||||
); |
||||
} |
||||
|
||||
export async function formatCall( |
||||
destinationContract: ethers.Contract, |
||||
functionStr: string, |
||||
functionArgs: any[], |
||||
): Promise<types.CallData> { |
||||
// Set up data for call message
|
||||
const callFunc = destinationContract.interface.getFunction(functionStr); |
||||
const callDataEncoded = destinationContract.interface.encodeFunctionData( |
||||
callFunc, |
||||
functionArgs, |
||||
); |
||||
|
||||
return { |
||||
to: utils.addressToBytes32(destinationContract.address), |
||||
data: callDataEncoded, |
||||
}; |
||||
} |
||||
|
||||
export const increaseTimestampBy = async ( |
||||
provider: ethers.providers.JsonRpcProvider, |
||||
increaseTime: number, |
||||
) => { |
||||
await provider.send('evm_increaseTime', [increaseTime]); |
||||
await provider.send('evm_mine', []); |
||||
}; |
@ -1,12 +0,0 @@ |
||||
import '@nomiclabs/hardhat-waffle'; |
||||
import { extendEnvironment } from 'hardhat/config'; |
||||
|
||||
import { abacus } from '@abacus-network/abacus-sol/test/lib/core'; |
||||
|
||||
import { bridge } from './bridge'; |
||||
|
||||
// HardhatRuntimeEnvironment
|
||||
extendEnvironment((hre) => { |
||||
hre.abacus = abacus; |
||||
hre.bridge = bridge; |
||||
}); |
@ -1,61 +0,0 @@ |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||
import { BytesLike, ethers } from 'ethers'; |
||||
import { BridgeMessageTypes } from './bridge'; |
||||
|
||||
/********* HRE *********/ |
||||
export interface HardhatBridgeHelpers { |
||||
BridgeMessageTypes: typeof BridgeMessageTypes; |
||||
typeToByte: Function; |
||||
MESSAGE_LEN: MessageLen; |
||||
serializeTransferAction: Function; |
||||
serializeDetailsAction: Function; |
||||
serializeRequestDetailsAction: Function; |
||||
serializeAction: Function; |
||||
serializeTokenId: Function; |
||||
serializeMessage: Function; |
||||
} |
||||
|
||||
declare module 'hardhat/types/runtime' { |
||||
interface HardhatRuntimeEnvironment { |
||||
bridge: HardhatBridgeHelpers; |
||||
} |
||||
} |
||||
|
||||
/********* TOKEN BRIDGE *********/ |
||||
|
||||
export type MessageLen = { |
||||
identifier: number; |
||||
tokenId: number; |
||||
transfer: number; |
||||
details: number; |
||||
requestDetails: number; |
||||
}; |
||||
|
||||
export type Action = DetailsAction | TransferAction | RequestDetailsAction; |
||||
|
||||
export type Message = { |
||||
tokenId: TokenIdentifier; |
||||
action: Action; |
||||
}; |
||||
|
||||
export type TransferAction = { |
||||
type: BridgeMessageTypes.TRANSFER; |
||||
recipient: ethers.BytesLike; |
||||
amount: number | ethers.BytesLike; |
||||
}; |
||||
|
||||
export type DetailsAction = { |
||||
type: BridgeMessageTypes.DETAILS; |
||||
name: string; |
||||
symbol: string; |
||||
decimals: number; |
||||
}; |
||||
|
||||
export type RequestDetailsAction = { |
||||
type: BridgeMessageTypes.REQUEST_DETAILS; |
||||
}; |
||||
|
||||
export interface TokenIdentifier { |
||||
domain: string | number; |
||||
id: BytesLike; |
||||
} |
@ -1,11 +1,11 @@ |
||||
import { getCoreDeploys, getBridgeDeploys, getEnvironment } from './utils'; |
||||
import { updateProviderDomain } from '../src/provider'; |
||||
import { updateSdkDomain } from '../src/sdk'; |
||||
|
||||
async function main() { |
||||
const environment = await getEnvironment(); |
||||
const coreDeploys = await getCoreDeploys(environment); |
||||
const bridgeDeploys = await getBridgeDeploys(environment); |
||||
updateProviderDomain(environment, coreDeploys, bridgeDeploys); |
||||
updateSdkDomain(environment, coreDeploys, bridgeDeploys); |
||||
} |
||||
|
||||
main().then(console.log).catch(console.error); |
@ -1 +1,2 @@ |
||||
**/*.yaml |
||||
**/*.yaml |
||||
dist/ |
||||
|
Loading…
Reference in new issue