Skip to content
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

feat: minimal delegation storage #1

Merged
merged 8 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
src = "src"
out = "out"
libs = ["lib"]
odyssey = true

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
34 changes: 33 additions & 1 deletion src/MinimalDelegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,37 @@
pragma solidity ^0.8.23;

import {IMinimalDelegation} from "./interfaces/IMinimalDelegation.sol";
import {Key, KeyLib} from "./lib/KeyLib.sol";
import {MinimalDelegationStorageLib} from "./lib/MinimalDelegationStorageLib.sol";

abstract contract MinimalDelegation is IMinimalDelegation {}
contract MinimalDelegation {
using KeyLib for Key;

/// @dev The key does not exist.
error KeyDoesNotExist();

/// @dev Emitted when a key is authorized.
event Authorized(bytes32 indexed keyHash, Key key);

/// @dev Emitted when a key is revoked.
event Revoked(bytes32 indexed keyHash);

/// @dev Authorizes the `key`.
function authorize(Key memory key) external returns (bytes32 keyHash) {
keyHash = key.hash();
MinimalDelegationStorageLib.get().keyStorage[keyHash] = abi.encode(key);
emit Authorized(keyHash, key);
}

/// @dev Returns the key corresponding to the `keyHash`. Reverts if the key does not exist.
function getKey(bytes32 keyHash) external view returns (Key memory key) {
bytes memory data = MinimalDelegationStorageLib.get().keyStorage[keyHash];
if (data.length == 0) revert KeyDoesNotExist();
return abi.decode(data, (Key));
}

function revoke(bytes32 keyHash) external {
delete MinimalDelegationStorageLib.get().keyStorage[keyHash];
emit Revoked(keyHash);
}
}
22 changes: 2 additions & 20 deletions src/interfaces/IKeyManagement.sol
Original file line number Diff line number Diff line change
@@ -1,27 +1,9 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface IKeyManagement {
/// @dev The type of key.
enum KeyType {
P256,
WebAuthnP256,
Secp256k1
}

struct Key {
/// @dev Unix timestamp at which the key expires (0 = never).
uint40 expiry;
/// @dev Type of key. See the {KeyType} enum.
KeyType keyType;
/// @dev Whether the key is a super admin key.
/// Super admin keys are allowed to call into super admin functions such as
/// `authorize` and `revoke` via `execute`.
bool isSuperAdmin;
/// @dev Public key in encoded form.
bytes publicKey;
}
import {Key} from "../lib/KeyLib.sol";

interface IKeyManagement {
function authorize(Key memory key) external returns (bytes32 keyHash);
function revoke(bytes32 keyHash) external;
function keyCount() external view returns (uint256);
Expand Down
27 changes: 27 additions & 0 deletions src/lib/KeyLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

/// @dev The type of key.
enum KeyType {
P256,
WebAuthnP256,
Secp256k1
}

struct Key {
/// @dev Unix timestamp at which the key expires (0 = never).
uint40 expiry;
/// @dev Type of key. See the {KeyType} enum.
KeyType keyType;
/// @dev Whether the key is a super admin key.
/// Super admin keys are allowed to execute any external call
bool isSuperAdmin;
/// @dev Public key in encoded form.
bytes publicKey;
}

library KeyLib {
function hash(Key memory key) internal pure returns (bytes32) {
return keccak256(abi.encode(key.keyType, keccak256(key.publicKey)));
}
}
18 changes: 18 additions & 0 deletions src/lib/MinimalDelegationStorageLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

library MinimalDelegationStorageLib {
struct MinimalDelegationStorage {
mapping(bytes32 keyHash => bytes encodedKey) keyStorage;
}

/// @dev keccak256(abi.encode(uint256(keccak256("Uniswap.MinimalDelegation")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant MINIMAL_DELEGATION_STORAGE_LOCATION =
0x21f3d48e9724698d61a2dadd352c365013ee5d0f841f7fc54fb8a78301ee0c00;

function get() internal pure returns (MinimalDelegationStorage storage $) {
assembly {
$.slot := MINIMAL_DELEGATION_STORAGE_LOCATION
}
}
}
27 changes: 27 additions & 0 deletions test/BaseTest.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {Test} from "forge-std/Test.sol";
import {Key, KeyLib, KeyType} from "../src/lib/KeyLib.sol";
import {MinimalDelegation} from "../src/MinimalDelegation.sol";

contract BaseTest is Test {
using KeyLib for Key;

MinimalDelegation public minimalDelegation;
uint256 signerPrivateKey = 0xa11ce;
address signer = vm.addr(signerPrivateKey);

address mockSecp256k1PublicKey = makeAddr("mockSecp256k1PublicKey");
Key public mockSecp256k1Key = Key(0, KeyType.Secp256k1, true, abi.encodePacked(mockSecp256k1PublicKey));

function setUp() public {
minimalDelegation = new MinimalDelegation();
_delegate(signer, address(minimalDelegation));
}

function _delegate(address _signer, address _implementation) internal {
vm.etch(_signer, bytes.concat(hex"ef0100", abi.encodePacked(_implementation)));
require(_signer.code.length > 0, "signer not delegated");
}
}
35 changes: 35 additions & 0 deletions test/MinimalDelegation.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {BaseTest} from "./BaseTest.t.sol";
import {Key, KeyType, KeyLib} from "../src/lib/KeyLib.sol";

contract MinimalDelegationTest is BaseTest {
using KeyLib for Key;

error KeyDoesNotExist();

function test_authorize() public {
bytes32 keyHash = mockSecp256k1Key.hash();

minimalDelegation.authorize(mockSecp256k1Key);

Key memory fetchedKey = minimalDelegation.getKey(keyHash);
assertEq(fetchedKey.expiry, 0);
assertEq(uint256(fetchedKey.keyType), uint256(KeyType.Secp256k1));
assertEq(fetchedKey.isSuperAdmin, true);
assertEq(fetchedKey.publicKey, abi.encodePacked(mockSecp256k1PublicKey));
}

function test_revoke() public {
// first authorize the key
bytes32 keyHash = minimalDelegation.authorize(mockSecp256k1Key);

// then revoke the key
minimalDelegation.revoke(keyHash);

// then expect the key to not exist
vm.expectRevert(KeyDoesNotExist.selector);
minimalDelegation.getKey(keyHash);
}
}
Loading