Skip to content

Commit

Permalink
STABLE-6895 (Part 1): Add initial BaseTokenMessenger implementation a…
Browse files Browse the repository at this point in the history
…nd tests (circlefin#10)

As discussed offline with @walkerq, breaking apart STABLE-6895 into 2
parts:

1) Extract base token messenger behavior and tests, copied from v1, into
a separate type (`BaseTokenMessenger`). This base type handles adding /
removing remote token messengers, the local minter, and encodes the
local message transmitter / version. `TokenMessengerV2` derives from
this type and layers on the v2-specific, messaging-layer differences. I
think this is more flexible for a potential v3 and is a natural
separation of concerns, but definitely open to further discussion!

2) (Future) integrate hooks, the first V2-specific functionality, into
`TokenMessengerV2`.

Some misc callouts:
- Intention is leave the v1 contract code unmodified (optional: in the
future we can refactor `TokenMessenger.t.sol` to use these shared
baseTokenMessenger tests to avoid duplication).
- Added some additional Ownable2Step test cases into `TestUtils.sol`.
- Copied over the v1 unit tests from `TokenMessenger.t.sol`, but
injected more fuzzable inputs into some of the test cases.

Further callouts below in-line.
  • Loading branch information
ams9198 authored and grantmike committed Jan 23, 2025
1 parent 377c9bd commit 3479e6a
Show file tree
Hide file tree
Showing 5 changed files with 712 additions and 9 deletions.
227 changes: 227 additions & 0 deletions src/v2/BaseTokenMessenger.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Copyright 2024 Circle Internet Group, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pragma solidity 0.7.6;

import {Ownable2Step} from "../roles/Ownable2Step.sol";
import {ITokenMinter} from "../interfaces/ITokenMinter.sol";
import {Rescuable} from "../roles/Rescuable.sol";

/**
* @title BaseTokenMessenger
* @notice Base administrative functionality for TokenMessenger implementations,
* including managing remote token messengers and the local token minter.
*/
abstract contract BaseTokenMessenger is Rescuable {
// ============ Events ============
/**
* @notice Emitted when a remote TokenMessenger is added
* @param domain remote domain
* @param tokenMessenger TokenMessenger on remote domain
*/
event RemoteTokenMessengerAdded(uint32 domain, bytes32 tokenMessenger);

/**
* @notice Emitted when a remote TokenMessenger is removed
* @param domain remote domain
* @param tokenMessenger TokenMessenger on remote domain
*/
event RemoteTokenMessengerRemoved(uint32 domain, bytes32 tokenMessenger);

/**
* @notice Emitted when the local minter is added
* @param localMinter address of local minter
* @notice Emitted when the local minter is added
*/
event LocalMinterAdded(address localMinter);

/**
* @notice Emitted when the local minter is removed
* @param localMinter address of local minter
* @notice Emitted when the local minter is removed
*/
event LocalMinterRemoved(address localMinter);

// ============ State Variables ============
// Local Message Transmitter responsible for sending and receiving messages to/from remote domains
address public immutable localMessageTransmitter;

// Version of message body format
uint32 public immutable messageBodyVersion;

// Minter responsible for minting and burning tokens on the local domain
ITokenMinter public localMinter;

// Valid TokenMessengers on remote domains
mapping(uint32 => bytes32) public remoteTokenMessengers;

// ============ Modifiers ============
/**
* @notice Only accept messages from a registered TokenMessenger contract on given remote domain
* @param domain The remote domain
* @param tokenMessenger The address of the TokenMessenger contract for the given remote domain
*/
modifier onlyRemoteTokenMessenger(uint32 domain, bytes32 tokenMessenger) {
require(
_isRemoteTokenMessenger(domain, tokenMessenger),
"Remote TokenMessenger unsupported"
);
_;
}

/**
* @notice Only accept messages from the registered message transmitter on local domain
*/
modifier onlyLocalMessageTransmitter() {
// Caller must be the registered message transmitter for this domain
require(_isLocalMessageTransmitter(), "Invalid message transmitter");
_;
}

// ============ Constructor ============
/**
* @param _messageTransmitter Message transmitter address
* @param _messageBodyVersion Message body version
*/
constructor(address _messageTransmitter, uint32 _messageBodyVersion) {
require(
_messageTransmitter != address(0),
"MessageTransmitter not set"
);
localMessageTransmitter = _messageTransmitter;
messageBodyVersion = _messageBodyVersion;
}

// ============ External Functions ============
/**
* @notice Add the TokenMessenger for a remote domain.
* @dev Reverts if there is already a TokenMessenger set for domain.
* @param domain Domain of remote TokenMessenger.
* @param tokenMessenger Address of remote TokenMessenger as bytes32.
*/
function addRemoteTokenMessenger(
uint32 domain,
bytes32 tokenMessenger
) external onlyOwner {
require(tokenMessenger != bytes32(0), "bytes32(0) not allowed");

require(
remoteTokenMessengers[domain] == bytes32(0),
"TokenMessenger already set"
);

remoteTokenMessengers[domain] = tokenMessenger;
emit RemoteTokenMessengerAdded(domain, tokenMessenger);
}

/**
* @notice Remove the TokenMessenger for a remote domain.
* @dev Reverts if there is no TokenMessenger set for `domain`.
* @param domain Domain of remote TokenMessenger
*/
function removeRemoteTokenMessenger(uint32 domain) external onlyOwner {
// No TokenMessenger set for given remote domain.
require(
remoteTokenMessengers[domain] != bytes32(0),
"No TokenMessenger set"
);

bytes32 _removedTokenMessenger = remoteTokenMessengers[domain];
delete remoteTokenMessengers[domain];
emit RemoteTokenMessengerRemoved(domain, _removedTokenMessenger);
}

/**
* @notice Add minter for the local domain.
* @dev Reverts if a minter is already set for the local domain.
* @param newLocalMinter The address of the minter on the local domain.
*/
function addLocalMinter(address newLocalMinter) external onlyOwner {
require(newLocalMinter != address(0), "Zero address not allowed");

require(
address(localMinter) == address(0),
"Local minter is already set."
);

localMinter = ITokenMinter(newLocalMinter);

emit LocalMinterAdded(newLocalMinter);
}

/**
* @notice Remove the minter for the local domain.
* @dev Reverts if the minter of the local domain is not set.
*/
function removeLocalMinter() external onlyOwner {
address _localMinterAddress = address(localMinter);
require(_localMinterAddress != address(0), "No local minter is set.");

delete localMinter;
emit LocalMinterRemoved(_localMinterAddress);
}

// ============ Internal Utils ============
/**
* @notice return the remote TokenMessenger for the given `_domain` if one exists, else revert.
* @param _domain The domain for which to get the remote TokenMessenger
* @return _tokenMessenger The address of the TokenMessenger on `_domain` as bytes32
*/
function _getRemoteTokenMessenger(
uint32 _domain
) internal view returns (bytes32) {
bytes32 _tokenMessenger = remoteTokenMessengers[_domain];
require(_tokenMessenger != bytes32(0), "No TokenMessenger for domain");
return _tokenMessenger;
}

/**
* @notice return the local minter address if it is set, else revert.
* @return local minter as ITokenMinter.
*/
function _getLocalMinter() internal view returns (ITokenMinter) {
require(address(localMinter) != address(0), "Local minter is not set");
return localMinter;
}

/**
* @notice Return true if the given remote domain and TokenMessenger is registered
* on this TokenMessenger.
* @param _domain The remote domain of the message.
* @param _tokenMessenger The address of the TokenMessenger on remote domain.
* @return true if a remote TokenMessenger is registered for `_domain` and `_tokenMessenger`,
* on this TokenMessenger.
*/
function _isRemoteTokenMessenger(
uint32 _domain,
bytes32 _tokenMessenger
) internal view returns (bool) {
return
_tokenMessenger != bytes32(0) &&
remoteTokenMessengers[_domain] == _tokenMessenger;
}

/**
* @notice Returns true if the message sender is the local registered MessageTransmitter
* @return true if message sender is the registered local message transmitter
*/
function _isLocalMessageTransmitter() internal view returns (bool) {
return
address(localMessageTransmitter) != address(0) &&
msg.sender == address(localMessageTransmitter);
}
}
42 changes: 42 additions & 0 deletions src/v2/TokenMessengerV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2024 Circle Internet Group, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
pragma solidity 0.7.6;

import {BaseTokenMessenger} from "./BaseTokenMessenger.sol";

contract TokenMessengerV2 is BaseTokenMessenger {
// ============ Events ============

// ============ State Variables ============

// ============ Modifiers ============

// ============ Constructor ============
/**
* @param _messageTransmitter Message transmitter address
* @param _messageBodyVersion Message body version
*/
constructor(
address _messageTransmitter,
uint32 _messageBodyVersion
) BaseTokenMessenger(_messageTransmitter, _messageBodyVersion) {}

// ============ External Functions ============

// ============ Internal Utils ============
}
55 changes: 46 additions & 9 deletions test/TestUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ contract TestUtils is Test {
address arbitraryAddress = vm.addr(1903);

// 8 KiB
uint32 maxMessageBodySize = 8 * 2**10;
uint32 maxMessageBodySize = 8 * 2 ** 10;
// zero signature
bytes zeroSignature =
"00000000000000000000000000000000000000000000000000000000000000000";
Expand Down Expand Up @@ -136,6 +136,11 @@ contract TestUtils is Test {
vm.expectRevert("Ownable: caller is not the owner");
}

function expectRevertWithWrongOwner(address wrongOwner) public {
vm.prank(wrongOwner);
vm.expectRevert("Ownable: caller is not the owner");
}

function expectRevertWithWrongTokenController() public {
vm.prank(arbitraryAddress);
vm.expectRevert("Caller is not tokenController");
Expand Down Expand Up @@ -215,6 +220,39 @@ contract TestUtils is Test {
assertEq(_pausableContract.pauser(), _newPauser);
}

function transferOwnershipFailsIfNotOwner(
address _ownableContractAddress,
address _notOwner,
address _newOwner
) public {
Ownable2Step _ownableContract = Ownable2Step(_ownableContractAddress);
address _initialOwner = _ownableContract.owner();
expectRevertWithWrongOwner(_notOwner);
_ownableContract.transferOwnership(_newOwner);

// Sanity check
assertEq(_initialOwner, _ownableContract.owner());
}

function acceptOwnershipFailsIfNotPendingOwner(
address _ownableContractAddress,
address _newOwner,
address _otherAccount
) public {
Ownable2Step _ownableContract = Ownable2Step(_ownableContractAddress);
address _initialOwner = _ownableContract.owner();
_ownableContract.transferOwnership(_newOwner);
assertEq(_ownableContract.pendingOwner(), _newOwner);

vm.prank(_otherAccount);
vm.expectRevert("Ownable2Step: caller is not the new owner");
_ownableContract.acceptOwnership();

// Sanity check
assertEq(_initialOwner, _ownableContract.owner());
assertEq(_newOwner, _ownableContract.pendingOwner());
}

function transferOwnershipAndAcceptOwnership(
address _ownableContractAddress,
address _newOwner
Expand Down Expand Up @@ -284,19 +322,18 @@ contract TestUtils is Test {
assertEq(_ownableContract.owner(), _secondNewOwner);
}

function _signMessageWithAttesterPK(bytes memory _message)
internal
returns (bytes memory)
{
function _signMessageWithAttesterPK(
bytes memory _message
) internal returns (bytes memory) {
uint256[] memory attesterPrivateKeys = new uint256[](1);
attesterPrivateKeys[0] = attesterPK;
return _signMessage(_message, attesterPrivateKeys);
}

function _signMessage(bytes memory _message, uint256[] memory _privKeys)
internal
returns (bytes memory)
{
function _signMessage(
bytes memory _message,
uint256[] memory _privKeys
) internal returns (bytes memory) {
bytes memory _signaturesConcatenated = "";

for (uint256 i = 0; i < _privKeys.length; i++) {
Expand Down
Loading

0 comments on commit 3479e6a

Please sign in to comment.