AccessController.sol

Overview

The AccessController abstract contract is a simple implementation that allows for wallet ownership/guardianship. It provides the functionality to check if an address is a owner or guardian, add a new owner an guardian, or remove an existing owner and guardian. It contains modifiers that check for ownership, guardianship and calls from EntryPoint. In it’s current iteration it is designed to be used with EtherspotWallet to allow for wallets to have multiple owners and guardians.

Version

Solidity pragma version ^0.8.12.

State Variables

  • MULTIPLY_FACTOR: immutable value of 1000 for calculation of percentages.
  • SIXTY_PERCENT: immutable value of 600 for calculation of percentages.
  • ownerCount: public value, tracks count of how many owners a wallet has.
  • guardianCount: public value, tracks count of how many guardians a wallet has.
  • proposalId: public value, tracks proposal ids for guardians adding new owners.

Structs

  • NewOwnerProposal: stores the following data for guardians proposing new owners:
    • newOwnerProposed: address of the new owner that a guardian is proposing to add.
    • approvalCount: how many guardians have approved this proposal (quorum required 60% of total guardians).
    • guardiansApproved: array of the guardian addresses that have approved this proposal.
    • resolved: boolean to indicate whether the proposal has been actioned or discarded.

Modifiers

  • onlyOwner(): check caller is an owner of the EtherspotWallet contract or EtherspotWallet contract itself.
  • onlyGuardian(): check caller is a guardian of the EtherspotWallet contract.
  • onlyOwnerOrGuardian(): check caller is an owner of the EtherspotWallet contract, a guardian of the EtherspotWallet contract or EtherspotWallet contract itself.
  • onlyOwnerOrEntryPoint(): check caller is an owner of the EtherspotWallet contract, the EntryPoint contract or EtherspotWallet contract itself.

Mappings

  • mapping(address => bool) private owners: A mapping of addresses to boolean values that indicate whether the address is an owner or not.
  • mapping(address => bool) private guardians: A mapping of addresses to boolean values that indicate whether the address is a guardian or not.
  • mapping(uint256 => NewOwnerProposal) private proposals: A mapping of proposal ids to NewOwnerProposals (see Structs).

Events

  • event OwnerAdded(address newOwner): Triggered when a new guardian is added.
  • event OwnerRemoved(address removedOwner): Triggered when a guardian is removed.
  • event GuardianAdded(address newGuardian): Triggered when a new guardian is added.
  • event GuardianRemoved(address removedGuardian): Triggered when a guardian is removed.
  • event ProposalSubmitted(uint256 proposalId, address newOwnerProposed, address proposer): Triggered when a guardian proposes a new owner to be added to EtherspotWallet.
  • event QuorumNotReached(uint256 proposalId, address newOwnerProposed, uint256 guardiansApproved): Triggered when a guardian cosigns a proposal to add a new owner to EtherspotWallet but the required quorum has not been reached (60% of total guardians).
  • event ProposalDiscarded(uint256 proposalId): Triggered when a proposal will not be actioned and is discarded.

Public/External Functions

  • function isOwner(address _address) public view returns (bool): Checks if an address is a owner or not.
  • function isGuardian(address _address) public view returns (bool): Checks if an address is a guardian or not.
  • function getProposal(uint256 _proposalId) public view returns (address ownerProposed_, uint256 approvalCount_, address[] memory guardiansApproved_): Returns stored information of a NewOwnerProposal for the specified proposal id.
    • Error ACL:: invalid proposal id: Has to be a valid proposal.
  • function guardianPropose(address _newOwner) external onlyGuardian: Allows a guardian to propose adding a new EtherspotWallet owner. Only one proposal is allowed at any time and needs to either be actioned or discarded for another proposal to be submitted.
    • Error ACL:: not enough guardians to propose new owner (minimum 3): Requires minimum amount of 3 guardians to add a new owner.
    • Emits ProposalSubmitted(proposalId, _newOwner, msg.sender).
  • function guardianCosign(uint256 _proposalId) external onlyGuardian: Allows other guardians than the one that proposed adding a new owner to cosign the proposal. If quorum (60% of total guardians) is not reached then QuorumNotReached event will be emitted. If quorum is reached, it will add a new owner.
    • Error ACL:: invalid proposal id: Has to be a valid proposal.
    • Error ACL:: guardian already signed proposal: Guardian cannot sign proposal more than once.
    • Emits QuorumNotReached(_proposalId, newOwner, proposals[_proposalId].approvalCount).
  • function discardCurrentProposal() external onlyOwnerOrGuardian: Allows for a proposal to be discarded if it is decided that it will not be required/actioned.

Internal Functions

  • function _addOwner(address _newOwner) internal: Adds a new owner.
    • Error ACL:: zero address: Cannot add zero address as owner.
    • Error ACL:: already owner: Address cannot already be an owner.
    • Error ACL:: guardian cannot be owner: Guardians cannot add themselves as an owner.
    • Emits OwnerAdded(_newOwner).
  • function _removeOwner(address _owner) internal: Removes an existing owner.
    • Error ACL:: removing self: An owner cannot remove themselves.
    • Error ACL:: non-existant owner: Must be a valid owner to be removed.
    • Emits OwnerRemoved(_owner).
  • function _addGuardian(address _newGuardian) internal: Adds a new guardian.
    • Error ACL:: zero address: Cannot add zero address as guardian.
    • Error ACL:: already guardian: Existing guardian cannot be re-added as a guardian.
    • Error ACL:: guardian cannot be owner: Guardians cannot be owners.
    • Emits GuardianAdded(_newGuardian).
  • function _removeGuardian(address _guardian) internal: Removes an existing guardian.
    • Error ACL:: non-existant guardian: Must be a valid guardian to be removed.
    • Emits GuardianRemoved(_guardian).
  • function _checkIfSigned(uint256 _proposalId) internal view returns (bool): Checks if a guardian has cosigned a NewOwnerProposal.
  • function _checkQuorumReached(uint256 _proposalId) internal view returns (bool): Checks if a NewOwnerProposal has reached the required quorum to be processed or not.

Contract Source Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;

import "../interfaces/IAccessController.sol";

abstract contract AccessController is IAccessController {
    uint128 immutable MULTIPLY_FACTOR = 1000;
    uint16 immutable SIXTY_PERCENT = 600;
    uint24 immutable INITIAL_PROPOSAL_TIMELOCK = 24 hours;

    uint256 public ownerCount;
    uint256 public guardianCount;
    uint256 public proposalId;
    uint256 public proposalTimelock;
    mapping(address => bool) private owners;
    mapping(address => bool) private guardians;
    mapping(uint256 => NewOwnerProposal) private proposals;

    struct NewOwnerProposal {
        address newOwnerProposed;
        bool resolved;
        uint256 approvalCount;
        address[] guardiansApproved;
        uint256 proposedAt;
    }

    modifier onlyOwner() {
        require(
            isOwner(msg.sender) || msg.sender == address(this),
            "ACL:: only owner"
        );
        _;
    }

    modifier onlyGuardian() {
        require(isGuardian(msg.sender), "ACL:: only guardian");
        _;
    }

    modifier onlyOwnerOrGuardian() {
        require(
            isOwner(msg.sender) || isGuardian(msg.sender),
            "ACL:: only owner or guardian"
        );
        _;
    }

    modifier onlyOwnerOrEntryPoint(address _entryPoint) {
        require(
            msg.sender == _entryPoint || isOwner(msg.sender),
            "ACL:: not owner or entryPoint"
        );
        _;
    }

    function isOwner(address _address) public view returns (bool) {
        return owners[_address];
    }

    function isGuardian(address _address) public view returns (bool) {
        return guardians[_address];
    }

    function addOwner(address _newOwner) external onlyOwner {
        _addOwner(_newOwner);
    }

    function removeOwner(address _owner) external onlyOwner {
        _removeOwner(_owner);
    }

    function addGuardian(address _newGuardian) external onlyOwner {
        _addGuardian(_newGuardian);
    }

    function removeGuardian(address _guardian) external onlyOwner {
        _removeGuardian(_guardian);
    }

    function changeProposalTimelock(uint256 _newTimelock) external onlyOwner {
        proposalTimelock = _newTimelock;
        emit ProposalTimelockChanged(_newTimelock);
    }

    function getProposal(
        uint256 _proposalId
    )
        public
        view
        returns (
            address ownerProposed_,
            uint256 approvalCount_,
            address[] memory guardiansApproved_,
            bool resolved_,
            uint256 proposedAt_
        )
    {
        require(
            _proposalId != 0 && _proposalId <= proposalId,
            "ACL:: invalid proposal id"
        );
        NewOwnerProposal memory proposal = proposals[_proposalId];
        return (
            proposal.newOwnerProposed,
            proposal.approvalCount,
            proposal.guardiansApproved,
            proposal.resolved,
            proposal.proposedAt
        );
    }

    function discardCurrentProposal() external onlyOwnerOrGuardian {
        require(
            !proposals[proposalId].resolved,
            "ACL:: proposal already resolved"
        );
        if (isGuardian(msg.sender) && proposalTimelock > 0)
            require(
                (proposals[proposalId].proposedAt + proposalTimelock) <
                    block.timestamp,
                "ACL:: guardian cannot discard proposal until timelock relased"
            );
        if (isGuardian(msg.sender) && proposalTimelock == 0)
            require(
                (proposals[proposalId].proposedAt + INITIAL_PROPOSAL_TIMELOCK) <
                    block.timestamp,
                "ACL:: guardian cannot discard proposal until timelock relased"
            );
        proposals[proposalId].resolved = true;
        emit ProposalDiscarded(proposalId, msg.sender);
    }

    function guardianPropose(address _newOwner) external onlyGuardian {
        require(
            guardianCount >= 3,
            "ACL:: not enough guardians to propose new owner (minimum 3)"
        );
        if (
            proposals[proposalId].guardiansApproved.length != 0 &&
            proposals[proposalId].resolved == false
        ) revert("ACL:: latest proposal not yet resolved");

        proposalId = proposalId + 1;
        proposals[proposalId].newOwnerProposed = _newOwner;
        proposals[proposalId].guardiansApproved.push(msg.sender);
        proposals[proposalId].approvalCount += 1;
        proposals[proposalId].resolved = false;
        proposals[proposalId].proposedAt = block.timestamp;
        emit ProposalSubmitted(proposalId, _newOwner, msg.sender);
    }

    function guardianCosign() external onlyGuardian {
        require(proposalId != 0, "ACL:: invalid proposal id");
        require(
            !_checkIfSigned(proposalId),
            "ACL:: guardian already signed proposal"
        );
        require(
            !proposals[proposalId].resolved,
            "ACL:: proposal already resolved"
        );
        proposals[proposalId].guardiansApproved.push(msg.sender);
        proposals[proposalId].approvalCount += 1;
        address newOwner = proposals[proposalId].newOwnerProposed;
        if (_checkQuorumReached(proposalId)) {
            proposals[proposalId].resolved = true;
            _addOwner(newOwner);
        } else {
            emit QuorumNotReached(
                proposalId,
                newOwner,
                proposals[proposalId].approvalCount
            );
        }
    }

    // INTERNAL

    function _addOwner(address _newOwner) internal {
        // no check for address(0) as used when creating wallet via BLS.
        require(_newOwner != address(0), "ACL:: zero address");
        require(!owners[_newOwner], "ACL:: already owner");
        if (isGuardian(_newOwner)) revert("ACL:: guardian cannot be owner");
        emit OwnerAdded(_newOwner);
        owners[_newOwner] = true;
        ownerCount = ownerCount + 1;
    }

    function _addGuardian(address _newGuardian) internal {
        require(_newGuardian != address(0), "ACL:: zero address");
        require(!guardians[_newGuardian], "ACL:: already guardian");
        require(!isOwner(_newGuardian), "ACL:: guardian cannot be owner");
        emit GuardianAdded(_newGuardian);
        guardians[_newGuardian] = true;
        guardianCount = guardianCount + 1;
    }

    function _removeOwner(address _owner) internal {
        require(owners[_owner], "ACL:: non-existant owner");
        require(ownerCount > 1, "ACL:: wallet cannot be ownerless");
        emit OwnerRemoved(_owner);
        owners[_owner] = false;
        ownerCount = ownerCount - 1;
    }

    function _removeGuardian(address _guardian) internal {
        require(guardians[_guardian], "ACL:: non-existant guardian");
        emit GuardianRemoved(_guardian);
        guardians[_guardian] = false;
        guardianCount = guardianCount - 1;
    }

    function _checkIfSigned(uint256 _proposalId) internal view returns (bool) {
        for (uint i; i < proposals[_proposalId].guardiansApproved.length; i++) {
            if (proposals[_proposalId].guardiansApproved[i] == msg.sender) {
                return true;
            }
        }
        return false;
    }

    function _checkQuorumReached(
        uint256 _proposalId
    ) internal view returns (bool) {
        return ((proposals[_proposalId].approvalCount * MULTIPLY_FACTOR) /
            guardianCount >=
            SIXTY_PERCENT);
    }
}

License

This contract is licensed under the MIT license.