Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dispatcher components #66

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitmodules
scnale marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ remappings = [
"forge-std/=lib/forge-std/src/",
"wormhole-sdk/=src/",
"IERC20/=src/interfaces/token/",
"@openzeppelin/=lib/openzeppelin-contracts/contracts/"
]

verbosity = 3
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at 329125
26 changes: 26 additions & 0 deletions src/TransferUtils.sol
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file name is awkward. I think it's good not to just pile all the code into a single util file, but having awkwardly named Utils.sol files next to a generic Utils.sol is not the answer. The answer that scales and is clean is to create a utils subdirectory and have the individual files live in there and then expose their totality through a Utils.sol in the root directory.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.19;

import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol";
scnale marked this conversation as resolved.
Show resolved Hide resolved

/**
* Payment to the target failed.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment.

error PaymentFailure(address target);

using SafeERC20 for IERC20;
function transferTokens(address token, address to, uint256 amount) {
if (token == address(0))
_transferEth(to, amount);
else
IERC20(token).safeTransfer(to, amount);
}

function _transferEth(address to, uint256 amount) {
if (amount == 0) return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a deliberate choice to short-circuit zero value native token transfers but allow them for ERC20s? Do we rely anywhere on an ERC20 Transfer event even for zero value transfers?


(bool success, ) = to.call{value: amount}(new bytes(0));
if (!success) revert PaymentFailure(to);
}
247 changes: 247 additions & 0 deletions src/dispatcherComponents/AccessControl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.4;

import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol";
import "./ids.sol";

//rationale for different roles (owner, admin):
// * owner should be a mulit-sig / ultra cold wallet that is only activated in exceptional
// circumstances.
// * admin should also be either a cold wallet or Admin contract. In either case,
// the expectation is that multiple, slightly less trustworthy parties than the owner will
// have access to it, lowering trust assumptions and increasing attack surface. Admins
// perform rare but not exceptional operations.

struct AccessControlState {
address owner; //puts owner address in eip1967 admin slot
address pendingOwner;
address[] admins;
mapping(address => uint256) isAdmin;
}

// we use the designated eip1967 admin storage slot:
// keccak256("eip1967.proxy.admin") - 1
bytes32 constant ACCESS_CONTROL_STORAGE_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

function accessControlState() pure returns (AccessControlState storage state) {
assembly ("memory-safe") { state.slot := ACCESS_CONTROL_STORAGE_SLOT }
}

error NotAuthorized();
error InvalidAccessControlCommand(uint8 command);
error InvalidAccessControlQuery(uint8 query);

event OwnerUpdated(address oldAddress, address newAddress, uint256 timestamp);
event AdminsUpdated(address addr, bool isAdmin, uint256 timestamp);

enum Role {
None,
Owner,
Admin
}

function senderHasAuth() view returns (Role) {
Role role = senderRole();
if (Role.None == role)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unconventional order comparison. Should be role == Role.None to match expectation.

revert NotAuthorized();
return role;
}

function senderRole() view returns (Role) {
AccessControlState storage state = accessControlState();
if (msg.sender == state.owner) //check highest privilege level first
return Role.Owner;
else if (state.isAdmin[msg.sender] != 0)
return Role.Admin;
else
return Role.None;
}

abstract contract AccessControl {
using BytesParsing for bytes;

// ---- construction ----

function _accessControlConstruction(
address owner,
address[] memory admins
) internal {
accessControlState().owner = owner;
for (uint i = 0; i < admins.length; ++i)
_updateAdmins(admins[i], true);
}

// ---- external -----

function transferOwnership(address newOwner) external {
AccessControlState storage state = accessControlState();
if (msg.sender != state.owner)
revert NotAuthorized();

state.pendingOwner = newOwner;
}

function cancelOwnershipTransfer() external {
AccessControlState storage state = accessControlState();
if (state.owner != msg.sender)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unconventional order comparison. Should be msg.sender != state.owner to be consistent and match convention.

revert NotAuthorized();

state.pendingOwner = address(0);
}

function receiveOwnership() external {
_acquireOwnership();
}

// ---- internals ----

/**
* Dispatch an execute function. Execute functions almost always modify contract state.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant comment.

function dispatchExecAccessControl(bytes calldata data, uint256 offset, uint8 command) internal returns (bool, uint256) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not adhere to 100 char max line length

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inconsistent use of uint256 and uint. In the original code uint is used for essentially unbounded uint values, akin to size_t while uint256 puts an emphasis on the importance of the datatype (like in a wire format, or for a value that is defined to be 256 bit).

if (command == ACCESS_CONTROL_ID)
offset = _batchAccessControlCommands(data, offset);
else if (command == ACQUIRE_OWNERSHIP_ID)
_acquireOwnership();
else return (false, offset);

return (true, offset);
}

/**
* Dispatch a query function. Query functions never modify contract state.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant comment.

function dispatchQueryAccessControl(bytes calldata data, uint256 offset, uint8 query) view internal returns (bool, bytes memory, uint256) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inconsistent use of uint256 and uint.

bytes memory result;
if (query == ACCESS_CONTROL_QUERIES_ID)
(result, offset) = _batchAccessControlQueries(data, offset);
else return (false, new bytes(0), offset);

return (true, result, offset);
}

function _batchAccessControlCommands(
bytes calldata commands,
uint offset
) internal returns (uint) {
AccessControlState storage state = accessControlState();
bool isOwner = senderHasAuth() == Role.Owner;

uint remainingCommands;
(remainingCommands, offset) = commands.asUint8CdUnchecked(offset);
for (uint i = 0; i < remainingCommands; ++i) {
uint8 command;
(command, offset) = commands.asUint8CdUnchecked(offset);
if (command == REVOKE_ADMIN_ID) {
address admin;
(admin, offset) = commands.asAddressCdUnchecked(offset);
_updateAdmins(admin, false);
}
else {
if (!isOwner)
revert NotAuthorized();

if (command == ADD_ADMIN_ID) {
address newAdmin;
(newAdmin, offset) = commands.asAddressCdUnchecked(offset);

_updateAdmins(newAdmin, true);
}
else if (command == PROPOSE_OWNERSHIP_TRANSFER_ID) {
address newOwner;
(newOwner, offset) = commands.asAddressCdUnchecked(offset);

state.pendingOwner = newOwner;
}
else if (command == RELINQUISH_OWNERSHIP_ID) {
_updateOwner(address(0));

//ownership relinquishment must be the last command in the batch
commands.checkLengthCd(offset);
}
else
revert InvalidAccessControlCommand(command);
}
}
return offset;
}

function _batchAccessControlQueries(
bytes calldata queries,
uint offset
) internal view returns (bytes memory, uint) {
AccessControlState storage state = accessControlState();
bytes memory ret;

uint remainingQueries;
(remainingQueries, offset) = queries.asUint8CdUnchecked(offset);
for (uint i = 0; i < remainingQueries; ++i) {
uint8 query;
(query, offset) = queries.asUint8CdUnchecked(offset);

if (query == IS_ADMIN_ID) {
address admin;
(admin, offset) = queries.asAddressCdUnchecked(offset);
ret = abi.encodePacked(ret, state.isAdmin[admin] != 0);
}
else if (query == ADMINS_ID) {
ret = abi.encodePacked(ret, uint8(state.admins.length));
for (uint j = 0; j < state.admins.length; ++j)
ret = abi.encodePacked(ret, state.admins[j]);
}
else {
address addr;
if (query == OWNER_ID)
addr = state.owner;
else if (query == PENDING_OWNER_ID)
addr = state.pendingOwner;
else
revert InvalidAccessControlQuery(query);

ret = abi.encodePacked(ret, addr);
}
}

return (ret, offset);
}

function _acquireOwnership() internal {
AccessControlState storage state = accessControlState();
if (state.pendingOwner != msg.sender)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent order comparison.

revert NotAuthorized();

state.pendingOwner = address(0);
_updateOwner(msg.sender);
}

// ---- private ----

function _updateOwner(address newOwner) private {
address oldAddress;
accessControlState().owner = newOwner;
emit OwnerUpdated(oldAddress, newOwner, block.timestamp);
}

function _updateAdmins(address admin, bool authorization) private { unchecked {
AccessControlState storage state = accessControlState();
if ((state.isAdmin[admin] != 0) == authorization)
return;

if (authorization) {
state.admins.push(admin);
state.isAdmin[admin] = state.admins.length;
}
else {
uint256 rawIndex = state.isAdmin[admin];
if (rawIndex != state.admins.length)
state.admins[rawIndex - 1] = state.admins[state.admins.length - 1];

state.isAdmin[admin] = 0;
state.admins.pop();
}

emit AdminsUpdated(admin, authorization, block.timestamp);
}}
}
38 changes: 38 additions & 0 deletions src/dispatcherComponents/SweepTokens.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.4;

import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol";
import {transferTokens} from "../TransferUtils.sol";
import {senderHasAuth} from "./AccessControl.sol";
import "./ids.sol";

abstract contract SweepTokens {
using BytesParsing for bytes;

/**
* Dispatch an execute function. Execute functions almost always modify contract state.
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment.

function dispatchExecSweepTokens(bytes calldata data, uint256 offset, uint8 command) internal returns (bool, uint256) {
if (command == SWEEP_TOKENS_ID)
offset = _sweepTokens(data, offset);
else return (false, offset);

return (true, offset);
}

function _sweepTokens(
bytes calldata commands,
uint offset
) internal returns (uint) {
senderHasAuth();

address token;
uint256 amount;
(token, offset) = commands.asAddressCdUnchecked(offset);
(amount, offset) = commands.asUint256CdUnchecked(offset);

transferTokens(token, msg.sender, amount);
return offset;
}
}
Loading
Loading