diff --git a/contracts/src/account/AccountDelegation.sol b/contracts/src/account/ExperimentalDelegation.sol similarity index 52% rename from contracts/src/account/AccountDelegation.sol rename to contracts/src/account/ExperimentalDelegation.sol index 2b39ff4..12c5e39 100644 --- a/contracts/src/account/AccountDelegation.sol +++ b/contracts/src/account/ExperimentalDelegation.sol @@ -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 -/// @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 //////////////////////////////////////////////////////////////////////// @@ -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 //////////////////////////////////////////////////////////////////////// @@ -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(); @@ -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); } @@ -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; } @@ -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. @@ -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. @@ -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)); } } diff --git a/contracts/src/experiment/ExperimentERC20.sol b/contracts/src/experiment/ExperimentERC20.sol deleted file mode 100644 index 45bc5fe..0000000 --- a/contracts/src/experiment/ExperimentERC20.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.23; - -import {ERC20} from "solady/tokens/ERC20.sol"; - -contract ExperimentERC20 is ERC20 { - address public authorizedOrigin; - - constructor(address origin) { - authorizedOrigin = origin; - } - - function name() public view virtual override returns (string memory) { - return "ExperimentERC20"; - } - - function symbol() public view virtual override returns (string memory) { - return "EXP"; - } - - function decimals() public view virtual override returns (uint8) { - return 18; - } - - function burnForEther(uint256 amount) public virtual onlyAuthorizedOrigin { - _burn(msg.sender, amount); - payable(msg.sender).transfer(amount / 1000); - } - - function mintForEther() public payable virtual onlyAuthorizedOrigin { - uint256 amount = msg.value * 1000; - _mint(msg.sender, amount); - } - - function mint( - address to, - uint256 value - ) public virtual onlyAuthorizedOrigin { - _mint(to, value); - } - - function transfer( - address to, - uint256 amount - ) public virtual override onlyAuthorizedOrigin returns (bool) { - return super.transfer(to, amount); - } - - modifier onlyAuthorizedOrigin() { - require(tx.origin == authorizedOrigin, "unauthorized origin"); - _; - } - - fallback() external payable {} - receive() external payable {} -} diff --git a/contracts/src/utils/ECDSA.sol b/contracts/src/utils/ECDSA.sol index 063e0a6..17a9381 100644 --- a/contracts/src/utils/ECDSA.sol +++ b/contracts/src/utils/ECDSA.sol @@ -10,11 +10,6 @@ library ECDSA { struct Signature { uint256 r; uint256 s; - } - - struct RecoveredSignature { - uint256 r; - uint256 s; uint8 yParity; } } diff --git a/contracts/src/utils/MultiSend.sol b/contracts/src/utils/MultiSend.sol index 15e6834..5212cc6 100644 --- a/contracts/src/utils/MultiSend.sol +++ b/contracts/src/utils/MultiSend.sol @@ -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()) diff --git a/contracts/test/account/AccountDelegation.t.sol b/contracts/test/account/ExperimentalDelegation.t.sol similarity index 50% rename from contracts/test/account/AccountDelegation.t.sol rename to contracts/test/account/ExperimentalDelegation.t.sol index 6379b39..9d2f129 100644 --- a/contracts/test/account/AccountDelegation.t.sol +++ b/contracts/test/account/ExperimentalDelegation.t.sol @@ -4,9 +4,10 @@ pragma solidity ^0.8.23; import {Test, console2} from "forge-std/Test.sol"; import {stdJson} from "forge-std/StdJson.sol"; import {VmSafe} from "forge-std/Vm.sol"; -import {AccountDelegation} from "../../src/account/AccountDelegation.sol"; +import {ExperimentalDelegation} from "../../src/account/ExperimentalDelegation.sol"; import {P256} from "../../src/utils/P256.sol"; import {ECDSA} from "../../src/utils/ECDSA.sol"; +import {WebAuthnP256} from "../../src/utils/WebAuthnP256.sol"; contract Callee { error UnexpectedSender(address expected, address actual); @@ -26,14 +27,14 @@ contract Callee { } } -contract AccountDelegationTest is Test { - AccountDelegation public delegation; +contract ExperimentalDelegationTest is Test { + ExperimentalDelegation public delegation; uint256 public p256PrivateKey; Callee public callee; function setUp() public { callee = new Callee(); - delegation = new AccountDelegation(); + delegation = new ExperimentalDelegation(); p256PrivateKey = 100366595829038452957523597440756290436854445761208339940577349703440345778405; vm.deal(address(delegation), 1.5 ether); vm.warp(69420); @@ -44,8 +45,8 @@ contract AccountDelegationTest is Test { (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(0, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); vm.expectRevert(); delegation.keys(0); @@ -57,7 +58,7 @@ contract AccountDelegationTest is Test { assertEq(delegation.label(), "wallet"); - (uint256 expiry, AccountDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = + (uint256 expiry, ExperimentalDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = delegation.keys(0); assertEq(authorizedPublicKey.x, x); assertEq(authorizedPublicKey.y, y); @@ -69,8 +70,8 @@ contract AccountDelegationTest is Test { (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(0, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); vm.expectRevert(); delegation.keys(0); @@ -80,25 +81,57 @@ contract AccountDelegationTest is Test { delegation.authorize(key); vm.pauseGasMetering(); - (uint256 expiry, AccountDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = + (uint256 expiry, ExperimentalDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = delegation.keys(0); assertEq(authorizedPublicKey.x, x); assertEq(authorizedPublicKey.y, y); assertEq(expiry, 0); } + function test_authorize_withAuthorizedKey() public { + vm.pauseGasMetering(); + + (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); + + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + vm.expectRevert(); + delegation.keys(0); + + vm.prank(address(delegation)); + delegation.authorize(key); + + ExperimentalDelegation.Key memory nextKey = + ExperimentalDelegation.Key(69420, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + bytes32 hash = keccak256(abi.encode(delegation.nonce(), nextKey)); + (bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash); + + ExperimentalDelegation.WrappedSignature memory wrappedSignature = + ExperimentalDelegation.WrappedSignature(0, ECDSA.Signature(uint256(r), uint256(s), 0), false, "0x"); + + delegation.authorize(nextKey, abi.encode(wrappedSignature)); + + (uint256 expiry, ExperimentalDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = + delegation.keys(1); + assertEq(authorizedPublicKey.x, x); + assertEq(authorizedPublicKey.y, y); + assertEq(expiry, 69420); + } + function test_authorize_revertInvalidAuthority() public { vm.pauseGasMetering(); (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(0, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); vm.expectRevert(); delegation.keys(0); vm.resumeGasMetering(); - vm.expectRevert(AccountDelegation.InvalidAuthority.selector); + vm.expectRevert(ExperimentalDelegation.InvalidAuthority.selector); delegation.authorize(key); } @@ -107,8 +140,8 @@ contract AccountDelegationTest is Test { (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(0, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); vm.prank(address(delegation)); delegation.authorize(key); @@ -120,7 +153,7 @@ contract AccountDelegationTest is Test { delegation.revoke(0); vm.pauseGasMetering(); - (uint256 expiry, AccountDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = + (uint256 expiry, ExperimentalDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = delegation.keys(0); assertEq(expiry, 1); } @@ -141,14 +174,21 @@ contract AccountDelegationTest is Test { (bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash); (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(0, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + ExperimentalDelegation.WrappedSignature memory wrappedSignature = ExperimentalDelegation.WrappedSignature( + 0, + ECDSA.Signature(uint256(r), uint256(s), uint8(0)), + false, + abi.encode(WebAuthnP256.Metadata("0x", "aa", 0, 0, false)) + ); vm.prank(address(delegation)); delegation.authorize(key); vm.resumeGasMetering(); - delegation.execute(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false); + delegation.execute(calls, abi.encode(wrappedSignature)); vm.pauseGasMetering(); assertEq(callee.counter(address(delegation)), 3); @@ -170,8 +210,11 @@ contract AccountDelegationTest is Test { (bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash); (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(0, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + ExperimentalDelegation.WrappedSignature memory wrappedSignature = + ExperimentalDelegation.WrappedSignature(0, ECDSA.Signature(uint256(r), uint256(s), uint8(0)), false, "0x"); vm.prank(address(delegation)); delegation.authorize(key); @@ -181,8 +224,8 @@ contract AccountDelegationTest is Test { delegation.revoke(0); vm.pauseGasMetering(); - vm.expectRevert(AccountDelegation.KeyExpiredOrUnauthorized.selector); - delegation.execute(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false); + vm.expectRevert(ExperimentalDelegation.KeyExpiredOrUnauthorized.selector); + delegation.execute(calls, abi.encode(wrappedSignature)); } function test_execute_revertExpired() public { @@ -198,16 +241,19 @@ contract AccountDelegationTest is Test { (bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash); (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(block.timestamp, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(block.timestamp, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + ExperimentalDelegation.WrappedSignature memory wrappedSignature = + ExperimentalDelegation.WrappedSignature(0, ECDSA.Signature(uint256(r), uint256(s), uint8(0)), false, "0x"); vm.prank(address(delegation)); delegation.authorize(key); vm.warp(block.timestamp + 1); - vm.expectRevert(AccountDelegation.KeyExpiredOrUnauthorized.selector); - delegation.execute(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false); + vm.expectRevert(ExperimentalDelegation.KeyExpiredOrUnauthorized.selector); + delegation.execute(calls, abi.encode(wrappedSignature)); } function test_revertReplay() public { @@ -223,17 +269,60 @@ contract AccountDelegationTest is Test { (bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash); (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); - AccountDelegation.Key memory key = - AccountDelegation.Key(0, AccountDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + ExperimentalDelegation.WrappedSignature memory wrappedSignature = + ExperimentalDelegation.WrappedSignature(0, ECDSA.Signature(uint256(r), uint256(s), uint8(0)), false, "0x"); vm.prank(address(delegation)); delegation.authorize(key); vm.resumeGasMetering(); - delegation.execute(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false); + delegation.execute(calls, abi.encode(wrappedSignature)); vm.pauseGasMetering(); - vm.expectRevert(AccountDelegation.InvalidSignature.selector); - delegation.execute(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false); + vm.expectRevert(ExperimentalDelegation.InvalidSignature.selector); + delegation.execute(calls, abi.encode(wrappedSignature)); + } + + function test_isValidSignature_forOwner() public { + VmSafe.Wallet memory alice = vm.createWallet("alice"); + vm.etch(alice.addr, bytes.concat(hex"ef0100", abi.encodePacked(delegation))); + ExperimentalDelegation delegation = ExperimentalDelegation(payable(alice.addr)); + + (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + vm.prank(alice.addr); + delegation.authorize(key); + + bytes32 hash = keccak256(abi.encodePacked(delegation.nonce(), keccak256("0xdeadbeef"))); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(alice, hash); + + bytes memory signature = abi.encodePacked(uint256(r), uint256(s), uint8(v == 27 ? 0 : 1)); + + assertEq(delegation.isValidSignature(hash, signature), bytes4(keccak256("isValidSignature(bytes32,bytes)"))); + } + + function test_isValidSignature_forAuthorizingP256Key() public { + bytes32 hash = keccak256(abi.encodePacked(delegation.nonce(), keccak256("0xdeadbeef"))); + (uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey); + + ExperimentalDelegation.Key memory key = + ExperimentalDelegation.Key(0, ExperimentalDelegation.KeyType.P256, ECDSA.PublicKey(x, y)); + + vm.prank(address(delegation)); + delegation.authorize(key); + + (bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash); + ExperimentalDelegation.WrappedSignature memory wrappedSignature = + ExperimentalDelegation.WrappedSignature(0, ECDSA.Signature(uint256(r), uint256(s), uint8(0)), false, "0x"); + + assertEq( + delegation.isValidSignature(hash, abi.encode(wrappedSignature)), + bytes4(keccak256("isValidSignature(bytes32,bytes)")) + ); } } diff --git a/contracts/test/experiment/ExperimentERC20.t.sol b/contracts/test/experiment/ExperimentERC20.t.sol deleted file mode 100644 index 6d601e6..0000000 --- a/contracts/test/experiment/ExperimentERC20.t.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test, console2} from "forge-std/Test.sol"; -import {stdJson} from "forge-std/StdJson.sol"; -import {VmSafe} from "forge-std/Vm.sol"; -import {ExperimentERC20} from "../../src/experiment/ExperimentERC20.sol"; - -contract ExperimentERC20Test is Test { - ExperimentERC20 public experimentERC20; - VmSafe.Wallet public wallet; - - function setUp() public { - wallet = vm.createWallet("wallet"); - experimentERC20 = new ExperimentERC20(address(wallet.addr)); - vm.deal(address(experimentERC20), 100 ether); - } - - function test_mint() public { - vm.startBroadcast(address(wallet.addr)); - - experimentERC20.mint(address(wallet.addr), 100); - assertEq(experimentERC20.balanceOf(address(wallet.addr)), 100); - - vm.stopBroadcast(); - } - - function test_transfer(address to) public { - vm.startBroadcast(address(wallet.addr)); - - experimentERC20.mint(address(wallet.addr), 100); - experimentERC20.transfer(address(to), 50); - assertEq(experimentERC20.balanceOf(address(wallet.addr)), 50); - assertEq(experimentERC20.balanceOf(address(to)), 50); - - vm.stopBroadcast(); - } - - function test_burnForEther() public { - vm.startBroadcast(address(wallet.addr)); - - experimentERC20.mint(address(wallet.addr), 10000); - experimentERC20.burnForEther(1000); - assertEq(address(wallet.addr).balance, 1); - assertEq(experimentERC20.balanceOf(address(wallet.addr)), 9000); - - vm.stopBroadcast(); - } - - function test_mintForEther() public { - vm.startBroadcast(address(wallet.addr)); - - vm.deal(address(wallet.addr), 2 ether); - experimentERC20.mintForEther{value: 1 ether}(); - assertEq(address(wallet.addr).balance, 1 ether); - assertEq( - experimentERC20.balanceOf(address(wallet.addr)), - 1000 * 1 ether - ); - - vm.stopBroadcast(); - } - - function test_mintForEther_revertUnauthorizedOrigin() public { - vm.startBroadcast(address(this)); - - vm.deal(address(wallet.addr), 2 ether); - vm.expectRevert("unauthorized origin"); - experimentERC20.mintForEther{value: 1 ether}(); - - vm.stopBroadcast(); - } -} diff --git a/contracts/test/utils/WebAuthnP256.t.sol b/contracts/test/utils/WebAuthnP256.t.sol index f3ccbd0..7880541 100644 --- a/contracts/test/utils/WebAuthnP256.t.sol +++ b/contracts/test/utils/WebAuthnP256.t.sol @@ -26,7 +26,8 @@ contract WebAuthnP256Test is Test { function test_verify() public view { ECDSA.Signature memory signature = ECDSA.Signature( 10330677067519063752777069525326520293658884904426299601620960859195372963151, - 47017859265388077754498411591757867926785106410894171160067329762716841868244 + 47017859265388077754498411591757867926785106410894171160067329762716841868244, + 0 ); bool res = WebAuthnP256.verify(challenge, metadata, signature, publicKey); assertEq(res, true); diff --git a/src/Client.ts b/src/Client.ts index 847bffd..68793e8 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -6,7 +6,7 @@ import type * as RpcSchema from 'ox/RpcSchema' import { http, type Chain, type Transport, createClient } from 'viem' import { odysseyTestnet } from 'viem/chains' -import { accountDelegationAddress } from './generated.js' +import { experimentalDelegationAddress } from './generated.js' import * as AccountDelegation from './internal/accountDelegation.js' /** @@ -142,7 +142,7 @@ export namespace create { export const defaultParameters = { chains: [odysseyTestnet], delegations: { - [odysseyTestnet.id]: accountDelegationAddress[odysseyTestnet.id], + [odysseyTestnet.id]: experimentalDelegationAddress[odysseyTestnet.id], }, transports: { [odysseyTestnet.id]: http(), diff --git a/src/generated.ts b/src/generated.ts index 0964d68..37b7891 100644 --- a/src/generated.ts +++ b/src/generated.ts @@ -2,9 +2,6 @@ // AccountDelegation ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/** - * [__View Contract on Odyssey Testnet Odyssey Explorer__](https://odyssey-explorer.ithaca.xyz/address/0x4c40985d65D01f941981187E7c191D3B4A764dC2) - */ export const accountDelegationAbi = [ { type: 'fallback', stateMutability: 'payable' }, { type: 'receive', stateMutability: 'payable' }, @@ -33,6 +30,7 @@ export const accountDelegationAbi = [ }, ], }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, ], name: 'authorize', outputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], @@ -63,16 +61,6 @@ export const accountDelegationAbi = [ }, ], }, - { - name: 'signature', - internalType: 'struct ECDSA.RecoveredSignature', - type: 'tuple', - components: [ - { name: 'r', internalType: 'uint256', type: 'uint256' }, - { name: 's', internalType: 'uint256', type: 'uint256' }, - { name: 'yParity', internalType: 'uint8', type: 'uint8' }, - ], - }, ], name: 'authorize', outputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], @@ -89,52 +77,7 @@ export const accountDelegationAbi = [ type: 'function', inputs: [ { name: 'calls', internalType: 'bytes', type: 'bytes' }, - { - name: 'signature', - internalType: 'struct ECDSA.Signature', - type: 'tuple', - components: [ - { name: 'r', internalType: 'uint256', type: 'uint256' }, - { name: 's', internalType: 'uint256', type: 'uint256' }, - ], - }, - { - name: 'metadata', - internalType: 'struct WebAuthnP256.Metadata', - type: 'tuple', - components: [ - { name: 'authenticatorData', internalType: 'bytes', type: 'bytes' }, - { name: 'clientDataJSON', internalType: 'string', type: 'string' }, - { name: 'challengeIndex', internalType: 'uint16', type: 'uint16' }, - { name: 'typeIndex', internalType: 'uint16', type: 'uint16' }, - { - name: 'userVerificationRequired', - internalType: 'bool', - type: 'bool', - }, - ], - }, - { name: 'keyIndex', internalType: 'uint32', type: 'uint32' }, - ], - name: 'execute', - outputs: [], - stateMutability: 'nonpayable', - }, - { - type: 'function', - inputs: [ - { name: 'calls', internalType: 'bytes', type: 'bytes' }, - { - name: 'signature', - internalType: 'struct ECDSA.Signature', - type: 'tuple', - components: [ - { name: 'r', internalType: 'uint256', type: 'uint256' }, - { name: 's', internalType: 'uint256', type: 'uint256' }, - ], - }, - { name: 'keyIndex', internalType: 'uint32', type: 'uint32' }, - { name: 'prehash', internalType: 'bool', type: 'bool' }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, ], name: 'execute', outputs: [], @@ -229,7 +172,7 @@ export const accountDelegationAbi = [ }, { name: 'signature', - internalType: 'struct ECDSA.RecoveredSignature', + internalType: 'struct ECDSA.Signature', type: 'tuple', components: [ { name: 'r', internalType: 'uint256', type: 'uint256' }, @@ -242,6 +185,16 @@ export const accountDelegationAbi = [ outputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], stateMutability: 'nonpayable', }, + { + type: 'function', + inputs: [ + { name: 'digest', internalType: 'bytes32', type: 'bytes32' }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, + ], + name: 'isValidSignature', + outputs: [{ name: 'magicValue', internalType: 'bytes4', type: 'bytes4' }], + stateMutability: 'view', + }, { type: 'function', inputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], @@ -272,13 +225,6 @@ export const accountDelegationAbi = [ outputs: [{ name: '', internalType: 'string', type: 'string' }], stateMutability: 'view', }, - { - type: 'function', - inputs: [{ name: 'transactions', internalType: 'bytes', type: 'bytes' }], - name: 'multiSend', - outputs: [], - stateMutability: 'payable', - }, { type: 'function', inputs: [], @@ -297,16 +243,7 @@ export const accountDelegationAbi = [ type: 'function', inputs: [ { name: 'keyIndex', internalType: 'uint32', type: 'uint32' }, - { - name: 'signature', - internalType: 'struct ECDSA.RecoveredSignature', - type: 'tuple', - components: [ - { name: 'r', internalType: 'uint256', type: 'uint256' }, - { name: 's', internalType: 'uint256', type: 'uint256' }, - { name: 'yParity', internalType: 'uint8', type: 'uint8' }, - ], - }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, ], name: 'revoke', outputs: [], @@ -318,21 +255,6 @@ export const accountDelegationAbi = [ { type: 'error', inputs: [], name: 'KeyExpiredOrUnauthorized' }, ] as const -/** - * [__View Contract on Odyssey Testnet Odyssey Explorer__](https://odyssey-explorer.ithaca.xyz/address/0x4c40985d65D01f941981187E7c191D3B4A764dC2) - */ -export const accountDelegationAddress = { - 911867: '0x4c40985d65D01f941981187E7c191D3B4A764dC2', -} as const - -/** - * [__View Contract on Odyssey Testnet Odyssey Explorer__](https://odyssey-explorer.ithaca.xyz/address/0x4c40985d65D01f941981187E7c191D3B4A764dC2) - */ -export const accountDelegationConfig = { - address: accountDelegationAddress, - abi: accountDelegationAbi, -} as const - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ERC20 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -709,6 +631,288 @@ export const experimentErc20Config = { abi: experimentErc20Abi, } as const +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ExperimentalDelegation +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * [__View Contract on Odyssey Testnet Odyssey Explorer__](https://odyssey-explorer.ithaca.xyz/address/0x50E075Dd0620DC401c0F0Cda83D0Eba170429AAc) + */ +export const experimentalDelegationAbi = [ + { type: 'fallback', stateMutability: 'payable' }, + { type: 'receive', stateMutability: 'payable' }, + { + type: 'function', + inputs: [], + name: 'OWNER_METADATA', + outputs: [{ name: '', internalType: 'bytes', type: 'bytes' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { + name: 'key', + internalType: 'struct ExperimentalDelegation.Key', + type: 'tuple', + components: [ + { name: 'expiry', internalType: 'uint256', type: 'uint256' }, + { + name: 'keyType', + internalType: 'enum ExperimentalDelegation.KeyType', + type: 'uint8', + }, + { + name: 'publicKey', + internalType: 'struct ECDSA.PublicKey', + type: 'tuple', + components: [ + { name: 'x', internalType: 'uint256', type: 'uint256' }, + { name: 'y', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, + ], + name: 'authorize', + outputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { + name: 'key', + internalType: 'struct ExperimentalDelegation.Key', + type: 'tuple', + components: [ + { name: 'expiry', internalType: 'uint256', type: 'uint256' }, + { + name: 'keyType', + internalType: 'enum ExperimentalDelegation.KeyType', + type: 'uint8', + }, + { + name: 'publicKey', + internalType: 'struct ECDSA.PublicKey', + type: 'tuple', + components: [ + { name: 'x', internalType: 'uint256', type: 'uint256' }, + { name: 'y', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + name: 'authorize', + outputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'calls', internalType: 'bytes', type: 'bytes' }], + name: 'execute', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'calls', internalType: 'bytes', type: 'bytes' }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, + ], + name: 'execute', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [], + name: 'getKeys', + outputs: [ + { + name: '', + internalType: 'struct ExperimentalDelegation.Key[]', + type: 'tuple[]', + components: [ + { name: 'expiry', internalType: 'uint256', type: 'uint256' }, + { + name: 'keyType', + internalType: 'enum ExperimentalDelegation.KeyType', + type: 'uint8', + }, + { + name: 'publicKey', + internalType: 'struct ECDSA.PublicKey', + type: 'tuple', + components: [ + { name: 'x', internalType: 'uint256', type: 'uint256' }, + { name: 'y', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'label_', internalType: 'string', type: 'string' }, + { + name: 'key', + internalType: 'struct ExperimentalDelegation.Key', + type: 'tuple', + components: [ + { name: 'expiry', internalType: 'uint256', type: 'uint256' }, + { + name: 'keyType', + internalType: 'enum ExperimentalDelegation.KeyType', + type: 'uint8', + }, + { + name: 'publicKey', + internalType: 'struct ECDSA.PublicKey', + type: 'tuple', + components: [ + { name: 'x', internalType: 'uint256', type: 'uint256' }, + { name: 'y', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + name: 'initialize', + outputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'label_', internalType: 'string', type: 'string' }, + { + name: 'key', + internalType: 'struct ExperimentalDelegation.Key', + type: 'tuple', + components: [ + { name: 'expiry', internalType: 'uint256', type: 'uint256' }, + { + name: 'keyType', + internalType: 'enum ExperimentalDelegation.KeyType', + type: 'uint8', + }, + { + name: 'publicKey', + internalType: 'struct ECDSA.PublicKey', + type: 'tuple', + components: [ + { name: 'x', internalType: 'uint256', type: 'uint256' }, + { name: 'y', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + { + name: 'signature', + internalType: 'struct ECDSA.Signature', + type: 'tuple', + components: [ + { name: 'r', internalType: 'uint256', type: 'uint256' }, + { name: 's', internalType: 'uint256', type: 'uint256' }, + { name: 'yParity', internalType: 'uint8', type: 'uint8' }, + ], + }, + ], + name: 'initialize', + outputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'digest', internalType: 'bytes32', type: 'bytes32' }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, + ], + name: 'isValidSignature', + outputs: [{ name: 'magicValue', internalType: 'bytes4', type: 'bytes4' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + name: 'keys', + outputs: [ + { name: 'expiry', internalType: 'uint256', type: 'uint256' }, + { + name: 'keyType', + internalType: 'enum ExperimentalDelegation.KeyType', + type: 'uint8', + }, + { + name: 'publicKey', + internalType: 'struct ECDSA.PublicKey', + type: 'tuple', + components: [ + { name: 'x', internalType: 'uint256', type: 'uint256' }, + { name: 'y', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'label', + outputs: [{ name: '', internalType: 'string', type: 'string' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'nonce', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'keyIndex', internalType: 'uint32', type: 'uint32' }], + name: 'revoke', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'keyIndex', internalType: 'uint32', type: 'uint32' }, + { name: 'signature', internalType: 'bytes', type: 'bytes' }, + ], + name: 'revoke', + outputs: [], + stateMutability: 'nonpayable', + }, + { type: 'error', inputs: [], name: 'AlreadyInitialized' }, + { type: 'error', inputs: [], name: 'InvalidAuthority' }, + { type: 'error', inputs: [], name: 'InvalidSignature' }, + { type: 'error', inputs: [], name: 'KeyExpiredOrUnauthorized' }, +] as const + +/** + * [__View Contract on Odyssey Testnet Odyssey Explorer__](https://odyssey-explorer.ithaca.xyz/address/0x50E075Dd0620DC401c0F0Cda83D0Eba170429AAc) + */ +export const experimentalDelegationAddress = { + 911867: '0x50E075Dd0620DC401c0F0Cda83D0Eba170429AAc', +} as const + +/** + * [__View Contract on Odyssey Testnet Odyssey Explorer__](https://odyssey-explorer.ithaca.xyz/address/0x50E075Dd0620DC401c0F0Cda83D0Eba170429AAc) + */ +export const experimentalDelegationConfig = { + address: experimentalDelegationAddress, + abi: experimentalDelegationAbi, +} as const + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // IMulticall3 ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -952,20 +1156,6 @@ export const iMulticall3Abi = [ }, ] as const -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// MultiSendCallOnly -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -export const multiSendCallOnlyAbi = [ - { - type: 'function', - inputs: [{ name: 'transactions', internalType: 'bytes', type: 'bytes' }], - name: 'multiSend', - outputs: [], - stateMutability: 'payable', - }, -] as const - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Receiver ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/internal/accountDelegation.ts b/src/internal/accountDelegation.ts index bbf197b..1f08c81 100644 --- a/src/internal/accountDelegation.ts +++ b/src/internal/accountDelegation.ts @@ -6,13 +6,14 @@ import { Hex, type PublicKey, Secp256k1, + type Signature, WebAuthnP256, } from 'ox' import type { Chain, Client, Transport } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { readContract, writeContract } from 'viem/actions' import { signAuthorization } from 'viem/experimental' -import { accountDelegationAbi } from '../generated.js' +import { experimentalDelegationAbi } from '../generated.js' //////////////////////////////////////////////////////////// // Types @@ -148,7 +149,7 @@ export async function initialize( }) const hash = await writeContract(client, { - abi: accountDelegationAbi, + abi: experimentalDelegationAbi, address: authority, functionName: 'initialize', args: [label, key_, signature], @@ -183,12 +184,12 @@ export async function load( const [keys, label] = await Promise.all([ readContract(client, { - abi: accountDelegationAbi, + abi: experimentalDelegationAbi, address, functionName: 'getKeys', }), readContract(client, { - abi: accountDelegationAbi, + abi: experimentalDelegationAbi, address, functionName: 'label', }), @@ -219,12 +220,12 @@ export async function execute( const { account, calls } = parameters const nonce = await readContract(client, { - abi: accountDelegationAbi, + abi: experimentalDelegationAbi, address: account.address, functionName: 'nonce', }) - const calls_encoded = Hex.concat( + const encodedCalls = Hex.concat( ...calls.map((call) => AbiParameters.encodePacked( ['uint8', 'address', 'uint256', 'uint256', 'bytes'], @@ -240,7 +241,7 @@ export async function execute( ) const challenge = Hash.keccak256( - AbiParameters.encodePacked(['uint256', 'bytes'], [nonce, calls_encoded]), + AbiParameters.encodePacked(['uint256', 'bytes'], [nonce, encodedCalls]), ) const key = account.keys[0]! @@ -251,11 +252,16 @@ export async function execute( credentialId: key.id, }) + const wrappedSignature = getWrappedSignature({ + metadata: getWebAuthnMetadata(metadata), + signature, + }) + return await writeContract(client, { - abi: accountDelegationAbi, + abi: experimentalDelegationAbi, address: account.address, functionName: 'execute', - args: [calls_encoded, { r: signature.r, s: signature.s }, metadata, 0], + args: [encodedCalls, wrappedSignature], account: null, chain: null, }) @@ -267,3 +273,51 @@ export declare namespace execute { calls: Calls } } + +function getWrappedSignature(parameters: { + keyIndex?: number | undefined + metadata?: Hex.Hex | undefined + prehash?: boolean | undefined + signature: Signature.Signature +}) { + const { + keyIndex = 0, + metadata = '0x', + prehash = false, + signature, + } = parameters + return AbiParameters.encode( + AbiParameters.from([ + 'struct Signature { uint256 r; uint256 s; uint8 yParity; }', + 'uint32 keyIndex', + 'ECDSA.Signature signature', + 'bool prehash', + 'bytes metadata', + ]), + [ + keyIndex, + { r: signature.r, s: signature.s, yParity: 0 }, + prehash, + metadata, + ], + ) +} + +function getWebAuthnMetadata(metadata: WebAuthnP256.SignMetadata) { + return AbiParameters.encode( + AbiParameters.from([ + 'bytes authenticatorData', + 'string clientDataJSON', + 'uint16 challengeIndex', + 'uint16 typeIndex', + 'bool userVerificationRequired', + ]), + [ + metadata.authenticatorData, + metadata.clientDataJSON, + metadata.challengeIndex, + metadata.typeIndex, + metadata.userVerificationRequired, + ], + ) +} diff --git a/wagmi.config.ts b/wagmi.config.ts index 45c3421..650c6eb 100644 --- a/wagmi.config.ts +++ b/wagmi.config.ts @@ -8,8 +8,8 @@ export default defineConfig({ plugins: [ foundry({ deployments: { - AccountDelegation: { - [odysseyTestnet.id]: '0x00d2C28AB9DB357f52706E86277B2Ed3541322C2', + ExperimentalDelegation: { + [odysseyTestnet.id]: '0x50E075Dd0620DC401c0F0Cda83D0Eba170429AAc', }, ExperimentERC20: { [odysseyTestnet.id]: '0x238c8CD93ee9F8c7Edf395548eF60c0d2e46665E',