Skip to content

Enable contract signers #58

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
53 changes: 13 additions & 40 deletions src/cores/ERC721Account/ERC721AccountRails.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ contract ERC721AccountRails is AccountRails, ERC6551Account, Initializable, IERC

/// @inheritdoc Account
function withdrawFromEntryPoint(address payable recipient, uint256 amount) public virtual override {
if (!_isAuthorized(Operations.ADMIN, _msgSender())) {
if (!_isAuthorizedForOperation(Operations.ADMIN, _msgSender())) {
revert IPermissions.PermissionDoesNotExist(Operations.ADMIN, _msgSender());
}

Expand All @@ -73,40 +73,6 @@ contract ERC721AccountRails is AccountRails, ERC6551Account, Initializable, IERC
super._checkSenderIsEntryPoint();
}

/// @dev When evaluating signatures that don't contain the `VALIDATOR_FLAG`, authenticate only the owner
function _defaultValidateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 /*missingAccountFunds*/ )
internal
view
virtual
override
returns (bool)
{
// recover signer address and any error
(address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(userOpHash, userOp.signature);
// return if signature is malformed
if (err != ECDSA.RecoverError.NoError) return false;

// return true only if signer is owner, owner-delegated, or AccountGroup admin
return _isAuthorized(Operations.ADMIN, signer);
}

/// @dev When evaluating signatures that don't contain the `VALIDATOR_FLAG`, authenticate only the owner
function _defaultIsValidSignature(bytes32 hash, bytes memory signature)
internal
view
virtual
override
returns (bool)
{
// support non-modular signatures by recovering signer address and reverting malleable or invalid signatures
(address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(hash, signature);
// return if signature is malformed
if (err != ECDSA.RecoverError.NoError) return false;

// return true only if signer is owner, owner-delegated, or AccountGroup admin
return _isAuthorized(Operations.ADMIN, signer);
}

function _isValidSigner(address signer, bytes memory) internal view override returns (bool) {
return hasPermission(Operations.CALL, signer);
}
Expand Down Expand Up @@ -154,14 +120,21 @@ contract ERC721AccountRails is AccountRails, ERC6551Account, Initializable, IERC
/// @dev Sensitive account operations restricted to three tiered authorization hierarchy:
/// TBA owner || TBA permission || AccountGroup admin
/// This provides owner autonomy, owner-delegated permissions, and multichain AccountGroup management
function _isAuthorized(bytes8 _operation, address _sender) internal view returns (bool) {
function _isAuthorizedForOperation(bytes8 _operation, address _sender) internal view returns (bool) {
// check sender is TBA owner or has been granted relevant permission (or admin) on this account
if (hasPermission(_operation, _sender)) return true;

// allow AccountGroup admins to manage accounts on non-origin chains
return _isAccountGroupAdmin(_sender);
}

/// @dev Granting signature authorization can be achieved by adding the `CALL_PERMIT` permission to an account
function _isAuthorizedSigner(address _signer) internal view virtual override returns (bool) {
if (_isAuthorizedForOperation(Operations.CALL_PERMIT, _signer)) return true;

return false;
}

/// @dev On non-origin chains, `owner()` returns the zero address, so multichain upgrades
/// are enabled by permitting trusted AccountGroup admins
function _isAccountGroupAdmin(address _sender) internal view returns (bool) {
Expand All @@ -174,28 +147,28 @@ contract ERC721AccountRails is AccountRails, ERC6551Account, Initializable, IERC

function _checkCanUpdateValidators() internal virtual override {
_updateState();
if (!_isAuthorized(Operations.VALIDATOR, _msgSender())) {
if (!_isAuthorizedForOperation(Operations.VALIDATOR, _msgSender())) {
revert IPermissions.PermissionDoesNotExist(Operations.VALIDATOR, _msgSender());
}
}

function _checkCanUpdatePermissions() internal override {
_updateState();
if (!_isAuthorized(Operations.PERMISSIONS, _msgSender())) {
if (!_isAuthorizedForOperation(Operations.PERMISSIONS, _msgSender())) {
revert IPermissions.PermissionDoesNotExist(Operations.PERMISSIONS, _msgSender());
}
}

function _checkCanUpdateGuards() internal override {
_updateState();
if (!_isAuthorized(Operations.GUARDS, _msgSender())) {
if (!_isAuthorizedForOperation(Operations.GUARDS, _msgSender())) {
revert IPermissions.PermissionDoesNotExist(Operations.GUARDS, _msgSender());
}
}

function _checkCanUpdateInterfaces() internal override {
_updateState();
if (!_isAuthorized(Operations.INTERFACE, _msgSender())) {
if (!_isAuthorizedForOperation(Operations.INTERFACE, _msgSender())) {
revert IPermissions.PermissionDoesNotExist(Operations.INTERFACE, _msgSender());
}
}
Expand Down
97 changes: 58 additions & 39 deletions src/cores/account/AccountRails.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {Operations} from "src/lib/Operations.sol";
import {Access} from "src/access/Access.sol";
import {SupportsInterface} from "src/lib/ERC165/SupportsInterface.sol";
import {ECDSA} from "openzeppelin-contracts/utils/cryptography/ECDSA.sol";
import {SignatureChecker} from "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol";
import {IERC1271} from "openzeppelin-contracts/interfaces/IERC1271.sol";
import {ERC1155Receiver} from "openzeppelin-contracts/token/ERC1155/utils/ERC1155Receiver.sol";
import {IERC1155Receiver} from "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol";
Expand Down Expand Up @@ -53,12 +54,12 @@ abstract contract AccountRails is Account, Rails, Validators, IERC1271 {

bytes32 ethSignedUserOpHash = ECDSA.toEthSignedMessageHash(userOpHash);

// extract validator address using cheap calldata slicing before decoding
// try extracting packed validator data to check for modular validation format
bytes8 flag = bytes8(userOp.signature[:8]);
address validator = address(bytes20(userOp.signature[12:32]));

if (flag == VALIDATOR_FLAG && isValidator(validator)) {
bytes memory formattedSig = userOp.signature[32:];
bytes calldata formattedSig = userOp.signature[32:];

// copy userOp into memory and format for Validator module
UserOperation memory formattedUserOp = userOp;
Expand All @@ -71,9 +72,19 @@ abstract contract AccountRails is Account, Rails, Validators, IERC1271 {
if (ret != 0) return ret;
} else {
// support non-modular signatures by default
// authenticate signer, terminating early with status code 1 on failure
bool validSigner = _defaultValidateUserOp(userOp, ethSignedUserOpHash, missingAccountFunds);
if (!validSigner) return 1;
address signer;
bool validSig;
// check if `v == 0` instead of the standard 27/28, in which case it is a contract signature
if (userOp.signature[64] == 0) {
(signer, validSig) = _isValidContractSignature(userOpHash, userOp.signature);
} else {
(signer, validSig) = _isValidECDSASignature(userOpHash, userOp.signature);
}

// to save gas, terminate early if a signature or authorization error was encountered
if (!validSig || !_isAuthorizedSigner(signer)) {
return 1;
}
}

/// @notice BLS sig aggregator and timestamp expiry are not currently supported by this contract
Expand All @@ -98,67 +109,75 @@ abstract contract AccountRails is Account, Rails, Validators, IERC1271 {
/// @dev Function to recover a signer address from the provided hash and signature
/// and then verify whether the recovered signer address is a recognized Turnkey
/// @param hash The 32 byte digest derived by hashing signed message data. Sadly, name is canonical in ERC1271.
/// @param signature The signature to be verified via recovery. Must be prepended with validator address
/// @notice To craft the signature, string concatenation or `abi.encodePacked` *must* be used
/// @param signature The signature to be verified via recovery. May be prepended with validator flag and address
/// @notice To craft the signature with prepend, string concatenation or `abi.encodePacked` *must* be used
/// Zero-padded data will fail. Ie: `abi.encodePacked(validatorData, signer, currentRSV)` is correct
/// @return magicValue The 4-byte value representing signature validity, as defined by EIP1271
/// Can be one of three values:
/// - `this.isValidSignature.selector` indicates a valid signature
/// - `bytes4(hex'ffffffff')` indicates a signature failure bubbled up from an external modular validator
/// - `bytes4(0)` indicates a default signature failure, ie not using the modular `VALIDATOR_FLAG`
function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) {
// set start index
uint256 start = 0x20;
function isValidSignature(bytes32 hash, bytes calldata signature) public view returns (bytes4 magicValue) {
// try extracting packed validator data to check for modular validation format
bytes32 data;
assembly {
data := mload(add(signature, start))
}
(bytes8 flag, address validator) = (bytes8(data), address(uint160(uint256(data))));
bytes8 flag = bytes8(signature[:8]);
address validator = address(bytes20(signature[12:32]));

// collision of a signature's first 8 bytes with flag is very unlikely; impossible when incl validator address
if (flag == VALIDATOR_FLAG && isValidator(validator)) {
uint256 len = signature.length - start;
bytes memory formattedSig = new bytes(len);

// copy relevant data into new bytes array, ie `abi.encodePacked(signer, nestedSig)`
for (uint256 i; i < len; ++i) {
formattedSig[i] = signature[start + i];
}

// format call for Validator module
bytes calldata formattedSig = signature[0x20:];
bytes4 ret = IValidator(validator).isValidSignature(hash, formattedSig);

// validator will return either correct `magicValue` or error code `INVALID_SIGNER`
magicValue = ret;
} else {
// support non-modular signatures by default
// authenticate signer using overridden internal func
bool validSigner = _defaultIsValidSignature(hash, signature);
// return `bytes4(0)` if default signature validation also fails
magicValue = validSigner ? this.isValidSignature.selector : bytes4(0);
address signer;
bool validSig;
// check if `v == 0` instead of the standard 27/28, in which case it is a contract signature
if (signature[64] == 0) {
(signer, validSig) = _isValidContractSignature(hash, signature);
} else {
(signer, validSig) = _isValidECDSASignature(hash, signature);
}

if (validSig && _isAuthorizedSigner(signer)) {
magicValue = this.isValidSignature.selector;
}
}
}

/*===============
INTERNALS
===============*/

/// @dev Function to recover and authenticate a signer address in the context of `isValidSignature()`,
function _isAuthorizedSigner(address _signer) internal view virtual returns (bool);

/// @dev Recover and authenticate a signer address in the context of `isValidSignature()`,
/// called only on signatures that were not constructed using the modular verification flag
/// @notice Accounts do not express opinion on whether the `signer` is encoded into `userOp.signature`,
/// so the OZ ECDSA library should be used rather than the SignatureChecker
function _defaultIsValidSignature(bytes32 hash, bytes memory signature) internal view virtual returns (bool);
function _isValidECDSASignature(bytes32 _hash, bytes memory _signature) internal view virtual returns (address _signer, bool _valid) {
// support non-modular signatures by recovering signer address and reverting malleable or invalid signatures
ECDSA.RecoverError err;
(_signer, err) = ECDSA.tryRecover(_hash, _signature);

// return if signature is malformed
if (err == ECDSA.RecoverError.NoError) {
_valid = true;
}
}

/// @dev Function to recover and authenticate a signer address in the context of `validateUserOp()`,
/// called only on signatures that were not constructed using the modular verification flag
/// @notice Accounts do not express opinion on whether the `signer` is available, ie encoded into `userOp.signature`,
/// so the OZ ECDSA library should be used rather than the SignatureChecker
function _defaultValidateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
internal
view
virtual
returns (bool);
/// @dev Smart contract signatures must provide a `v` value of 0 and abi encode the contract signer address into `r`
/// as well as provide an `s` value which is the current context's calldata offset where the originator's signature is located
/// In short, contract signatures follow this format: `abi.encodePacked(bytes32(signerAddress), uint256(sigOffset), uint8(0), bytes(sig))`
function _isValidContractSignature(bytes32 _hash, bytes calldata _signature) internal view virtual returns (address _signer, bool _valid) {
// for contract signatures, `_signer` refers to the contract whose address is encoded into `r` rather than signature originator
_signer = address(bytes20(_signature[12:32]));

uint256 offset = uint256(bytes32(_signature[32:64]));
bytes calldata _sig = _signature[offset:];
_valid = SignatureChecker.isValidERC1271SignatureNow(_signer, _hash, _sig);
}

/// @dev View function to limit callers to only the EntryPoint contract of this chain
function _checkSenderIsEntryPoint() internal virtual {
Expand Down
35 changes: 3 additions & 32 deletions src/cores/account/BotAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,39 +52,10 @@ contract BotAccount is AccountRails, Ownable, Initializable {
===============*/

/// @dev When evaluating signatures that don't contain the `VALIDATOR_FLAG`, authenticate only the owner
function _defaultValidateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 /*missingAccountFunds*/ )
internal
view
virtual
override
returns (bool)
{
// recover signer address and any error
(address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(userOpHash, userOp.signature);
// return if signature is malformed
if (err != ECDSA.RecoverError.NoError) return false;
// return if signer is not owner
if (signer != owner()) return false;

return true;
}

/// @dev When evaluating signatures that don't contain the `VALIDATOR_FLAG`, authenticate only the owner
function _defaultIsValidSignature(bytes32 hash, bytes memory signature)
internal
view
virtual
override
returns (bool)
{
// support non-modular signatures by recovering signer address and reverting malleable or invalid signatures
(address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(hash, signature);
// return if signature is malformed
if (err != ECDSA.RecoverError.NoError) return false;
// return if signer is not owner
if (signer != owner()) return false;
function _isAuthorizedSigner(address _signer) internal view virtual override returns (bool) {
if (hasPermission(Operations.CALL_PERMIT, _signer)) return true;

return true;
return false;
}

/// @notice This function must be overridden by contracts inheriting `Account` to delineate
Expand Down
27 changes: 9 additions & 18 deletions src/validator/CallPermitValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ contract CallPermitValidator is Validator {
bytes6 validAfter;
uint256 successData = uint256(bytes32(abi.encodePacked(authorizer, validUntil, validAfter)));

bytes memory signerData = userOp.signature[:20];
address signer = address((bytes20(signerData)));

bytes memory nestedSig = userOp.signature[20:];
// prepend is 20 since only signer remains prepended after processing validator flag
uint256 prepend = 20;
address signer = address(bytes20(userOp.signature[:prepend]));
bytes calldata nestedSig = userOp.signature[prepend:];

// terminate if recovered signer address does not match packed signer
if (!SignatureChecker.isValidSignatureNow(signer, userOpHash, nestedSig)) return SIG_VALIDATION_FAILED;
Expand All @@ -57,25 +57,16 @@ contract CallPermitValidator is Validator {
/// @param signature The signature to be recovered and verified
/// @notice The top level call context to an `Account` implementation must prepend
/// an additional 32-byte word packed with the `VALIDATOR_FLAG` and this address
function isValidSignature(bytes32 msgHash, bytes memory signature)
function isValidSignature(bytes32 msgHash, bytes calldata signature)
external
view
virtual
returns (bytes4 magicValue)
{
bytes32 signerData;
assembly {
signerData := mload(add(signature, 0x20))
}
address signer = address(bytes20(signerData));

// start is now 20th index since only signer is prepended
uint256 start = 20;
uint256 len = signature.length - start;
bytes memory nestedSig = new bytes(len);
for (uint256 i; i < len; ++i) {
nestedSig[i] = signature[start + i];
}
// prepend is 20 since only signer remains prepended after processing validator flag
uint256 prepend = 20;
address signer = address(bytes20(signature[:prepend]));
bytes calldata nestedSig = signature[prepend:];

// use SignatureChecker to evaluate `signer` and `nestedSig`
bool validSig = SignatureChecker.isValidSignatureNow(signer, msgHash, nestedSig);
Expand Down
Loading