Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ackee audit fixes #22

Merged
merged 30 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4fd94ba
H1: Multiple vulnerabilities in recovery configuration process
JohnGuilding Jul 16, 2024
b31a844
H2: Premature guardian configuration update in addGuardian function
JohnGuilding Jul 17, 2024
06bf755
M1: templateIdx function parameter check is in incorrect place
JohnGuilding Jul 17, 2024
ba217b6
M2: Maximum guardians DoS
JohnGuilding Jul 18, 2024
5f1fc9e
M4: MAX + 1 validators may be configured in
JohnGuilding Jul 18, 2024
ad4fc63
Fix simulate test bug
JohnGuilding Jul 19, 2024
9b235d8
L1: Validators can be added/removed before module initialization in U…
JohnGuilding Jul 19, 2024
184f5e5
L2: Validators cannot be disallowed after being uninstalled in Univer…
JohnGuilding Jul 19, 2024
b1811d4
Add missing error selectors from helper test
JohnGuilding Jul 19, 2024
8f3fa22
L3: cancelRecovery does not revert when no recovery is in process
JohnGuilding Jul 19, 2024
0e4234c
W1: isInitialized function returns false if initialized without guard…
JohnGuilding Jul 19, 2024
c9028df
W2: Unused bytes32 function parameter in EmailRecoveryManager
JohnGuilding Jul 22, 2024
f80edd7
W3: Unnecessary computation of calldataHash value in validateRecovery…
JohnGuilding Jul 23, 2024
bc1410a
W5: Events missing parameters
JohnGuilding Jul 23, 2024
8a4afd8
W6: Missing AddedGuardian event emission in setupGuardians function
JohnGuilding Jul 23, 2024
c8d8686
W7: ERC-4337 violation in onInstall
JohnGuilding Jul 23, 2024
7dff8fc
I1: getTrustedRecoveryManager function returns public variable emailR…
JohnGuilding Jul 24, 2024
c750719
I2: Non-immutable state variables in
JohnGuilding Jul 24, 2024
51755ee
I3: Misleading naming
JohnGuilding Jul 24, 2024
4c3a5bb
I4: Unchecked return values in EnumerableGuardianMap library
JohnGuilding Jul 24, 2024
cd22e1f
I5: Use calldata in favor of memory in function parameters
JohnGuilding Jul 24, 2024
142452a
I7: Missing zero-address validation in constructors
JohnGuilding Jul 24, 2024
2ec79b9
I8: Modifiers not above constructors
JohnGuilding Jul 24, 2024
5ec1cfa
I9: Safe validateRecoverySubject optimization
JohnGuilding Jul 24, 2024
cef7751
I10: Unused using-for directive
JohnGuilding Jul 24, 2024
397e64c
Add missing zero address check
JohnGuilding Jul 25, 2024
9c7db07
M3: Selector collisions in
JohnGuilding Jul 28, 2024
d24daeb
W4: Gas inefficiencies in UniversalRecoveryModule
JohnGuilding Jul 28, 2024
88371b8
Fix build warnings
JohnGuilding Jul 29, 2024
5b26a9a
Add additional selector checks to modules
JohnGuilding Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ The `recover()` function on the module is the key entry point where recovery is

When writing a custom subject handler, an account developer would likely chose to deploy a `EmailRecoveryModule` instance rather than a `UniversalEmailRecoveryModule` instance. This is because a custom subject handler would likely be specific to an validator implementation, so using the recovery module for specific validators is more appropriate than the generic recovery module.

**Note:** This module is an executor and does not abide by the 4337 validation rules. The `onInstall` function breaks the validation rules and it is possible for it to be called during account deployment in the first userOp. So you cannot install this module during account deployment as onInstall will be called as part of the validation phase. Supporting executor initialization during account deployment is not mandated by ERC7579 - if required, install this module after the account has been setup.

### UniversalEmailRecoveryModule.sol
A recovery module that recovers any validator.

Expand All @@ -198,6 +200,8 @@ The `recover()` function on the module is the key entry point where recovery is

`completeRecovery()` calls into the account specific recovery module and can call executeFromExecutor to execute the account specific recovery logic. The call from the executor retains the context of the account so the `msg.sender` of the next call is the account itself. This simplifies access control in the validator being recovered as it can just do a `msg.sender` check.

**Note:** This module is an executor and does not abide by the 4337 validation rules. The `onInstall` function breaks the validation rules and it is possible for it to be called during account deployment in the first userOp. So you cannot install this module during account deployment as `onInstall` will be called as part of the validation phase. Supporting executor initialization during account deployment is not mandated by ERC7579 - if required, install this module after the account has been setup.

### EmailRecoveryFactory.sol
The factory for deploying new instances of `EmailRecoveryModule.sol` and associated managers and subject handlers. Because the relationship between the recovery module and manager is security critical, The factory ensures there is a tight coupling between a deployed module, and associated manager and subject handler.

Expand Down
111 changes: 66 additions & 45 deletions src/EmailRecoveryManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
/**
* Deployer address stored to prevent frontrunning at initialization
*/
address private deployer;
address private immutable deployer;

/**
* Account address to recovery config
Expand All @@ -82,19 +82,38 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
mapping(address account => EnumerableGuardianMap.AddressToGuardianMap guardian) internal
guardiansStorage;

/**
* @notice Modifier to check recovery status. Reverts if recovery is in process for the account
*/
modifier onlyWhenNotRecovering() {
if (recoveryRequests[msg.sender].currentWeight > 0) {
revert RecoveryInProcess();
}
_;
}

constructor(
address _verifier,
address _dkimRegistry,
address _emailAuthImpl,
address _subjectHandler
) {
if (_verifier == address(0)) {
revert InvalidVerifier();
}
if (_dkimRegistry == address(0)) {
revert InvalidDkimRegistry();
}
if (_emailAuthImpl == address(0)) {
revert InvalidEmailAuthImpl();
}
if (_subjectHandler == address(0)) {
revert InvalidSubjectHandler();
}
verifierAddr = _verifier;
dkimAddr = _dkimRegistry;
emailAuthImplementationAddr = _emailAuthImpl;
subjectHandler = _subjectHandler;
if (_subjectHandler == address(0)) {
revert InvalidSubjectHandler();
}
deployer = msg.sender;
}

Expand All @@ -108,16 +127,6 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
emailRecoveryModule = _emailRecoveryModule;
}

/**
* @notice Modifier to check recovery status. Reverts if recovery is in process for the account
*/
modifier onlyWhenNotRecovering() {
if (recoveryRequests[msg.sender].currentWeight > 0) {
revert RecoveryInProcess();
}
_;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* RECOVERY CONFIG, REQUEST AND TEMPLATE GETTERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -225,8 +234,8 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
* @param expiry The expiry time after which the recovery attempt is invalid
*/
function configureRecovery(
address[] memory guardians,
uint256[] memory weights,
address[] calldata guardians,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no changes for guardians, calldata is more appropriate than memory. Is that the reason this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that is why they are now calldata?

Although I change them back in the consolidation PR as the function is called directly by the module. As they are abi decoded in onInstall, this means they are in memory so these params have to be changed back to memory. That is for the other PR though

uint256[] calldata weights,
uint256 threshold,
uint256 delay,
uint256 expiry
Expand All @@ -241,21 +250,17 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
revert SetupAlreadyCalled();
}

if (!IEmailRecoveryModule(emailRecoveryModule).isAuthorizedToRecover(account)) {
if (!IEmailRecoveryModule(emailRecoveryModule).isAuthorizedToBeRecovered(account)) {
revert RecoveryModuleNotAuthorized();
}

// Allow recovery configuration without configuring guardians
if (guardians.length == 0 && weights.length == 0 && threshold == 0) {
guardianConfigs[account].initialized = true;
} else {
(uint256 guardianCount, uint256 totalWeight) =
setupGuardians(account, guardians, weights, threshold);
}

RecoveryConfig memory recoveryConfig = RecoveryConfig(delay, expiry);
updateRecoveryConfig(recoveryConfig);

emit RecoveryConfigured(account, guardians.length);
emit RecoveryConfigured(account, guardianCount, totalWeight, threshold);
}

/**
Expand All @@ -270,7 +275,7 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
{
address account = msg.sender;

if (!guardianConfigs[account].initialized) {
if (guardianConfigs[account].threshold == 0) {
revert AccountNotConfigured();
}
if (recoveryConfig.delay > recoveryConfig.expiry) {
Expand Down Expand Up @@ -299,20 +304,18 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
* @param guardian The address of the guardian to be accepted
* @param templateIdx The index of the template used for acceptance
* @param subjectParams An array of bytes containing the subject parameters
* @param {nullifier} Unused parameter. The nullifier acts as a unique identifier for an email,
* but it is not required in this implementation
*/
function acceptGuardian(
address guardian,
uint256 templateIdx,
bytes[] memory subjectParams,
bytes32
bytes32 /* nullifier */
)
internal
override
{
if (templateIdx != 0) {
revert InvalidTemplateIndex();
}

address account = IEmailRecoverySubjectHandler(subjectHandler).validateAcceptanceSubject(
templateIdx, subjectParams
);
Expand All @@ -321,7 +324,7 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
revert RecoveryInProcess();
}

if (!IEmailRecoveryModule(emailRecoveryModule).isAuthorizedToRecover(account)) {
if (!IEmailRecoveryModule(emailRecoveryModule).isAuthorizedToBeRecovered(account)) {
revert RecoveryModuleNotAuthorized();
}

Expand All @@ -333,6 +336,7 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
}

guardiansStorage.updateGuardianStatus(account, guardian, GuardianStatus.ACCEPTED);
guardianConfigs[account].acceptedWeight += guardianStorage.weight;

emit GuardianAccepted(account, guardian);
}
Expand All @@ -348,27 +352,31 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
* @param guardian The address of the guardian initiating the recovery
* @param templateIdx The index of the template used for the recovery request
* @param subjectParams An array of bytes containing the subject parameters
* @param {nullifier} Unused parameter. The nullifier acts as a unique identifier for an email,
* but it is not required in this implementation
*/
function processRecovery(
address guardian,
uint256 templateIdx,
bytes[] memory subjectParams,
bytes32
bytes32 /* nullifier */
)
internal
override
{
if (templateIdx != 0) {
revert InvalidTemplateIndex();
}

(address account, bytes32 calldataHash) = IEmailRecoverySubjectHandler(subjectHandler)
.validateRecoverySubject(templateIdx, subjectParams, address(this));
address account = IEmailRecoverySubjectHandler(subjectHandler).validateRecoverySubject(
templateIdx, subjectParams, address(this)
);

if (!IEmailRecoveryModule(emailRecoveryModule).isAuthorizedToRecover(account)) {
if (!IEmailRecoveryModule(emailRecoveryModule).isAuthorizedToBeRecovered(account)) {
revert RecoveryModuleNotAuthorized();
}

GuardianConfig memory guardianConfig = guardianConfigs[account];
if (guardianConfig.threshold > guardianConfig.acceptedWeight) {
revert ThresholdExceedsAcceptedWeight();
}

// This check ensures GuardianStatus is correct and also implicitly that the
// account in email is a valid account
GuardianStorage memory guardianStorage = getGuardian(account, guardian);
Expand All @@ -380,16 +388,18 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco

recoveryRequest.currentWeight += guardianStorage.weight;

uint256 threshold = guardianConfigs[account].threshold;
if (recoveryRequest.currentWeight >= threshold) {
if (recoveryRequest.currentWeight >= guardianConfig.threshold) {
bytes32 calldataHash = IEmailRecoverySubjectHandler(subjectHandler)
.parseRecoveryCalldataHash(templateIdx, subjectParams);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this function separated from validateRecoverySubject to return calldataHash efficiently?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it was a recommendation from the auditors to reduce unnecessary computation every time processRecovery was called


uint256 executeAfter = block.timestamp + recoveryConfigs[account].delay;
uint256 executeBefore = block.timestamp + recoveryConfigs[account].expiry;

recoveryRequest.executeAfter = executeAfter;
recoveryRequest.executeBefore = executeBefore;
recoveryRequest.calldataHash = calldataHash;

emit RecoveryProcessed(account, executeAfter, executeBefore);
emit RecoveryProcessed(account, guardian, executeAfter, executeBefore, calldataHash);
}
}

Expand All @@ -409,7 +419,7 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
* @param account The address of the account for which the recovery is being completed
* @param recoveryCalldata The calldata that is passed to recover the validator
*/
function completeRecovery(address account, bytes memory recoveryCalldata) public override {
function completeRecovery(address account, bytes calldata recoveryCalldata) public override {
if (account == address(0)) {
revert InvalidAccountAddress();
}
Expand Down Expand Up @@ -453,6 +463,9 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
* @dev Deletes the current recovery request associated with the caller's account
*/
function cancelRecovery() external virtual {
if (recoveryRequests[msg.sender].currentWeight == 0) {
revert NoRecoveryInProcess();
}
delete recoveryRequests[msg.sender];
emit RecoveryCancelled(msg.sender);
}
Expand Down Expand Up @@ -524,13 +537,15 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
*/
function setupGuardians(
address account,
address[] memory guardians,
uint256[] memory weights,
address[] calldata guardians,
uint256[] calldata weights,
uint256 threshold
)
internal
returns (uint256, uint256)
{
guardianConfigs.setupGuardians(guardiansStorage, account, guardians, weights, threshold);
return
guardianConfigs.setupGuardians(guardiansStorage, account, guardians, weights, threshold);
}

/**
Expand All @@ -541,6 +556,12 @@ contract EmailRecoveryManager is EmailAccountRecovery, Initializable, IEmailReco
* @param weight The weight assigned to the guardian
*/
function addGuardian(address guardian, uint256 weight) external onlyWhenNotRecovering {
// Threshold can only be 0 at initialization.
// Check ensures that setup function should be called first
if (guardianConfigs[msg.sender].threshold == 0) {
revert SetupNotCalled();
}

guardiansStorage.addGuardian(guardianConfigs, msg.sender, guardian, weight);
}

Expand Down
21 changes: 18 additions & 3 deletions src/factories/EmailRecoveryFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,24 @@ contract EmailRecoveryFactory {
*/
address public immutable emailAuthImpl;

error InvalidVerifier();
error InvalidEmailAuthImpl();

event EmailRecoveryModuleDeployed(
address emailRecoveryModule, address emailRecoveryManager, address subjectHandler
address emailRecoveryModule,
address emailRecoveryManager,
address subjectHandler,
address validator,
bytes4 functionSelector
);

constructor(address _verifier, address _emailAuthImpl) {
if (_verifier == address(0)) {
revert InvalidVerifier();
}
if (_emailAuthImpl == address(0)) {
revert InvalidEmailAuthImpl();
}
verifier = _verifier;
emailAuthImpl = _emailAuthImpl;
}
Expand Down Expand Up @@ -57,7 +70,7 @@ contract EmailRecoveryFactory {
bytes32 subjectHandlerSalt,
bytes32 recoveryManagerSalt,
bytes32 recoveryModuleSalt,
bytes memory subjectHandlerBytecode,
bytes calldata subjectHandlerBytecode,
address dkimRegistry,
address validator,
bytes4 functionSelector
Expand All @@ -84,7 +97,9 @@ contract EmailRecoveryFactory {

// Initialize recovery manager with module address
EmailRecoveryManager(emailRecoveryManager).initialize(emailRecoveryModule);
emit EmailRecoveryModuleDeployed(emailRecoveryModule, emailRecoveryManager, subjectHandler);
emit EmailRecoveryModuleDeployed(
emailRecoveryModule, emailRecoveryManager, subjectHandler, validator, functionSelector
);

return (emailRecoveryModule, emailRecoveryManager, subjectHandler);
}
Expand Down
17 changes: 14 additions & 3 deletions src/factories/EmailRecoveryUniversalFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ contract EmailRecoveryUniversalFactory {
address public immutable verifier;
address public immutable emailAuthImpl;

event EmailRecoveryModuleDeployed(
event UniversalEmailRecoveryModuleDeployed(
address emailRecoveryModule, address emailRecoveryManager, address subjectHandler
);

error InvalidVerifier();
error InvalidEmailAuthImpl();

constructor(address _verifier, address _emailAuthImpl) {
if (_verifier == address(0)) {
revert InvalidVerifier();
}
if (_emailAuthImpl == address(0)) {
revert InvalidEmailAuthImpl();
}
verifier = _verifier;
emailAuthImpl = _emailAuthImpl;
}
Expand Down Expand Up @@ -52,7 +61,7 @@ contract EmailRecoveryUniversalFactory {
bytes32 subjectHandlerSalt,
bytes32 recoveryManagerSalt,
bytes32 recoveryModuleSalt,
bytes memory subjectHandlerBytecode,
bytes calldata subjectHandlerBytecode,
address dkimRegistry
)
external
Expand All @@ -75,7 +84,9 @@ contract EmailRecoveryUniversalFactory {

// Initialize recovery manager with module address
EmailRecoveryManager(emailRecoveryManager).initialize(emailRecoveryModule);
emit EmailRecoveryModuleDeployed(emailRecoveryModule, emailRecoveryManager, subjectHandler);
emit UniversalEmailRecoveryModuleDeployed(
emailRecoveryModule, emailRecoveryManager, subjectHandler
);

return (emailRecoveryModule, emailRecoveryManager, subjectHandler);
}
Expand Down
Loading