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 all commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ For example, if you are using a solc version newer than `0.8.19` and are plannin

It is strongly recommended that you run the forge test suite of this SDK with your own compiler version to catch potential errors that stem from differences in compiler versions early. Yes, strictly speaking the Solidity version pragma should prevent these issues, but better to be safe than sorry, especially given that some components make extensive use of inline assembly.

**IERC20 Remapping**
**IERC20 and SafeERC20 Remapping**

This SDK comes with its own IERC20 interface. Given that projects tend to combine different SDKs, there's often this annoying issue of clashes of IERC20 interfaces, even though the are effectively the same. We handle this issue by importing `IERC20/IERC20.sol` which allows remapping the `IERC20/` prefix to whatever directory contains `IERC20.sol` in your project, thus providing an override mechanism that should allow dealing with this problem seamlessly until forge allows remapping of individual files.
This SDK comes with its own IERC20 interface and SafeERC20 implementation. Given that projects tend to combine different SDKs, there's often this annoying issue of clashes of IERC20 interfaces, even though they are effectively the same. We handle this issue by importing `IERC20/IERC20.sol` which allows remapping the `IERC20/` prefix to whatever directory contains `IERC20.sol` in your project, thus providing an override mechanism that should allow dealing with this problem seamlessly until forge allows remapping of individual files. The same approach is used for SafeERC20.

## Components

Expand Down
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/",
"SafeERC20/=src/libraries/",
]

verbosity = 3
27 changes: 3 additions & 24 deletions src/Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,6 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.19;

error NotAnEvmAddress(bytes32);

function toUniversalAddress(address addr) pure returns (bytes32 universalAddr) {
universalAddr = bytes32(uint256(uint160(addr)));
}

function fromUniversalAddress(bytes32 universalAddr) pure returns (address addr) {
if (bytes12(universalAddr) != 0)
revert NotAnEvmAddress(universalAddr);

assembly ("memory-safe") {
addr := universalAddr
}
}

/**
* Reverts with a given buffer data.
* Meant to be used to easily bubble up errors from low level calls when they fail.
*/
function reRevert(bytes memory err) pure {
assembly ("memory-safe") {
revert(add(err, 32), mload(err))
}
}
import {tokenOrNativeTransfer} from "wormhole-sdk/utils/Transfer.sol";
import {reRevert} from "wormhole-sdk/utils/Revert.sol";
import {toUniversalAddress, fromUniversalAddress} from "wormhole-sdk/utils/UniversalAddress.sol";
264 changes: 264 additions & 0 deletions src/components/dispatcher/AccessControl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.4;

import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol";
import {
ACCESS_CONTROL_ID,
ACCESS_CONTROL_QUERIES_ID,
OWNER_ID,
PENDING_OWNER_ID,
IS_ADMIN_ID,
ADMINS_ID,
REVOKE_ADMIN_ID,
ADD_ADMIN_ID,
PROPOSE_OWNERSHIP_TRANSFER_ID,
ACQUIRE_OWNERSHIP_ID,
RELINQUISH_OWNERSHIP_ID
} from "wormhole-sdk/components/dispatcher/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 failAuthIf(bool condition) pure {
if (condition)
revert NotAuthorized();
}

function senderAtLeastAdmin() view returns (Role) {
Role role = senderRole();
failAuthIf(role == Role.None);

return role;
}

function senderRole() view returns (Role) {
AccessControlState storage state = accessControlState();
if (msg.sender == state.owner) //check highest privilege level first
return Role.Owner;

return state.isAdmin[msg.sender] != 0 ? Role.Admin : 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();
failAuthIf(msg.sender != state.owner);

state.pendingOwner = newOwner;
}

function cancelOwnershipTransfer() external {
AccessControlState storage state = accessControlState();
failAuthIf(msg.sender != state.owner);

state.pendingOwner = address(0);
}

function receiveOwnership() external {
_acquireOwnership();
}

// ---- internals ----

function dispatchExecAccessControl(
bytes calldata data,
uint offset,
uint8 command
) internal returns (bool, uint) {
if (command == ACCESS_CONTROL_ID)
offset = _batchAccessControlCommands(data, offset);
else if (command == ACQUIRE_OWNERSHIP_ID)
_acquireOwnership();
else
return (false, offset);

return (true, offset);
}

function dispatchQueryAccessControl(
bytes calldata data,
uint offset,
uint8 query
) view internal returns (bool, bytes memory, 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 = senderAtLeastAdmin() == 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 (msg.sender !=state.pendingOwner)
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);
}}
}
36 changes: 36 additions & 0 deletions src/components/dispatcher/Ids.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.4;

// ----------- Dispatcher Ids -----------

// Execute commands

uint8 constant ACCESS_CONTROL_ID = 0x60;
uint8 constant ACQUIRE_OWNERSHIP_ID = 0x61;
uint8 constant UPGRADE_CONTRACT_ID = 0x62;
uint8 constant SWEEP_TOKENS_ID = 0x63;

// Query commands

uint8 constant ACCESS_CONTROL_QUERIES_ID = 0xe0;
uint8 constant IMPLEMENTATION_ID = 0xe1;

// ----------- Access Control Ids -----------

// Execute commands

//admin:
uint8 constant REVOKE_ADMIN_ID = 0x00;

//owner only:
uint8 constant PROPOSE_OWNERSHIP_TRANSFER_ID = 0x10;
uint8 constant RELINQUISH_OWNERSHIP_ID = 0x11;
uint8 constant ADD_ADMIN_ID = 0x12;

// Query commands

uint8 constant OWNER_ID = 0x80;
uint8 constant PENDING_OWNER_ID = 0x81;
uint8 constant IS_ADMIN_ID = 0x82;
uint8 constant ADMINS_ID = 0x83;
37 changes: 37 additions & 0 deletions src/components/dispatcher/SweepTokens.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache 2

pragma solidity ^0.8.4;

import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol";
import {tokenOrNativeTransfer} from "wormhole-sdk/utils/Transfer.sol";
import {senderAtLeastAdmin} from "wormhole-sdk/components/dispatcher/AccessControl.sol";
import {SWEEP_TOKENS_ID} from "wormhole-sdk/components/dispatcher/Ids.sol";

abstract contract SweepTokens {
using BytesParsing for bytes;

function dispatchExecSweepTokens(
bytes calldata data,
uint offset,
uint8 command
) internal returns (bool, uint) {
return command == SWEEP_TOKENS_ID
? (true, _sweepTokens(data, offset))
: (false, offset);
}

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

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

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