-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
65f905f
commit 69ec4b2
Showing
14 changed files
with
1,296 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule openzeppelin-contracts
added at
329125
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.