Contract Name:
ArgusAccountHelper
Contract Source Code:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "../../interfaces/IVersion.sol";
import "../CoboFactory.sol";
import "./ArgusAuthorizerHelper.sol";
contract ArgusAccountHelper is ArgusAuthorizerHelper, IVersion {
bytes32 public constant NAME = "ArgusAccountHelper";
uint256 public constant VERSION = 1;
event ArgusInitialized(address indexed cobosafe, address indexed safe, address indexed factory);
function initArgus(CoboFactory factory, bytes32 coboSafeAccountSalt) external {
address safe = address(this);
// 1. Create and enable CoboSafe.
CoboSafeAccount coboSafe = CoboSafeAccount(
payable(factory.create2AndRecord("CoboSafeAccount", coboSafeAccountSalt))
);
coboSafe.initialize(safe);
IGnosisSafe(safe).enableModule(address(coboSafe));
// 2. Set roleManager.
FlatRoleManager roleManager = FlatRoleManager(factory.create("FlatRoleManager"));
roleManager.initialize(safe);
coboSafe.setRoleManager(address(roleManager));
// 3. Set authorizer
BaseAuthorizer authorizer = BaseAuthorizer(factory.create("ArgusRootAuthorizer"));
authorizer.initialize(safe, address(coboSafe), address(coboSafe));
coboSafe.setAuthorizer(address(authorizer));
emit ArgusInitialized(address(coboSafe), safe, address(factory));
}
function grantRoles(address coboSafeAddress, bytes32[] calldata roles, address[] calldata delegates) external {
// 1. Add delegates to CoboSafe.
CoboSafeAccount coboSafe = CoboSafeAccount(payable(coboSafeAddress));
coboSafe.addDelegates(delegates);
// 2. Grant role/delegate in roleManager.
FlatRoleManager roleManager = FlatRoleManager(coboSafe.roleManager());
roleManager.grantRoles(roles, delegates);
}
function revokeRoles(address coboSafeAddress, bytes32[] calldata roles, address[] calldata delegates) external {
// 1. Revoke role/delegate for roleManager.
CoboSafeAccount coboSafe = CoboSafeAccount(payable(coboSafeAddress));
FlatRoleManager roleManager = FlatRoleManager(coboSafe.roleManager());
roleManager.revokeRoles(roles, delegates);
}
function createAuthorizer(
CoboFactory factory,
address coboSafeAddress,
bytes32 authorizerName,
bytes32 tag
) public returns (address) {
address safe = address(this);
// 1. Get ArgusRootAuthorizer.
CoboSafeAccount coboSafe = CoboSafeAccount(payable(coboSafeAddress));
ArgusRootAuthorizer rootAuthorizer = ArgusRootAuthorizer(coboSafe.authorizer());
// 2. Create authorizer and add to root authorizer set
BaseAuthorizer authorizer = BaseAuthorizer(factory.create2(authorizerName, tag));
authorizer.initialize(safe, address(rootAuthorizer));
authorizer.setTag(tag);
return address(authorizer);
}
function addAuthorizer(
address coboSafeAddress,
address authorizerAddress,
bool isDelegateCall,
bytes32[] calldata roles
) public {
// 1. Get ArgusRootAuthorizer.
CoboSafeAccount coboSafe = CoboSafeAccount(payable(coboSafeAddress));
ArgusRootAuthorizer rootAuthorizer = ArgusRootAuthorizer(coboSafe.authorizer());
// 2. Add authorizer to root authorizer set
for (uint256 i = 0; i < roles.length; i++) {
rootAuthorizer.addAuthorizer(isDelegateCall, roles[i], authorizerAddress);
}
}
function removeAuthorizer(
address coboSafeAddress,
address authorizerAddress,
bool isDelegateCall,
bytes32[] calldata roles
) external {
// 1. Get ArgusRootAuthorizer.
CoboSafeAccount coboSafe = CoboSafeAccount(payable(coboSafeAddress));
ArgusRootAuthorizer rootAuthorizer = ArgusRootAuthorizer(coboSafe.authorizer());
// 2. Remove authorizer from root authorizer set
for (uint256 i = 0; i < roles.length; i++) {
rootAuthorizer.removeAuthorizer(isDelegateCall, roles[i], authorizerAddress);
}
}
function addFuncAuthorizer(
CoboFactory factory,
address coboSafeAddress,
bool isDelegateCall,
bytes32[] calldata roles,
address[] calldata _contracts,
string[][] calldata funcLists,
bytes32 tag
) external {
// 1. create FuncAuthorizer
address authorizerAddress = createAuthorizer(factory, coboSafeAddress, "FuncAuthorizer", tag);
// 2. Set params
setFuncAuthorizerParams(authorizerAddress, _contracts, funcLists);
// 3. Add authorizer to root authorizer set
addAuthorizer(coboSafeAddress, authorizerAddress, isDelegateCall, roles);
}
function addTransferAuthorizer(
CoboFactory factory,
address coboSafeAddress,
bool isDelegateCall,
bytes32[] calldata roles,
TransferAuthorizer.TokenReceiver[] calldata tokenReceivers,
bytes32 tag
) external {
// 1. create TransferAuthorizer
address authorizerAddress = createAuthorizer(factory, coboSafeAddress, "TransferAuthorizer", tag);
// 2. Set params
setTransferAuthorizerParams(authorizerAddress, tokenReceivers);
// 3. Add authorizer to root authorizer set
addAuthorizer(coboSafeAddress, authorizerAddress, isDelegateCall, roles);
}
function addDexAuthorizer(
CoboFactory factory,
address coboSafeAddress,
bytes32 dexAuthorizerName,
bool isDelegateCall,
bytes32[] calldata roles,
address[] calldata _swapInTokens,
address[] calldata _swapOutTokens,
bytes32 tag
) external {
// 1. create DexAuthorizer
address authorizerAddress = createAuthorizer(factory, coboSafeAddress, dexAuthorizerName, tag);
// 2. Set params
setDexAuthorizerParams(authorizerAddress, _swapInTokens, _swapOutTokens);
// 3. Add authorizer to root authorizer set
addAuthorizer(coboSafeAddress, authorizerAddress, isDelegateCall, roles);
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
interface IVersion {
function NAME() external view returns (bytes32 name);
function VERSION() external view returns (uint256 version);
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/proxy/Clones.sol";
import "./base/BaseOwnable.sol";
/// @title CoboFactory - A contract factory referenced by bytes32 name.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @notice Mostly used to manage proxy logic contract. But also ok to manage non-proxy contracts.
/// @dev Contracts to add should extend IVersion interface and implement the `NAME()` function.
contract CoboFactory is BaseOwnable {
bytes32 public constant NAME = "CoboFactory";
uint256 public constant VERSION = 1;
bytes32[] public names;
// The last one added.
mapping(bytes32 => address) public latestImplementations;
// Name => All added contracts.
mapping(bytes32 => address[]) public implementations;
// deployer => name => proxy contract list
// This is expensive. Query ProxyCreated event in SubGraph is a better solution.
mapping(address => mapping(bytes32 => address[])) public records;
event ProxyCreated(address indexed deployer, bytes32 indexed name, address indexed implementation, address proxy);
event ImplementationAdded(bytes32 indexed name, address indexed implementation);
constructor(address _owner) BaseOwnable(_owner) {}
function _getLatestImplStrict(bytes32 name) internal view returns (address impl) {
impl = getLatestImplementation(name);
require(impl != address(0), "No implementation");
}
/// View functions.
function getLatestImplementation(bytes32 name) public view returns (address impl) {
impl = latestImplementations[name];
}
function getAllImplementations(bytes32 name) external view returns (address[] memory impls) {
impls = implementations[name];
}
function getAllNames() external view returns (bytes32[] memory _names) {
_names = names;
}
/// @dev For etherscan view.
function getNameString(uint i) public view returns (string memory _name) {
_name = string(abi.encodePacked(names[i]));
}
function getAllNameStrings() external view returns (string[] memory _names) {
_names = new string[](names.length);
for (uint i = 0; i < names.length; ++i) {
_names[i] = getNameString(i);
}
}
function getLastRecord(address deployer, bytes32 name) external view returns (address proxy) {
address[] storage record = records[deployer][name];
if (record.length == 0) return address(0);
proxy = record[record.length - 1];
}
function getRecordSize(address deployer, bytes32 name) external view returns (uint256 size) {
address[] storage record = records[deployer][name];
size = record.length;
}
function getAllRecord(address deployer, bytes32 name) external view returns (address[] memory proxies) {
return records[deployer][name];
}
function getRecords(
address deployer,
bytes32 name,
uint256 start,
uint256 end
) external view returns (address[] memory proxies) {
address[] storage record = records[deployer][name];
uint256 size = record.length;
if (end > size) end = size;
require(end > start, "end > start");
proxies = new address[](end - start);
for (uint i = start; i < end; ++i) {
proxies[i - start] = record[i];
}
}
function getCreate2Address(address creator, bytes32 name, bytes32 salt) external view returns (address instance) {
address implementation = getLatestImplementation(name);
if (implementation == address(0)) return address(0);
salt = keccak256(abi.encode(creator, salt));
return Clones.predictDeterministicAddress(implementation, salt);
}
/// External functions.
/// @dev Create EIP 1167 proxy.
function create(bytes32 name) public returns (address instance) {
address implementation = _getLatestImplStrict(name);
instance = Clones.clone(implementation);
emit ProxyCreated(msg.sender, name, implementation, instance);
}
/// @dev Create EIP 1167 proxy with create2.
function create2(bytes32 name, bytes32 salt) public returns (address instance) {
address implementation = _getLatestImplStrict(name);
// Add msg.sender to the salt so no address collissions will occur between different users.
salt = keccak256(abi.encode(msg.sender, salt));
instance = Clones.cloneDeterministic(implementation, salt);
emit ProxyCreated(msg.sender, name, implementation, instance);
}
/// @notice Create and record the creation in the contract.
function createAndRecord(bytes32 name) external returns (address instance) {
instance = create(name);
records[msg.sender][name].push(instance);
}
function create2AndRecord(bytes32 name, bytes32 salt) public returns (address instance) {
instance = create2(name, salt);
records[msg.sender][name].push(instance);
}
/// @notice Register a logic contract to the factory. Only the owner is allowed.
function addImplementation(address impl) external onlyOwner {
bytes32 name = IVersion(impl).NAME();
// If new name found, add to `names`.
if (latestImplementations[name] == address(0)) {
names.push(name);
}
latestImplementations[name] = impl;
implementations[name].push(impl);
emit ImplementationAdded(name, impl);
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "../CoboSafeAccount.sol";
import "../role/FlatRoleManager.sol";
import "../auth/ArgusRootAuthorizer.sol";
import "../auth/FuncAuthorizer.sol";
import "../auth/TransferAuthorizer.sol";
import "../auth/DEXBaseACL.sol";
abstract contract ArgusAuthorizerHelper {
function setFuncAuthorizerParams(
address authorizerAddress,
address[] calldata _contracts,
string[][] calldata funcLists
) public {
if (_contracts.length == 0) return;
require(_contracts.length == funcLists.length, "Length differs");
FuncAuthorizer authorizer = FuncAuthorizer(authorizerAddress);
for (uint i = 0; i < _contracts.length; i++) {
authorizer.addContractFuncs(_contracts[i], funcLists[i]);
}
}
function unsetFuncAuthorizerParams(
address authorizerAddress,
address[] calldata _contracts,
string[][] calldata funcLists
) external {
if (_contracts.length == 0) return;
require(_contracts.length == funcLists.length, "Length differs");
FuncAuthorizer authorizer = FuncAuthorizer(authorizerAddress);
for (uint i = 0; i < _contracts.length; i++) {
authorizer.removeContractFuncs(_contracts[i], funcLists[i]);
}
}
function setTransferAuthorizerParams(
address authorizerAddress,
TransferAuthorizer.TokenReceiver[] calldata tokenReceivers
) public {
if (tokenReceivers.length == 0) return;
TransferAuthorizer authorizer = TransferAuthorizer(authorizerAddress);
authorizer.addTokenReceivers(tokenReceivers);
}
function unsetTransferAuthorizerParams(
address authorizerAddress,
TransferAuthorizer.TokenReceiver[] calldata tokenReceivers
) external {
if (tokenReceivers.length == 0) return;
TransferAuthorizer authorizer = TransferAuthorizer(authorizerAddress);
authorizer.removeTokenReceivers(tokenReceivers);
}
function setDexAuthorizerParams(
address authorizerAddress,
address[] calldata _swapInTokens,
address[] calldata _swapOutTokens
) public {
DEXBaseACL authorizer = DEXBaseACL(authorizerAddress);
if (_swapInTokens.length > 0) {
authorizer.addSwapInTokens(_swapInTokens);
}
if (_swapOutTokens.length > 0) {
authorizer.addSwapOutTokens(_swapOutTokens);
}
}
function unsetDexAuthorizerParams(
address authorizerAddress,
address[] calldata _swapInTokens,
address[] calldata _swapOutTokens
) external {
DEXBaseACL authorizer = DEXBaseACL(authorizerAddress);
if (_swapInTokens.length > 0) {
authorizer.removeSwapInTokens(_swapInTokens);
}
if (_swapOutTokens.length > 0) {
authorizer.removeSwapOutTokens(_swapOutTokens);
}
}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/Clones.sol)
pragma solidity ^0.8.20;
/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
*
* > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
* (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
* deterministic method.
*/
library Clones {
/**
* @dev A clone instance deployment failed.
*/
error ERC1167FailedCreateClone();
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(0, 0x09, 0x37)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple time will revert, since
* the clones cannot be deployed twice at the same address.
*/
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(0, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(add(ptr, 0x38), deployer)
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
mstore(add(ptr, 0x14), implementation)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
mstore(add(ptr, 0x58), salt)
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := keccak256(add(ptr, 0x43), 0x55)
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddress(implementation, salt, address(this));
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "../Errors.sol";
import "./BaseVersion.sol";
/// @title BaseOwnable - Simple ownership access control contract.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @dev Can be used in both proxy and non-proxy mode.
abstract contract BaseOwnable is BaseVersion {
address public owner;
address public pendingOwner;
bool private initialized = false;
event PendingOwnerSet(address indexed to);
event NewOwnerSet(address indexed owner);
modifier onlyOwner() {
require(owner == msg.sender, Errors.CALLER_IS_NOT_OWNER);
_;
}
/// @dev `owner` is set by argument, thus the owner can any address.
/// When used in non-proxy mode, `initialize` can not be called
/// after deployment.
constructor(address _owner) {
initialize(_owner);
}
/// @dev When used in proxy mode, `initialize` can be called by anyone
/// to claim the ownership.
/// This function can be called only once.
function initialize(address _owner) public {
require(!initialized, "Already initialized");
_setOwner(_owner);
initialized = true;
}
/// @notice User should ensure the corrent owner address set, or the
/// ownership may be transferred to blackhole. It is recommended to
/// take a safer way with setPendingOwner() + acceptOwner().
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "New Owner is zero");
_setOwner(newOwner);
}
/// @notice The original owner calls `setPendingOwner(newOwner)` and the new
/// owner calls `acceptOwner()` to take the ownership.
function setPendingOwner(address to) external onlyOwner {
pendingOwner = to;
emit PendingOwnerSet(pendingOwner);
}
function acceptOwner() external {
require(msg.sender == pendingOwner);
_setOwner(pendingOwner);
}
/// @notice Make the contract immutable.
function renounceOwnership() external onlyOwner {
_setOwner(address(0));
}
// Internal functions
/// @dev Clear pendingOwner to prevent from reclaiming the ownership.
function _setOwner(address _owner) internal {
owner = _owner;
pendingOwner = address(0);
emit NewOwnerSet(owner);
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "./base/BaseAccount.sol";
contract Enum {
enum Operation {
Call,
DelegateCall
}
}
interface IGnosisSafe {
/// @dev Allows a Module to execute a Safe transaction without any further confirmations and return data
function execTransactionFromModuleReturnData(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) external returns (bool success, bytes memory returnData);
function enableModule(address module) external;
function isModuleEnabled(address module) external view returns (bool);
}
/// @title CoboSafeAccount - A GnosisSafe module that implements customized access control
/// @author Cobo Safe Dev Team https://www.cobo.com/
contract CoboSafeAccount is BaseAccount {
using TxFlags for uint256;
bytes32 public constant NAME = "CoboSafeAccount";
uint256 public constant VERSION = 2;
constructor(address _owner) BaseAccount(_owner) {}
/// @notice The Safe of the CoboSafeAccount.
function safe() public view returns (address) {
return owner;
}
/// @dev Execute the transaction from the Safe.
function _executeTransaction(
TransactionData memory transaction
) internal override returns (TransactionResult memory result) {
// execute the transaction from Gnosis Safe, note this call will bypass
// Safe owners confirmation.
(result.success, result.data) = IGnosisSafe(payable(safe())).execTransactionFromModuleReturnData(
transaction.to,
transaction.value,
transaction.data,
transaction.flag.isDelegateCall() ? Enum.Operation.DelegateCall : Enum.Operation.Call
);
}
/// @dev The account address is the Safe address.
function _getAccountAddress() internal view override returns (address account) {
account = safe();
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../../interfaces/IRoleManager.sol";
import "../base/BaseOwnable.sol";
/// @title TransferAuthorizer - Manages delegate-role mapping.
/// @author Cobo Safe Dev Team https://www.cobo.com/
contract FlatRoleManager is IFlatRoleManager, BaseOwnable {
bytes32 public constant NAME = "FlatRoleManager";
uint256 public constant VERSION = 1;
using EnumerableSet for EnumerableSet.Bytes32Set;
using EnumerableSet for EnumerableSet.AddressSet;
event DelegateAdded(address indexed delegate, address indexed sender);
event DelegateRemoved(address indexed delegate, address indexed sender);
event RoleAdded(bytes32 indexed role, address indexed sender);
event RoleGranted(bytes32 indexed role, address indexed delegate, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed delegate, address indexed sender);
EnumerableSet.AddressSet delegates;
EnumerableSet.Bytes32Set roles;
/// @dev mapping from `delegate` address => `role` set;
mapping(address => EnumerableSet.Bytes32Set) delegateToRoles;
constructor(address _owner) BaseOwnable(_owner) {}
/// @notice Add new roles without delegates assigned.
function addRoles(bytes32[] calldata _roles) external onlyOwner {
for (uint256 i = 0; i < _roles.length; i++) {
if (roles.add(_roles[i])) {
emit RoleAdded(_roles[i], msg.sender);
}
}
}
/// @notice Grant roles to delegates. Roles and delegates should be one-to-one.
function grantRoles(bytes32[] calldata _roles, address[] calldata _delegates) external onlyOwner {
require(
_roles.length > 0 && _roles.length == _delegates.length,
"FlatRoleManager: Invalid _roles or _delegates"
);
for (uint256 i = 0; i < _roles.length; i++) {
if (!delegateToRoles[_delegates[i]].add(_roles[i])) {
// If already bound, skip.
continue;
}
// In case when role is not added.
if (roles.add(_roles[i])) {
// Only fired when new one is added.
emit RoleAdded(_roles[i], msg.sender);
}
// Emit `DelegateAdded` before `RoleGranted` to allow
// subgraph event handler to process in sensible order.
if (delegates.add(_delegates[i])) {
emit DelegateAdded(_delegates[i], msg.sender);
}
emit RoleGranted(_roles[i], _delegates[i], msg.sender);
}
}
/// @notice Revoke roles from delegates. Roles and delegates should be one-to-one.
function revokeRoles(bytes32[] calldata _roles, address[] calldata _delegates) external onlyOwner {
require(
_roles.length > 0 && _roles.length == _delegates.length,
"FlatRoleManager: Invalid _roles or _delegates"
);
for (uint256 i = 0; i < _roles.length; i++) {
if (!delegateToRoles[_delegates[i]].remove(_roles[i])) {
continue;
}
// Ensure `RoleRevoked` is fired before `DelegateRemoved`
// so that the event handlers in subgraphs are triggered in the
// right order.
emit RoleRevoked(_roles[i], _delegates[i], msg.sender);
if (delegateToRoles[_delegates[i]].length() == 0) {
delegates.remove(_delegates[i]);
emit DelegateRemoved(_delegates[i], msg.sender);
}
}
}
/// @notice Get all the roles owned by the delegate
function getRoles(address delegate) external view returns (bytes32[] memory) {
return delegateToRoles[delegate].values();
}
/// @notice Check if the delegate owns the role.
function hasRole(address delegate, bytes32 role) external view returns (bool) {
return delegateToRoles[delegate].contains(role);
}
/// @notice Get the entire delegates list in the account.
function getDelegates() external view returns (address[] memory) {
return delegates.values();
}
/// @notice Get the entire roles list in the account.
function getAllRoles() external view returns (bytes32[] memory) {
return roles.values();
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../base/BaseAuthorizer.sol";
/// @title ArgusRootAuthorizer - Default root authorizers for Argus platform.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @notice ArgusRootAuthorizer is a authorizer manager which dispatch the correct
/// sub authorizer according to role of delegate and call type.
/// Hint is supported here so user can get the hint, the correct authorizer
/// in this case, off-chain (this can be expensive on-chain) and preform
/// on-chain transaction to save gas.
contract ArgusRootAuthorizer is BaseAuthorizer, IAuthorizerSupportingHint {
using EnumerableSet for EnumerableSet.AddressSet;
using EnumerableSet for EnumerableSet.Bytes32Set;
using TxFlags for uint256;
using AuthFlags for uint256;
bytes32 public constant NAME = "ArgusRootAuthorizer";
uint256 public constant VERSION = 1;
bytes32 public constant override TYPE = AuthType.SET;
/// @dev This changes when authorizer adds.
uint256 private _unionFlag;
// Roles in the authorizer. Only used for enumeration.
EnumerableSet.Bytes32Set roles;
// `isDelegateCall` => `Role` => `Authorizer address set`
// true for delegatecall, false for call.
mapping(bool => mapping(bytes32 => EnumerableSet.AddressSet)) internal authorizerSet;
// Authorizers who implement process handler (with flag `HAS_POST_PROC_MASK` or `HAS_POST_PROC_MASK`)
// will added into `processSet` and will be invoked unconditionally at each tx.
mapping(bool => EnumerableSet.AddressSet) internal processSet;
/// Events.
event NewAuthorizerAdded(bool indexed isDelegateCall, bytes32 indexed role, address indexed authorizer);
event NewProcessAdded(bool indexed isDelegateCall, address indexed authorizer);
event AuthorizerRemoved(bool indexed isDelegateCall, bytes32 indexed role, address indexed authorizer);
event ProcessRemoved(bool indexed isDelegateCall, address indexed authorizer);
constructor(address _owner, address _caller, address _account) BaseAuthorizer(_owner, _caller) {
// We need role manager.
account = _account;
}
/// @dev pack/unpack should match.
function _packHint(bytes32 role, address auth, bytes memory subHint) internal pure returns (bytes memory hint) {
return abi.encodePacked(abi.encode(role, auth), subHint);
}
function _unpackHint(bytes calldata hint) internal pure returns (bytes32 role, address auth, bytes memory subHint) {
(role, auth) = abi.decode(hint[0:64], (bytes32, address));
subHint = hint[64:];
}
/// @dev Catch error of sub authorizers to prevent the case when one authorizer fails reverts the entire
/// check chain process.
function _safePreExecCheck(
address auth,
TransactionData calldata transaction
) internal returns (AuthorizerReturnData memory preData) {
try IAuthorizer(auth).preExecCheck(transaction) returns (AuthorizerReturnData memory _preData) {
return _preData;
} catch Error(string memory reason) {
preData.result = AuthResult.FAILED;
preData.message = reason;
} catch (bytes memory reason) {
preData.result = AuthResult.FAILED;
preData.message = string(reason);
}
}
function _safePostExecCheck(
address auth,
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData memory preData
) internal returns (AuthorizerReturnData memory postData) {
try IAuthorizer(auth).postExecCheck(transaction, callResult, preData) returns (
AuthorizerReturnData memory _postData
) {
return _postData;
} catch Error(string memory reason) {
postData.result = AuthResult.FAILED;
postData.message = reason;
} catch (bytes memory reason) {
postData.result = AuthResult.FAILED;
postData.message = string(reason);
}
}
function _safeCollectHint(
address auth,
AuthorizerReturnData memory preData,
AuthorizerReturnData memory postData
) internal returns (bytes memory subHint) {
try IAuthorizerSupportingHint(auth).collectHint(preData, postData) returns (bytes memory _subHint) {
return _subHint;
} catch {
return subHint;
}
}
/// @dev preExecCheck and postExecCheck use extractly the same hint thus
/// the same sub authorizer is called.
function _preExecCheckWithHint(
TransactionData calldata transaction
) internal returns (AuthorizerReturnData memory authData) {
(bytes32 role, address auth, bytes memory subHint) = _unpackHint(transaction.hint);
uint256 _flag = IAuthorizer(auth).flag();
// The authorizer from hint should have either PreCheck or PostCheck.
require(_flag.isValid(), Errors.INVALID_AUTHORIZER_FLAG);
if (!_flag.hasPreCheck()) {
// If pre check handler not exist, default success.
authData.result = AuthResult.SUCCESS;
return authData;
}
// Important: Validate the hint.
// (1) The role from hint should be validated.
require(_hasRole(transaction, role), Errors.INVALID_HINT);
// (2) The authorizer from hint should have been registered with the role.
bool isDelegateCall = transaction.flag.isDelegateCall();
require(authorizerSet[isDelegateCall][role].contains(auth), Errors.INVALID_HINT);
// Cut the hint to sub hint.
TransactionData memory txn = transaction;
txn.hint = subHint;
// In hint path, this should never revert so `_safePreExecCheck()` is not used here.
return IAuthorizer(auth).preExecCheck(txn);
}
function _postExecCheckWithHint(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preData
) internal returns (AuthorizerReturnData memory authData) {
(bytes32 role, address auth, bytes memory subHint) = _unpackHint(transaction.hint);
uint256 _flag = IAuthorizer(auth).flag();
require(_flag.isValid(), Errors.INVALID_AUTHORIZER_FLAG);
if (!_flag.hasPostCheck()) {
// If post check handler not exist, default success.
authData.result = AuthResult.SUCCESS;
return authData;
}
// Important: Validate the hint.
// (1) The role from hint should be validated.
require(_hasRole(transaction, role), Errors.INVALID_HINT);
// (2) The authorizer from hint should have been registered with the role.
bool isDelegateCall = transaction.flag.isDelegateCall();
require(authorizerSet[isDelegateCall][role].contains(auth), Errors.INVALID_HINT);
TransactionData memory txn = transaction;
txn.hint = subHint;
return IAuthorizer(auth).postExecCheck(txn, callResult, preData);
}
struct PreCheckData {
bytes32 role;
address authorizer;
AuthorizerReturnData authData;
}
// This is very expensive on-chain.
// Should only used to collect hint off-chain.
PreCheckData[] internal preCheckDataCache;
function _preExecCheck(
TransactionData calldata transaction
) internal override returns (AuthorizerReturnData memory authData) {
if (transaction.hint.length > 0) {
return _preExecCheckWithHint(transaction);
}
authData.result = AuthResult.FAILED;
bytes32[] memory txRoles = _getRoles(transaction);
uint256 roleLength = txRoles.length;
if (roleLength == 0) {
authData.message = Errors.EMPTY_ROLE_SET;
return authData;
}
bool isDelegateCall = transaction.flag.isDelegateCall();
for (uint256 i = 0; i < roleLength; ++i) {
bytes32 role = txRoles[i];
EnumerableSet.AddressSet storage authSet = authorizerSet[isDelegateCall][role];
uint256 length = authSet.length();
// Run all pre checks and record auth results.
for (uint256 j = 0; j < length; ++j) {
address auth = authSet.at(j);
AuthorizerReturnData memory preData = _safePreExecCheck(auth, transaction);
if (preData.result == AuthResult.SUCCESS) {
authData.result = AuthResult.SUCCESS;
// Only save success results.
preCheckDataCache.push(PreCheckData(role, auth, preData));
}
}
}
if (authData.result == AuthResult.SUCCESS) {
// Temporary data for post checker to collect hint.
authData.data = abi.encode(preCheckDataCache);
} else {
authData.message = Errors.ALL_AUTH_FAILED;
}
delete preCheckDataCache; // gas refund.
}
function _postExecCheck(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preData
) internal override returns (AuthorizerReturnData memory postData) {
if (transaction.hint.length > 0) {
return _postExecCheckWithHint(transaction, callResult, preData);
}
// Get pre check results from preData.
PreCheckData[] memory preResults = abi.decode(preData.data, (PreCheckData[]));
uint256 length = preResults.length;
// We should have reverted in preExecCheck. But safer is better.
require(length > 0, Errors.INVALID_HINT_COLLECTED);
bool isDelegateCall = transaction.flag.isDelegateCall();
for (uint256 i = 0; i < length; ++i) {
bytes32 role = preResults[i].role;
address authAddress = preResults[i].authorizer;
require(authorizerSet[isDelegateCall][role].contains(authAddress), Errors.INVALID_HINT_COLLECTED);
// Run post check.
AuthorizerReturnData memory preCheckData = preResults[i].authData;
postData = _safePostExecCheck(authAddress, transaction, callResult, preCheckData);
// If pre and post both succeeded, we pass.
if (postData.result == AuthResult.SUCCESS) {
// Collect hint of sub authorizer if needed.
bytes memory subHint;
if (IAuthorizer(authAddress).flag().supportHint()) {
subHint = _safeCollectHint(authAddress, preCheckData, postData);
}
postData.data = _packHint(role, authAddress, subHint);
return postData;
}
}
postData.result = AuthResult.FAILED;
postData.message = Errors.ALL_AUTH_FAILED;
}
function collectHint(
AuthorizerReturnData calldata preAuthData,
AuthorizerReturnData calldata postAuthData
) public view returns (bytes memory hint) {
// Use post data as hint.
hint = postAuthData.data;
}
/// @dev All sub preExecProcess / postExecProcess handlers are supposed be called.
function _preExecProcess(TransactionData calldata transaction) internal virtual override {
if (!_unionFlag.hasPreProcess()) return;
bool isDelegateCall = transaction.flag.isDelegateCall();
EnumerableSet.AddressSet storage procSet = processSet[isDelegateCall];
uint256 length = procSet.length();
for (uint256 i = 0; i < length; i++) {
IAuthorizer auth = IAuthorizer(procSet.at(i));
if (auth.flag().hasPreProcess()) {
// Ignore reverts.
try auth.preExecProcess(transaction) {} catch {}
}
}
}
function _postExecProcess(
TransactionData calldata transaction,
TransactionResult calldata callResult
) internal virtual override {
if (!_unionFlag.hasPostProcess()) return;
bool isDelegateCall = transaction.flag.isDelegateCall();
EnumerableSet.AddressSet storage procSet = processSet[isDelegateCall];
uint256 length = procSet.length();
for (uint256 i = 0; i < length; i++) {
IAuthorizer auth = IAuthorizer(procSet.at(i));
if (auth.flag().hasPostProcess()) {
// Ignore reverts.
try auth.postExecProcess(transaction, callResult) {} catch {}
}
}
}
/// External / Public funtions.
function addAuthorizer(bool isDelegateCall, bytes32 role, address authorizer) external onlyOwner {
uint256 _flag = IAuthorizer(authorizer).flag();
roles.add(role);
if (authorizerSet[isDelegateCall][role].add(authorizer)) {
emit NewAuthorizerAdded(isDelegateCall, role, authorizer);
// Collect flag.
_unionFlag |= _flag;
if (_flag.hasPreProcess() || _flag.hasPostProcess()) {
// An authorizer with process handler can NOT be installed twice as this cause
// confusion when running process handler twice in one transaction.
require(processSet[isDelegateCall].add(authorizer), Errors.SAME_PROCESS_TWICE);
emit NewProcessAdded(isDelegateCall, authorizer);
}
}
}
function removeAuthorizer(bool isDelegateCall, bytes32 role, address authorizer) external onlyOwner {
uint256 _flag = IAuthorizer(authorizer).flag();
if (authorizerSet[isDelegateCall][role].remove(authorizer)) {
emit AuthorizerRemoved(isDelegateCall, role, authorizer);
if (_flag.hasPreProcess() || _flag.hasPostProcess()) {
// It is ok to remove here as we has checked duplication in `addAuthorizer()`.
if (processSet[isDelegateCall].remove(authorizer)) {
emit ProcessRemoved(isDelegateCall, authorizer);
if (processSet[isDelegateCall].length() == 0 && processSet[!isDelegateCall].length() == 0) {
_unionFlag -= (_unionFlag & (AuthFlags.HAS_PRE_PROC_MASK | AuthFlags.HAS_POST_PROC_MASK));
}
}
}
}
}
/// External view funtions.
function flag() external view returns (uint256) {
return _unionFlag | AuthFlags.SUPPORT_HINT_MASK;
}
function authorizerSize(bool isDelegateCall, bytes32 role) external view returns (uint256) {
return authorizerSet[isDelegateCall][role].length();
}
function hasAuthorizer(bool isDelegateCall, bytes32 role, address auth) external view returns (bool) {
return authorizerSet[isDelegateCall][role].contains(auth);
}
function getAuthorizer(bool isDelegateCall, bytes32 role, uint256 i) external view returns (address) {
return authorizerSet[isDelegateCall][role].at(i);
}
/// @dev View function allow user to specify the range in case we have very big set
/// which can exhaust the gas of block limit when enumerating the entire list.
function getAuthorizers(
bool isDelegateCall,
bytes32 role,
uint256 start,
uint256 end
) external view returns (address[] memory auths) {
uint256 authorizerSetSize = authorizerSet[isDelegateCall][role].length();
if (end > authorizerSetSize) end = authorizerSetSize;
auths = new address[](end - start);
for (uint256 i = 0; i < end - start; i++) {
auths[i] = authorizerSet[isDelegateCall][role].at(start + i);
}
}
function getAllAuthorizers(bool isDelegateCall, bytes32 role) external view returns (address[] memory) {
return authorizerSet[isDelegateCall][role].values();
}
function getAllRoles() external view returns (bytes32[] memory) {
return roles.values();
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../base/BaseAuthorizer.sol";
/// @title FuncAuthorizer - Manages contract, method pairs which can be accessed by delegates.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @notice FuncAuthorizer only checks selector. Use ACL if function arguments check is needed.
contract FuncAuthorizer is BaseAuthorizer {
using EnumerableSet for EnumerableSet.Bytes32Set;
using EnumerableSet for EnumerableSet.AddressSet;
bytes32 public constant NAME = "FuncAuthorizer";
uint256 public constant VERSION = 1;
uint256 public constant flag = AuthFlags.HAS_PRE_CHECK_MASK;
bytes32 public constant override TYPE = AuthType.FUNC;
/// @dev Tracks the set of contract address.
EnumerableSet.AddressSet contractSet;
/// @dev `contract address` => `function selectors`
mapping(address => EnumerableSet.Bytes32Set) allowContractToFuncs;
/// Events
event AddContractFunc(address indexed _contract, string func, address indexed sender);
event AddContractFuncSig(address indexed _contract, bytes4 indexed funcSig, address indexed sender);
event RemoveContractFunc(address indexed _contract, string func, address indexed sender);
event RemoveContractFuncSig(address indexed _contract, bytes4 indexed funcSig, address indexed sender);
constructor(address _owner, address _caller) BaseAuthorizer(_owner, _caller) {}
function _preExecCheck(
TransactionData calldata transaction
) internal view override returns (AuthorizerReturnData memory authData) {
// If calldata size is less than a selector, deny it.
// Use TransferAuthorizer to check ETH transfer.
if (transaction.data.length < 4) {
authData.result = AuthResult.FAILED;
authData.message = "invalid data length";
return authData;
}
bytes4 selector = _getSelector(transaction.data);
if (_isAllowedSelector(transaction.to, selector)) {
authData.result = AuthResult.SUCCESS;
} else {
authData.result = AuthResult.FAILED;
authData.message = "function not allowed";
}
}
function _getSelector(bytes calldata data) internal pure returns (bytes4 selector) {
assembly {
selector := calldataload(data.offset)
}
}
function _isAllowedSelector(address target, bytes4 selector) internal view returns (bool) {
return allowContractToFuncs[target].contains(selector);
}
/// @dev Default success.
function _postExecCheck(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preData
) internal view override returns (AuthorizerReturnData memory authData) {
authData.result = AuthResult.SUCCESS;
}
/// @notice Add contract and related function signature list. The function signature should be
/// canonicalized removing argument names and blanks chars.
/// ref: https://docs.soliditylang.org/en/v0.8.19/abi-spec.html#function-selector
/// @dev keccak256 hash is calcuated and only 4 bytes selector is stored to reduce storage usage.
function addContractFuncs(address _contract, string[] calldata funcList) external onlyOwner {
require(funcList.length > 0, "empty funcList");
for (uint256 index = 0; index < funcList.length; index++) {
bytes4 funcSelector = bytes4(keccak256(bytes(funcList[index])));
bytes32 funcSelector32 = bytes32(funcSelector);
if (allowContractToFuncs[_contract].add(funcSelector32)) {
emit AddContractFunc(_contract, funcList[index], msg.sender);
emit AddContractFuncSig(_contract, funcSelector, msg.sender);
}
}
contractSet.add(_contract);
}
/// @notice Remove contract and its function signature list from access list.
function removeContractFuncs(address _contract, string[] calldata funcList) external onlyOwner {
require(funcList.length > 0, "empty funcList");
for (uint256 index = 0; index < funcList.length; index++) {
bytes4 funcSelector = bytes4(keccak256(bytes(funcList[index])));
bytes32 funcSelector32 = bytes32(funcSelector);
if (allowContractToFuncs[_contract].remove(funcSelector32)) {
emit RemoveContractFunc(_contract, funcList[index], msg.sender);
emit RemoveContractFuncSig(_contract, funcSelector, msg.sender);
}
}
if (allowContractToFuncs[_contract].length() == 0) {
contractSet.remove(_contract);
}
}
/// @notice Similar to `addContractFuncs()` but bytes4 selector is used.
/// @dev keccak256 hash should be performed off-chain.
function addContractFuncsSig(address _contract, bytes4[] calldata funcSigList) external onlyOwner {
require(funcSigList.length > 0, "empty funcList");
for (uint256 index = 0; index < funcSigList.length; index++) {
bytes32 funcSelector32 = bytes32(funcSigList[index]);
if (allowContractToFuncs[_contract].add(funcSelector32)) {
emit AddContractFuncSig(_contract, funcSigList[index], msg.sender);
}
}
contractSet.add(_contract);
}
/// @notice Remove contract and its function selector list from access list.
function removeContractFuncsSig(address _contract, bytes4[] calldata funcSigList) external onlyOwner {
require(funcSigList.length > 0, "empty funcList");
for (uint256 index = 0; index < funcSigList.length; index++) {
bytes32 funcSelector32 = bytes32(funcSigList[index]);
if (allowContractToFuncs[_contract].remove(funcSelector32)) {
emit RemoveContractFuncSig(_contract, funcSigList[index], msg.sender);
}
}
if (allowContractToFuncs[_contract].length() == 0) {
contractSet.remove(_contract);
}
}
/// @notice Get all the contracts ever associated with any role
/// @return list of contract addresses
function getAllContracts() public view returns (address[] memory) {
return contractSet.values();
}
/// @notice Given a contract, list all the function selectors of this contract associated with a role
/// @param _contract the contract
/// @return list of function selectors in the contract ever associated with a role
function getFuncsByContract(address _contract) public view returns (bytes32[] memory) {
return allowContractToFuncs[_contract].values();
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../base/BaseACL.sol";
/// @title TransferAuthorizer - Manages ERC20/ETH transfer permissons.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @notice This checks token-receiver pairs, no amount is restricted.
contract TransferAuthorizer is BaseAuthorizer {
bytes32 public constant NAME = "TransferAuthorizer";
uint256 public constant VERSION = 2;
address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
bytes32 public constant override TYPE = AuthType.TRANSFER;
uint256 public constant flag = AuthFlags.HAS_PRE_CHECK_MASK;
// function transfer(address recipient, uint256 amount)
bytes4 constant TRANSFER_SELECTOR = 0xa9059cbb;
using EnumerableSet for EnumerableSet.AddressSet;
EnumerableSet.AddressSet tokenSet;
mapping(address => EnumerableSet.AddressSet) tokenToReceivers;
event TokenAdded(address indexed token);
event TokenRemoved(address indexed token);
event TokenReceiverAdded(address indexed token, address indexed receiver);
event TokenReceiverRemoved(address indexed token, address indexed receiver);
struct TokenReceiver {
address token;
address receiver;
}
constructor(address _owner, address _caller) BaseAuthorizer(_owner, _caller) {}
/// @notice Add token-receiver pairs. Use 0xee..ee for native ETH.
function addTokenReceivers(TokenReceiver[] calldata tokenReceivers) external onlyOwner {
for (uint i = 0; i < tokenReceivers.length; i++) {
address token = tokenReceivers[i].token;
address receiver = tokenReceivers[i].receiver;
if (tokenSet.add(token)) {
emit TokenAdded(token);
}
if (tokenToReceivers[token].add(receiver)) {
emit TokenReceiverAdded(token, receiver);
}
}
}
function removeTokenReceivers(TokenReceiver[] calldata tokenReceivers) external onlyOwner {
for (uint i = 0; i < tokenReceivers.length; i++) {
address token = tokenReceivers[i].token;
address receiver = tokenReceivers[i].receiver;
if (tokenToReceivers[token].remove(receiver)) {
emit TokenReceiverRemoved(token, receiver);
if (tokenToReceivers[tokenReceivers[i].token].length() == 0) {
if (tokenSet.remove(token)) {
emit TokenRemoved(token);
}
}
}
}
}
// View functions.
function getAllToken() external view returns (address[] memory) {
return tokenSet.values();
}
/// @dev View function allow user to specify the range in case we have very big token set
/// which can exhaust the gas of block limit.
function getTokens(uint256 start, uint256 end) external view returns (address[] memory) {
uint256 size = tokenSet.length();
if (end > size) end = size;
require(start < end, "start >= end");
address[] memory _tokens = new address[](end - start);
for (uint i = 0; i < end - start; i++) {
_tokens[i] = tokenSet.at(start + i);
}
return _tokens;
}
function getTokenReceivers(address token) external view returns (address[] memory) {
return tokenToReceivers[token].values();
}
function _preExecCheck(
TransactionData calldata transaction
) internal virtual override returns (AuthorizerReturnData memory authData) {
if (
transaction.data.length >= 68 && // 4 + 32 + 32
bytes4(transaction.data[0:4]) == TRANSFER_SELECTOR &&
transaction.value == 0
) {
// ETH transfer not allowed and token in white list.
(address recipient /*uint256 amount*/, ) = abi.decode(transaction.data[4:], (address, uint256));
address token = transaction.to;
if (tokenToReceivers[token].contains(recipient)) {
authData.result = AuthResult.SUCCESS;
return authData;
}
} else if (transaction.data.length == 0 && transaction.value > 0) {
// Contract call not allowed and token in white list.
address recipient = transaction.to;
if (tokenToReceivers[ETH].contains(recipient)) {
authData.result = AuthResult.SUCCESS;
return authData;
}
}
authData.result = AuthResult.FAILED;
authData.message = "transfer not allowed";
}
function _postExecCheck(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preData
) internal virtual override returns (AuthorizerReturnData memory authData) {
authData.result = AuthResult.SUCCESS;
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../base/BaseACL.sol";
/// @title DEXBaseACL - ACL template for DEX.
/// @author Cobo Safe Dev Team https://www.cobo.com/
abstract contract DEXBaseACL is BaseACL {
using EnumerableSet for EnumerableSet.AddressSet;
bytes32 public constant override TYPE = AuthType.DEX;
EnumerableSet.AddressSet swapInTokenWhitelist;
EnumerableSet.AddressSet swapOutTokenWhitelist;
event SwapInTokenAdded(address indexed token);
event SwapInTokenRemoved(address indexed token);
event SwapOutTokenAdded(address indexed token);
event SwapOutTokenRemoved(address indexed token);
struct SwapInToken {
address token;
bool tokenStatus;
}
struct SwapOutToken {
address token;
bool tokenStatus;
}
constructor(address _owner, address _caller) BaseACL(_owner, _caller) {}
// External set functions.
function addSwapInTokens(address[] calldata _tokens) external onlyOwner {
for (uint256 i = 0; i < _tokens.length; i++) {
address token = _tokens[i];
if (swapInTokenWhitelist.add(token)) {
emit SwapInTokenAdded(token);
}
}
}
function removeSwapInTokens(address[] calldata _tokens) external onlyOwner {
for (uint256 i = 0; i < _tokens.length; i++) {
address token = _tokens[i];
if (swapInTokenWhitelist.remove(token)) {
emit SwapInTokenRemoved(token);
}
}
}
function addSwapOutTokens(address[] calldata _tokens) external onlyOwner {
for (uint256 i = 0; i < _tokens.length; i++) {
address token = _tokens[i];
if (swapOutTokenWhitelist.add(token)) {
emit SwapOutTokenAdded(token);
}
}
}
function removeSwapOutTokens(address[] calldata _tokens) external onlyOwner {
for (uint256 i = 0; i < _tokens.length; i++) {
address token = _tokens[i];
if (swapOutTokenWhitelist.remove(token)) {
emit SwapOutTokenRemoved(token);
}
}
}
// External view functions.
function hasSwapInToken(address _token) public view returns (bool) {
return swapInTokenWhitelist.contains(_token);
}
function getSwapInTokens() external view returns (address[] memory tokens) {
return swapInTokenWhitelist.values();
}
function hasSwapOutToken(address _token) public view returns (bool) {
return swapOutTokenWhitelist.contains(_token);
}
function getSwapOutTokens() external view returns (address[] memory tokens) {
return swapOutTokenWhitelist.values();
}
// Internal check utility functions.
function _swapInTokenCheck(address _token) internal view {
require(hasSwapInToken(_token), "In token not allowed");
}
function _swapOutTokenCheck(address _token) internal view {
require(hasSwapOutToken(_token), "Out token not allowed");
}
function _swapInOutTokenCheck(address _inToken, address _outToken) internal view {
_swapInTokenCheck(_inToken);
_swapOutTokenCheck(_outToken);
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
/// @dev Common errors. This helps reducing the contract size.
library Errors {
// "E1";
// Call/Static-call failed.
string constant CALL_FAILED = "E2";
// Argument's type not supported in View Variant.
string constant INVALID_VIEW_ARG_SOL_TYPE = "E3";
// Invalid length for variant raw data.
string constant INVALID_VARIANT_RAW_DATA = "E4";
// "E5";
// Invalid variant type.
string constant INVALID_VAR_TYPE = "E6";
// Rule not exists
string constant RULE_NOT_EXISTS = "E7";
// Variant name not found.
string constant VAR_NAME_NOT_FOUND = "E8";
// Rule: v1/v2 solType mismatch
string constant SOL_TYPE_MISMATCH = "E9";
// "E10";
// Invalid rule OP.
string constant INVALID_RULE_OP = "E11";
// "E12";
// "E13";
// "E14";
// "E15";
// "E16";
// "E17";
// "E18";
// "E19";
// "E20";
// checkCmpOp: OP not support
string constant CMP_OP_NOT_SUPPORT = "E21";
// checkBySolType: Invalid op for bool
string constant INVALID_BOOL_OP = "E22";
// checkBySolType: Invalid op
string constant CHECK_INVALID_OP = "E23";
// Invalid solidity type.
string constant INVALID_SOL_TYPE = "E24";
// computeBySolType: invalid vm op
string constant INVALID_VM_BOOL_OP = "E25";
// computeBySolType: invalid vm arith op
string constant INVALID_VM_ARITH_OP = "E26";
// onlyCaller: Invalid caller
string constant INVALID_CALLER = "E27";
// "E28";
// Side-effect is not allowed here.
string constant SIDE_EFFECT_NOT_ALLOWED = "E29";
// Invalid variant count for the rule op.
string constant INVALID_VAR_COUNT = "E30";
// extractCallData: Invalid op.
string constant INVALID_EXTRACTOR_OP = "E31";
// extractCallData: Invalid array index.
string constant INVALID_ARRAY_INDEX = "E32";
// extractCallData: No extract op.
string constant NO_EXTRACT_OP = "E33";
// extractCallData: No extract path.
string constant NO_EXTRACT_PATH = "E34";
// BaseOwnable: caller is not owner
string constant CALLER_IS_NOT_OWNER = "E35";
// BaseOwnable: Already initialized
string constant ALREADY_INITIALIZED = "E36";
// "E37";
// "E38";
// BaseACL: ACL check method should not return anything.
string constant ACL_FUNC_RETURNS_NON_EMPTY = "E39";
// "E40";
// BaseAccount: Invalid delegate.
string constant INVALID_DELEGATE = "E41";
// RootAuthorizer: delegateCallAuthorizer not set
string constant DELEGATE_CALL_AUTH_NOT_SET = "E42";
// RootAuthorizer: callAuthorizer not set.
string constant CALL_AUTH_NOT_SET = "E43";
// BaseAccount: Authorizer not set.
string constant AUTHORIZER_NOT_SET = "E44";
// BaseAccount: Invalid authorizer flag.
string constant INVALID_AUTHORIZER_FLAG = "E45";
// BaseAuthorizer: Authorizer paused.
string constant AUTHORIZER_PAUSED = "E46";
// Authorizer set: Invalid hint.
string constant INVALID_HINT = "E47";
// Authorizer set: All auth deny.
string constant ALL_AUTH_FAILED = "E48";
// BaseACL: Method not allow.
string constant METHOD_NOT_ALLOW = "E49";
// AuthorizerUnionSet: Invalid hint collected.
string constant INVALID_HINT_COLLECTED = "E50";
// AuthorizerSet: Empty auth set
string constant EMPTY_AUTH_SET = "E51";
// AuthorizerSet: hint not implement.
string constant HINT_NOT_IMPLEMENT = "E52";
// RoleAuthorizer: Empty role set
string constant EMPTY_ROLE_SET = "E53";
// RoleAuthorizer: No auth for the role
string constant NO_AUTH_FOR_THE_ROLE = "E54";
// BaseACL: No in contract white list.
string constant NOT_IN_CONTRACT_LIST = "E55";
// BaseACL: Same process not allowed to install twice.
string constant SAME_PROCESS_TWICE = "E56";
// BaseAuthorizer: Account not set (then can not find roleManger)
string constant ACCOUNT_NOT_SET = "E57";
// BaseAuthorizer: roleManger not set
string constant ROLE_MANAGER_NOT_SET = "E58";
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "../../interfaces/IVersion.sol";
/// @title BaseVersion - Provides version information
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @dev
/// Implement NAME() and VERSION() methods according to IVersion interface.
///
/// Or just:
/// bytes32 public constant NAME = "<Your contract name>";
/// uint256 public constant VERSION = <Your contract version>;
///
/// Change the NAME when writing new kind of contract.
/// Change the VERSION when upgrading existing contract.
abstract contract BaseVersion is IVersion {
/// @dev Convert to `string` which looks prettier on Etherscan viewer.
function _NAME() external view virtual returns (string memory) {
return string(abi.encodePacked(this.NAME()));
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "../Types.sol";
import "./BaseOwnable.sol";
import "../../interfaces/IAuthorizer.sol";
import "../../interfaces/IAccount.sol";
/// @title BaseAccount - A basic smart contract wallet with access control supported.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @dev Extend this and implement `_executeTransaction()` and `_getFromAddress()`.
abstract contract BaseAccount is IAccount, BaseOwnable {
using EnumerableSet for EnumerableSet.AddressSet;
using TxFlags for uint256;
using AuthFlags for uint256;
address public roleManager;
address public authorizer;
// Simple and basic delegate check.
EnumerableSet.AddressSet delegates;
event RoleManagerSet(address indexed roleManager);
event AuthorizerSet(address indexed authorizer);
event DelegateAdded(address indexed delegate);
event DelegateRemoved(address indexed delegate);
event TransactionExecuted(
address indexed to,
bytes4 indexed selector,
uint256 indexed value,
TransactionData transaction
);
/// @param _owner Who owns the wallet.
constructor(address _owner) BaseOwnable(_owner) {}
/// @dev Only used in proxy mode. Can be called only once.
function initialize(address _owner, address _roleManager, address _authorizer) public {
initialize(_owner);
_setRoleManager(_roleManager);
_setAuthorizer(_authorizer);
}
/// Modifiers
/// @dev Only added delegates are allowed to call `execTransaction`. This provides a kind
/// of catch-all rule and simple but strong protection from malicious/compromised/buggy
/// authorizers which permit any operations.
modifier onlyDelegate() {
require(hasDelegate(msg.sender), Errors.INVALID_DELEGATE);
_;
}
// Public/External functions.
function setRoleManager(address _roleManager) external onlyOwner {
_setRoleManager(_roleManager);
}
function setAuthorizer(address _authorizer) external onlyOwner {
_setAuthorizer(_authorizer);
}
function addDelegate(address _delegate) external onlyOwner {
_addDelegate(_delegate);
}
function addDelegates(address[] calldata _delegates) external onlyOwner {
for (uint256 i = 0; i < _delegates.length; i++) {
_addDelegate(_delegates[i]);
}
}
function removeDelegate(address _delegate) external onlyOwner {
_removeDelegate(_delegate);
}
function removeDelegates(address[] calldata _delegates) external onlyOwner {
for (uint256 i = 0; i < _delegates.length; i++) {
_removeDelegate(_delegates[i]);
}
}
/// @notice Called by authenticated delegates to execute transaction on behalf of the wallet account.
function execTransaction(
CallData calldata callData
) external onlyDelegate returns (TransactionResult memory result) {
TransactionData memory transaction;
transaction.from = _getAccountAddress();
transaction.delegate = msg.sender;
transaction.flag = callData.flag;
transaction.to = callData.to;
transaction.value = callData.value;
transaction.data = callData.data;
transaction.hint = callData.hint;
transaction.extra = callData.extra;
result = _executeTransactionWithCheck(transaction);
emit TransactionExecuted(callData.to, bytes4(callData.data), callData.value, transaction);
}
/// @notice A Multicall method.
/// @param callDataList `CallData` array to execute in sequence.
function execTransactions(
CallData[] calldata callDataList
) external onlyDelegate returns (TransactionResult[] memory resultList) {
TransactionData memory transaction;
transaction.from = _getAccountAddress();
transaction.delegate = msg.sender;
resultList = new TransactionResult[](callDataList.length);
for (uint256 i = 0; i < callDataList.length; i++) {
CallData calldata callData = callDataList[i];
transaction.to = callData.to;
transaction.value = callData.value;
transaction.data = callData.data;
transaction.flag = callData.flag;
transaction.hint = callData.hint;
transaction.extra = callData.extra;
resultList[i] = _executeTransactionWithCheck(transaction);
emit TransactionExecuted(callData.to, bytes4(callData.data), callData.value, transaction);
}
}
/// Public/External view functions.
function hasDelegate(address _delegate) public view returns (bool) {
return delegates.contains(_delegate);
}
function getAllDelegates() external view returns (address[] memory) {
return delegates.values();
}
/// @notice The real address of your smart contract wallet address where
/// stores your assets and sends transactions from.
function getAccountAddress() external view returns (address account) {
account = _getAccountAddress();
}
/// Internal functions.
function _addDelegate(address _delegate) internal {
if (delegates.add(_delegate)) {
emit DelegateAdded(_delegate);
}
}
function _removeDelegate(address _delegate) internal {
if (delegates.remove(_delegate)) {
emit DelegateRemoved(_delegate);
}
}
function _setRoleManager(address _roleManager) internal {
roleManager = _roleManager;
emit RoleManagerSet(_roleManager);
}
function _setAuthorizer(address _authorizer) internal {
authorizer = _authorizer;
emit AuthorizerSet(_authorizer);
}
/// @dev Override this if we prefer not to revert the entire transaction in
// out wallet contract implementation.
function _preExecCheck(
TransactionData memory transaction
) internal virtual returns (AuthorizerReturnData memory authData) {
authData = IAuthorizer(authorizer).preExecCheck(transaction);
require(authData.result == AuthResult.SUCCESS, authData.message);
}
function _revertIfTxFails(TransactionResult memory callResult) internal pure {
bool success = callResult.success;
bytes memory data = callResult.data;
if (!success) {
assembly {
revert(add(data, 32), data)
}
}
}
function _postExecCheck(
TransactionData memory transaction,
TransactionResult memory callResult,
AuthorizerReturnData memory predata
) internal virtual returns (AuthorizerReturnData memory authData) {
authData = IAuthorizer(authorizer).postExecCheck(transaction, callResult, predata);
require(authData.result == AuthResult.SUCCESS, authData.message);
}
function _preExecProcess(TransactionData memory transaction) internal virtual {
IAuthorizer(authorizer).preExecProcess(transaction);
}
function _postExecProcess(
TransactionData memory transaction,
TransactionResult memory callResult
) internal virtual {
IAuthorizer(authorizer).postExecProcess(transaction, callResult);
}
function _executeTransactionWithCheck(
TransactionData memory transaction
) internal virtual returns (TransactionResult memory result) {
require(authorizer != address(0), Errors.AUTHORIZER_NOT_SET);
uint256 flag = IAuthorizer(authorizer).flag();
bool doCollectHint = transaction.hint.length == 0;
// Ensures either _preExecCheck or _postExecCheck (or both) will run.
require(flag.isValid(), Errors.INVALID_AUTHORIZER_FLAG);
// 1. Do pre check, revert the entire txn if failed.
AuthorizerReturnData memory preData;
if (doCollectHint || flag.hasPreCheck()) {
// Always run _preExecCheck When collecting hint.
// If not collecting hint, only run if the sub authorizer requires.
preData = _preExecCheck(transaction);
}
// 2. Do pre process.
if (flag.hasPreProcess()) _preExecProcess(transaction);
// 3. Execute the transaction.
result = _executeTransaction(transaction);
if (!transaction.flag.allowsRevert()) _revertIfTxFails(result);
// 4. Do post check, revert the entire txn if failed.
AuthorizerReturnData memory postData;
if (doCollectHint || flag.hasPostCheck()) {
postData = _postExecCheck(transaction, result, preData);
}
// 5. Do post process.
if (flag.hasPostProcess()) _postExecProcess(transaction, result);
// 6. Collect hint if when (1) no hint provided and (2) the authorizer supports hint mode.
if (doCollectHint && flag.supportHint()) {
result.hint = IAuthorizerSupportingHint(authorizer).collectHint(preData, postData);
}
}
/// @dev Instance should implement at least two `virtual` function below.
/// @param transaction Transaction to execute.
/// @return result `TransactionResult` which contains call status and return/revert data.
function _executeTransaction(
TransactionData memory transaction
) internal virtual returns (TransactionResult memory result);
/// @dev The address of wallet which sends the transaction a.k.a `msg.sender`
function _getAccountAddress() internal view virtual returns (address account);
// To receive ETH as a wallet.
receive() external payable {}
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableSet.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.
pragma solidity ^0.8.20;
/**
* @dev Library for managing
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
* types.
*
* Sets have the following properties:
*
* - Elements are added, removed, and checked for existence in constant time
* (O(1)).
* - Elements are enumerated in O(n). No guarantees are made on the ordering.
*
* ```solidity
* contract Example {
* // Add the library methods
* using EnumerableSet for EnumerableSet.AddressSet;
*
* // Declare a set state variable
* EnumerableSet.AddressSet private mySet;
* }
* ```
*
* As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
* and `uint256` (`UintSet`) are supported.
*
* [WARNING]
* ====
* Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
* unusable.
* See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
*
* In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
* array of EnumerableSet.
* ====
*/
library EnumerableSet {
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Set type with
// bytes32 values.
// The Set implementation uses private functions, and user-facing
// implementations (such as AddressSet) are just wrappers around the
// underlying Set.
// This means that we can only create new EnumerableSets for types that fit
// in bytes32.
struct Set {
// Storage of set values
bytes32[] _values;
// Position is the index of the value in the `values` array plus 1.
// Position 0 is used to mean a value is not in the set.
mapping(bytes32 value => uint256) _positions;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function _add(Set storage set, bytes32 value) private returns (bool) {
if (!_contains(set, value)) {
set._values.push(value);
// The value is stored at length-1, but we add 1 to all indexes
// and use 0 as a sentinel value
set._positions[value] = set._values.length;
return true;
} else {
return false;
}
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function _remove(Set storage set, bytes32 value) private returns (bool) {
// We cache the value's position to prevent multiple reads from the same storage slot
uint256 position = set._positions[value];
if (position != 0) {
// Equivalent to contains(set, value)
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in
// the array, and then remove the last element (sometimes called as 'swap and pop').
// This modifies the order of the array, as noted in {at}.
uint256 valueIndex = position - 1;
uint256 lastIndex = set._values.length - 1;
if (valueIndex != lastIndex) {
bytes32 lastValue = set._values[lastIndex];
// Move the lastValue to the index where the value to delete is
set._values[valueIndex] = lastValue;
// Update the tracked position of the lastValue (that was just moved)
set._positions[lastValue] = position;
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the tracked position for the deleted slot
delete set._positions[value];
return true;
} else {
return false;
}
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function _contains(Set storage set, bytes32 value) private view returns (bool) {
return set._positions[value] != 0;
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function _length(Set storage set) private view returns (uint256) {
return set._values.length;
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function _at(Set storage set, uint256 index) private view returns (bytes32) {
return set._values[index];
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function _values(Set storage set) private view returns (bytes32[] memory) {
return set._values;
}
// Bytes32Set
struct Bytes32Set {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _add(set._inner, value);
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _remove(set._inner, value);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
return _contains(set._inner, value);
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(Bytes32Set storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
return _at(set._inner, index);
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
bytes32[] memory store = _values(set._inner);
bytes32[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
// AddressSet
struct AddressSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(AddressSet storage set, address value) internal returns (bool) {
return _add(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(AddressSet storage set, address value) internal returns (bool) {
return _remove(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(AddressSet storage set, address value) internal view returns (bool) {
return _contains(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(AddressSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(AddressSet storage set, uint256 index) internal view returns (address) {
return address(uint160(uint256(_at(set._inner, index))));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(AddressSet storage set) internal view returns (address[] memory) {
bytes32[] memory store = _values(set._inner);
address[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
// UintSet
struct UintSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(UintSet storage set, uint256 value) internal returns (bool) {
return _add(set._inner, bytes32(value));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(UintSet storage set, uint256 value) internal returns (bool) {
return _remove(set._inner, bytes32(value));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(UintSet storage set, uint256 value) internal view returns (bool) {
return _contains(set._inner, bytes32(value));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(UintSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(UintSet storage set, uint256 index) internal view returns (uint256) {
return uint256(_at(set._inner, index));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(UintSet storage set) internal view returns (uint256[] memory) {
bytes32[] memory store = _values(set._inner);
uint256[] memory result;
/// @solidity memory-safe-assembly
assembly {
result := store
}
return result;
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "../src/Types.sol";
interface IRoleManager {
function getRoles(address delegate) external view returns (bytes32[] memory);
function hasRole(address delegate, bytes32 role) external view returns (bool);
}
interface IFlatRoleManager is IRoleManager {
function addRoles(bytes32[] calldata roles) external;
function grantRoles(bytes32[] calldata roles, address[] calldata delegates) external;
function revokeRoles(bytes32[] calldata roles, address[] calldata delegates) external;
function getDelegates() external view returns (address[] memory);
function getAllRoles() external view returns (bytes32[] memory);
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "./BaseOwnable.sol";
import "../Errors.sol";
import "../../interfaces/IAuthorizer.sol";
import "../../interfaces/IAccount.sol";
import "../../interfaces/IRoleManager.sol";
/// @title BaseAuthorizer - A basic pausable authorizer with caller restriction.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @dev Base contract to extend to implement specific authorizer.
abstract contract BaseAuthorizer is IAuthorizer, BaseOwnable {
/// @dev Override such constants while extending BaseAuthorizer.
bool public paused = false;
// Often used for off-chain system.
// Each contract instance has its own value.
bytes32 public tag = "";
// The caller which is able to call this contract's pre/postExecProcess
// and pre/postExecCheck having side-effect.
// It is usually the account or the parent authorizer(set) on higher level.
address public caller;
// This is the account this authorizer works for.
// Currently only used to lookup `roleManager`.
// If not used it is OK to keep it unset.
address public account;
event CallerSet(address indexed caller);
event AccountSet(address indexed account);
event TagSet(bytes32 indexed tag);
event PausedSet(bool indexed status);
constructor(address _owner, address _caller) BaseOwnable(_owner) {
caller = _caller;
}
function initialize(address _owner, address _caller) public {
initialize(_owner);
caller = _caller;
emit CallerSet(_caller);
}
function initialize(address _owner, address _caller, address _account) public {
initialize(_owner, _caller);
account = _account;
emit AccountSet(_account);
}
modifier onlyCaller() virtual {
require(msg.sender == caller, Errors.INVALID_CALLER);
_;
}
/// @notice Change the caller.
/// @param _caller the caller which calls the authorizer.
function setCaller(address _caller) external onlyOwner {
require(_caller != address(0), "Invalid caller");
caller = _caller;
emit CallerSet(_caller);
}
/// @notice Change the account.
/// @param _account the account which the authorizer get role manager from.
function setAccount(address _account) external onlyOwner {
require(_account != address(0), "Invalid account");
account = _account;
emit AccountSet(_account);
}
/// @notice Change the tag for the contract instance.
/// @dev For off-chain index.
/// @param _tag the tag
function setTag(bytes32 _tag) external onlyOwner {
tag = _tag;
emit TagSet(_tag);
}
/// @notice Set the pause status. Authorizer just denies all when paused.
/// @param _paused the paused status: true or false.
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
emit PausedSet(_paused);
}
/// @dev `onlyCaller` check is forced on pre/post Check/Process handlers
/// to prevent attackers from polluting our data by calling this directly.
/// @notice Check if the transaction can be executed.
/// @return authData Return check status, error message and other data.
function preExecCheck(
TransactionData calldata transaction
) external virtual onlyCaller returns (AuthorizerReturnData memory authData) {
if (paused) {
authData.result = AuthResult.FAILED;
authData.message = Errors.AUTHORIZER_PAUSED;
} else {
authData = _preExecCheck(transaction);
}
}
/// @notice Check after transaction execution.
/// @param callResult Transaction call status and return data.
function postExecCheck(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preData
) external virtual onlyCaller returns (AuthorizerReturnData memory authData) {
if (paused) {
authData.result = AuthResult.FAILED;
authData.message = Errors.AUTHORIZER_PAUSED;
} else {
authData = _postExecCheck(transaction, callResult, preData);
}
}
/// @dev Perform actions before the transaction execution.
function preExecProcess(TransactionData calldata transaction) external virtual onlyCaller {
if (!paused) _preExecProcess(transaction);
}
/// @dev Perform actions after the transaction execution.
function postExecProcess(
TransactionData calldata transaction,
TransactionResult calldata callResult
) external virtual onlyCaller {
if (!paused) _postExecProcess(transaction, callResult);
}
/// @dev Extract the roles of the delegate. If no roleManager set return empty lists.
function _getRoleManager() internal view returns (address roleManager) {
require(account != address(0), Errors.ACCOUNT_NOT_SET);
roleManager = IAccount(account).roleManager();
require(roleManager != address(0), Errors.ROLE_MANAGER_NOT_SET);
}
function _getRoles(TransactionData calldata transaction) internal view returns (bytes32[] memory roles) {
address roleManager = _getRoleManager();
roles = IRoleManager(roleManager).getRoles(transaction.delegate);
}
/// @dev Call `roleManager` to validate the role of delegate.
function _hasRole(TransactionData calldata transaction, bytes32 role) internal view returns (bool) {
address roleManager = _getRoleManager();
return IRoleManager(roleManager).hasRole(transaction.delegate, role);
}
/// @dev Override these functions to while extending this contract.
function _preExecCheck(
TransactionData calldata transaction
) internal virtual returns (AuthorizerReturnData memory authData) {}
function _postExecCheck(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preData
) internal virtual returns (AuthorizerReturnData memory) {}
function _preExecProcess(TransactionData calldata transaction) internal virtual {}
function _postExecProcess(
TransactionData calldata transaction,
TransactionResult calldata callResult
) internal virtual {}
/// @dev Override this if you implement new type of authorizer.
function TYPE() external view virtual returns (bytes32) {
return AuthType.COMMON;
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./BaseAuthorizer.sol";
/// @title BaseACL - Basic ACL template which uses the call-self trick to perform function and parameters check.
/// @author Cobo Safe Dev Team https://www.cobo.com/
/// @dev Steps to extend this:
/// 1. Set the NAME, VERSION, TYPE.
/// 2. Write ACL functions according the target contract.
/// 3. Add a constructor. eg:
/// `constructor(address _owner, address _caller) BaseACL(_owner, _caller) {}`
/// 4. Override `contracts()` to only target contracts that you checks. Transactions
//// whose `to` address is not in the list will revert.
/// 5. (Optional) If state changing operation in the checking method is required,
/// override `_preExecCheck()` to change `staticcall` to `call`.
///
/// NOTE for ACL developers:
/// 1. The checking functions can be defined extractly the same as the target method
/// to control thus developers do not bother to write a lot `abi.decode` code.
/// 2. Checking funtions should NOT contain return value, use `require` to perform check.
/// 3. BaseACL may serve for multiple target contracts.
/// - Implement contracts() to manage the target contracts set.
/// - Use `onlyContract` modifier or check `_txn().to` in checking functions.
/// 4. `onlyOwner` modifier should be used for customized setter functions.
abstract contract BaseACL is BaseAuthorizer {
using EnumerableSet for EnumerableSet.Bytes32Set;
/// @dev Set such constants in sub contract.
// bytes32 public constant NAME = "BaseACL";
// bytes32 public constant override TYPE = "ACLType";
// uint256 public constant VERSION = 0;
/// Only preExecCheck is used in BaseACL and hint is not supported.
uint256 public constant flag = AuthFlags.HAS_PRE_CHECK_MASK;
constructor(address _owner, address _caller) BaseAuthorizer(_owner, _caller) {}
/// Internal functions.
function _parseReturnData(
bool success,
bytes memory revertData
) internal pure returns (AuthorizerReturnData memory authData) {
if (success) {
// ACL checking functions should not return any bytes which differs from normal view functions.
require(revertData.length == 0, Errors.ACL_FUNC_RETURNS_NON_EMPTY);
authData.result = AuthResult.SUCCESS;
} else {
if (revertData.length < 68) {
// 4(Error sig) + 32(offset) + 32(length)
authData.message = string(revertData);
} else {
assembly {
// Slice the sighash.
revertData := add(revertData, 0x04)
}
authData.message = abi.decode(revertData, (string));
}
}
}
function _contractCheck(TransactionData calldata transaction) internal virtual returns (bool result) {
// This works as a catch-all check. Sample but safer.
address to = transaction.to;
address[] memory _contracts = contracts();
for (uint i = 0; i < _contracts.length; i++) {
if (to == _contracts[i]) return true;
}
return false;
}
function _packTxn(TransactionData calldata transaction) internal pure virtual returns (bytes memory) {
bytes memory txnData = abi.encode(transaction);
bytes memory callDataSize = abi.encode(transaction.data.length);
return abi.encodePacked(transaction.data, txnData, callDataSize);
}
function _unpackTxn() internal pure virtual returns (TransactionData memory transaction) {
uint256 end = msg.data.length;
uint256 callDataSize = abi.decode(msg.data[end - 32:end], (uint256));
transaction = abi.decode(msg.data[callDataSize:], (TransactionData));
}
// @dev Only valid in self-call checking functions.
function _txn() internal pure virtual returns (TransactionData memory transaction) {
return _unpackTxn();
}
function _preExecCheck(
TransactionData calldata transaction
) internal virtual override returns (AuthorizerReturnData memory authData) {
if (!_contractCheck(transaction)) {
authData.result = AuthResult.FAILED;
authData.message = Errors.NOT_IN_CONTRACT_LIST;
return authData;
}
(bool success, bytes memory revertData) = address(this).staticcall(_packTxn(transaction));
return _parseReturnData(success, revertData);
}
function _postExecCheck(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preData
) internal virtual override returns (AuthorizerReturnData memory authData) {
authData.result = AuthResult.SUCCESS;
}
// Internal view functions.
// Utilities for checking functions.
function _checkRecipient(address _recipient) internal view {
require(_recipient == _txn().from, "Invalid recipient");
}
function _checkContract(address _contract) internal view {
require(_contract == _txn().to, "Invalid contract");
}
// Modifiers.
modifier onlyContract(address _contract) {
_checkContract(_contract);
_;
}
/// External functions
/// @dev Implement your own access control checking functions here.
// example:
// function transfer(address to, uint256 amount)
// onlyContract(USDT_ADDR)
// external view
// {
// require(amount > 0 & amount < 10000, "amount not in range");
// }
/// @dev Override this cause it is used by `_preExecCheck`.
/// @notice Target contracts this BaseACL controls.
function contracts() public view virtual returns (address[] memory _contracts) {}
fallback() external virtual {
revert(Errors.METHOD_NOT_ALLOW);
}
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
struct CallData {
uint256 flag; // 0x1 delegate call, 0x0 call.
address to;
uint256 value;
bytes data; // calldata
bytes hint;
bytes extra; // for future support: signatures etc.
}
struct TransactionData {
address from; // `msg.sender` who performs the transaction a.k.a wallet address.
address delegate; // Delegate who calls executeTransactions().
// Same as CallData
uint256 flag; // 0x1 delegate call, 0x0 call.
address to;
uint256 value;
bytes data; // calldata
bytes hint;
bytes extra;
}
/// @dev Use enum instead of bool in case of when other status, like PENDING,
/// is needed in the future.
enum AuthResult {
FAILED,
SUCCESS
}
struct AuthorizerReturnData {
AuthResult result;
string message;
bytes data; // Authorizer return data. usually used for hint purpose.
}
struct TransactionResult {
bool success; // Call status.
bytes data; // Return/Revert data.
bytes hint;
}
library TxFlags {
uint256 internal constant DELEGATE_CALL_MASK = 0x1; // 1 for delegatecall, 0 for call
uint256 internal constant ALLOW_REVERT_MASK = 0x2; // 1 for allow, 0 for not
function isDelegateCall(uint256 flag) internal pure returns (bool) {
return flag & DELEGATE_CALL_MASK > 0;
}
function allowsRevert(uint256 flag) internal pure returns (bool) {
return flag & ALLOW_REVERT_MASK > 0;
}
}
library AuthType {
bytes32 internal constant FUNC = "FunctionType";
bytes32 internal constant TRANSFER = "TransferType";
bytes32 internal constant DEX = "DexType";
bytes32 internal constant LENDING = "LendingType";
bytes32 internal constant COMMON = "CommonType";
bytes32 internal constant SET = "SetType";
bytes32 internal constant VM = "VM";
}
library AuthFlags {
uint256 internal constant HAS_PRE_CHECK_MASK = 0x1;
uint256 internal constant HAS_POST_CHECK_MASK = 0x2;
uint256 internal constant HAS_PRE_PROC_MASK = 0x4;
uint256 internal constant HAS_POST_PROC_MASK = 0x8;
uint256 internal constant SUPPORT_HINT_MASK = 0x40;
uint256 internal constant FULL_MODE =
HAS_PRE_CHECK_MASK | HAS_POST_CHECK_MASK | HAS_PRE_PROC_MASK | HAS_POST_PROC_MASK;
function isValid(uint256 flag) internal pure returns (bool) {
// At least one check handler is activated.
return hasPreCheck(flag) || hasPostCheck(flag);
}
function hasPreCheck(uint256 flag) internal pure returns (bool) {
return flag & HAS_PRE_CHECK_MASK > 0;
}
function hasPostCheck(uint256 flag) internal pure returns (bool) {
return flag & HAS_POST_CHECK_MASK > 0;
}
function hasPreProcess(uint256 flag) internal pure returns (bool) {
return flag & HAS_PRE_PROC_MASK > 0;
}
function hasPostProcess(uint256 flag) internal pure returns (bool) {
return flag & HAS_POST_PROC_MASK > 0;
}
function supportHint(uint256 flag) internal pure returns (bool) {
return flag & SUPPORT_HINT_MASK > 0;
}
}
// For Rule VM.
// For each VariantType, an extractor should be implement.
enum VariantType {
INVALID, // Mark for delete.
EXTRACT_CALLDATA, // extract calldata by path bytes.
NAME, // name for user-defined variant.
RAW, // encoded solidity values.
VIEW, // staticcall view non-side-effect function and get return value.
CALL, // call state changing function and get returned value.
RULE, // rule expression.
ANY
}
// How the data should be decoded.
enum SolidityType {
_invalid, // Mark for delete.
_any,
_bytes,
_bool,
///// START 1
///// Generated by gen_rulelib.py (start)
_address,
_uint256,
_int256,
///// Generated by gen_rulelib.py (end)
///// END 1
_end
}
// A common operand in rule.
struct Variant {
VariantType varType;
SolidityType solType;
bytes data;
}
library VarName {
bytes5 internal constant TEMP = "temp.";
function isTemp(bytes32 name) internal pure returns (bool) {
return bytes5(name) == TEMP;
}
}
// OpCode for rule expression which returns v0.
enum OP {
INVALID,
// One opnd.
VAR, // v1
NOT, // !v1
// Two opnds.
// checkBySolType() which returns boolean.
EQ, // v1 == v2
NE, // v1 != v2
GT, // v1 > v2
GE, // v1 >= v2
LT, // v1 < v2
LE, // v1 <= v2
IN, // v1 in [...]
NOTIN, // v1 not in [...]
// computeBySolType() which returns bytes (with same solType)
AND, // v1 & v2
OR, // v1 | v2
ADD, // v1 + v2
SUB, // v1 - v2
MUL, // v1 * v2
DIV, // v1 / v2
MOD, // v1 % v2
// Three opnds.
IF, // v1? v2: v3
// Side-effect ones.
ASSIGN, // v1 := v2
VM, // rule list bytes.
NOP // as end.
}
struct Rule {
OP op;
Variant[] vars;
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "../src/Types.sol";
interface IAuthorizer {
function flag() external view returns (uint256 authFlags);
function setCaller(address _caller) external;
function preExecCheck(TransactionData calldata transaction) external returns (AuthorizerReturnData memory authData);
function postExecCheck(
TransactionData calldata transaction,
TransactionResult calldata callResult,
AuthorizerReturnData calldata preAuthData
) external returns (AuthorizerReturnData memory authData);
function preExecProcess(TransactionData calldata transaction) external;
function postExecProcess(TransactionData calldata transaction, TransactionResult calldata callResult) external;
}
interface IAuthorizerSupportingHint is IAuthorizer {
// When IAuthorizer(auth).flag().supportHint() == true;
function collectHint(
AuthorizerReturnData calldata preAuthData,
AuthorizerReturnData calldata postAuthData
) external view returns (bytes memory hint);
}
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.19;
import "../src/Types.sol";
interface IAccount {
function execTransaction(CallData calldata callData) external returns (TransactionResult memory result);
function execTransactions(
CallData[] calldata callDataList
) external returns (TransactionResult[] memory resultList);
function setAuthorizer(address _authorizer) external;
function setRoleManager(address _roleManager) external;
function addDelegate(address _delegate) external;
function addDelegates(address[] calldata _delegates) external;
/// @dev Sub instance should override this to set `from` for transaction
/// @return account The address for the contract wallet, also the
/// `msg.sender` address which send the transaction.
function getAccountAddress() external view returns (address account);
function roleManager() external view returns (address _roleManager);
function authorizer() external view returns (address _authorizer);
}