Remove controller from monorepo (#497)
* Remove apps dep from sdk * Remove controller from sdk * Remove controller from apps * Remove apps packagepull/498/head
parent
d40b2d776c
commit
87ee40ec3d
@ -1 +0,0 @@ |
||||
ETHERSCAN_API_KEY= |
@ -1,13 +0,0 @@ |
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"es2021": true |
||||
}, |
||||
"extends": ["eslint:recommended"], |
||||
"parserOptions": { |
||||
"ecmaVersion": 12, |
||||
"sourceType": "module" |
||||
}, |
||||
"rules": { |
||||
} |
||||
} |
@ -1,8 +0,0 @@ |
||||
node_modules/ |
||||
cache/ |
||||
artifacts/ |
||||
types/ |
||||
dist/ |
||||
coverage/ |
||||
coverage.json |
||||
.env |
@ -1,3 +0,0 @@ |
||||
module.exports = { |
||||
skipFiles: ["test"], |
||||
}; |
@ -1,8 +0,0 @@ |
||||
{ |
||||
"extends": "solhint:recommended", |
||||
"rules": { |
||||
"compiler-version": ["error", "^0.6.11"], |
||||
"func-visibility": ["warn", {"ignoreConstructors":true}], |
||||
"not-rely-on-time": "off" |
||||
} |
||||
} |
@ -1 +0,0 @@ |
||||
contracts/OZERC20.sol |
@ -1,40 +0,0 @@ |
||||
# Abacus Token Bridge |
||||
|
||||
### Message Format |
||||
|
||||
Bridge messages follow the following format: |
||||
|
||||
``` |
||||
TokenID (36 bytes) |
||||
- 4 bytes - domain specifier |
||||
- 32 bytes - id (address) on that domain |
||||
|
||||
Actions (variable) |
||||
EITHER |
||||
- Transfer (64 bytes) |
||||
- 32 bytes - to. local recipient |
||||
- 32 bytes - amount. number of tokens to send |
||||
- Details |
||||
- 32 bytes - name. new name of token |
||||
- 32 bytes - symbol. new symbol for token |
||||
- 1 byte - decimals. new decimal place count |
||||
|
||||
Message (100 or 101 bytes) |
||||
- TokenID (36 bytes) |
||||
- Action (64 or 65 bytes) |
||||
``` |
||||
|
||||
Given a message we know the following: |
||||
|
||||
- the `TokenID` is at indices 0-35 |
||||
- 0 - 3 Domain |
||||
- 4 - 35 Id |
||||
- the `Action` is at indices 36 - end. |
||||
- the action type can be determined from the message length alone. |
||||
- for `Transfer` actions |
||||
- 36 - 67 To |
||||
- 68 - 99 Amount |
||||
- for `Details` actions |
||||
- 36 - 67 Name |
||||
- 68 - 99 Symbol |
||||
- 100 Decimals |
@ -1,359 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
pragma experimental ABIEncoderV2; |
||||
|
||||
// ============ External Imports ============ |
||||
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
|
||||
library ControllerMessage { |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
|
||||
uint256 private constant CALL_PREFIX_LEN = 64; |
||||
uint256 private constant MSG_PREFIX_NUM_ITEMS = 2; |
||||
uint256 private constant MSG_PREFIX_LEN = 2; |
||||
uint256 private constant ENROLL_REMOTE_ROUTER_LEN = 37; |
||||
uint256 private constant SET_ADDRESS_LEN = 33; |
||||
|
||||
enum Types { |
||||
Invalid, // 0 |
||||
Call, // 1 |
||||
SetController, // 2 |
||||
EnrollRemoteRouter, // 3 |
||||
Data, // 4 |
||||
SetAbacusConnectionManager // 5 |
||||
} |
||||
|
||||
struct Call { |
||||
bytes32 to; |
||||
bytes data; |
||||
} |
||||
|
||||
modifier typeAssert(bytes29 _view, Types _t) { |
||||
_view.assertType(uint40(_t)); |
||||
_; |
||||
} |
||||
|
||||
// Types.Call |
||||
function data(bytes29 _view) internal view returns (bytes memory _data) { |
||||
_data = TypedMemView.clone( |
||||
_view.slice(CALL_PREFIX_LEN, dataLen(_view), uint40(Types.Data)) |
||||
); |
||||
} |
||||
|
||||
function formatCalls(Call[] memory _calls) |
||||
internal |
||||
view |
||||
returns (bytes memory _msg) |
||||
{ |
||||
uint256 _numCalls = _calls.length; |
||||
bytes29[] memory _encodedCalls = new bytes29[]( |
||||
_numCalls + MSG_PREFIX_NUM_ITEMS |
||||
); |
||||
|
||||
// Add Types.Call identifier |
||||
_encodedCalls[0] = abi.encodePacked(Types.Call).ref(0); |
||||
// Add number of calls |
||||
_encodedCalls[1] = abi.encodePacked(uint8(_numCalls)).ref(0); |
||||
|
||||
for (uint256 i = 0; i < _numCalls; i++) { |
||||
Call memory _call = _calls[i]; |
||||
bytes29 _callMsg = abi |
||||
.encodePacked(_call.to, _call.data.length, _call.data) |
||||
.ref(0); |
||||
|
||||
_encodedCalls[i + MSG_PREFIX_NUM_ITEMS] = _callMsg; |
||||
} |
||||
|
||||
_msg = TypedMemView.join(_encodedCalls); |
||||
} |
||||
|
||||
function formatSetController(bytes32 _controller) |
||||
internal |
||||
view |
||||
returns (bytes memory _msg) |
||||
{ |
||||
_msg = TypedMemView.clone( |
||||
mustBeSetController( |
||||
abi.encodePacked(Types.SetController, _controller).ref(0) |
||||
) |
||||
); |
||||
} |
||||
|
||||
function formatEnrollRemoteRouter(uint32 _domain, bytes32 _router) |
||||
internal |
||||
view |
||||
returns (bytes memory _msg) |
||||
{ |
||||
_msg = TypedMemView.clone( |
||||
mustBeEnrollRemoteRouter( |
||||
abi |
||||
.encodePacked(Types.EnrollRemoteRouter, _domain, _router) |
||||
.ref(0) |
||||
) |
||||
); |
||||
} |
||||
|
||||
function formatSetAbacusConnectionManager(bytes32 _abacusConnectionManager) |
||||
internal |
||||
view |
||||
returns (bytes memory _msg) |
||||
{ |
||||
_msg = TypedMemView.clone( |
||||
mustBeSetAbacusConnectionManager( |
||||
abi |
||||
.encodePacked( |
||||
Types.SetAbacusConnectionManager, |
||||
_abacusConnectionManager |
||||
) |
||||
.ref(0) |
||||
) |
||||
); |
||||
} |
||||
|
||||
function getCalls(bytes29 _msg) internal view returns (Call[] memory) { |
||||
uint8 _numCalls = uint8(_msg.indexUint(1, 1)); |
||||
|
||||
// Skip message prefix |
||||
bytes29 _msgPtr = _msg.slice( |
||||
MSG_PREFIX_LEN, |
||||
_msg.len() - MSG_PREFIX_LEN, |
||||
uint40(Types.Call) |
||||
); |
||||
|
||||
Call[] memory _calls = new Call[](_numCalls); |
||||
|
||||
uint256 counter = 0; |
||||
while (_msgPtr.len() > 0) { |
||||
_calls[counter].to = to(_msgPtr); |
||||
_calls[counter].data = data(_msgPtr); |
||||
|
||||
_msgPtr = nextCall(_msgPtr); |
||||
counter++; |
||||
} |
||||
|
||||
return _calls; |
||||
} |
||||
|
||||
function nextCall(bytes29 _view) |
||||
internal |
||||
pure |
||||
typeAssert(_view, Types.Call) |
||||
returns (bytes29) |
||||
{ |
||||
uint256 lastCallLen = CALL_PREFIX_LEN + dataLen(_view); |
||||
return |
||||
_view.slice( |
||||
lastCallLen, |
||||
_view.len() - lastCallLen, |
||||
uint40(Types.Call) |
||||
); |
||||
} |
||||
|
||||
function messageType(bytes29 _view) internal pure returns (Types) { |
||||
return Types(uint8(_view.typeOf())); |
||||
} |
||||
|
||||
/* |
||||
Message fields |
||||
*/ |
||||
|
||||
// All Types |
||||
function identifier(bytes29 _view) internal pure returns (uint8) { |
||||
return uint8(_view.indexUint(0, 1)); |
||||
} |
||||
|
||||
// Types.Call |
||||
function to(bytes29 _view) internal pure returns (bytes32) { |
||||
return _view.index(0, 32); |
||||
} |
||||
|
||||
// Types.Call |
||||
function dataLen(bytes29 _view) internal pure returns (uint256) { |
||||
return uint256(_view.index(32, 32)); |
||||
} |
||||
|
||||
// Types.EnrollRemoteRouter |
||||
function domain(bytes29 _view) internal pure returns (uint32) { |
||||
return uint32(_view.indexUint(1, 4)); |
||||
} |
||||
|
||||
// Types.EnrollRemoteRouter |
||||
function router(bytes29 _view) internal pure returns (bytes32) { |
||||
return _view.index(5, 32); |
||||
} |
||||
|
||||
// Types.SetController |
||||
function controller(bytes29 _view) internal pure returns (bytes32) { |
||||
return _view.index(1, 32); |
||||
} |
||||
|
||||
// Types.SetAbacusConnectionManager |
||||
function abacusConnectionManager(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bytes32) |
||||
{ |
||||
return _view.index(1, 32); |
||||
} |
||||
|
||||
/* |
||||
Message Type: CALL |
||||
struct Call { |
||||
identifier, // message ID -- 1 byte |
||||
numCalls, // number of calls -- 1 byte |
||||
calls[], { |
||||
to, // address to call -- 32 bytes |
||||
dataLen, // call data length -- 32 bytes, |
||||
data // call data -- 0+ bytes (length unknown) |
||||
} |
||||
} |
||||
*/ |
||||
|
||||
function isValidCall(bytes29 _view) internal pure returns (bool) { |
||||
return |
||||
identifier(_view) == uint8(Types.Call) && |
||||
_view.len() >= CALL_PREFIX_LEN; |
||||
} |
||||
|
||||
function isCall(bytes29 _view) internal pure returns (bool) { |
||||
return isValidCall(_view) && messageType(_view) == Types.Call; |
||||
} |
||||
|
||||
function tryAsCall(bytes29 _view) internal pure returns (bytes29) { |
||||
if (isValidCall(_view)) { |
||||
return _view.castTo(uint40(Types.Call)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function mustBeCall(bytes29 _view) internal pure returns (bytes29) { |
||||
return tryAsCall(_view).assertValid(); |
||||
} |
||||
|
||||
/* |
||||
Message Type: SET CONTROLLER |
||||
struct SetController { |
||||
identifier, // message ID -- 1 byte |
||||
addr // address of new controller -- 32 bytes |
||||
} |
||||
*/ |
||||
|
||||
function isValidSetController(bytes29 _view) internal pure returns (bool) { |
||||
return |
||||
identifier(_view) == uint8(Types.SetController) && |
||||
_view.len() == SET_ADDRESS_LEN; |
||||
} |
||||
|
||||
function isSetController(bytes29 _view) internal pure returns (bool) { |
||||
return |
||||
isValidSetController(_view) && |
||||
messageType(_view) == Types.SetController; |
||||
} |
||||
|
||||
function tryAsSetController(bytes29 _view) internal pure returns (bytes29) { |
||||
if (isValidSetController(_view)) { |
||||
return _view.castTo(uint40(Types.SetController)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function mustBeSetController(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bytes29) |
||||
{ |
||||
return tryAsSetController(_view).assertValid(); |
||||
} |
||||
|
||||
/* |
||||
Message Type: ENROLL REMOTE ROUTER |
||||
struct EnrollRemoteRouter { |
||||
identifier, // message ID -- 1 byte |
||||
domain, // domain of new router -- 4 bytes |
||||
addr // address of new router -- 32 bytes |
||||
} |
||||
*/ |
||||
|
||||
function isValidEnrollRemoteRouter(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bool) |
||||
{ |
||||
return |
||||
identifier(_view) == uint8(Types.EnrollRemoteRouter) && |
||||
_view.len() == ENROLL_REMOTE_ROUTER_LEN; |
||||
} |
||||
|
||||
function isEnrollRemoteRouter(bytes29 _view) internal pure returns (bool) { |
||||
return |
||||
isValidEnrollRemoteRouter(_view) && |
||||
messageType(_view) == Types.EnrollRemoteRouter; |
||||
} |
||||
|
||||
function tryAsEnrollRemoteRouter(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bytes29) |
||||
{ |
||||
if (isValidEnrollRemoteRouter(_view)) { |
||||
return _view.castTo(uint40(Types.EnrollRemoteRouter)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function mustBeEnrollRemoteRouter(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bytes29) |
||||
{ |
||||
return tryAsEnrollRemoteRouter(_view).assertValid(); |
||||
} |
||||
|
||||
/* |
||||
Message Type: SET XAPPCONNECTIONMANAGER |
||||
struct SetAbacusConnectionManager { |
||||
identifier, // message ID -- 1 byte |
||||
addr // address of new abacusConnectionManager -- 32 bytes |
||||
} |
||||
*/ |
||||
|
||||
function isValidSetAbacusConnectionManager(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bool) |
||||
{ |
||||
return |
||||
identifier(_view) == uint8(Types.SetAbacusConnectionManager) && |
||||
_view.len() == SET_ADDRESS_LEN; |
||||
} |
||||
|
||||
function isSetAbacusConnectionManager(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bool) |
||||
{ |
||||
return |
||||
isValidSetAbacusConnectionManager(_view) && |
||||
messageType(_view) == Types.SetAbacusConnectionManager; |
||||
} |
||||
|
||||
function tryAsSetAbacusConnectionManager(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bytes29) |
||||
{ |
||||
if (isValidSetAbacusConnectionManager(_view)) { |
||||
return _view.castTo(uint40(Types.SetAbacusConnectionManager)); |
||||
} |
||||
return TypedMemView.nullView(); |
||||
} |
||||
|
||||
function mustBeSetAbacusConnectionManager(bytes29 _view) |
||||
internal |
||||
pure |
||||
returns (bytes29) |
||||
{ |
||||
return tryAsSetAbacusConnectionManager(_view).assertValid(); |
||||
} |
||||
} |
@ -1,388 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
pragma experimental ABIEncoderV2; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {ControllerMessage} from "./ControllerMessage.sol"; |
||||
// ============ External Imports ============ |
||||
import {Router} from "@abacus-network/app/contracts/Router.sol"; |
||||
import {Version0} from "@abacus-network/core/contracts/Version0.sol"; |
||||
import {TypeCasts} from "@abacus-network/core/libs/TypeCasts.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 ControllerRouter has two modes of operation, normal and recovery. |
||||
* During normal mode, `owner()` returns the `controller`, giving it permission |
||||
* to call `onlyOwner` functions. |
||||
* During recovery mode, `owner()` returns the `_owner`, giving it permission |
||||
* to call `onlyOwner` functions. |
||||
*/ |
||||
contract ControllerRouter is Version0, Router { |
||||
// ============ Libraries ============ |
||||
|
||||
using SafeMath for uint256; |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
using ControllerMessage 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 permissioned functions during normal |
||||
// operation, typically set to 0x0 on all chains but one |
||||
address public controller; |
||||
|
||||
// ============ Upgrade Gap ============ |
||||
|
||||
// gap for upgrade safety |
||||
uint256[48] private __GAP; |
||||
|
||||
// ============ Events ============ |
||||
|
||||
/** |
||||
* @notice Emitted when the controller role is set |
||||
* @param controller the address of the new controller |
||||
*/ |
||||
event SetController(address indexed controller); |
||||
|
||||
/** |
||||
* @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, ControllerMessage.Types _type) { |
||||
_view.assertType(uint40(_type)); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyInRecovery() { |
||||
require(inRecovery(), "!recovery"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyNotInRecovery() { |
||||
require(!inRecovery(), "recovery"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyController() { |
||||
require(msg.sender == controller, "!controller"); |
||||
_; |
||||
} |
||||
|
||||
modifier onlyRecoveryManager() { |
||||
require(msg.sender == recoveryManager(), "!recoveryManager"); |
||||
_; |
||||
} |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor(uint256 _recoveryTimelock) { |
||||
recoveryTimelock = _recoveryTimelock; |
||||
} |
||||
|
||||
// ============ Initializer ============ |
||||
|
||||
function initialize(address _abacusConnectionManager) public initializer { |
||||
__Router_initialize(_abacusConnectionManager); |
||||
controller = msg.sender; |
||||
} |
||||
|
||||
// ============ External Functions ============ |
||||
|
||||
/** |
||||
* @notice Handle Abacus messages |
||||
* For all non-controlling chains to handle messages |
||||
* sent from the controlling chain via Abacus. |
||||
* Controlling chain should never receive messages, |
||||
* because non-controlling chains are not able to send them |
||||
* @param _message The message |
||||
*/ |
||||
function _handle( |
||||
uint32, |
||||
bytes32, |
||||
bytes memory _message |
||||
) internal override { |
||||
bytes29 _msg = _message.ref(0); |
||||
if (_msg.isValidCall()) { |
||||
_handleCall(_msg.tryAsCall()); |
||||
} else if (_msg.isValidSetController()) { |
||||
_handleSetController(_msg.tryAsSetController()); |
||||
} else if (_msg.isValidEnrollRemoteRouter()) { |
||||
_handleEnrollRemoteRouter(_msg.tryAsEnrollRemoteRouter()); |
||||
} else if (_msg.isValidSetAbacusConnectionManager()) { |
||||
_handleSetAbacusConnectionManager( |
||||
_msg.tryAsSetAbacusConnectionManager() |
||||
); |
||||
} else { |
||||
require(false, "!valid message type"); |
||||
} |
||||
} |
||||
|
||||
// ============ External Local Functions ============ |
||||
|
||||
/** |
||||
* @notice Make local calls. |
||||
* @param _calls The calls |
||||
*/ |
||||
function call(ControllerMessage.Call[] calldata _calls) external onlyOwner { |
||||
for (uint256 i = 0; i < _calls.length; i++) { |
||||
_makeCall(_calls[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Sets the controller of the router. |
||||
* @param _controller The address of the new controller |
||||
*/ |
||||
function setController(address _controller) external onlyOwner { |
||||
_setController(_controller); |
||||
} |
||||
|
||||
/** |
||||
* @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 ControllerRouter. |
||||
* Any value paid to this function is used to pay for message processing on the remote chain. |
||||
* @param _destination The domain of the remote chain |
||||
* @param _calls The calls |
||||
*/ |
||||
function callRemote( |
||||
uint32 _destination, |
||||
ControllerMessage.Call[] calldata _calls |
||||
) external payable onlyController onlyNotInRecovery { |
||||
bytes memory _msg = ControllerMessage.formatCalls(_calls); |
||||
_dispatchWithGasAndCheckpoint(_destination, _msg, msg.value); |
||||
} |
||||
|
||||
/** |
||||
* @notice Enroll a remote router on a remote router. Any value paid to this |
||||
* function is used to pay for message processing on the remote chain. |
||||
* @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 payable onlyController onlyNotInRecovery { |
||||
bytes memory _msg = ControllerMessage.formatEnrollRemoteRouter( |
||||
_domain, |
||||
_router |
||||
); |
||||
_dispatchWithGasAndCheckpoint(_destination, _msg, msg.value); |
||||
} |
||||
|
||||
/** |
||||
* @notice Sets the abacusConnectionManager of a remote router. Any value paid to this |
||||
* function is used to pay for message processing on the remote chain. |
||||
* @param _destination The domain of router on which to set the abacusConnectionManager |
||||
* @param _abacusConnectionManager The address of the abacusConnectionManager contract |
||||
*/ |
||||
function setAbacusConnectionManagerRemote( |
||||
uint32 _destination, |
||||
address _abacusConnectionManager |
||||
) external payable onlyController onlyNotInRecovery { |
||||
bytes memory _msg = ControllerMessage.formatSetAbacusConnectionManager( |
||||
TypeCasts.addressToBytes32(_abacusConnectionManager) |
||||
); |
||||
_dispatchWithGasAndCheckpoint(_destination, _msg, msg.value); |
||||
} |
||||
|
||||
/** |
||||
* @notice Sets the controller of a remote router. Any value paid to this |
||||
* function is used to pay for message processing on the remote chain. |
||||
* @param _destination The domain of router on which to set the controller |
||||
* @param _controller The address of the new controller |
||||
*/ |
||||
function setControllerRemote(uint32 _destination, address _controller) |
||||
external |
||||
payable |
||||
onlyController |
||||
onlyNotInRecovery |
||||
{ |
||||
bytes memory _msg = ControllerMessage.formatSetController( |
||||
TypeCasts.addressToBytes32(_controller) |
||||
); |
||||
_dispatchWithGasAndCheckpoint(_destination, _msg, msg.value); |
||||
} |
||||
|
||||
// ============ Public Functions ============ |
||||
|
||||
/** |
||||
* @notice Transfers the recovery manager to a new address. |
||||
* @dev Callable by the controller 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 controller owns the contract. |
||||
*/ |
||||
function owner() public view virtual override returns (address) { |
||||
return inRecovery() ? recoveryManager() : controller; |
||||
} |
||||
|
||||
/** |
||||
* @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, ControllerMessage.Types.Call) |
||||
{ |
||||
ControllerMessage.Call[] memory _calls = _msg.getCalls(); |
||||
for (uint256 i = 0; i < _calls.length; i++) { |
||||
_makeCall(_calls[i]); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message transferring control to a new Controller |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleSetController(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, ControllerMessage.Types.SetController) |
||||
{ |
||||
address _controller = TypeCasts.bytes32ToAddress(_msg.controller()); |
||||
_setController(_controller); |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message setting the router address for a given domain |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleEnrollRemoteRouter(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, ControllerMessage.Types.EnrollRemoteRouter) |
||||
{ |
||||
uint32 _domain = _msg.domain(); |
||||
bytes32 _router = _msg.router(); |
||||
_enrollRemoteRouter(_domain, _router); |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle message setting the abacusConnectionManager address |
||||
* @param _msg The message |
||||
*/ |
||||
function _handleSetAbacusConnectionManager(bytes29 _msg) |
||||
internal |
||||
typeAssert(_msg, ControllerMessage.Types.SetAbacusConnectionManager) |
||||
{ |
||||
address _abacusConnectionManager = TypeCasts.bytes32ToAddress( |
||||
_msg.abacusConnectionManager() |
||||
); |
||||
_setAbacusConnectionManager(_abacusConnectionManager); |
||||
} |
||||
|
||||
/** |
||||
* @notice Call local contract. |
||||
* @param _call The call |
||||
* @return _ret |
||||
*/ |
||||
function _makeCall(ControllerMessage.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 controller. |
||||
* @param _controller The address of the new controller |
||||
*/ |
||||
function _setController(address _controller) internal { |
||||
controller = _controller; |
||||
emit SetController(_controller); |
||||
} |
||||
} |
@ -1,14 +0,0 @@ |
||||
// 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,99 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
|
||||
library PingPongMessage { |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
|
||||
/// @dev Each message is encoded as a 1-byte type distinguisher, a 4-byte |
||||
/// match id, and a 32-byte volley counter. The messages are therefore all |
||||
/// 37 bytes |
||||
enum Types { |
||||
Invalid, // 0 |
||||
Ping, // 1 |
||||
Pong // 2 |
||||
} |
||||
|
||||
// ============ Formatters ============ |
||||
|
||||
/** |
||||
* @notice Format a Ping volley |
||||
* @param _count The number of volleys in this match |
||||
* @return The encoded bytes message |
||||
*/ |
||||
function formatPing(uint32 _match, uint256 _count) |
||||
internal |
||||
pure |
||||
returns (bytes memory) |
||||
{ |
||||
return abi.encodePacked(uint8(Types.Ping), _match, _count); |
||||
} |
||||
|
||||
/** |
||||
* @notice Format a Pong volley |
||||
* @param _count The number of volleys in this match |
||||
* @return The encoded bytes message |
||||
*/ |
||||
function formatPong(uint32 _match, uint256 _count) |
||||
internal |
||||
pure |
||||
returns (bytes memory) |
||||
{ |
||||
return abi.encodePacked(uint8(Types.Pong), _match, _count); |
||||
} |
||||
|
||||
// ============ Identifiers ============ |
||||
|
||||
/** |
||||
* @notice Get the type that the TypedMemView is cast to |
||||
* @param _view The message |
||||
* @return _type The type of the message (either Ping or Pong) |
||||
*/ |
||||
function messageType(bytes29 _view) internal pure returns (Types _type) { |
||||
_type = Types(uint8(_view.typeOf())); |
||||
} |
||||
|
||||
/** |
||||
* @notice Determine whether the message contains a Ping volley |
||||
* @param _view The message |
||||
* @return True if the volley is Ping |
||||
*/ |
||||
function isPing(bytes29 _view) internal pure returns (bool) { |
||||
return messageType(_view) == Types.Ping; |
||||
} |
||||
|
||||
/** |
||||
* @notice Determine whether the message contains a Pong volley |
||||
* @param _view The message |
||||
* @return True if the volley is Pong |
||||
*/ |
||||
function isPong(bytes29 _view) internal pure returns (bool) { |
||||
return messageType(_view) == Types.Pong; |
||||
} |
||||
|
||||
// ============ Getters ============ |
||||
|
||||
/** |
||||
* @notice Parse the match ID sent within a Ping or Pong message |
||||
* @dev The number is encoded as a uint32 at index 1 |
||||
* @param _view The message |
||||
* @return The match id encoded in the message |
||||
*/ |
||||
function matchId(bytes29 _view) internal pure returns (uint32) { |
||||
// At index 1, read 4 bytes as a uint, and cast to a uint32 |
||||
return uint32(_view.indexUint(1, 4)); |
||||
} |
||||
|
||||
/** |
||||
* @notice Parse the volley count sent within a Ping or Pong message |
||||
* @dev The number is encoded as a uint256 at index 1 |
||||
* @param _view The message |
||||
* @return The count encoded in the message |
||||
*/ |
||||
function count(bytes29 _view) internal pure returns (uint256) { |
||||
// At index 1, read 32 bytes as a uint |
||||
return _view.indexUint(1, 32); |
||||
} |
||||
} |
@ -1,159 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
// ============ External Imports ============ |
||||
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
import {Router} from "@abacus-network/app/contracts/Router.sol"; |
||||
// ============ Internal Imports ============ |
||||
import {PingPongMessage} from "./PingPongMessage.sol"; |
||||
|
||||
/* |
||||
============ PingPong Application ============ |
||||
The PingPong Application is capable of initiating PingPong "matches" between two chains. |
||||
A match consists of "volleys" sent back-and-forth between the two chains via Abacus. |
||||
|
||||
The first volley in a match is always a Ping volley. |
||||
When a Router receives a Ping volley, it returns a Pong. |
||||
When a Router receives a Pong volley, it returns a Ping. |
||||
|
||||
The Routers keep track of the number of volleys in a given match, |
||||
and emit events for each Sent and Received volley so that spectators can watch. |
||||
*/ |
||||
contract PingPongRouter is Router { |
||||
// ============ Libraries ============ |
||||
|
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
using PingPongMessage for bytes29; |
||||
|
||||
// ============ Mutable State ============ |
||||
uint32 nextMatch; |
||||
|
||||
// ============ Events ============ |
||||
|
||||
event Received( |
||||
uint32 indexed domain, |
||||
uint32 indexed matchId, |
||||
uint256 count, |
||||
bool isPing |
||||
); |
||||
event Sent( |
||||
uint32 indexed domain, |
||||
uint32 indexed matchId, |
||||
uint256 count, |
||||
bool isPing |
||||
); |
||||
|
||||
// ============ Constructor ============ |
||||
constructor(address _abacusConnectionManager) { |
||||
require(false, "example application, do not deploy"); |
||||
|
||||
__AbacusConnectionClient_initialize(_abacusConnectionManager); |
||||
} |
||||
|
||||
// ============ Handle message functions ============ |
||||
|
||||
/** |
||||
* @notice Handle "volleys" sent via Abacus from other remote PingPong Routers |
||||
* @param _origin The domain the message is coming from |
||||
* @param _message The message in the form of raw bytes |
||||
*/ |
||||
function _handle( |
||||
uint32 _origin, |
||||
bytes32, |
||||
bytes memory _message |
||||
) internal override { |
||||
bytes29 _msg = _message.ref(0); |
||||
if (_msg.isPing()) { |
||||
_handlePing(_origin, _msg); |
||||
} else if (_msg.isPong()) { |
||||
_handlePong(_origin, _msg); |
||||
} else { |
||||
// if _message doesn't match any valid actions, revert |
||||
require(false, "!valid action"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle a Ping volley |
||||
* @param _origin The domain that sent the volley |
||||
* @param _message The message in the form of raw bytes |
||||
*/ |
||||
function _handlePing(uint32 _origin, bytes29 _message) internal { |
||||
bool _isPing = true; |
||||
_handle(_origin, _isPing, _message); |
||||
} |
||||
|
||||
/** |
||||
* @notice Handle a Pong volley |
||||
* @param _origin The domain that sent the volley |
||||
* @param _message The message in the form of raw bytes |
||||
*/ |
||||
function _handlePong(uint32 _origin, bytes29 _message) internal { |
||||
bool _isPing = false; |
||||
_handle(_origin, _isPing, _message); |
||||
} |
||||
|
||||
/** |
||||
* @notice Upon receiving a volley, emit an event, increment the count and return a the opposite volley |
||||
* @param _origin The domain that sent the volley |
||||
* @param _isPing True if the volley received is a Ping, false if it is a Pong |
||||
* @param _message The message in the form of raw bytes |
||||
*/ |
||||
function _handle( |
||||
uint32 _origin, |
||||
bool _isPing, |
||||
bytes29 _message |
||||
) internal { |
||||
// get the volley count for this game |
||||
uint256 _count = _message.count(); |
||||
uint32 _match = _message.matchId(); |
||||
// emit a Received event |
||||
emit Received(_origin, _match, _count, _isPing); |
||||
// send the opposite volley back |
||||
_send(_origin, !_isPing, _match, _count + 1); |
||||
} |
||||
|
||||
// ============ Dispatch message functions ============ |
||||
|
||||
/** |
||||
* @notice Initiate a PingPong match with the destination domain |
||||
* by sending the first Ping volley. |
||||
* @param _destinationDomain The domain to initiate the match with |
||||
*/ |
||||
function initiatePingPongMatch(uint32 _destinationDomain) external { |
||||
// the PingPong match always begins with a Ping volley |
||||
bool _isPing = true; |
||||
// increment match counter |
||||
uint32 _match = nextMatch; |
||||
nextMatch = _match + 1; |
||||
// send the first volley to the destination domain |
||||
_send(_destinationDomain, _isPing, _match, 0); |
||||
} |
||||
|
||||
/** |
||||
* @notice Send a Ping or Pong volley to the destination domain |
||||
* @param _destinationDomain The domain to send the volley to |
||||
* @param _isPing True if the volley to send is a Ping, false if it is a Pong |
||||
* @param _count The number of volleys in this match |
||||
*/ |
||||
function _send( |
||||
uint32 _destinationDomain, |
||||
bool _isPing, |
||||
uint32 _match, |
||||
uint256 _count |
||||
) internal { |
||||
// get the Application Router at the destinationDomain |
||||
bytes32 _remoteRouterAddress = _mustHaveRemoteRouter( |
||||
_destinationDomain |
||||
); |
||||
// format the ping message |
||||
bytes memory _message = _isPing |
||||
? PingPongMessage.formatPing(_match, _count) |
||||
: PingPongMessage.formatPong(_match, _count); |
||||
// send the message to the Application Router |
||||
_outbox().dispatch(_destinationDomain, _remoteRouterAddress, _message); |
||||
// emit a Sent event |
||||
emit Sent(_destinationDomain, _match, _count, _isPing); |
||||
} |
||||
} |
@ -1,92 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
|
||||
/* |
||||
============ Overview: Application Message Library ============ |
||||
Messages are the actual data passed between chains. |
||||
We define messages as a byte vector in memory. |
||||
|
||||
To make sure that messages are compact and chain agnostic, |
||||
we recommend a simple, custom serialization format (rather than ABI encoding). |
||||
|
||||
TypedMemView is a library for working with memory in Solidity. |
||||
We use TypedMemView to create a custom serialization format for Application messages. |
||||
We use a 1-byte type tag on the front of the message. |
||||
(The typed tag is optional if the user is familiar with writing wire protocols) |
||||
|
||||
Message Flow Between Applications: |
||||
1. Application Router A receives a command on chain A |
||||
2. Application Router A encodes (formats) the information into a message |
||||
2. Application Router A sends the message to Application Router B on chain B via Abacus |
||||
3. Application Router B receives the message via Abacus |
||||
4. Application Router B decodes (gets) the information from the message and acts on it |
||||
|
||||
The Message Library should contain the following for each type of message: |
||||
1. Formatter: a function which takes information as Solidity arguments and |
||||
encodes it as a byte vector in a defined format, producing the message |
||||
|
||||
2. Identifier: a function which takes a byte vector and returns TRUE |
||||
if the vector is matches the expected format of this message type |
||||
|
||||
3. Getter(s): function(s) which parse the information stored in the message |
||||
and return them in the form of Solidity arguments |
||||
*/ |
||||
library Message { |
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
|
||||
enum Types { |
||||
Invalid, // 0 |
||||
A // 1 - a message which contains a single number |
||||
} |
||||
|
||||
// ============ Formatters ============ |
||||
|
||||
/** |
||||
* @notice Given the information needed for a message TypeA |
||||
* (in this example case, the information is just a single number) |
||||
* format a bytes message encoding the information |
||||
* @param _number The number to be included in the TypeA message |
||||
* @return The encoded bytes message |
||||
*/ |
||||
function formatTypeA(uint256 _number) internal pure returns (bytes memory) { |
||||
return abi.encodePacked(uint8(Types.A), _number); |
||||
} |
||||
|
||||
// ============ Identifiers ============ |
||||
|
||||
/** |
||||
* @notice Get the type that the TypedMemView is cast to |
||||
* @param _view The message |
||||
* @return _type The type of the message (one of the enum Types) |
||||
*/ |
||||
function messageType(bytes29 _view) internal pure returns (Types _type) { |
||||
_type = Types(uint8(_view.typeOf())); |
||||
} |
||||
|
||||
/** |
||||
* @notice Determine whether the message is a message TypeA |
||||
* @param _view The message |
||||
* @return _isTypeA True if the message is TypeA |
||||
*/ |
||||
function isTypeA(bytes29 _view) internal pure returns (bool _isTypeA) { |
||||
_isTypeA = messageType(_view) == Types.A; |
||||
} |
||||
|
||||
// ============ Getters ============ |
||||
|
||||
/** |
||||
* @notice Parse the number sent within a TypeA message |
||||
* @param _view The message |
||||
* @return _number The number encoded in the message |
||||
*/ |
||||
function number(bytes29 _view) internal pure returns (uint256 _number) { |
||||
require( |
||||
isTypeA(_view), |
||||
"MessageTemplate/number: view must be of type A" |
||||
); |
||||
_number = uint256(_view.index(0, 32)); |
||||
} |
||||
} |
@ -1,104 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
// ============ External Imports ============ |
||||
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol"; |
||||
import {Router} from "@abacus-network/app/contracts/Router.sol"; |
||||
// ============ Internal Imports ============ |
||||
import {Message} from "./MessageTemplate.sol"; |
||||
|
||||
/* |
||||
============ Overview: Building a Application ============ |
||||
To implement a Application, define the actions you would like to execute across chains. |
||||
For each type of action, |
||||
- in the Application Router |
||||
- implement a function like doTypeA to initiate the action from one domain to another (add your own parameters and logic) |
||||
- implement a corresponding _handle function to receive, parse, and execute this type of message on the remote domain |
||||
- add logic to the handle function to route incoming messages to the appropriate _handle function |
||||
- in the Message library, |
||||
- implement functions to *format* the message to send to the other chain (encodes all necessary information for the action) |
||||
- implement functions to *parse* the message once it is received on the other chain (decode all necessary information for the action) |
||||
*/ |
||||
contract RouterTemplate is Router { |
||||
// ============ Libraries ============ |
||||
|
||||
using TypedMemView for bytes; |
||||
using TypedMemView for bytes29; |
||||
using Message for bytes29; |
||||
|
||||
// ============ Events ============ |
||||
|
||||
event TypeAReceived(uint256 number); |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
constructor(address _abacusConnectionManager) { |
||||
__AbacusConnectionClient_initialize(_abacusConnectionManager); |
||||
} |
||||
|
||||
// ============ Handle message functions ============ |
||||
|
||||
/** |
||||
* @notice Receive messages sent via Abacus from other remote Application Routers; |
||||
* parse the contents of the message and enact the message's effects on the local chain |
||||
* @dev Called by an Abacus Inbox contract while processing a message sent via Abacus |
||||
* @param _message The message in the form of raw bytes |
||||
*/ |
||||
function _handle( |
||||
uint32, |
||||
bytes32, |
||||
bytes memory _message |
||||
) internal override { |
||||
bytes29 _msg = _message.ref(0); |
||||
// route message to appropriate _handle function |
||||
// based on what type of message is encoded |
||||
if (_msg.isTypeA()) { |
||||
_handleTypeA(_msg); |
||||
} else { |
||||
// if _message doesn't match any valid actions, revert |
||||
require(false, "!valid action"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @notice Once the Router has parsed a message in the handle function and determined it is Type A, |
||||
* call this internal function to parse specific information from the message, |
||||
* and enact the message's action on this chain |
||||
* @param _message The message in the form of raw bytes |
||||
*/ |
||||
function _handleTypeA(bytes29 _message) internal { |
||||
// parse the information from the message |
||||
uint256 _number = _message.number(); |
||||
// implement the logic for executing the action |
||||
// (in this example case, emit an event with the number that was sent) |
||||
emit TypeAReceived(_number); |
||||
} |
||||
|
||||
// ============ Dispatch message functions ============ |
||||
|
||||
/** |
||||
* @notice Send a message of "Type A" to a remote Application Router via Abacus; |
||||
* this message is called to take some action in the cross-chain context |
||||
* Example message types: |
||||
* Sending tokens from this chain to the destination chain; |
||||
* params would be the address of the token, the amount of the token to send, and the address of the recipient |
||||
* @param _destinationDomain The domain to send the message to |
||||
* @param _number Example parameter used in message TypeA - a number to send to another chain |
||||
*/ |
||||
function dispatchTypeA(uint32 _destinationDomain, uint256 _number) |
||||
external |
||||
{ |
||||
// get the Application Router at the destinationDomain |
||||
bytes32 _remoteRouterAddress = _mustHaveRemoteRouter( |
||||
_destinationDomain |
||||
); |
||||
// encode a message to send to the remote Application Router |
||||
bytes memory _outboundMessage = Message.formatTypeA(_number); |
||||
// send the message to the Application Router |
||||
_outbox().dispatch( |
||||
_destinationDomain, |
||||
_remoteRouterAddress, |
||||
_outboundMessage |
||||
); |
||||
} |
||||
} |
@ -1,28 +0,0 @@ |
||||
import 'solidity-coverage'; |
||||
import '@typechain/hardhat'; |
||||
import '@nomiclabs/hardhat-waffle'; |
||||
import 'hardhat-gas-reporter'; |
||||
import '@abacus-network/hardhat'; |
||||
|
||||
/** |
||||
* @type import('hardhat/config').HardhatUserConfig |
||||
*/ |
||||
module.exports = { |
||||
solidity: { |
||||
version: '0.7.6', |
||||
settings: { |
||||
optimizer: { |
||||
enabled: true, |
||||
runs: 999999, |
||||
}, |
||||
}, |
||||
}, |
||||
gasReporter: { |
||||
currency: 'USD', |
||||
}, |
||||
typechain: { |
||||
outDir: './types', |
||||
target: 'ethers-v5', |
||||
alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads?
|
||||
}, |
||||
}; |
@ -1,24 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
interface IBridgeToken { |
||||
function initialize() external; |
||||
|
||||
function name() external returns (string memory); |
||||
|
||||
function balanceOf(address _account) external view returns (uint256); |
||||
|
||||
function symbol() external view returns (string memory); |
||||
|
||||
function decimals() external view returns (uint8); |
||||
|
||||
function burn(address _from, uint256 _amnt) external; |
||||
|
||||
function mint(address _to, uint256 _amnt) external; |
||||
|
||||
function setDetails( |
||||
string calldata _name, |
||||
string calldata _symbol, |
||||
uint8 _decimals |
||||
) external; |
||||
} |
@ -1,7 +0,0 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
interface IWeth { |
||||
function deposit() external payable; |
||||
|
||||
function approve(address _who, uint256 _wad) external; |
||||
} |
@ -1,47 +0,0 @@ |
||||
{ |
||||
"name": "@abacus-network/apps", |
||||
"devDependencies": { |
||||
"@nomiclabs/hardhat-ethers": "^2.0.1", |
||||
"@nomiclabs/hardhat-waffle": "^2.0.1", |
||||
"@typechain/ethers-v5": "~7.0.0", |
||||
"@typechain/hardhat": "^2.0.1", |
||||
"@types/mocha": "^9.1.0", |
||||
"chai": "^4.3.0", |
||||
"eslint": "^7.20.0", |
||||
"ethereum-waffle": "^3.2.2", |
||||
"ethers": "^5.4.4", |
||||
"hardhat": "^2.8.3", |
||||
"hardhat-gas-reporter": "^1.0.7", |
||||
"prettier": "^2.2.1", |
||||
"prettier-plugin-solidity": "^1.0.0-beta.5", |
||||
"solhint": "^3.3.2", |
||||
"solhint-plugin-prettier": "^0.0.5", |
||||
"solidity-coverage": "^0.7.14", |
||||
"ts-node": "^10.1.0", |
||||
"typechain": "^5.0.0", |
||||
"typescript": "^4.3.5" |
||||
}, |
||||
"version": "0.1.1", |
||||
"main": "dist/index.js", |
||||
"types": "dist/index.d.ts", |
||||
"directories": { |
||||
"test": "test" |
||||
}, |
||||
"scripts": { |
||||
"prettier": "prettier --write ./contracts ./test", |
||||
"build": "hardhat compile && hardhat typechain && tsc && npm run copy-types", |
||||
"copy-types": "cp types/*.d.ts dist/", |
||||
"coverage": "hardhat coverage", |
||||
"test": "hardhat test" |
||||
}, |
||||
"license": "MIT OR Apache-2.0", |
||||
"dependencies": { |
||||
"@abacus-network/app": "^0.1.1", |
||||
"@abacus-network/core": "^0.1.1", |
||||
"@abacus-network/hardhat": "^0.1.1", |
||||
"@abacus-network/utils": "^0.1.1", |
||||
"@openzeppelin/contracts": "~3.4.2", |
||||
"@openzeppelin/contracts-upgradeable": "~3.4.2", |
||||
"@summa-tx/memview-sol": "^2.0.0" |
||||
} |
||||
} |
@ -1,514 +0,0 @@ |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||
import { expect } from 'chai'; |
||||
import { abacus, ethers } from 'hardhat'; |
||||
|
||||
import { InterchainGasPaymaster, Outbox } from '@abacus-network/core'; |
||||
import { utils } from '@abacus-network/utils'; |
||||
|
||||
import { ControllerRouter, TestSet, TestSet__factory } from '../../types'; |
||||
|
||||
import { ControllerConfig, ControllerDeploy } from './lib/ControllerDeploy'; |
||||
import { formatCall, increaseTimestampBy } from './lib/utils'; |
||||
|
||||
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('ControllerRouter', async () => { |
||||
let controller: SignerWithAddress, |
||||
recoveryManager: SignerWithAddress, |
||||
router: ControllerRouter, |
||||
remote: ControllerRouter, |
||||
testSet: TestSet, |
||||
controllerDeploy: ControllerDeploy; |
||||
let outbox: Outbox; |
||||
let interchainGasPaymaster: InterchainGasPaymaster; |
||||
const testInterchainGasPayment = 123456789; |
||||
|
||||
before(async () => { |
||||
[controller, recoveryManager] = await ethers.getSigners(); |
||||
|
||||
const testSetFactory = new TestSet__factory(controller); |
||||
testSet = await testSetFactory.deploy(); |
||||
}); |
||||
|
||||
beforeEach(async () => { |
||||
const config: ControllerConfig = { |
||||
signer: controller, |
||||
timelock: recoveryTimelock, |
||||
recoveryManager: recoveryManager.address, |
||||
controller: { |
||||
domain: localDomain, |
||||
address: controller.address, |
||||
}, |
||||
}; |
||||
await abacus.deploy(domains, controller); |
||||
controllerDeploy = new ControllerDeploy(config); |
||||
await controllerDeploy.deploy(abacus); |
||||
router = controllerDeploy.router(localDomain); |
||||
remote = controllerDeploy.router(remoteDomain); |
||||
outbox = abacus.outbox(localDomain); |
||||
interchainGasPaymaster = abacus.interchainGasPaymaster(localDomain); |
||||
}); |
||||
|
||||
it('Cannot be initialized twice', async () => { |
||||
await expect( |
||||
router.initialize(ethers.constants.AddressZero), |
||||
).to.be.revertedWith('Initializable: contract is already initialized'); |
||||
}); |
||||
|
||||
describe('when not in recovery mode', async () => { |
||||
it('controller is the owner', async () => { |
||||
expect(await router.owner()).to.equal(controller.address); |
||||
}); |
||||
|
||||
it('controller 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('controller can make local calls', async () => { |
||||
const value = 12; |
||||
const call = formatCall(testSet, 'set', [value]); |
||||
await router.call([call]); |
||||
expect(await testSet.get()).to.equal(value); |
||||
}); |
||||
|
||||
it('controller can set local controller', async () => { |
||||
expect(await router.controller()).to.equal(controller.address); |
||||
await router.setController(ethers.constants.AddressZero); |
||||
expect(await router.controller()).to.equal(ethers.constants.AddressZero); |
||||
}); |
||||
|
||||
it('controller can set local abacusConnectionManager', async () => { |
||||
expect(await router.abacusConnectionManager()).to.equal( |
||||
abacus.abacusConnectionManager(localDomain).address, |
||||
); |
||||
await router.setAbacusConnectionManager(ethers.constants.AddressZero); |
||||
expect(await router.abacusConnectionManager()).to.equal( |
||||
ethers.constants.AddressZero, |
||||
); |
||||
}); |
||||
|
||||
it('controller 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('controller can make remote calls', async () => { |
||||
const value = 13; |
||||
const call = formatCall(testSet, 'set', [value]); |
||||
await router.callRemote(domains[1], [call]); |
||||
await abacus.processMessages(); |
||||
expect(await testSet.get()).to.equal(value); |
||||
}); |
||||
|
||||
it('allows interchain gas payment for remote calls', async () => { |
||||
const leafIndex = await outbox.count(); |
||||
const value = 13; |
||||
const call = formatCall(testSet, 'set', [value]); |
||||
await expect( |
||||
await router.callRemote(domains[1], [call], { |
||||
value: testInterchainGasPayment, |
||||
}), |
||||
) |
||||
.to.emit(interchainGasPaymaster, 'GasPayment') |
||||
.withArgs(leafIndex, testInterchainGasPayment); |
||||
}); |
||||
|
||||
it('creates a checkpoint for remote calls', async () => { |
||||
const value = 13; |
||||
const call = formatCall(testSet, 'set', [value]); |
||||
await expect(await router.callRemote(domains[1], [call])).to.emit( |
||||
outbox, |
||||
'Checkpoint', |
||||
); |
||||
}); |
||||
|
||||
it('controller can set remote controller', async () => { |
||||
const newController = controller.address; |
||||
expect(await remote.controller()).to.not.equal(newController); |
||||
await router.setControllerRemote(remoteDomain, newController); |
||||
await abacus.processMessages(); |
||||
expect(await remote.controller()).to.equal(newController); |
||||
}); |
||||
|
||||
it('allows interchain gas payment when setting a remote controller', async () => { |
||||
const newController = controller.address; |
||||
const leafIndex = await outbox.count(); |
||||
await expect( |
||||
router.setControllerRemote(remoteDomain, newController, { |
||||
value: testInterchainGasPayment, |
||||
}), |
||||
) |
||||
.to.emit(interchainGasPaymaster, 'GasPayment') |
||||
.withArgs(leafIndex, testInterchainGasPayment); |
||||
}); |
||||
|
||||
it('creates a checkpoint when setting a remote controller', async () => { |
||||
const newController = controller.address; |
||||
await expect( |
||||
router.setControllerRemote(remoteDomain, newController), |
||||
).to.emit(outbox, 'Checkpoint'); |
||||
}); |
||||
|
||||
it('controller can set remote abacusConnectionManager', async () => { |
||||
const newConnectionManager = ethers.constants.AddressZero; |
||||
expect(await remote.abacusConnectionManager()).to.not.equal( |
||||
newConnectionManager, |
||||
); |
||||
await router.setAbacusConnectionManagerRemote( |
||||
remoteDomain, |
||||
newConnectionManager, |
||||
); |
||||
await abacus.processMessages(); |
||||
expect(await remote.abacusConnectionManager()).to.equal( |
||||
newConnectionManager, |
||||
); |
||||
}); |
||||
|
||||
it('allows interchain gas payment when setting a remote abacusConnectionManager', async () => { |
||||
const leafIndex = await outbox.count(); |
||||
await expect( |
||||
router.setAbacusConnectionManagerRemote( |
||||
remoteDomain, |
||||
ethers.constants.AddressZero, |
||||
{ value: testInterchainGasPayment }, |
||||
), |
||||
) |
||||
.to.emit(interchainGasPaymaster, 'GasPayment') |
||||
.withArgs(leafIndex, testInterchainGasPayment); |
||||
}); |
||||
|
||||
it('creates a checkpoint when setting a remote abacusConnectionManager', async () => { |
||||
await expect( |
||||
router.setAbacusConnectionManagerRemote( |
||||
remoteDomain, |
||||
ethers.constants.AddressZero, |
||||
), |
||||
).to.emit(outbox, 'Checkpoint'); |
||||
}); |
||||
|
||||
it('controller 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('allows interchain gas payment when enrolling a remote router', async () => { |
||||
const leafIndex = await outbox.count(); |
||||
const newRouter = utils.addressToBytes32(router.address); |
||||
await expect( |
||||
router.enrollRemoteRouterRemote(remoteDomain, testDomain, newRouter, { |
||||
value: testInterchainGasPayment, |
||||
}), |
||||
) |
||||
.to.emit(interchainGasPaymaster, 'GasPayment') |
||||
.withArgs(leafIndex, testInterchainGasPayment); |
||||
}); |
||||
|
||||
it('creates a checkpoint when enrolling a remote router', async () => { |
||||
const newRouter = utils.addressToBytes32(router.address); |
||||
await expect( |
||||
router.enrollRemoteRouterRemote(remoteDomain, testDomain, newRouter), |
||||
).to.emit(outbox, 'Checkpoint'); |
||||
}); |
||||
|
||||
it('controller cannot initiate recovery', async () => { |
||||
await expect(router.initiateRecoveryTimelock()).to.be.revertedWith( |
||||
'!recoveryManager', |
||||
); |
||||
}); |
||||
|
||||
it('controller 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 = formatCall(testSet, 'set', [value]); |
||||
await expect( |
||||
router.connect(recoveryManager).call([call]), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('recovery manager cannot set local controller', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setController(ethers.constants.AddressZero), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('recovery manager cannot set local abacusConnectionManager', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setAbacusConnectionManager(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 = formatCall(testSet, 'set', [value]); |
||||
await expect( |
||||
router.connect(recoveryManager).callRemote(domains[1], [call]), |
||||
).to.be.revertedWith('!controller'); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote controller', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setControllerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!controller'); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote abacusConnectionManager', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.setAbacusConnectionManagerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!controller'); |
||||
}); |
||||
|
||||
it('recovery manager cannot enroll remote remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(recoveryManager) |
||||
.enrollRemoteRouterRemote( |
||||
testDomain, |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith('!controller'); |
||||
}); |
||||
|
||||
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 = formatCall(testSet, 'set', [value]); |
||||
await router.call([call]); |
||||
expect(await testSet.get()).to.equal(value); |
||||
}); |
||||
|
||||
it('recovery manager can set local controller', async () => { |
||||
expect(await router.controller()).to.equal(controller.address); |
||||
await router.setController(ethers.constants.AddressZero); |
||||
expect(await router.controller()).to.equal(ethers.constants.AddressZero); |
||||
}); |
||||
|
||||
it('recovery manager can set local abacusConnectionManager', async () => { |
||||
expect(await router.abacusConnectionManager()).to.equal( |
||||
abacus.abacusConnectionManager(localDomain).address, |
||||
); |
||||
await router.setAbacusConnectionManager(ethers.constants.AddressZero); |
||||
expect(await router.abacusConnectionManager()).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 = formatCall(testSet, 'set', [value]); |
||||
await expect(router.callRemote(domains[1], [call])).to.be.revertedWith( |
||||
'!controller', |
||||
); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote controller', async () => { |
||||
await expect( |
||||
router.setControllerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!controller'); |
||||
}); |
||||
|
||||
it('recovery manager cannot set remote abacusConnectionManager', async () => { |
||||
await expect( |
||||
router.setAbacusConnectionManagerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('!controller'); |
||||
}); |
||||
|
||||
it('recovery manager cannot enroll remote remote router', async () => { |
||||
await expect( |
||||
router.enrollRemoteRouterRemote( |
||||
remoteDomain, |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith('!controller'); |
||||
}); |
||||
|
||||
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('controller cannot make local calls', async () => { |
||||
const value = 12; |
||||
const call = formatCall(testSet, 'set', [value]); |
||||
await expect(router.connect(controller).call([call])).to.be.revertedWith( |
||||
ONLY_OWNER_REVERT_MESSAGE, |
||||
); |
||||
}); |
||||
|
||||
it('controller cannot set local controller', async () => { |
||||
await expect( |
||||
router.connect(controller).setController(ethers.constants.AddressZero), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('controller cannot set local recovery manager', async () => { |
||||
await expect( |
||||
router.connect(controller).transferOwnership(router.address), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('controller cannot set local abacusConnectionManager', async () => { |
||||
await expect( |
||||
router.connect(controller).setAbacusConnectionManager(router.address), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('controller cannot enroll local remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(controller) |
||||
.enrollRemoteRouter( |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MESSAGE); |
||||
}); |
||||
|
||||
it('controller cannot make remote calls', async () => { |
||||
const value = 13; |
||||
const call = formatCall(testSet, 'set', [value]); |
||||
await expect( |
||||
router.connect(controller).callRemote(domains[1], [call]), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('controller cannot set remote controller', async () => { |
||||
await expect( |
||||
router |
||||
.connect(controller) |
||||
.setControllerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('controller cannot set remote abacusConnectionManager', async () => { |
||||
await expect( |
||||
router |
||||
.connect(controller) |
||||
.setAbacusConnectionManagerRemote(remoteDomain, router.address), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('controller cannot enroll remote remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(controller) |
||||
.enrollRemoteRouterRemote( |
||||
remoteDomain, |
||||
testDomain, |
||||
utils.addressToBytes32(router.address), |
||||
), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('controller cannot initiate recovery', async () => { |
||||
await expect( |
||||
router.connect(controller).initiateRecoveryTimelock(), |
||||
).to.be.revertedWith('recovery'); |
||||
}); |
||||
|
||||
it('controller cannot exit recovery', async () => { |
||||
await expect( |
||||
router.connect(controller).exitRecovery(), |
||||
).to.be.revertedWith('!recoveryManager'); |
||||
}); |
||||
}); |
||||
}); |
@ -1,49 +0,0 @@ |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { TestAbacusDeploy, TestRouterDeploy } from '@abacus-network/hardhat'; |
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
import { ControllerRouter, ControllerRouter__factory } from '../../../types'; |
||||
|
||||
export type ControllingEntity = { |
||||
domain: types.Domain; |
||||
address: types.Address; |
||||
}; |
||||
|
||||
export type ControllerConfig = { |
||||
signer: ethers.Signer; |
||||
timelock: number; |
||||
controller: ControllingEntity; |
||||
recoveryManager: types.Address; |
||||
}; |
||||
|
||||
export class ControllerDeploy extends TestRouterDeploy< |
||||
ControllerRouter, |
||||
ControllerConfig |
||||
> { |
||||
async deploy(abacus: TestAbacusDeploy) { |
||||
await super.deploy(abacus); |
||||
for (const domain of this.domains) { |
||||
if (domain == this.config.controller.domain) { |
||||
await this.router(domain).setController(this.config.controller.address); |
||||
} else { |
||||
await this.router(domain).setController(ethers.constants.AddressZero); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async deployInstance( |
||||
domain: types.Domain, |
||||
abacus: TestAbacusDeploy, |
||||
): Promise<ControllerRouter> { |
||||
const routerFactory = new ControllerRouter__factory(this.config.signer); |
||||
const router = await routerFactory.deploy(this.config.timelock); |
||||
await router.initialize(abacus.abacusConnectionManager(domain).address); |
||||
await router.transferOwnership(this.config.recoveryManager); |
||||
return router; |
||||
} |
||||
|
||||
router(domain: types.Domain): ControllerRouter { |
||||
return this.instances[domain]; |
||||
} |
||||
} |
@ -1,98 +0,0 @@ |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { types, utils } from '@abacus-network/utils'; |
||||
|
||||
export enum ControllerMessage { |
||||
CALL = 1, |
||||
SETCONTROLLER = 2, |
||||
ENROLLREMOTEROUTER = 3, |
||||
SETXAPPCONNECTIONMANAGER = 5, |
||||
} |
||||
|
||||
export function formatSetController(address: types.Address): string { |
||||
return ethers.utils.solidityPack( |
||||
['bytes1', 'bytes32'], |
||||
[ControllerMessage.SETCONTROLLER, utils.addressToBytes32(address)], |
||||
); |
||||
} |
||||
|
||||
export function formatSetAbacusConnectionManager( |
||||
address: types.Address, |
||||
): string { |
||||
return ethers.utils.solidityPack( |
||||
['bytes1', 'bytes32'], |
||||
[ |
||||
ControllerMessage.SETXAPPCONNECTIONMANAGER, |
||||
utils.addressToBytes32(address), |
||||
], |
||||
); |
||||
} |
||||
|
||||
export function formatEnrollRemoteRouter( |
||||
domain: types.Domain, |
||||
address: types.Address, |
||||
): string { |
||||
return ethers.utils.solidityPack( |
||||
['bytes1', 'uint32', 'bytes32'], |
||||
[ |
||||
ControllerMessage.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'], |
||||
[ControllerMessage.CALL, numCalls, callBody], |
||||
); |
||||
} |
||||
|
||||
export function formatCall< |
||||
C extends ethers.Contract, |
||||
I extends Parameters<C['interface']['encodeFunctionData']>, |
||||
>( |
||||
destinationContract: C, |
||||
functionName: I[0], |
||||
functionArgs: I[1], |
||||
): types.CallData { |
||||
// Set up data for call message
|
||||
const callData = utils.formatCallData( |
||||
destinationContract, |
||||
functionName as any, |
||||
functionArgs as any, |
||||
); |
||||
return { |
||||
to: utils.addressToBytes32(destinationContract.address), |
||||
data: callData, |
||||
}; |
||||
} |
||||
|
||||
export const increaseTimestampBy = async ( |
||||
provider: ethers.providers.JsonRpcProvider, |
||||
increaseTime: number, |
||||
) => { |
||||
await provider.send('evm_increaseTime', [increaseTime]); |
||||
await provider.send('evm_mine', []); |
||||
}; |
@ -1,16 +0,0 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"outDir": "./dist/", |
||||
"rootDir": "./types/" |
||||
}, |
||||
"exclude": [ |
||||
"./node_modules/", |
||||
"./dist/", |
||||
"./types/hardhat.d.ts" |
||||
], |
||||
"extends": "../../tsconfig.package.json", |
||||
"include": [ |
||||
"./types/*.ts", |
||||
"./types/factories/*.ts" |
||||
] |
||||
} |
@ -1,20 +0,0 @@ |
||||
import { ChainMap } from '@abacus-network/sdk'; |
||||
|
||||
import { ControllerConfig } from '../../../src/controller'; |
||||
|
||||
import { DevChains } from './chains'; |
||||
|
||||
const defaultControllerConfig = { |
||||
recoveryManager: '0x3909CFACD7a568634716CbCE635F76b9Cf37364B', |
||||
recoveryTimelock: 180, |
||||
}; |
||||
|
||||
const addresses = { |
||||
alfajores: { |
||||
...defaultControllerConfig, |
||||
controller: '0x3909CFACD7a568634716CbCE635F76b9Cf37364B', |
||||
}, |
||||
kovan: defaultControllerConfig, |
||||
}; |
||||
|
||||
export const controller: ChainMap<DevChains, ControllerConfig> = addresses; |
@ -1,21 +0,0 @@ |
||||
import { ChainMap } from '@abacus-network/sdk'; |
||||
|
||||
import { ControllerConfig } from '../../../src/controller'; |
||||
|
||||
import { TestChains } from './chains'; |
||||
|
||||
const defaultControllerConfig: ControllerConfig = { |
||||
recoveryManager: '0x4FbBB2b0820CF0cF027BbB58DC7F7f760BC0c57e', |
||||
recoveryTimelock: 180, |
||||
}; |
||||
|
||||
const addresses = { |
||||
test1: { |
||||
...defaultControllerConfig, |
||||
controller: '0x4FbBB2b0820CF0cF027BbB58DC7F7f760BC0c57e', |
||||
}, |
||||
test2: defaultControllerConfig, |
||||
test3: defaultControllerConfig, |
||||
}; |
||||
|
||||
export const controller: ChainMap<TestChains, ControllerConfig> = addresses; |
@ -1,24 +0,0 @@ |
||||
import { ChainMap } from '@abacus-network/sdk'; |
||||
|
||||
import { ControllerConfig } from '../../../src/controller'; |
||||
|
||||
const defaultControllerConfig: ControllerConfig = { |
||||
recoveryManager: '0xfaD1C94469700833717Fa8a3017278BC1cA8031C', |
||||
recoveryTimelock: 180, |
||||
}; |
||||
|
||||
const addresses = { |
||||
alfajores: { |
||||
...defaultControllerConfig, |
||||
controller: '0xfaD1C94469700833717Fa8a3017278BC1cA8031C', |
||||
}, |
||||
kovan: defaultControllerConfig, |
||||
fuji: defaultControllerConfig, |
||||
mumbai: defaultControllerConfig, |
||||
bsctestnet: defaultControllerConfig, |
||||
arbitrumrinkeby: defaultControllerConfig, |
||||
optimismkovan: defaultControllerConfig, |
||||
}; |
||||
|
||||
export const controller: ChainMap<keyof typeof addresses, ControllerConfig> = |
||||
addresses; |
@ -1,35 +0,0 @@ |
||||
import { AbacusCoreDeployer } from '@abacus-network/deploy'; |
||||
import { AbacusCore, objMap } from '@abacus-network/sdk'; |
||||
|
||||
import { ControllerDeployer } from '../src/controller'; |
||||
|
||||
import { |
||||
getControllerContractsSdkFilepath, |
||||
getControllerVerificationDirectory, |
||||
getCoreEnvironmentConfig, |
||||
getEnvironment, |
||||
} from './utils'; |
||||
|
||||
async function main() { |
||||
const environment = await getEnvironment(); |
||||
const config = getCoreEnvironmentConfig(environment); |
||||
const multiProvider = await config.getMultiProvider(); |
||||
const core = AbacusCore.fromEnvironment(environment, multiProvider); |
||||
|
||||
const deployer = new ControllerDeployer( |
||||
multiProvider, |
||||
config.controller, |
||||
core, |
||||
); |
||||
const addresses = await deployer.deploy(); |
||||
deployer.writeContracts( |
||||
addresses, |
||||
getControllerContractsSdkFilepath(environment), |
||||
); |
||||
deployer.writeVerification(getControllerVerificationDirectory(environment)); |
||||
|
||||
const owners = objMap(addresses, (_, r) => r.router.proxy); |
||||
await AbacusCoreDeployer.transferOwnership(core, owners, multiProvider); |
||||
} |
||||
|
||||
main().then(console.log).catch(console.error); |
@ -1,42 +0,0 @@ |
||||
import { AbacusCore, ControllerApp } from '@abacus-network/sdk'; |
||||
|
||||
import { AbacusCoreControllerChecker, CoreViolationType } from '../src/core'; |
||||
|
||||
import { getCoreEnvironmentConfig, getEnvironment } from './utils'; |
||||
|
||||
async function main() { |
||||
const environment = await getEnvironment(); |
||||
const config = await getCoreEnvironmentConfig(environment); |
||||
const multiProvider = await config.getMultiProvider(); |
||||
const core = AbacusCore.fromEnvironment(environment, multiProvider); |
||||
if (environment !== 'test') { |
||||
throw new Error(`No governanace addresses for ${environment} in SDK`); |
||||
} |
||||
const controllerApp = ControllerApp.fromEnvironment( |
||||
environment, |
||||
multiProvider, |
||||
); |
||||
|
||||
const checker = new AbacusCoreControllerChecker( |
||||
multiProvider, |
||||
core, |
||||
controllerApp, |
||||
config.core, |
||||
); |
||||
await checker.check(); |
||||
// Sanity check: for each chain, expect one validator violation.
|
||||
checker.expectViolations( |
||||
[CoreViolationType.Validator], |
||||
[core.chains().length], |
||||
); |
||||
// Sanity check: for each chain, expect one call to set the validator.
|
||||
checker.expectCalls(core.chains(), new Array(core.chains().length).fill(1)); |
||||
|
||||
// Change to `batch.execute` in order to run.
|
||||
const controllerActor = await controllerApp.controller(); |
||||
const provider = multiProvider.getChainConnection(controllerActor.chain) |
||||
.provider!; |
||||
const receipts = await checker.controllerApp.estimateGas(provider); |
||||
console.log(receipts); |
||||
} |
||||
main().then(console.log).catch(console.error); |
@ -1,71 +0,0 @@ |
||||
import { expect } from 'chai'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { AbacusRouterChecker } from '@abacus-network/deploy'; |
||||
import { |
||||
ChainMap, |
||||
ChainName, |
||||
ControllerApp, |
||||
MultiProvider, |
||||
objMap, |
||||
} from '@abacus-network/sdk'; |
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
import { ControllerConfig } from './types'; |
||||
|
||||
export class ControllerChecker< |
||||
Chain extends ChainName, |
||||
> extends AbacusRouterChecker< |
||||
Chain, |
||||
ControllerApp<Chain>, |
||||
ControllerConfig & { |
||||
owner: types.Address; |
||||
} |
||||
> { |
||||
constructor( |
||||
multiProvider: MultiProvider<any>, |
||||
app: ControllerApp<Chain>, |
||||
configMap: ChainMap<Chain, ControllerConfig>, |
||||
) { |
||||
const joinedConfig = objMap(configMap, (_, config) => ({ |
||||
...config, |
||||
owner: config.controller ?? ethers.constants.AddressZero, |
||||
})); |
||||
super(multiProvider, app, joinedConfig); |
||||
} |
||||
|
||||
// ControllerRouter's owner is 0x0 on all chains except the controlling chain as setup in the constructor
|
||||
async checkOwnership(chain: Chain): Promise<void> { |
||||
const contracts = this.app.getContracts(chain); |
||||
|
||||
// check router's owner with the config
|
||||
const routerOwner = await contracts.router.owner(); |
||||
expect(routerOwner).to.equal(this.configMap[chain].owner); |
||||
|
||||
// check ubc is owned by local router
|
||||
const ubcOwner = await contracts.upgradeBeaconController.owner(); |
||||
expect(ubcOwner).to.equal(contracts.router.address); |
||||
} |
||||
|
||||
async checkChain(chain: Chain): Promise<void> { |
||||
await super.checkChain(chain); |
||||
await this.checkProxiedContracts(chain); |
||||
await this.checkRecoveryManager(chain); |
||||
} |
||||
|
||||
async checkProxiedContracts(chain: Chain): Promise<void> { |
||||
const addresses = this.app.getAddresses(chain); |
||||
// Outbox upgrade setup contracts are defined
|
||||
await this.checkUpgradeBeacon(chain, 'ControllerRouter', addresses.router); |
||||
} |
||||
|
||||
async checkRecoveryManager(chain: Chain): Promise<void> { |
||||
const actual = await this.mustGetRouter(chain).recoveryManager(); |
||||
const config = this.configMap[chain]; |
||||
expect(actual).to.equal(config.recoveryManager); |
||||
} |
||||
|
||||
mustGetRouter(chain: Chain) { |
||||
return this.app.getContracts(chain).router; |
||||
} |
||||
} |
@ -1,87 +0,0 @@ |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { ControllerRouter__factory } from '@abacus-network/apps'; |
||||
import { UpgradeBeaconController__factory } from '@abacus-network/core'; |
||||
import { AbacusRouterDeployer } from '@abacus-network/deploy'; |
||||
import { |
||||
ChainName, |
||||
ControllerAddresses, |
||||
objMap, |
||||
promiseObjAll, |
||||
} from '@abacus-network/sdk'; |
||||
|
||||
import { ControllerConfig } from './types'; |
||||
|
||||
export class ControllerDeployer< |
||||
Chain extends ChainName, |
||||
> extends AbacusRouterDeployer<Chain, ControllerConfig, ControllerAddresses> { |
||||
async deployContracts( |
||||
chain: Chain, |
||||
config: ControllerConfig, |
||||
): Promise<ControllerAddresses> { |
||||
const dc = this.multiProvider.getChainConnection(chain); |
||||
const signer = dc.signer!; |
||||
|
||||
const abacusConnectionManager = |
||||
await this.deployConnectionManagerIfNotConfigured(chain); |
||||
|
||||
const upgradeBeaconController = await this.deployContract( |
||||
chain, |
||||
'UpgradeBeaconController', |
||||
new UpgradeBeaconController__factory(signer), |
||||
[], |
||||
); |
||||
|
||||
const router = await this.deployProxiedContract( |
||||
chain, |
||||
'ControllerRouter', |
||||
new ControllerRouter__factory(signer), |
||||
[config.recoveryTimelock], |
||||
upgradeBeaconController.address, |
||||
[abacusConnectionManager.address], |
||||
); |
||||
|
||||
// Only transfer ownership if a new ACM was deployed.
|
||||
if (abacusConnectionManager.deployTransaction) { |
||||
await abacusConnectionManager.transferOwnership( |
||||
router.address, |
||||
dc.overrides, |
||||
); |
||||
} |
||||
await upgradeBeaconController.transferOwnership( |
||||
router.address, |
||||
dc.overrides, |
||||
); |
||||
|
||||
return { |
||||
router: router.addresses, |
||||
upgradeBeaconController: upgradeBeaconController.address, |
||||
abacusConnectionManager: abacusConnectionManager.address, |
||||
}; |
||||
} |
||||
|
||||
async deploy() { |
||||
const deploymentOutput = await super.deploy(); |
||||
|
||||
// Transfer ownership of routers to governor and recovery manager.
|
||||
await promiseObjAll( |
||||
objMap(deploymentOutput, async (local, addresses) => { |
||||
const router = this.mustGetRouter(local, addresses); |
||||
const config = this.configMap[local]; |
||||
await router.transferOwnership(config.recoveryManager); |
||||
await router.setController( |
||||
config.controller ?? ethers.constants.AddressZero, |
||||
); |
||||
}), |
||||
); |
||||
|
||||
return deploymentOutput; |
||||
} |
||||
|
||||
mustGetRouter(chain: Chain, addresses: ControllerAddresses) { |
||||
return ControllerRouter__factory.connect( |
||||
addresses.router.proxy, |
||||
this.multiProvider.getChainConnection(chain).signer!, |
||||
); |
||||
} |
||||
} |
@ -1,3 +0,0 @@ |
||||
export { ControllerDeployer } from './deploy'; |
||||
export { ControllerChecker } from './check'; |
||||
export { ControllerConfigAddresses, ControllerConfig } from './types'; |
@ -1,10 +0,0 @@ |
||||
import { RouterConfig } from '@abacus-network/deploy'; |
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
export type ControllerConfigAddresses = { |
||||
recoveryManager: types.Address; |
||||
controller?: types.Address; |
||||
}; |
||||
|
||||
export type ControllerConfig = RouterConfig & |
||||
ControllerConfigAddresses & { recoveryTimelock: number }; |
@ -1,138 +0,0 @@ |
||||
import { expect } from 'chai'; |
||||
import { PopulatedTransaction } from 'ethers'; |
||||
|
||||
import { MultisigValidatorManager__factory } from '@abacus-network/core'; |
||||
import { |
||||
CheckerViolation, |
||||
CoreConfig, |
||||
ProxyViolationType, |
||||
UpgradeBeaconViolation, |
||||
} from '@abacus-network/deploy'; |
||||
import { |
||||
AbacusCore, |
||||
Call, |
||||
ChainMap, |
||||
ChainName, |
||||
ControllerApp, |
||||
MultiProvider, |
||||
objMap, |
||||
} from '@abacus-network/sdk'; |
||||
|
||||
import { |
||||
AbacusCoreChecker, |
||||
CoreViolationType, |
||||
ValidatorViolation, |
||||
ValidatorViolationType, |
||||
} from './check'; |
||||
|
||||
interface CallWithTarget { |
||||
chain: ChainName; |
||||
call: Call; |
||||
} |
||||
|
||||
export class AbacusCoreControllerChecker< |
||||
Chain extends ChainName, |
||||
> extends AbacusCoreChecker<Chain> { |
||||
readonly controllerApp: ControllerApp<Chain>; |
||||
|
||||
constructor( |
||||
multiProvider: MultiProvider<Chain>, |
||||
app: AbacusCore<Chain>, |
||||
controllerApp: ControllerApp<Chain>, |
||||
config: ChainMap<Chain, CoreConfig>, |
||||
) { |
||||
const owners = controllerApp.routerAddresses(); |
||||
const joinedConfigMap = objMap(config, (chain, coreConfig) => { |
||||
return { |
||||
...coreConfig, |
||||
owner: owners[chain], |
||||
}; |
||||
}); |
||||
super(multiProvider, app, joinedConfigMap); |
||||
this.controllerApp = controllerApp; |
||||
} |
||||
|
||||
async check(): Promise<void[]> { |
||||
await super.check(); |
||||
const txs = await Promise.all( |
||||
this.violations.map((v) => this.handleViolation(v)), |
||||
); |
||||
txs.map((call) => |
||||
this.controllerApp.pushCall(call.chain as Chain, call.call), |
||||
); |
||||
return []; |
||||
} |
||||
|
||||
handleViolation(v: CheckerViolation): Promise<CallWithTarget> { |
||||
switch (v.type) { |
||||
case ProxyViolationType.UpgradeBeacon: |
||||
return this.handleUpgradeBeaconViolation(v as UpgradeBeaconViolation); |
||||
case CoreViolationType.Validator: |
||||
return this.handleValidatorViolation(v as ValidatorViolation); |
||||
default: |
||||
throw new Error(`No handler for violation type ${v.type}`); |
||||
} |
||||
} |
||||
|
||||
async handleUpgradeBeaconViolation( |
||||
violation: UpgradeBeaconViolation, |
||||
): Promise<CallWithTarget> { |
||||
const chain = violation.chain; |
||||
const ubc = this.app.getContracts(chain as Chain).upgradeBeaconController; |
||||
if (ubc === undefined) throw new Error('Undefined ubc'); |
||||
const tx = await ubc.populateTransaction.upgrade( |
||||
violation.data.proxiedAddress.beacon, |
||||
violation.expected, |
||||
); |
||||
if (tx.to === undefined) throw new Error('undefined tx.to'); |
||||
return { chain, call: tx as Call }; |
||||
} |
||||
|
||||
async handleValidatorViolation( |
||||
violation: ValidatorViolation, |
||||
): Promise<CallWithTarget> { |
||||
const dc = this.multiProvider.getChainConnection(violation.chain as Chain); |
||||
const provider = dc.provider!; |
||||
|
||||
const validatorManager = MultisigValidatorManager__factory.connect( |
||||
violation.data.validatorManagerAddress, |
||||
provider, |
||||
); |
||||
|
||||
let tx: PopulatedTransaction; |
||||
|
||||
switch (violation.data.type) { |
||||
case ValidatorViolationType.EnrollValidator: |
||||
// Enrolling a new validator
|
||||
tx = await validatorManager.populateTransaction.enrollValidator( |
||||
violation.expected, |
||||
); |
||||
break; |
||||
case ValidatorViolationType.UnenrollValidator: |
||||
// Unenrolling an existing validator
|
||||
tx = await validatorManager.populateTransaction.unenrollValidator( |
||||
violation.actual, |
||||
); |
||||
break; |
||||
case ValidatorViolationType.Threshold: |
||||
tx = await validatorManager.populateTransaction.setThreshold( |
||||
violation.expected, |
||||
); |
||||
break; |
||||
default: |
||||
throw new Error( |
||||
`Invalid validator violation type: ${violation.data.type}`, |
||||
); |
||||
} |
||||
|
||||
if (tx.to === undefined) throw new Error('undefined tx.to'); |
||||
return { chain: violation.chain, call: tx as Call }; |
||||
} |
||||
|
||||
expectCalls(chains: Chain[], count: number[]) { |
||||
expect(chains).to.have.lengthOf(count.length); |
||||
chains.forEach((chain, i) => { |
||||
expect(this.controllerApp.getCalls(chain)).to.have.lengthOf(count[i]); |
||||
}); |
||||
} |
||||
} |
@ -1,60 +0,0 @@ |
||||
import '@nomiclabs/hardhat-waffle'; |
||||
import { ethers } from 'hardhat'; |
||||
import path from 'path'; |
||||
|
||||
import { getMultiProviderFromConfigAndSigner } from '@abacus-network/deploy/dist/src/utils'; |
||||
import { |
||||
AbacusCore, |
||||
ChainMap, |
||||
ControllerAddresses, |
||||
ControllerApp, |
||||
MultiProvider, |
||||
} from '@abacus-network/sdk'; |
||||
|
||||
import { environment as config } from '../config/environments/test'; |
||||
import { TestChains } from '../config/environments/test/chains'; |
||||
import { |
||||
ControllerChecker, |
||||
ControllerConfig, |
||||
ControllerDeployer, |
||||
} from '../src/controller'; |
||||
|
||||
describe('controller', async () => { |
||||
let multiProvider: MultiProvider<TestChains>; |
||||
let deployer: ControllerDeployer<TestChains>; |
||||
let addresses: ChainMap<TestChains, ControllerAddresses>; |
||||
let controllerConfig: ChainMap<TestChains, ControllerConfig>; |
||||
|
||||
before(async () => { |
||||
controllerConfig = config.controller; |
||||
const [signer] = await ethers.getSigners(); |
||||
// This is kind of awkward and really these tests shouldn't live here
|
||||
multiProvider = getMultiProviderFromConfigAndSigner( |
||||
config.transactionConfigs, |
||||
signer, |
||||
); |
||||
const core = AbacusCore.fromEnvironment('test', multiProvider); |
||||
console.log(core); |
||||
deployer = new ControllerDeployer(multiProvider, controllerConfig, core); |
||||
}); |
||||
|
||||
it('deploys', async () => { |
||||
addresses = await deployer.deploy(); |
||||
}); |
||||
|
||||
it('writes', async () => { |
||||
const base = './test/outputs/controller'; |
||||
deployer.writeVerification(path.join(base, 'verification')); |
||||
deployer.writeContracts(addresses, path.join(base, 'contracts.ts')); |
||||
}); |
||||
|
||||
it('checks', async () => { |
||||
const controller = new ControllerApp(addresses, multiProvider); |
||||
const checker = new ControllerChecker( |
||||
multiProvider, |
||||
controller, |
||||
controllerConfig, |
||||
); |
||||
await checker.check(); |
||||
}); |
||||
}); |
@ -1,123 +0,0 @@ |
||||
import { ControllerContracts } from '.'; |
||||
import { Call } from '..'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
import { AbacusApp } from '../app'; |
||||
import { MultiProvider } from '../provider'; |
||||
import { ChainMap, ChainName, ChainNameToDomainId } from '../types'; |
||||
import { objMap, promiseObjAll } from '../utils'; |
||||
|
||||
import { ControllerAddresses } from './contracts'; |
||||
import { environments } from './environments'; |
||||
|
||||
type Environments = typeof environments; |
||||
type EnvironmentName = keyof Environments; |
||||
|
||||
export type Controller = { |
||||
domain: number; |
||||
identifier: string; |
||||
}; |
||||
|
||||
export class ControllerApp< |
||||
Chain extends ChainName = ChainName, |
||||
> extends AbacusApp<ControllerContracts, Chain> { |
||||
constructor( |
||||
addresses: ChainMap<Chain, ControllerAddresses>, |
||||
multiProvider: MultiProvider<Chain>, |
||||
) { |
||||
super(ControllerContracts, addresses, multiProvider); |
||||
} |
||||
|
||||
static fromEnvironment( |
||||
name: EnvironmentName, |
||||
multiProvider: MultiProvider<any>, |
||||
) { |
||||
return new ControllerApp(environments[name], multiProvider); |
||||
} |
||||
|
||||
pushCall(chain: Chain, call: Call) { |
||||
this.get(chain).push(call); |
||||
} |
||||
|
||||
getCalls(chain: Chain) { |
||||
return this.get(chain).calls; |
||||
} |
||||
|
||||
chainCalls = () => |
||||
Object.fromEntries( |
||||
this.chains().map((chain) => [chain, this.getCalls(chain)]), |
||||
) as ChainMap<Chain, Call[]>; |
||||
|
||||
routers = () => objMap(this.contractsMap, (_, d) => d.contracts.router); |
||||
|
||||
routerAddresses = () => objMap(this.routers(), (_, r) => r.address); |
||||
|
||||
controller = async (): Promise<{ |
||||
chain: Chain; |
||||
address: types.Address; |
||||
}> => { |
||||
const controllers = await promiseObjAll( |
||||
objMap(this.routers(), (_chain, router) => router.controller()), |
||||
); |
||||
const match = Object.entries(controllers).find( |
||||
([_, controller]) => controller !== ethers.constants.AddressZero, |
||||
) as [Chain, types.Address] | undefined; |
||||
if (match) { |
||||
return { chain: match[0], address: match[1] }; |
||||
} |
||||
throw new Error('No controller found'); |
||||
}; |
||||
|
||||
build = async (): Promise<ethers.PopulatedTransaction[]> => { |
||||
const controller = await this.controller(); |
||||
const controllerRouter = this.routers()[controller.chain]; |
||||
|
||||
const chainTransactions = await promiseObjAll( |
||||
objMap(this.chainCalls(), (chain, calls) => { |
||||
if (chain === controller.chain) { |
||||
return controllerRouter.populateTransaction.call(calls); |
||||
} else { |
||||
return controllerRouter.populateTransaction.callRemote( |
||||
ChainNameToDomainId[chain], |
||||
calls, |
||||
); |
||||
} |
||||
}), |
||||
); |
||||
return Object.values(chainTransactions); |
||||
}; |
||||
|
||||
execute = async (signer: ethers.Signer) => { |
||||
const controller = await this.controller(); |
||||
|
||||
const signerAddress = await signer.getAddress(); |
||||
if (signerAddress !== controller.address) { |
||||
throw new Error( |
||||
`Signer ${signerAddress} is not the controller ${controller.address}`, |
||||
); |
||||
} |
||||
|
||||
const transactions = await this.build(); |
||||
|
||||
return Promise.all( |
||||
transactions.map(async (tx) => { |
||||
const response = await signer.sendTransaction(tx); |
||||
return response.wait(5); |
||||
}), |
||||
); |
||||
}; |
||||
|
||||
estimateGas = async ( |
||||
provider: ethers.providers.Provider, |
||||
): Promise<ethers.BigNumber[]> => { |
||||
const transactions = await this.build(); |
||||
const controller = await this.controller(); |
||||
return Promise.all( |
||||
transactions.map( |
||||
(tx) => provider.estimateGas({ ...tx, from: controller.address }), // Estimate gas as the controller
|
||||
), |
||||
); |
||||
}; |
||||
} |
@ -1,40 +0,0 @@ |
||||
import { Call } from '..'; |
||||
|
||||
import { ControllerRouter__factory } from '@abacus-network/apps'; |
||||
import { UpgradeBeaconController__factory } from '@abacus-network/core'; |
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
import { AbacusContracts, routerFactories } from '../contracts'; |
||||
import { ProxiedAddress } from '../types'; |
||||
|
||||
import { normalizeCall } from './utils'; |
||||
|
||||
export type ControllerAddresses = { |
||||
// Basically copy RouterAddresses
|
||||
abacusConnectionManager: types.Address; |
||||
router: ProxiedAddress; |
||||
upgradeBeaconController: types.Address; |
||||
}; |
||||
|
||||
export const controllerFactories = { |
||||
...routerFactories, |
||||
upgradeBeaconController: UpgradeBeaconController__factory.connect, |
||||
router: ControllerRouter__factory.connect, |
||||
}; |
||||
|
||||
export type ControllerFactories = typeof controllerFactories; |
||||
|
||||
export class ControllerContracts extends AbacusContracts< |
||||
ControllerAddresses, |
||||
ControllerFactories |
||||
> { |
||||
// necessary for factories be defined in the constructor
|
||||
factories() { |
||||
return controllerFactories; |
||||
} |
||||
calls: Call[] = []; |
||||
|
||||
push = (call: Call) => this.calls.push(normalizeCall(call)); |
||||
router = this.contracts.router; |
||||
controller = () => this.router.controller(); |
||||
} |
@ -1,3 +0,0 @@ |
||||
import { addresses as test } from './test'; |
||||
|
||||
export const environments = { test }; |
@ -1,38 +0,0 @@ |
||||
export const addresses = { |
||||
alfajores: { |
||||
router: { |
||||
proxy: '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9', |
||||
implementation: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', |
||||
beacon: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0', |
||||
}, |
||||
upgradeBeaconController: '0x5FbDB2315678afecb367f032d93F642f64180aa3', |
||||
abacusConnectionManager: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', |
||||
}, |
||||
kovan: { |
||||
router: { |
||||
proxy: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853', |
||||
implementation: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707', |
||||
beacon: '0x0165878A594ca255338adfa4d48449f69242Eb8F', |
||||
}, |
||||
upgradeBeaconController: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9', |
||||
abacusConnectionManager: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', |
||||
}, |
||||
mumbai: { |
||||
router: { |
||||
proxy: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e', |
||||
implementation: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318', |
||||
beacon: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788', |
||||
}, |
||||
upgradeBeaconController: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6', |
||||
abacusConnectionManager: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', |
||||
}, |
||||
fuji: { |
||||
router: { |
||||
proxy: '0x0B306BF915C4d645ff596e518fAf3F9669b97016', |
||||
implementation: '0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82', |
||||
beacon: '0x9A676e781A523b5d0C0e43731313A708CB607508', |
||||
}, |
||||
upgradeBeaconController: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0', |
||||
abacusConnectionManager: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', |
||||
}, |
||||
}; |
@ -1,4 +0,0 @@ |
||||
export { ControllerApp } from './app'; |
||||
export { ControllerAddresses, ControllerContracts } from './contracts'; |
||||
export { environments as controllerEnvironments } from './environments'; |
||||
export { Call } from './utils'; |
@ -1,44 +0,0 @@ |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
import { canonizeId } from '../utils'; |
||||
|
||||
export interface Call { |
||||
to: types.Address; |
||||
data: ethers.utils.BytesLike; |
||||
} |
||||
|
||||
// Returns the length (in bytes) of a BytesLike.
|
||||
export function byteLength(bytesLike: ethers.utils.BytesLike): number { |
||||
return ethers.utils.arrayify(bytesLike).length; |
||||
} |
||||
|
||||
/** |
||||
* Serialize a call to its packed ControllerMessage representation |
||||
* @param call The function call to serialize |
||||
* @returns The serialized function call, as a '0x'-prepended hex string |
||||
*/ |
||||
export function serializeCall(call: Call): string { |
||||
const { to, data } = call; |
||||
const dataLen = byteLength(data); |
||||
|
||||
if (!to || !data) { |
||||
throw new Error(`Missing data in Call: \n ${call}`); |
||||
} |
||||
|
||||
return ethers.utils.solidityPack( |
||||
['bytes32', 'uint32', 'bytes'], |
||||
[to, dataLen, data], |
||||
); |
||||
} |
||||
|
||||
export function normalizeCall(partial: Partial<Call>): Readonly<Call> { |
||||
const to = ethers.utils.hexlify(canonizeId(partial.to!)); |
||||
const data = partial.data ?? '0x'; |
||||
|
||||
return Object.freeze({ |
||||
to, |
||||
data, |
||||
}); |
||||
} |
Loading…
Reference in new issue