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

Implement ERC7579 Simple Recovery Module Prototype #76

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
148 changes: 148 additions & 0 deletions src/simpleRecoveryPrototype/EOA712Verifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";


/**
* @title EOA712Verifier
* @notice A contract that verifies EIP-712 signatures from EOA guardians using OpenZeppelin’s ECDSA and SignatureChecker utilities.
*/
contract SimpleRecoveryVerifier {
using ECDSA for bytes32;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERRORS & EVENTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

error InvalidSignature(address recoveredSigner, bytes signature);

event GuardianVerified(
address indexed signer,
address indexed recoveredAccount,
uint256 templateIdx,
bytes32 commandParamsHash
);

mapping(address => mapping(address => uint256)) private nonces;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EIP-712 DOMAIN PARAMETERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

string private constant NAME = "EOAGuardianVerifier";
string private constant VERSION = "1.0.0";

bytes32 private constant _EIP712_DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);

bytes32 private constant _GUARDIAN_TYPEHASH = keccak256(
"GuardianAcceptance(address recoveredAccount,uint256 templateIdx,bytes32 commandParamsHash, uint256 nonce)"
);

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PUBLIC FUNCTION: VERIFY EOA GUARDIAN SIGNATURE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Verifies an EOA-based guardian’s EIP-712 signature.
* @param recoveredAccount The account that is being protected.
* @param templateIdx Numeric index indicating the acceptance template.
* @param commandParams Variable-length acceptance parameters (encoded bytes).
* @param signature The ECDSA signature from the guardian.
* @return signer The address recovered from the signature (reverts if invalid).
*/
function verifyEOAGuardian(
address recoveredAccount,
uint256 templateIdx,
bytes[] memory commandParams,
bytes memory signature
) public virtual returns (address signer) {
// Encode command parameters and hash them
bytes memory encodedParams = abi.encode(commandParams);
bytes32 commandParamsHash = keccak256(encodedParams);

// Create the struct hash for GuardianAcceptance
bytes32 structHash = keccak256(
abi.encode(
_GUARDIAN_TYPEHASH,
recoveredAccount,
templateIdx,
commandParamsHash,
nonces[msg.sender][recoveredAccount]++
)
);

// Create the final EIP-712 digest
bytes32 digest = toTypedDataHash(_domainSeparatorV4(), structHash);

// Recover the signer from the digest and signature
address recoveredSigner = ECDSA.recover(digest, signature);

// Check the validity of the signature
bool isValid = SignatureChecker.isValidSignatureNow(
recoveredSigner,
digest,
signature
);

if (!isValid) {
revert InvalidSignature(recoveredSigner, signature);
}

signer = recoveredSigner;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTION: DOMAIN SEPARATOR */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @dev Calculates the EIP-712 domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
return keccak256(
abi.encode(
_EIP712_DOMAIN_TYPEHASH,
keccak256(bytes(NAME)),
keccak256(bytes(VERSION)),
block.chainid,
address(this)
)
);
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL FUNCTION: CREATE TYPED DATA HASH */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @dev Combines the domain separator and struct hash into a single typed data hash.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash)
internal
pure
returns (bytes32)
{
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PUBLIC FUNCTION: GET DOMAIN SEPARATOR */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Returns the current domain separator.
* @return The EIP-712 domain separator.
*/
function getDomainSeparator() public view returns (bytes32) {
return _domainSeparatorV4();
}

function getNonce(address account, address guardian) public view returns (uint256) {
return nonces[account][guardian];
}

}
245 changes: 245 additions & 0 deletions src/simpleRecoveryPrototype/SimpleGuardianManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { EnumerableGuardianMap, GuardianStorage, GuardianStatus } from "../libraries/EnumerableGuardianMap.sol";
import { ISimpleRecoveryModuleManager } from "./interfaces/ISimpleRecoveryModuleManager.sol";
import { ISimpleGuardianManager } from "./interfaces/ISimpleGuardianManager.sol";

/**
* @title GuardianManager
* @notice A contract to manage guardians
*/
abstract contract SimpleGuardianManager is ISimpleGuardianManager {
using EnumerableGuardianMap for EnumerableGuardianMap.AddressToGuardianMap;

/**
* @notice Account to guardian configuration
*/
mapping(address account => SimpleGuardianManager.GuardianConfig guardianConfig)
internal guardianConfigs;

/**
* @notice Account address to guardian storage map
*/
mapping(address account => EnumerableGuardianMap.AddressToGuardianMap guardian)
internal guardiansStorage;

/**
* @notice Account to guardian address to guardian type
*/
mapping(address => mapping(address => GuardianType)) public guardianTypes;

/**
* @notice Modifier to check recovery status. Reverts if recovery is in process for the account.
*/
modifier onlyWhenNotRecovering() {
(, , uint256 currentWeight, ) = ISimpleRecoveryModuleManager(address(this))
.getRecoveryRequest(msg.sender);
if (currentWeight > 0) {
revert RecoveryInProcess();
}
_;
}

/**
* @notice Modifier to check if the kill switch has been enabled
*/
modifier onlyWhenActive() {
bool killSwitchEnabled = ISimpleRecoveryModuleManager(address(this)).killSwitchEnabled();
if (killSwitchEnabled) {
revert KillSwitchEnabled();
}
_;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* GUARDIAN LOGIC */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Retrieves the guardian configuration for a given account.
* @param account The address of the account.
* @return GuardianConfig The guardian configuration for the specified account.
*/
function getGuardianConfig(address account) public view returns (GuardianConfig memory) {
return guardianConfigs[account];
}

/**
* @notice Retrieves the guardian storage details for a given guardian and account.
* @param account The address of the account associated with the guardian.
* @param guardian The address of the guardian.
* @return GuardianStorage The guardian storage details.
*/
function getGuardian(address account, address guardian) public view returns (GuardianStorage memory) {
return guardiansStorage[account].get(guardian);
}

function getGuardianType(address account, address guardian) public view returns (GuardianType) {
return guardianTypes[account][guardian];
}

/**
* @notice Sets up guardians for an account with specified weights and threshold.
* @param account The account address.
* @param guardians An array of guardian addresses.
* @param weights An array of weights corresponding to each guardian.
* @param guardiantypes An array of guardian types.
* @param threshold The threshold weight required for recovery.
*/
function setupGuardians(
address account,
address[] memory guardians,
uint256[] memory weights,
GuardianType[] memory guardiantypes,
uint256 threshold
) internal returns (uint256, uint256) {
uint256 guardianCount = guardians.length;

if (guardianCount != weights.length) {
revert IncorrectNumberOfWeights(guardianCount, weights.length);
}

if (guardianCount != guardiantypes.length) {
revert IncorrectNumberOfGuardianTypes(guardianCount, guardiantypes.length);
}

if (threshold == 0) {
revert ThresholdCannotBeZero();
}

for (uint256 i = 0; i < guardianCount; i++) {
_addGuardian(account, guardians[i], weights[i], guardiantypes[i]);
}

uint256 totalWeight = guardianConfigs[account].totalWeight;
if (threshold > totalWeight) {
revert ThresholdExceedsTotalWeight(threshold, totalWeight);
}

guardianConfigs[account].threshold = threshold;
return (guardianCount, totalWeight);
}

/**
* @notice Adds a guardian for the caller's account.
* @param guardian The address of the guardian.
* @param weight The weight assigned to the guardian.
* @param guardianType The type of the guardian.
*/
function addGuardian(
address guardian,
uint256 weight,
GuardianType guardianType
) public onlyWhenNotRecovering {
if (guardianConfigs[msg.sender].threshold == 0) {
revert SetupNotCalled();
}

_addGuardian(msg.sender, guardian, weight, guardianType);
}

/**
* @notice Internal function to add a guardian with specified weight.
* @param account The account address.
* @param guardian The guardian address.
* @param weight The weight assigned to the guardian.
* @param guardianType The type of the guardian.
*/
function _addGuardian(address account, address guardian, uint256 weight, GuardianType guardianType) internal {
if (guardian == address(0) || guardian == account) {
revert InvalidGuardianAddress(guardian);
}

if (weight == 0) {
revert InvalidGuardianWeight();
}

bool success = guardiansStorage[account].set({
key: guardian,
value: GuardianStorage(GuardianStatus.REQUESTED, weight)
});

if (!success) {
revert AddressAlreadyGuardian();
}

guardianTypes[account][guardian] = guardianType;

guardianConfigs[account].guardianCount++;
guardianConfigs[account].totalWeight += weight;

emit AddedGuardian(account, guardian, weight, guardianType);
}

/**
* @notice Removes a guardian for the caller's account.
* @param guardian The address of the guardian to be removed.
*/
function removeGuardian(address guardian) external onlyWhenNotRecovering {
GuardianConfig memory guardianConfig = guardianConfigs[msg.sender];
GuardianStorage memory guardianStorage = guardiansStorage[msg.sender].get(guardian);

bool success = guardiansStorage[msg.sender].remove(guardian);
if (!success) {
revert AddressNotGuardianForAccount();
}

uint256 newTotalWeight = guardianConfig.totalWeight - guardianStorage.weight;
if (newTotalWeight < guardianConfig.threshold) {
revert ThresholdExceedsTotalWeight(newTotalWeight, guardianConfig.threshold);
}

guardianConfigs[msg.sender].guardianCount--;
guardianConfigs[msg.sender].totalWeight -= guardianStorage.weight;
if (guardianStorage.status == GuardianStatus.ACCEPTED) {
guardianConfigs[msg.sender].acceptedWeight -= guardianStorage.weight;
}

emit RemovedGuardian(msg.sender, guardian, guardianStorage.weight);
}

/**
* @notice Changes the threshold for guardian approvals for the caller's account.
* @param threshold The new threshold for guardian approvals.
*/
function changeThreshold(uint256 threshold) external onlyWhenNotRecovering {
if (guardianConfigs[msg.sender].threshold == 0) {
revert SetupNotCalled();
}

if (threshold > guardianConfigs[msg.sender].totalWeight) {
revert ThresholdExceedsTotalWeight(threshold, guardianConfigs[msg.sender].totalWeight);
}

if (threshold == 0) {
revert ThresholdCannotBeZero();
}

guardianConfigs[msg.sender].threshold = threshold;
emit ChangedThreshold(msg.sender, threshold);
}

/**
* @notice Updates the status for a guardian.
* @param account The account address.
* @param guardian The guardian address.
* @param newStatus The new status for the guardian.
*/
function updateGuardianStatus(
address account,
address guardian,
GuardianStatus newStatus
) internal {
GuardianStorage memory guardianStorage = guardiansStorage[account].get(guardian);
if (newStatus == guardianStorage.status) {
revert StatusCannotBeTheSame(newStatus);
}

guardiansStorage[account].set({
key: guardian,
value: GuardianStorage(newStatus, guardianStorage.weight)
});
emit GuardianStatusUpdated(account, guardian, newStatus);
}
}
Loading