Skip to content

Commit

Permalink
add dispatcher components
Browse files Browse the repository at this point in the history
  • Loading branch information
AgusVelez5 authored and nonergodic committed Nov 15, 2024
1 parent 65f905f commit 69ec4b2
Show file tree
Hide file tree
Showing 14 changed files with 1,296 additions and 1 deletion.
5 changes: 4 additions & 1 deletion .gitmodules
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
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";

/**
* Payment to the target failed.
*/
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;

(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)
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)
revert NotAuthorized();

state.pendingOwner = address(0);
}

function receiveOwnership() external {
_acquireOwnership();
}

// ---- internals ----

/**
* Dispatch an execute function. Execute functions almost always modify contract state.
*/
function dispatchExecAccessControl(bytes calldata data, uint256 offset, uint8 command) internal returns (bool, uint256) {
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.
*/
function dispatchQueryAccessControl(bytes calldata data, uint256 offset, uint8 query) view internal returns (bool, bytes memory, uint256) {
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)
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.
*/
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

0 comments on commit 69ec4b2

Please sign in to comment.