> ## Documentation Index
> Fetch the complete documentation index at: https://etherspot.fyi/llms.txt
> Use this file to discover all available pages before exploring further.

# AccessController

# 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

```Solidity theme={null}
// 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.
