Contracts
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 of1000
for calculation of percentages.SIXTY_PERCENT
: immutable value of600
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 theEtherspotWallet
contract orEtherspotWallet
contract itself.onlyGuardian()
: check caller is a guardian of theEtherspotWallet
contract.onlyOwnerOrGuardian()
: check caller is an owner of theEtherspotWallet
contract, a guardian of theEtherspotWallet
contract orEtherspotWallet
contract itself.onlyOwnerOrEntryPoint()
: check caller is an owner of theEtherspotWallet
contract, theEntryPoint
contract orEtherspotWallet
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 toEtherspotWallet
.event QuorumNotReached(uint256 proposalId, address newOwnerProposed, uint256 guardiansApproved)
: Triggered when a guardian cosigns a proposal to add a new owner toEtherspotWallet
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.
- Error
function guardianPropose(address _newOwner) external onlyGuardian
: Allows a guardian to propose adding a newEtherspotWallet
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)
.
- Error
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 thenQuorumNotReached
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)
.
- Error
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)
.
- Error
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)
.
- Error
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)
.
- Error
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)
.
- Error
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.