Remove controller from monorepo (#497)

* Remove apps dep from sdk
* Remove controller from sdk
* Remove controller from apps
* Remove apps package
pull/498/head
Yorke Rhodes 3 years ago committed by GitHub
parent d40b2d776c
commit 87ee40ec3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/node.yml
  2. 1
      solidity/apps/.env.example
  3. 13
      solidity/apps/.eslintrc.json
  4. 8
      solidity/apps/.gitignore
  5. 3
      solidity/apps/.solcover.js
  6. 8
      solidity/apps/.solhint.json
  7. 1
      solidity/apps/.solhintignore
  8. 40
      solidity/apps/README.md
  9. 359
      solidity/apps/contracts/controller/ControllerMessage.sol
  10. 388
      solidity/apps/contracts/controller/ControllerRouter.sol
  11. 14
      solidity/apps/contracts/controller/test/TestSet.sol
  12. 99
      solidity/apps/contracts/ping-pong/PingPongMessage.sol
  13. 159
      solidity/apps/contracts/ping-pong/PingPongRouter.sol
  14. 92
      solidity/apps/contracts/xapp-template/MessageTemplate.sol
  15. 104
      solidity/apps/contracts/xapp-template/RouterTemplate.sol
  16. 28
      solidity/apps/hardhat.config.ts
  17. 24
      solidity/apps/interfaces/bridge/IBridgeToken.sol
  18. 7
      solidity/apps/interfaces/bridge/IWeth.sol
  19. 47
      solidity/apps/package.json
  20. 514
      solidity/apps/test/controller/controller.test.ts
  21. 49
      solidity/apps/test/controller/lib/ControllerDeploy.ts
  22. 98
      solidity/apps/test/controller/lib/utils.ts
  23. 16
      solidity/apps/tsconfig.json
  24. 20
      typescript/infra/config/environments/dev/controller.ts
  25. 2
      typescript/infra/config/environments/dev/index.ts
  26. 21
      typescript/infra/config/environments/test/controller.ts
  27. 2
      typescript/infra/config/environments/test/index.ts
  28. 24
      typescript/infra/config/environments/testnet/controller.ts
  29. 2
      typescript/infra/config/environments/testnet/index.ts
  30. 1
      typescript/infra/package.json
  31. 25
      typescript/infra/scripts/check-deploy.ts
  32. 35
      typescript/infra/scripts/deploy-controller.ts
  33. 7
      typescript/infra/scripts/output-agent-env-vars.ts
  34. 42
      typescript/infra/scripts/set-validator.ts
  35. 8
      typescript/infra/scripts/update-agents-diff.ts
  36. 18
      typescript/infra/scripts/utils.ts
  37. 2
      typescript/infra/src/config/environment.ts
  38. 71
      typescript/infra/src/controller/check.ts
  39. 87
      typescript/infra/src/controller/deploy.ts
  40. 3
      typescript/infra/src/controller/index.ts
  41. 10
      typescript/infra/src/controller/types.ts
  42. 39
      typescript/infra/src/core/check.ts
  43. 138
      typescript/infra/src/core/control.ts
  44. 1
      typescript/infra/src/core/index.ts
  45. 60
      typescript/infra/test/controller.test.ts
  46. 1
      typescript/sdk/package.json
  47. 2
      typescript/sdk/src/contracts.ts
  48. 123
      typescript/sdk/src/controller/app.ts
  49. 40
      typescript/sdk/src/controller/contracts.ts
  50. 3
      typescript/sdk/src/controller/environments/index.ts
  51. 38
      typescript/sdk/src/controller/environments/test.ts
  52. 4
      typescript/sdk/src/controller/index.ts
  53. 44
      typescript/sdk/src/controller/utils.ts
  54. 13
      typescript/sdk/src/index.ts
  55. 53
      yarn.lock

@ -108,5 +108,3 @@ jobs:
run: yarn workspace @abacus-network/core run test
- name: app
run: yarn workspace @abacus-network/app run test
- name: apps
run: yarn workspace @abacus-network/apps run test

@ -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;

@ -3,7 +3,6 @@ import { CoreEnvironmentConfig } from '../../../src/config';
import { agent } from './agent';
import { DevChains, devConfigs } from './chains';
import { controller } from './controller';
import { core } from './core';
import { infrastructure } from './infrastructure';
@ -12,6 +11,5 @@ export const environment: CoreEnvironmentConfig<DevChains> = {
getMultiProvider: () => getMultiProviderFromGCP(devConfigs, 'dev'),
agent,
core,
controller,
infra: infrastructure,
};

@ -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;

@ -6,7 +6,6 @@ import { CoreEnvironmentConfig } from '../../../src/config';
import { agent } from './agent';
import { TestChains, testConfigs } from './chains';
import { controller } from './controller';
import { core } from './core';
import { infra } from './infra';
@ -14,7 +13,6 @@ export const environment: CoreEnvironmentConfig<TestChains> = {
transactionConfigs: testConfigs,
agent,
core,
controller,
infra,
// NOTE: Does not work from hardhat.config.ts
getMultiProvider: async () => {

@ -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;

@ -3,7 +3,6 @@ import { CoreEnvironmentConfig } from '../../../src/config';
import { agent } from './agent';
import { TestnetChains, testnetConfigs } from './chains';
import { controller } from './controller';
import { core } from './core';
import { infrastructure } from './infrastructure';
@ -12,6 +11,5 @@ export const environment: CoreEnvironmentConfig<TestnetChains> = {
getMultiProvider: () => getMultiProviderFromGCP(testnetConfigs, 'testnet'),
agent,
core,
controller,
infra: infrastructure,
};

@ -26,7 +26,6 @@
},
"license": "MIT OR Apache-2.0",
"dependencies": {
"@abacus-network/apps": "^0.1.1",
"@abacus-network/core": "^0.1.1",
"@abacus-network/deploy": "^0.1.1",
"@abacus-network/sdk": "^0.1.1",

@ -1,6 +1,5 @@
import { AbacusCore, ControllerApp } from '@abacus-network/sdk';
import { AbacusCore } from '@abacus-network/sdk';
import { ControllerChecker } from '../src/controller';
import { AbacusCoreChecker } from '../src/core';
import { getCoreEnvironmentConfig, getEnvironment } from './utils';
@ -10,28 +9,12 @@ async function check() {
const config = getCoreEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider();
if (environment !== 'test') {
throw new Error(
`Do not have controller addresses for ${environment} in SDK`,
);
}
const core = AbacusCore.fromEnvironment(environment, multiProvider);
const controller = ControllerApp.fromEnvironment(environment, multiProvider);
const controllerChecker = new ControllerChecker(
const coreChecker = new AbacusCoreChecker<any>(
multiProvider,
controller,
config.controller,
core,
config.core,
);
await controllerChecker.check();
controllerChecker.expectEmpty();
const owners = controller.routerAddresses();
const coreChecker = new AbacusCoreChecker(multiProvider, core, {
...config.core,
owners,
} as any);
await coreChecker.check();
coreChecker.expectEmpty();
}

@ -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);

@ -16,7 +16,12 @@ async function main() {
const environment = await getEnvironment();
const config = getCoreEnvironmentConfig(environment);
const envVars = await getAgentEnvVars(argv.c, argv.r, config.agent, argv.i);
const envVars = await getAgentEnvVars<any>(
argv.c,
argv.r,
config.agent,
argv.i,
);
await writeFile(argv.f, envVars.join('\n'));
}

@ -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);

@ -5,9 +5,13 @@ import { getCoreEnvironmentConfig, getEnvironment } from './utils';
async function deploy() {
const environment = await getEnvironment();
const config = await getCoreEnvironmentConfig(environment);
const config = getCoreEnvironmentConfig(environment);
for (const chain of config.agent.chainNames) {
await runAgentHelmCommand(HelmCommand.UpgradeDiff, config.agent, chain);
await runAgentHelmCommand<any>(
HelmCommand.UpgradeDiff,
config.agent,
chain,
);
}
}

@ -28,7 +28,7 @@ export function assertEnvironment(env: string): DeployEnvironment {
export function getCoreEnvironmentConfig<Env extends DeployEnvironment>(
env: Env,
) {
return environments[env] as any; // TODO: make indexed union compatible
return environments[env];
}
export async function getEnvironment() {
@ -66,12 +66,6 @@ export function getCoreContractsSdkFilepath(environment: DeployEnvironment) {
return getContractsSdkFilepath('core', environment);
}
export function getControllerContractsSdkFilepath(
environment: DeployEnvironment,
) {
return getContractsSdkFilepath('controller', environment);
}
export function getEnvironmentDirectory(environment: DeployEnvironment) {
return path.join('./config/environments/', environment);
}
@ -88,16 +82,6 @@ export function getCoreRustDirectory(environment: DeployEnvironment) {
return path.join(getCoreDirectory(environment), 'rust');
}
export function getControllerDirectory(environment: DeployEnvironment) {
return path.join(getEnvironmentDirectory(environment), 'controller');
}
export function getControllerVerificationDirectory(
environment: DeployEnvironment,
) {
return path.join(getControllerDirectory(environment), 'verification');
}
export function getKeyRoleAndChainArgs() {
return utils
.getArgs()

@ -2,7 +2,6 @@ import { CoreConfig, EnvironmentConfig } from '@abacus-network/deploy';
import { ChainMap, ChainName, MultiProvider } from '@abacus-network/sdk';
import { environments } from '../../config/environments';
import { ControllerConfig } from '../controller';
import { AgentConfig } from './agent';
import { InfrastructureConfig } from './infrastructure';
@ -18,7 +17,6 @@ export type CoreEnvironmentConfig<Chain extends ChainName> = {
transactionConfigs: EnvironmentConfig<Chain>;
agent: AgentConfig<Chain>;
core: ChainMap<Chain, CoreConfig>;
controller: ChainMap<Chain, ControllerConfig>;
infra: InfrastructureConfig;
getMultiProvider: () => Promise<MultiProvider<Chain>>;
};

@ -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 };

@ -14,7 +14,6 @@ import {
objMap,
promiseObjAll,
} from '@abacus-network/sdk';
import { types } from '@abacus-network/utils';
import { setDifference } from '../utils/utils';
@ -43,15 +42,9 @@ export interface ValidatorViolation extends CheckerViolation {
export class AbacusCoreChecker<
Chain extends ChainName,
> extends AbacusAppChecker<
Chain,
AbacusCore<Chain>,
CoreConfig & {
owner: types.Address;
}
> {
> extends AbacusAppChecker<Chain, AbacusCore<Chain>, CoreConfig> {
async checkChain(chain: Chain): Promise<void> {
await this.checkDomainOwnership(chain);
// await this.checkDomainOwnership(chain);
await this.checkProxiedContracts(chain);
await this.checkOutbox(chain);
await this.checkInboxes(chain);
@ -59,20 +52,20 @@ export class AbacusCoreChecker<
await this.checkValidatorManagers(chain);
}
async checkDomainOwnership(chain: Chain): Promise<void> {
const config = this.configMap[chain];
const contracts = this.app.getContracts(chain);
const ownables = [
contracts.abacusConnectionManager,
contracts.upgradeBeaconController,
contracts.outbox.outbox,
contracts.outbox.validatorManager,
...Object.values(contracts.inboxes)
.map((inbox: any) => [inbox.inbox, inbox.validatorManager])
.flat(),
];
return AbacusAppChecker.checkOwnership(config.owner, ownables);
}
// async checkDomainOwnership(chain: Chain): Promise<void> {
// const config = this.configMap[chain];
// const contracts = this.app.getContracts(chain);
// const ownables = [
// contracts.abacusConnectionManager,
// contracts.upgradeBeaconController,
// contracts.outbox.outbox,
// contracts.outbox.validatorManager,
// ...Object.values(contracts.inboxes)
// .map((inbox: any) => [inbox.inbox, inbox.validatorManager])
// .flat(),
// ];
// return AbacusAppChecker.checkOwnership(config.owner, ownables);
// }
async checkOutbox(chain: Chain): Promise<void> {
const contracts = this.app.getContracts(chain);

@ -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,2 +1 @@
export { AbacusCoreChecker, CoreViolationType } from './check';
export { AbacusCoreControllerChecker } from './control';

@ -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();
});
});

@ -27,7 +27,6 @@
"typescript": "^4.4.3"
},
"dependencies": {
"@abacus-network/apps": "^0.1.1",
"@abacus-network/core": "^0.1.1",
"@abacus-network/utils": "^0.1.1",
"@ethersproject/bignumber": "^5.5.0",

@ -1,6 +1,6 @@
import { Contract } from 'ethers';
import { Router__factory } from '@abacus-network/apps';
import { Router__factory } from '@abacus-network/app';
import { AbacusConnectionManager__factory } from '@abacus-network/core';
import { types } from '@abacus-network/utils';

@ -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,
});
}

@ -1,4 +1,6 @@
export { AbacusApp } from './app';
export { chainMetadata } from './chain-metadata';
export { addSignerToConnection, chainConnectionConfigs } from './chains';
export {
AbacusContractAddresses,
AbacusContracts,
@ -25,7 +27,6 @@ export {
resolveId,
resolveNetworks,
} from './core';
export { chainMetadata } from './chain-metadata';
export {
Annotated,
getEvents,
@ -37,13 +38,6 @@ export {
InterchainGasCalculator,
TokenPriceGetter,
} from './gas';
export {
ControllerApp,
Call,
ControllerAddresses,
ControllerContracts,
controllerEnvironments,
} from './controller';
export { ChainConnection, IChainConnection, MultiProvider } from './provider';
export {
AllChains,
@ -59,5 +53,4 @@ export {
RemoteChainMap,
Remotes,
} from './types';
export { utils, objMap, objMapEntries, promiseObjAll } from './utils';
export { chainConnectionConfigs, addSignerToConnection } from './chains';
export { objMap, objMapEntries, promiseObjAll, utils } from './utils';

@ -5,7 +5,7 @@ __metadata:
version: 6
cacheKey: 8
"@abacus-network/app@^0.1.1, @abacus-network/app@workspace:solidity/app":
"@abacus-network/app@workspace:solidity/app":
version: 0.0.0-use.local
resolution: "@abacus-network/app@workspace:solidity/app"
dependencies:
@ -34,39 +34,6 @@ __metadata:
languageName: unknown
linkType: soft
"@abacus-network/apps@^0.1.1, @abacus-network/apps@workspace:solidity/apps":
version: 0.0.0-use.local
resolution: "@abacus-network/apps@workspace:solidity/apps"
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
"@nomiclabs/hardhat-ethers": ^2.0.1
"@nomiclabs/hardhat-waffle": ^2.0.1
"@openzeppelin/contracts": ~3.4.2
"@openzeppelin/contracts-upgradeable": ~3.4.2
"@summa-tx/memview-sol": ^2.0.0
"@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
languageName: unknown
linkType: soft
"@abacus-network/core@^0.1.1, @abacus-network/core@workspace:solidity/core":
version: 0.0.0-use.local
resolution: "@abacus-network/core@workspace:solidity/core"
@ -118,7 +85,7 @@ __metadata:
languageName: unknown
linkType: soft
"@abacus-network/hardhat@^0.1.1, @abacus-network/hardhat@workspace:typescript/hardhat":
"@abacus-network/hardhat@workspace:typescript/hardhat":
version: 0.0.0-use.local
resolution: "@abacus-network/hardhat@workspace:typescript/hardhat"
dependencies:
@ -141,7 +108,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@abacus-network/infra@workspace:typescript/infra"
dependencies:
"@abacus-network/apps": ^0.1.1
"@abacus-network/core": ^0.1.1
"@abacus-network/deploy": ^0.1.1
"@abacus-network/sdk": ^0.1.1
@ -188,7 +154,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@abacus-network/sdk@workspace:typescript/sdk"
dependencies:
"@abacus-network/apps": ^0.1.1
"@abacus-network/core": ^0.1.1
"@abacus-network/utils": ^0.1.1
"@ethersproject/bignumber": ^5.5.0
@ -3075,13 +3040,6 @@ __metadata:
languageName: node
linkType: hard
"@openzeppelin/contracts-upgradeable@npm:~3.4.2":
version: 3.4.2
resolution: "@openzeppelin/contracts-upgradeable@npm:3.4.2"
checksum: dd461cdc8353ee6219e746fa71eced068df46c6df4bfe29207a8ad35064b476c2aa532ec0026c505d9515857ada5838c445b2433e2dadc0878fc678046c4e8b0
languageName: node
linkType: hard
"@openzeppelin/contracts@npm:^4.6.0":
version: 4.6.0
resolution: "@openzeppelin/contracts@npm:4.6.0"
@ -3089,13 +3047,6 @@ __metadata:
languageName: node
linkType: hard
"@openzeppelin/contracts@npm:~3.4.2":
version: 3.4.2
resolution: "@openzeppelin/contracts@npm:3.4.2"
checksum: 0c90f029fe50a49643588e4c8670dae3bbf31795133a6ddce9bdcbc258486332700bb732287baabf7bf807f39182fe8ea2ffa19aa5caf359b1b9c0f083280748
languageName: node
linkType: hard
"@resolver-engine/core@npm:^0.3.3":
version: 0.3.3
resolution: "@resolver-engine/core@npm:0.3.3"

Loading…
Cancel
Save