Skip to content

Commit

Permalink
feat: tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Sep 25, 2024
1 parent b6b5397 commit 3002f0d
Show file tree
Hide file tree
Showing 16 changed files with 623 additions and 262 deletions.
6 changes: 3 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/lib/openzeppelin-contracts"]
path = contracts/lib/openzeppelin-contracts
url = git@github.com:OpenZeppelin/openzeppelin-contracts.git
[submodule "contracts/lib/solady"]
path = contracts/lib/solady
url = git@github.com:Vectorized/solady.git
1 change: 0 additions & 1 deletion contracts/lib/openzeppelin-contracts
Submodule openzeppelin-contracts deleted from 2f0bc5
1 change: 1 addition & 0 deletions contracts/lib/solady
Submodule solady added at 42af39
72 changes: 49 additions & 23 deletions contracts/src/P256BatchDelegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.23;

import {MultiSendCallOnly} from "./MultiSend.sol";
import "./utils/ECDSA.sol";
import "./utils/P256.sol";
import "./utils/WebAuthnP256.sol";

Expand All @@ -16,24 +17,29 @@ contract P256BatchDelegation is MultiSendCallOnly {
error InvalidSignature();

/// @notice List of authorized delegate public keys.
P256.PublicKey[] public delegates;
ECDSA.PublicKey[] public delegates;

/// @notice Internal nonce used for replay protection.
uint256 public nonce;
/// @notice Internal `authorize` nonce used for replay protection.
uint256 public authorizeNonce;

/// @notice Authorizes a new public key to execute on behalf of the Authority.
/// @notice Internal `execute` nonce used for replay protection.
uint256 public executeNonce;

/// @notice Authorizes a new public key.
/// @param publicKey - The public key to authorize.
function authorize(P256.PublicKey calldata publicKey) public {
function authorize(ECDSA.PublicKey calldata publicKey) public {
if (msg.sender != address(this)) revert InvalidAuthority();
delegates.push(publicKey);
}

/// @notice Authorizes multiple public keys to execute on behalf of the Authority.
/// @param publicKeys - The public keys to authorize.
function authorize(P256.PublicKey[] calldata publicKeys) public {
for (uint32 i = 0; i < publicKeys.length; i++) {
authorize(publicKeys[i]);
}
/// @notice Authorizes a new public key on behalf of the Authority, provided the Authority's signature.
/// @param publicKey - The public key to authorize.
/// @param signature - EOA secp256k1 signature over the public key.
function authorize(ECDSA.PublicKey calldata publicKey, ECDSA.RecoveredSignature calldata signature) public {
bytes32 digest = keccak256(abi.encodePacked(authorizeNonce++, publicKey.x, publicKey.y));
address signer = ecrecover(digest, signature.v, bytes32(signature.r), bytes32(signature.s));
if (signer != address(this)) revert InvalidSignature();
delegates.push(publicKey);
}

/// @notice Revokes a delegate public key.
Expand All @@ -44,20 +50,37 @@ contract P256BatchDelegation is MultiSendCallOnly {
delegates.pop();
}

/// @notice Revokes multiple delegate public keys.
/// @param delegateIndices - The indices of the public keys to revoke.
function revoke(uint32[] calldata delegateIndices) public {
for (uint32 i = 0; i < delegateIndices.length; i++) {
revoke(delegateIndices[i]);
}
/// @notice Revokes a delegate public key on behalf of the Authority, provided the Authority's signature.
/// @param delegateIndex - The index of the public key to revoke.
/// @param signature - EOA secp256k1 signature over the delegate index.
function revoke(uint32 delegateIndex, ECDSA.RecoveredSignature calldata signature) public {
bytes32 digest = keccak256(abi.encodePacked(authorizeNonce++, delegateIndex));
address signer = ecrecover(digest, signature.v, bytes32(signature.r), bytes32(signature.s));
if (signer != address(this)) revert InvalidSignature();
delegates[delegateIndex] = delegates[delegates.length - 1];
delegates.pop();
}

/// @notice Executes a set of calls.
/// @param calls - The calls to execute.
function execute(bytes memory calls) public {
if (msg.sender != address(this)) revert InvalidAuthority();
multiSend(calls);
}

/// @notice Executes a set of calls on behalf of the Authority, provided a P256 signature over the calls and a delegate index.
/// @param calls - The calls to execute.
/// @param signature - The P256 signature over the calls: `p256.sign(keccak256(nonce ‖ calls))`.
/// @param delegateIndex - The index of the delegate public key to use.
function execute(bytes memory calls, P256.Signature memory signature, uint32 delegateIndex) public {
bytes32 digest = keccak256(abi.encodePacked(nonce++, calls));
/// @param prehash - Whether to SHA-256 hash the digest.
function executeWithDelegate(
bytes memory calls,
ECDSA.Signature memory signature,
uint32 delegateIndex,
bool prehash
) public {
bytes32 digest = keccak256(abi.encodePacked(executeNonce++, calls));
if (prehash) digest = sha256(abi.encodePacked(digest));
if (!P256.verify(digest, signature, delegates[delegateIndex])) {
revert InvalidSignature();
}
Expand All @@ -69,13 +92,16 @@ contract P256BatchDelegation is MultiSendCallOnly {
/// @param signature - The WebAuthn-wrapped P256 signature over the calls: `p256.sign(keccak256(nonce ‖ calls))`.
/// @param metadata - The WebAuthn metadata.
/// @param delegateIndex - The index of the delegate public key to use.
function executeWebAuthn(
/// @param prehash - Whether to SHA-256 hash the digest.
function executeWithDelegate(
bytes memory calls,
P256.Signature memory signature,
ECDSA.Signature memory signature,
WebAuthnP256.Metadata memory metadata,
uint32 delegateIndex
uint32 delegateIndex,
bool prehash
) public {
bytes32 challenge = keccak256(abi.encodePacked(nonce++, calls));
bytes32 challenge = keccak256(abi.encodePacked(executeNonce++, calls));
if (prehash) challenge = sha256(abi.encodePacked(challenge));
if (!WebAuthnP256.verify(challenge, metadata, signature, delegates[delegateIndex])) revert InvalidSignature();
multiSend(calls);
}
Expand Down
31 changes: 0 additions & 31 deletions contracts/src/utils/Base64URL.sol

This file was deleted.

20 changes: 20 additions & 0 deletions contracts/src/utils/ECDSA.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

library ECDSA {
struct PublicKey {
uint256 x;
uint256 y;
}

struct Signature {
uint256 r;
uint256 s;
}

struct RecoveredSignature {
uint256 r;
uint256 s;
uint8 v;
}
}
14 changes: 3 additions & 11 deletions contracts/src/utils/P256.sol
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "./ECDSA.sol";

//( @title P256
/// @author klkvr <https://github.com/klkvr>
/// @author jxom <https://github.com/jxom>
/// @notice Wrapper function to abstract low level details of call to the P256
/// signature verification precompile as defined in EIP-7212, see
/// <https://eips.ethereum.org/EIPS/eip-7212>.
library P256 {
struct PublicKey {
uint256 x;
uint256 y;
}

struct Signature {
uint256 r;
uint256 s;
}

/// @notice P256VERIFY operation
/// @param digest 32 bytes of the signed data hash
/// @param signature Signature of the signer
/// @param publicKey Public key of the signer
/// @return success Represents if the operation was successful
function verify(bytes32 digest, P256.Signature memory signature, P256.PublicKey memory publicKey)
function verify(bytes32 digest, ECDSA.Signature memory signature, ECDSA.PublicKey memory publicKey)
internal
view
returns (bool)
Expand Down
9 changes: 5 additions & 4 deletions contracts/src/utils/WebAuthnP256.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "./Base64URL.sol";
import {Base64} from "solady/utils/Base64.sol";
import "./ECDSA.sol";
import "./P256.sol";

/// @title WebAuthnP256
Expand Down Expand Up @@ -123,8 +124,8 @@ library WebAuthnP256 {
function verify(
bytes32 challenge,
Metadata memory metadata,
P256.Signature memory signature,
P256.PublicKey memory publicKey
ECDSA.Signature memory signature,
ECDSA.PublicKey memory publicKey
) internal view returns (bool) {
// Check that authenticatorData has good flags
if (
Expand All @@ -141,7 +142,7 @@ library WebAuthnP256 {
}

// Check that challenge is in the clientDataJSON
string memory challengeB64url = Base64URL.encode(abi.encodePacked(challenge));
string memory challengeB64url = Base64.encode(abi.encodePacked(challenge), true, true);
string memory challengeProperty = string.concat('"challenge":"', challengeB64url, '"');

if (!contains(challengeProperty, metadata.clientDataJSON, metadata.challengeIndex)) {
Expand Down
71 changes: 58 additions & 13 deletions contracts/test/P256BatchDelegation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {stdJson} from "forge-std/StdJson.sol";
import {VmSafe} from "forge-std/Vm.sol";
import {P256BatchDelegation} from "../src/P256BatchDelegation.sol";
import {P256} from "../src/utils/P256.sol";
import {ECDSA} from "../src/utils/ECDSA.sol";

contract Callee {
error UnexpectedSender(address expected, address actual);
Expand All @@ -19,7 +20,9 @@ contract Callee {
}

function expectSender(address expected) public payable {
if (msg.sender != expected) revert UnexpectedSender(expected, msg.sender);
if (msg.sender != expected) {
revert UnexpectedSender(expected, msg.sender);
}
}
}

Expand All @@ -35,7 +38,46 @@ contract P256BatchDelegationTest is Test {
vm.deal(address(delegation), 1.5 ether);
}

function test_execute() public {
function test_authorize() public {
vm.pauseGasMetering();

(uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey);
ECDSA.PublicKey memory publicKey = ECDSA.PublicKey(x, y);

vm.expectRevert();
delegation.delegates(0);

vm.prank(address(delegation));
vm.resumeGasMetering();
delegation.authorize(publicKey);
vm.pauseGasMetering();

(uint256 authorizedX, uint256 authorizedY) = delegation.delegates(0);
assertEq(authorizedX, publicKey.x);
assertEq(authorizedY, publicKey.y);
}

function test_revoke() public {
vm.pauseGasMetering();

(uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey);
ECDSA.PublicKey memory publicKey = ECDSA.PublicKey(x, y);

vm.prank(address(delegation));
delegation.authorize(publicKey);

delegation.delegates(0);

vm.prank(address(delegation));
vm.resumeGasMetering();
delegation.revoke(0);
vm.pauseGasMetering();

vm.expectRevert();
delegation.delegates(0);
}

function test_executeWithDelegate() public {
vm.pauseGasMetering();

assertEq(address(delegation).balance, 1.5 ether);
Expand All @@ -47,15 +89,16 @@ contract P256BatchDelegationTest is Test {
calls = abi.encodePacked(calls, uint8(0), address(callee), uint256(0.5 ether), data.length, data);
calls = abi.encodePacked(calls, uint8(0), address(callee), uint256(0.5 ether), data.length, data);

bytes32 hash = keccak256(abi.encodePacked(delegation.nonce(), calls));
bytes32 hash = keccak256(abi.encodePacked(delegation.executeNonce(), calls));
(bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash);
(uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey);

vm.prank(address(delegation));
delegation.authorize(P256.PublicKey(x, y));
delegation.authorize(ECDSA.PublicKey(x, y));

vm.resumeGasMetering();
delegation.execute(calls, P256.Signature(uint256(r), uint256(s)), 0);
delegation.executeWithDelegate(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false);
vm.pauseGasMetering();

assertEq(callee.counter(address(delegation)), 3);
assertEq(callee.values(address(delegation)), 1.5 ether);
Expand All @@ -72,18 +115,19 @@ contract P256BatchDelegationTest is Test {
calls = abi.encodePacked(calls, uint8(0), address(callee), uint256(0), data.length, data);
calls = abi.encodePacked(calls, uint8(0), address(callee), uint256(0), data.length, data);

bytes32 hash = keccak256(abi.encodePacked(delegation.nonce(), calls));
bytes32 hash = keccak256(abi.encodePacked(delegation.executeNonce(), calls));
(bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash);
(uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey);

vm.prank(address(delegation));
delegation.authorize(P256.PublicKey(x, y));
delegation.authorize(ECDSA.PublicKey(x, y));

vm.resumeGasMetering();
delegation.execute(calls, P256.Signature(uint256(r), uint256(s)), 0);
delegation.executeWithDelegate(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false);
vm.pauseGasMetering();

vm.expectRevert(P256BatchDelegation.InvalidSignature.selector);
delegation.execute(calls, P256.Signature(uint256(r), uint256(s)), 0);
delegation.executeWithDelegate(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false);
}

function test_revert_revokeAuth() public {
Expand All @@ -95,18 +139,19 @@ contract P256BatchDelegationTest is Test {
calls = abi.encodePacked(calls, uint8(0), address(callee), uint256(0), data.length, data);
calls = abi.encodePacked(calls, uint8(0), address(callee), uint256(0), data.length, data);

bytes32 hash = keccak256(abi.encodePacked(delegation.nonce(), calls));
bytes32 hash = keccak256(abi.encodePacked(delegation.executeNonce(), calls));
(bytes32 r, bytes32 s) = vm.signP256(p256PrivateKey, hash);
(uint256 x, uint256 y) = vm.publicKeyP256(p256PrivateKey);

vm.prank(address(delegation));
delegation.authorize(P256.PublicKey(x, y));
delegation.authorize(ECDSA.PublicKey(x, y));

vm.resumeGasMetering();
vm.prank(address(delegation));
vm.resumeGasMetering();
delegation.revoke(0);
vm.pauseGasMetering();

vm.expectRevert();
delegation.execute(calls, P256.Signature(uint256(r), uint256(s)), 0);
delegation.executeWithDelegate(calls, ECDSA.Signature(uint256(r), uint256(s)), 0, false);
}
}
Loading

0 comments on commit 3002f0d

Please sign in to comment.