Skip to content

Commit

Permalink
feat(contract): universal signatures; erc-1271
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Nov 13, 2024
1 parent 91e7696 commit 48a81ed
Show file tree
Hide file tree
Showing 11 changed files with 633 additions and 363 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {ECDSA} from "../utils/ECDSA.sol";
import {P256} from "../utils/P256.sol";
import {WebAuthnP256} from "../utils/WebAuthnP256.sol";

/// @title AccountDelegation
/// @title ExperimentalDelegation
/// @author jxom <https://github.com/jxom>
/// @notice EIP-7702 Delegation contract that allows authorized Keys to invoke calls on behalf of an EOA.
contract AccountDelegation is Receiver, MultiSendCallOnly {
/// @notice Experimental EIP-7702 Delegation contract that allows authorized Keys to invoke calls on behalf of an EOA.
contract ExperimentalDelegation is Receiver, MultiSendCallOnly {
////////////////////////////////////////////////////////////////////////
// Data Structures
////////////////////////////////////////////////////////////////////////
Expand All @@ -33,6 +33,23 @@ contract AccountDelegation is Receiver, MultiSendCallOnly {
ECDSA.PublicKey publicKey;
}

/// @notice A wrapped signature.
/// @custom:property keyIndex - The index of the authorized key.
/// @custom:property signature - The ECDSA signature.
/// @custom:property metadata - (Optional) Key-specific metadata.
struct WrappedSignature {
uint32 keyIndex;
ECDSA.Signature signature;
bool prehash;
bytes metadata;
}

////////////////////////////////////////////////////////////////////////
// Constants
////////////////////////////////////////////////////////////////////////

bytes public constant OWNER_METADATA = abi.encodePacked(bytes4(0xdeadbeef));

////////////////////////////////////////////////////////////////////////
// Errors
////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -83,10 +100,17 @@ contract AccountDelegation is Receiver, MultiSendCallOnly {
function initialize(
string calldata label_,
Key calldata key,
ECDSA.RecoveredSignature calldata signature
ECDSA.Signature calldata signature
) public returns (uint32 keyIndex) {
bytes32 digest = keccak256(abi.encode(nonce++, label_, key));
_ecverify(digest, signature);

address signer = ecrecover(
digest,
signature.yParity == 0 ? 27 : 28,
bytes32(signature.r),
bytes32(signature.s)
);
if (signer != address(this)) revert InvalidSignature();

if (keys.length > 0) revert AlreadyInitialized();

Expand All @@ -108,10 +132,16 @@ contract AccountDelegation is Receiver, MultiSendCallOnly {
/// @param signature - The signature over the key: `sign(keccak256(abi.encode(nonce, key)))`.
function authorize(
Key calldata key,
ECDSA.RecoveredSignature calldata signature
bytes calldata signature
) public returns (uint32 keyIndex) {
WrappedSignature memory wrappedSignature = _parseSignature(signature);
Key memory authorizingKey = keys[wrappedSignature.keyIndex];

// Revert for expiring keys. Assume that non-expiring keys are "admins".
if (authorizingKey.expiry != 0) revert KeyExpiredOrUnauthorized();

bytes32 digest = keccak256(abi.encode(nonce++, key));
_ecverify(digest, signature);
_assertSignature(digest, signature);

return _authorize(key);
}
Expand All @@ -125,12 +155,15 @@ contract AccountDelegation is Receiver, MultiSendCallOnly {
/// @notice Revokes an authorized public key on behalf of the EOA, provided the EOA's signature.
/// @param keyIndex - The index of the public key to revoke.
/// @param signature - The signature over the key index: `sign(keccak256(abi.encodePacked(nonce, keyIndex)))`.
function revoke(
uint32 keyIndex,
ECDSA.RecoveredSignature calldata signature
) public {
function revoke(uint32 keyIndex, bytes calldata signature) public {
WrappedSignature memory wrappedSignature = _parseSignature(signature);
Key memory authorizingKey = keys[wrappedSignature.keyIndex];

// Revert for expiring keys. Assume that non-expiring keys are "admins".
if (authorizingKey.expiry != 0) revert KeyExpiredOrUnauthorized();

bytes32 digest = keccak256(abi.encodePacked(nonce++, keyIndex));
_ecverify(digest, signature);
_assertSignature(digest, signature);

keys[keyIndex].expiry = 1;
}
Expand All @@ -141,53 +174,75 @@ contract AccountDelegation is Receiver, MultiSendCallOnly {
multiSend(calls);
}

/// @notice Executes a set of calls on behalf of the EOA, provided a P256 signature over the calls and a public key index.
/// @notice Executes a set of calls on behalf of the EOA, provided a signature that was signed by an authorized key.
/// @param calls - The calls to execute.
/// @param signature - The signature over the calls: `sign(keccak256(abi.encodePacked(nonce, calls)))`.
/// @param keyIndex - The index of the authorized public key to use.
/// @param prehash - Whether to SHA-256 hash the digest.
function execute(
bytes memory calls,
ECDSA.Signature memory signature,
uint32 keyIndex,
bool prehash
) public {
_assertAuthorized(keyIndex);

/// @param signature - The wrapped signature over the calls.
function execute(bytes memory calls, bytes calldata signature) public {
bytes32 digest = keccak256(abi.encodePacked(nonce++, calls));

Key memory key = keys[keyIndex];
if (prehash) digest = sha256(abi.encodePacked(digest));
if (!P256.verify(digest, signature, key.publicKey)) {
revert InvalidSignature();
}
_assertSignature(digest, signature);

multiSend(calls);
}

/// @notice Executes a set of calls on behalf of the EOA, provided a WebAuthn-wrapped P256 signature over the calls, the WebAuthn metadata, and an invoker index.
/// @param calls - The calls to execute.
/// @param signature - The signature over the calls: `sign(keccak256(abi.encodePacked(nonce, calls)))`.
/// @param metadata - The WebAuthn metadata.
/// @param keyIndex - The index of the authorized public key to use.
function execute(
bytes memory calls,
ECDSA.Signature memory signature,
WebAuthnP256.Metadata memory metadata,
uint32 keyIndex
) public {
_assertAuthorized(keyIndex);

bytes32 challenge = keccak256(abi.encodePacked(nonce++, calls));

Key memory key = keys[keyIndex];
if (
!WebAuthnP256.verify(challenge, metadata, signature, key.publicKey)
) {
revert InvalidSignature();
/// @notice Checks if a signature is valid.
/// @param digest - The digest to verify.
/// @param signature - The wrapped signature to verify.
/// @return magicValue - The magic value indicating the validity of the signature.
function isValidSignature(
bytes32 digest,
bytes calldata signature
) public view returns (bytes4 magicValue) {
WrappedSignature memory wrappedSignature = _parseSignature(signature);

// If prehash flag is set (usually for WebCrypto P256), SHA-256 hash the digest.
if (wrappedSignature.prehash) digest = sha256(abi.encodePacked(digest));

bytes4 success = bytes4(keccak256("isValidSignature(bytes32,bytes)"));
bytes4 failure = bytes4(0);

// If the signature was computed by the EOA, the signature is valid.
if (keccak256(wrappedSignature.metadata) == keccak256(OWNER_METADATA)) {
if (
ecrecover(
digest,
wrappedSignature.signature.yParity == 0 ? 27 : 28,
bytes32(wrappedSignature.signature.r),
bytes32(wrappedSignature.signature.s)
) == address(this)
) return success;
}

multiSend(calls);
if (keys.length > 0) {
Key memory key = keys[wrappedSignature.keyIndex];

// If the key has expired, the signature is invalid.
if (key.expiry > 0 && key.expiry < block.timestamp) return failure;

// Verify based on key type.
if (
key.keyType == KeyType.P256 &&
P256.verify(digest, wrappedSignature.signature, key.publicKey)
) {
return success;
}
if (key.keyType == KeyType.WebAuthnP256) {
WebAuthnP256.Metadata memory metadata = abi.decode(
wrappedSignature.metadata,
(WebAuthnP256.Metadata)
);
if (
WebAuthnP256.verify(
digest,
metadata,
wrappedSignature.signature,
key.publicKey
)
) return success;
}
}

// If the signature is not valid, return the failure magic value.
return failure;
}

/// @notice Gets the keys associated with the EOA.
Expand All @@ -209,14 +264,6 @@ contract AccountDelegation is Receiver, MultiSendCallOnly {
// Internal
////////////////////////////////////////////////////////////////////////

/// @notice Ensures a key is authorized.
function _assertAuthorized(uint32 keyIndex) internal view {
Key memory key = keys[keyIndex];
if (key.expiry > 0 && key.expiry < block.timestamp) {
revert KeyExpiredOrUnauthorized();
}
}

/// @notice Authorizes a new public key.
/// @param key - The key to authorize.
/// @return keyIndex - The index of the authorized key.
Expand All @@ -225,19 +272,46 @@ contract AccountDelegation is Receiver, MultiSendCallOnly {
return uint32(keys.length - 1);
}

/// @notice Verifies the signature and returns the signer address.
/// @param digest - The message digest.
/// @param signature - The signature to verify.
function _ecverify(
/// @notice Asserts that a signature is valid.
/// @param digest - The digest to verify.
/// @param signature - The wrapped signature to verify.
function _assertSignature(
bytes32 digest,
ECDSA.RecoveredSignature calldata signature
) private view {
address signer = ecrecover(
digest,
signature.yParity == 0 ? 27 : 28,
bytes32(signature.r),
bytes32(signature.s)
bytes calldata signature
) internal view {
WrappedSignature memory wrappedSignature = abi.decode(
signature,
(WrappedSignature)
);
if (signer != address(this)) revert InvalidSignature();

Key memory key = keys[wrappedSignature.keyIndex];
if (key.expiry > 0 && key.expiry < block.timestamp) {
revert KeyExpiredOrUnauthorized();
}

if (isValidSignature(digest, signature) == bytes4(0)) {
revert InvalidSignature();
}
}

/// @notice Parses a signature from bytes format.
/// @param signature - The signature to parse.
/// @return wrappedSignature - The parsed signature.
function _parseSignature(
bytes calldata signature
) internal pure returns (WrappedSignature memory) {
if (signature.length == 65) {
bytes32 r = bytes32(signature[0:32]);
bytes32 s = bytes32(signature[32:64]);
uint8 yParity = uint8(signature[64]);
return
WrappedSignature(
0,
ECDSA.Signature(uint256(r), uint256(s), yParity),
false,
OWNER_METADATA
);
}
return abi.decode(signature, (WrappedSignature));
}
}
56 changes: 0 additions & 56 deletions contracts/src/experiment/ExperimentERC20.sol

This file was deleted.

5 changes: 0 additions & 5 deletions contracts/src/utils/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ library ECDSA {
struct Signature {
uint256 r;
uint256 s;
}

struct RecoveredSignature {
uint256 r;
uint256 s;
uint8 yParity;
}
}
8 changes: 2 additions & 6 deletions contracts/src/utils/MultiSend.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,9 @@ contract MultiSendCallOnly {
let data := add(transactions, add(i, 0x55))
let success := 0
switch operation
case 0 {
success := call(gas(), to, value, data, dataLength, 0, 0)
}
case 0 { success := call(gas(), to, value, data, dataLength, 0, 0) }
// This version does not allow delegatecalls
case 1 {
revert(0, 0)
}
case 1 { revert(0, 0) }
if eq(success, 0) {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
Expand Down
Loading

0 comments on commit 48a81ed

Please sign in to comment.