mirror of
https://github.com/luxfi/eoa-contracts.git
synced 2026-10-30 16:58:32 +00:00
164 lines
5.7 KiB
Solidity
164 lines
5.7 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.17;
|
|
import "../SmartAccount.sol";
|
|
import {Enum} from "../common/Enum.sol";
|
|
import {IAuthorizationModule} from "../interfaces/IAuthorizationModule.sol";
|
|
|
|
// TODO: To be rebuilt for an ownerless setup => like which validation method does it recover?
|
|
|
|
contract SocialRecoveryModule is IAuthorizationModule {
|
|
struct Friends {
|
|
address[] friends;
|
|
uint256 threshold;
|
|
}
|
|
|
|
string public constant NAME = "Social Recovery Module";
|
|
string public constant VERSION = "0.1.0";
|
|
uint256 internal constant SIG_VALIDATION_FAILED = 1;
|
|
|
|
// @review
|
|
// Might as well keep a state to mark seen userOpHashes
|
|
mapping(bytes32 => bool) public opsSeen;
|
|
|
|
// @todo
|
|
// Notice validateAndUpdateNonce in just skipped in case of modules. To avoid replay of same userOpHash I think it should be done.
|
|
|
|
mapping(address => Friends) internal _friendsEntries;
|
|
mapping(address => mapping(address => bool)) public isFriend;
|
|
|
|
// isConfirmed - map of [recoveryHash][friend] to bool
|
|
mapping(bytes32 => mapping(address => bool)) public isConfirmed;
|
|
mapping(address => uint256) internal _walletsNonces;
|
|
|
|
/**
|
|
* @dev Setup function sets initial storage of contract.
|
|
*/
|
|
function setup(
|
|
address[] memory _friends,
|
|
uint256 _threshold
|
|
) public returns (address) {
|
|
require(
|
|
_threshold <= _friends.length,
|
|
"Threshold exceeds friends count"
|
|
);
|
|
require(_threshold >= 2, "At least 2 friends required");
|
|
Friends storage entry = _friendsEntries[msg.sender];
|
|
// check for duplicates in friends list
|
|
for (uint256 i = 0; i < _friends.length; i++) {
|
|
address friend = _friends[i];
|
|
require(friend != address(0), "Invalid friend address provided");
|
|
require(
|
|
!isFriend[msg.sender][friend],
|
|
"Duplicate friends provided"
|
|
);
|
|
isFriend[msg.sender][friend] = true;
|
|
}
|
|
// update friends list and threshold for smart account
|
|
entry.friends = _friends;
|
|
entry.threshold = _threshold;
|
|
return address(this);
|
|
}
|
|
|
|
function validateUserOp(
|
|
UserOperation calldata userOp,
|
|
bytes32 userOpHash
|
|
) external virtual returns (uint256) {
|
|
(bytes memory moduleSignature, ) = abi.decode(
|
|
userOp.signature,
|
|
(bytes, address)
|
|
);
|
|
return _validateSignature(userOp, userOpHash, moduleSignature);
|
|
}
|
|
|
|
/**
|
|
* @dev standard validateSignature for modules to validate and mark userOpHash as seen
|
|
* @param userOp the operation that is about to be executed.
|
|
* @param userOpHash hash of the user's request data. can be used as the basis for signature.
|
|
* @return sigValidationResult sigAuthorizer to be passed back to trusting Account, aligns with validationData
|
|
*/
|
|
function _validateSignature(
|
|
UserOperation calldata userOp,
|
|
bytes32 userOpHash,
|
|
bytes memory moduleSignature
|
|
) internal virtual returns (uint256 sigValidationResult) {
|
|
(userOp, moduleSignature);
|
|
if (opsSeen[userOpHash] == true) return SIG_VALIDATION_FAILED;
|
|
opsSeen[userOpHash] = true;
|
|
// can perform it's own access control logic, verify agaisnt expected signer and return SIG_VALIDATION_FAILED
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @dev Confirm friend recovery transaction. Only by friends.
|
|
*/
|
|
function confirmTransaction(address _wallet, address _newOwner) public {
|
|
require(onlyFriends(_wallet, msg.sender), "sender not a friend");
|
|
bytes32 recoveryHash = getRecoveryHash(
|
|
_wallet,
|
|
_newOwner,
|
|
_walletsNonces[_wallet]
|
|
);
|
|
isConfirmed[recoveryHash][msg.sender] = true;
|
|
}
|
|
|
|
function recoverAccess(address payable _wallet, address _newOwner) public {
|
|
// require(onlyFriends(_wallet, msg.sender), "sender not a friend");
|
|
bytes32 recoveryHash = getRecoveryHash(
|
|
_wallet,
|
|
_newOwner,
|
|
_walletsNonces[_wallet]
|
|
);
|
|
require(
|
|
isConfirmedByRequiredFriends(recoveryHash, _wallet),
|
|
"Not enough confirmations"
|
|
);
|
|
SmartAccount smartAccount = SmartAccount(payable(_wallet));
|
|
require(
|
|
smartAccount.execTransactionFromModule(
|
|
_wallet,
|
|
0,
|
|
// abi.encodeCall("setOwner", (newOwner)),
|
|
abi.encodeWithSignature("setOwner(address)", _newOwner),
|
|
Enum.Operation.Call
|
|
),
|
|
"Could not execute recovery"
|
|
);
|
|
_walletsNonces[_wallet]++;
|
|
}
|
|
|
|
function isConfirmedByRequiredFriends(
|
|
bytes32 recoveryHash,
|
|
address _wallet
|
|
) public view returns (bool) {
|
|
uint256 confirmationCount;
|
|
Friends storage entry = _friendsEntries[_wallet];
|
|
for (uint256 i = 0; i < entry.friends.length; i++) {
|
|
if (isConfirmed[recoveryHash][entry.friends[i]])
|
|
confirmationCount++;
|
|
if (confirmationCount == entry.threshold) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function onlyFriends(
|
|
address _wallet,
|
|
address _friend
|
|
) public view returns (bool) {
|
|
Friends storage entry = _friendsEntries[_wallet];
|
|
for (uint256 i = 0; i < entry.friends.length; i++) {
|
|
if (entry.friends[i] == _friend) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// @dev Returns hash of data encoding owner replacement.
|
|
/// @return Data hash.
|
|
function getRecoveryHash(
|
|
address _wallet,
|
|
address _newOwner,
|
|
uint256 _nonce
|
|
) public pure returns (bytes32) {
|
|
return keccak256(abi.encode(_wallet, _newOwner, _nonce));
|
|
}
|
|
}
|