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

Add samples directory and custom session key plugins #22

Merged
merged 41 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0fb2113
Add session key plugin and corresponding tests
sm-stack Nov 9, 2023
e4b429c
Fix name of execution function
sm-stack Nov 9, 2023
f12a0ce
Refactor the logic of updating session key to include selector
sm-stack Nov 9, 2023
c7fd76f
Fix lint
sm-stack Nov 9, 2023
f77df4c
Remove unused internal functions
sm-stack Nov 16, 2023
57cf21d
Update comments
sm-stack Nov 16, 2023
3b63b0d
Fix prank overriding issue
sm-stack Nov 16, 2023
e097c21
Fix lint
sm-stack Nov 17, 2023
d3ad5a7
Fix lint
sm-stack Nov 21, 2023
82fc8fe
Merge branch 'main' into session-key
sm-stack Nov 29, 2023
cfd4289
Merge branch 'main' into session-key
sm-stack Dec 1, 2023
f2ad9a6
Fix plugin codes to be compatible with the spec update
sm-stack Dec 2, 2023
f61b085
Fix nits
sm-stack Dec 19, 2023
3e5d7fb
Fix functionId
sm-stack Dec 19, 2023
65e6d1a
Fix session key test corresponding to code changes
sm-stack Dec 25, 2023
f2a74ad
Add batch owner operations and additional error handling logics
sm-stack Dec 25, 2023
b1367b5
Revert changes for pnpm-lock.yaml
sm-stack Dec 25, 2023
c12d26d
Remove unused dependencies at test
sm-stack Dec 25, 2023
3cb63a6
Fix nits
sm-stack Dec 25, 2023
2252299
Add onInstall and unOninstall function at TokenSessionKey.sol
sm-stack Dec 29, 2023
4b99521
Fix format with forge fmt
sm-stack Jan 2, 2024
d21659e
Add smaples directory
sm-stack Jan 8, 2024
76aa433
Fix nits with forge fmt
sm-stack Jan 8, 2024
efb951f
Merge pull request #1 from decipherhub/session-key
sm-stack Jan 8, 2024
691a9bb
Merge branch 'main' into sample-with-session-key
sm-stack Jan 10, 2024
610533c
Fix the implementation and test with new PluginStorageLib
sm-stack Jan 11, 2024
fd0cdeb
Merge branch 'main' into sample-with-session-key
sm-stack Jan 15, 2024
a5133fa
Merge branch 'main' into sample-with-session-key
sm-stack Jan 20, 2024
c38624c
Update test codes to be compatible with v0.7
sm-stack Jan 21, 2024
bc48866
Merge branch 'erc6900:main' into sample-with-session-key
sm-stack Jan 24, 2024
b59391c
Update expressions and nits
sm-stack Jan 27, 2024
844b47e
Merge branch 'main' into sample-with-session-key
sm-stack Jan 27, 2024
6a63c80
Apply forge fmt
sm-stack Jan 27, 2024
cb031e4
Update installPlugin
sm-stack Jan 27, 2024
5a64a38
Apply suggested changes
sm-stack Feb 13, 2024
6288888
Fix onUninstall to be able to run with msg.sender
sm-stack Feb 15, 2024
a8c6d36
Add getSessionKeysAndSelectors function
sm-stack Feb 15, 2024
f3436ad
Reduce amount to shift to 32
sm-stack Feb 15, 2024
7c833ac
Remove indexed field at the array parameter of events
sm-stack Feb 15, 2024
0f755fb
Fix nits
sm-stack Feb 16, 2024
874b578
Apply formatting
sm-stack Feb 16, 2024
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
336 changes: 336 additions & 0 deletions src/samples/plugins/ModularSessionKeyPlugin.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {UserOperation} from "@eth-infinitism/account-abstraction/interfaces/UserOperation.sol";
import {UpgradeableModularAccount} from "../../account/UpgradeableModularAccount.sol";
import {
ManifestFunction,
ManifestAssociatedFunctionType,
ManifestAssociatedFunction,
PluginManifest,
PluginMetadata,
SelectorPermission
} from "../../interfaces/IPlugin.sol";
import {BasePlugin} from "../../plugins/BasePlugin.sol";
import {IModularSessionKeyPlugin} from "./interfaces/ISessionKeyPlugin.sol";
import {ISingleOwnerPlugin} from "../../plugins/owner/ISingleOwnerPlugin.sol";
import {SingleOwnerPlugin} from "../../plugins/owner/SingleOwnerPlugin.sol";
import {PluginStorageLib, StoragePointer} from "../../libraries/PluginStorageLib.sol";

/// @title Modular Session Key Plugin
/// @author Decipher ERC-6900 Team
/// @notice This plugin allows some designated EOA or smart contract to temporarily
/// own a modular account. Note that this plugin is ONLY for demonstrating the purpose
/// of the functionalities of ERC-6900, and MUST not be used at the production level.
/// This modular session key plugin acts as a 'parent plugin' for all specific session
/// keys. Using dependency, this plugin can be thought as a parent contract that stores
/// session key duration information, and validation functions for session keys. All
/// logics for session keys will be implemented in child plugins.
/// It allows for session key owners to access MSCA both through user operation and
/// runtime, with its own validation functions.
/// Also, it has a dependency on SingleOwnerPlugin, to make sure that only the owner of
/// the MSCA can add or remove session keys.
contract ModularSessionKeyPlugin is BasePlugin, IModularSessionKeyPlugin {
using ECDSA for bytes32;
using PluginStorageLib for address;
using PluginStorageLib for bytes;

string public constant NAME = "Modular Session Key Plugin";
string public constant VERSION = "1.0.0";
string public constant AUTHOR = "Decipher ERC-6900 Team";

uint256 internal constant _SIG_VALIDATION_FAILED = 1;

struct SessionInfo {
uint48 validAfter;
uint48 validUntil;
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Execution functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc IModularSessionKeyPlugin
function addSessionKey(address tempOwner, bytes4 allowedSelector, uint48 validAfter, uint48 validUntil)
sm-stack marked this conversation as resolved.
Show resolved Hide resolved
external
{
_addSessionKey(msg.sender, tempOwner, allowedSelector, validAfter, validUntil);
emit SessionKeyAdded(msg.sender, tempOwner, allowedSelector, validAfter, validUntil);
}

/// @inheritdoc IModularSessionKeyPlugin
function removeSessionKey(address tempOwner, bytes4 allowedSelector) external {
_removeSessionKey(msg.sender, tempOwner, allowedSelector);
emit SessionKeyRemoved(msg.sender, tempOwner, allowedSelector);
}

/// @inheritdoc IModularSessionKeyPlugin
function addSessionKeyBatch(
address[] calldata tempOwners,
bytes4[] calldata allowedSelectors,
uint48[] calldata validAfters,
uint48[] calldata validUntils
) external {
if (
tempOwners.length != allowedSelectors.length || tempOwners.length != validAfters.length
|| tempOwners.length != validUntils.length
) {
revert WrongDataLength();
}
for (uint256 i = 0; i < tempOwners.length; i++) {
_addSessionKey(msg.sender, tempOwners[i], allowedSelectors[i], validAfters[i], validUntils[i]);
}
emit SessionKeysAdded(msg.sender, tempOwners, allowedSelectors, validAfters, validUntils);
}

function removeSessionKeyBatch(address[] calldata tempOwners, bytes4[] calldata allowedSelectors) external {
if (tempOwners.length != allowedSelectors.length) {
revert WrongDataLength();
}
for (uint256 i = 0; i < tempOwners.length; i++) {
_removeSessionKey(msg.sender, tempOwners[i], allowedSelectors[i]);
}
emit SessionKeysRemoved(msg.sender, tempOwners, allowedSelectors);
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin view functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc IModularSessionKeyPlugin
function getSessionDuration(address account, address tempOwner, bytes4 allowedSelector)
external
view
returns (uint48 _validAfter, uint48 _validUntil)
sm-stack marked this conversation as resolved.
Show resolved Hide resolved
{
bytes memory key = account.allocateAssociatedStorageKey(0, 1);
StoragePointer ptr = key.associatedStorageLookup(keccak256(abi.encodePacked(tempOwner, allowedSelector)));
SessionInfo storage sessionInfo = _castPtrToStruct(ptr);
_validAfter = sessionInfo.validAfter;
_validUntil = sessionInfo.validUntil;
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Plugin interface functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc BasePlugin
function onInstall(bytes calldata data) external override {
if (data.length != 0) {
(
address[] memory tempOwners,
bytes4[] memory allowedSelectors,
uint48[] memory validAfters,
uint48[] memory validUntils
) = abi.decode(data, (address[], bytes4[], uint48[], uint48[]));
if (
tempOwners.length != allowedSelectors.length || tempOwners.length != validAfters.length
|| tempOwners.length != validUntils.length
) {
revert WrongDataLength();
}
for (uint256 i = 0; i < tempOwners.length; i++) {
_addSessionKey(msg.sender, tempOwners[i], allowedSelectors[i], validAfters[i], validUntils[i]);
}
}
}

/// @inheritdoc BasePlugin
function onUninstall(bytes calldata data) external override {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Area of improvement here would be to be able to clean up all of an account's session keys just based on msg.sender, without relying on what is passed into data. Ideally, the responsibility of proper cleanup lies with the plugin.

This is probably a larger change though. Feel free to tackle this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though it will add some gas overhead, this sounds necessary to keep the consistency in the storage of plugin. I added an additional mapping that stores the information of session key and allowed selector to implement this.

if (data.length != 0) {
(address[] memory tempOwners, bytes4[] memory allowedSelectors) =
abi.decode(data, (address[], bytes4[]));
if (tempOwners.length != allowedSelectors.length) {
revert WrongDataLength();
}
for (uint256 i = 0; i < tempOwners.length; i++) {
_removeSessionKey(msg.sender, tempOwners[i], allowedSelectors[i]);
}
}
}

/// @inheritdoc BasePlugin
function userOpValidationFunction(uint8 functionId, UserOperation calldata userOp, bytes32 userOpHash)
external
view
override
returns (uint256)
{
if (functionId == uint8(FunctionId.USER_OP_VALIDATION_TEMPORARY_OWNER)) {
(address signer,) = userOpHash.toEthSignedMessageHash().tryRecover(userOp.signature);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the result of tryRecover, and if err != ECDSA.RecoverError.NoError, revert with InvalidSignature().

bytes4 selector = bytes4(userOp.callData[0:4]);
bytes memory key = userOp.sender.allocateAssociatedStorageKey(0, 1);
sm-stack marked this conversation as resolved.
Show resolved Hide resolved
StoragePointer ptr = key.associatedStorageLookup(keccak256(abi.encodePacked(signer, selector)));
SessionInfo storage duration = _castPtrToStruct(ptr);
uint48 validAfter = duration.validAfter;
uint48 validUntil = duration.validUntil;

if (validUntil != 0) {
return _packValidationData(false, validUntil, validAfter);
}
return _SIG_VALIDATION_FAILED;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delaying SIG_VALIDATION_FAILED like this helps with gas estimation from bundlers when using dummy signatures.

Suggested change
if (validUntil != 0) {
return _packValidationData(false, validUntil, validAfter);
}
return _SIG_VALIDATION_FAILED;
return _packValidationData(validUntil == 0, validUntil, validAfter);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems valid. Fixed it.

}
revert NotImplemented();
}

/// @inheritdoc BasePlugin
function runtimeValidationFunction(uint8 functionId, address sender, uint256, bytes calldata data)
external
view
override
{
if (functionId == uint8(FunctionId.RUNTIME_VALIDATION_TEMPORARY_OWNER)) {
bytes4 selector = bytes4(data[0:4]);
bytes memory key = address(msg.sender).allocateAssociatedStorageKey(0, 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit

Suggested change
bytes memory key = address(msg.sender).allocateAssociatedStorageKey(0, 1);
bytes memory key = msg.sender.allocateAssociatedStorageKey(0, 1);

StoragePointer ptr = key.associatedStorageLookup(keccak256(abi.encodePacked(sender, selector)));
SessionInfo storage duration = _castPtrToStruct(ptr);
uint48 validAfter = duration.validAfter;
uint48 validUntil = duration.validUntil;

if (validUntil != 0) {
if (block.timestamp < validAfter || block.timestamp > validUntil) {
revert WrongTimeRangeForSession();
}
return;
}
revert NotAuthorized();
}
revert NotImplemented();
}

/// @inheritdoc BasePlugin
function pluginManifest() external pure override returns (PluginManifest memory) {
PluginManifest memory manifest;

manifest.executionFunctions = new bytes4[](4);
manifest.executionFunctions[0] = this.addSessionKey.selector;
manifest.executionFunctions[1] = this.removeSessionKey.selector;
manifest.executionFunctions[2] = this.addSessionKeyBatch.selector;
manifest.executionFunctions[3] = this.removeSessionKeyBatch.selector;

ManifestFunction memory ownerUserOpValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.DEPENDENCY,
functionId: 0, // Unused.
dependencyIndex: 0 // Used as first index.
});
manifest.userOpValidationFunctions = new ManifestAssociatedFunction[](4);
manifest.userOpValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.addSessionKey.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: this.removeSessionKey.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: this.addSessionKeyBatch.selector,
associatedFunction: ownerUserOpValidationFunction
});
manifest.userOpValidationFunctions[3] = ManifestAssociatedFunction({
executionSelector: this.removeSessionKeyBatch.selector,
associatedFunction: ownerUserOpValidationFunction
});

ManifestFunction memory ownerOrSelfRuntimeValidationFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.DEPENDENCY,
functionId: 0, // Unused.
dependencyIndex: 1
});
ManifestFunction memory alwaysAllowFunction = ManifestFunction({
functionType: ManifestAssociatedFunctionType.RUNTIME_VALIDATION_ALWAYS_ALLOW,
functionId: 0, // Unused.
dependencyIndex: 0 // Unused.
});

manifest.runtimeValidationFunctions = new ManifestAssociatedFunction[](5);
manifest.runtimeValidationFunctions[0] = ManifestAssociatedFunction({
executionSelector: this.addSessionKey.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[1] = ManifestAssociatedFunction({
executionSelector: this.removeSessionKey.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[2] = ManifestAssociatedFunction({
executionSelector: this.addSessionKeyBatch.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[3] = ManifestAssociatedFunction({
executionSelector: this.removeSessionKeyBatch.selector,
associatedFunction: ownerOrSelfRuntimeValidationFunction
});
manifest.runtimeValidationFunctions[4] = ManifestAssociatedFunction({
executionSelector: this.getSessionDuration.selector,
associatedFunction: alwaysAllowFunction
});

manifest.dependencyInterfaceIds = new bytes4[](2);
manifest.dependencyInterfaceIds[0] = type(ISingleOwnerPlugin).interfaceId;
manifest.dependencyInterfaceIds[1] = type(ISingleOwnerPlugin).interfaceId;

return manifest;
}

/// @inheritdoc BasePlugin
function pluginMetadata() external pure virtual override returns (PluginMetadata memory) {
PluginMetadata memory metadata;
metadata.name = NAME;
metadata.version = VERSION;
metadata.author = AUTHOR;

return metadata;
}

// ┏━━━━━━━━━━━━━━━┓
// ┃ EIP-165 ┃
// ┗━━━━━━━━━━━━━━━┛

/// @inheritdoc BasePlugin
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
return interfaceId == type(IModularSessionKeyPlugin).interfaceId || super.supportsInterface(interfaceId);
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Internal / Private functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

function _addSessionKey(
address account,
address tempOwner,
bytes4 allowedSelector,
uint48 _validAfter,
uint48 _validUntil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
uint48 _validAfter,
uint48 _validUntil
uint48 validAfter,
uint48 validUntil

) internal {
if (_validUntil <= _validAfter) {
revert WrongTimeRangeForSession();
}
bytes memory key = account.allocateAssociatedStorageKey(0, 1);
StoragePointer ptr = key.associatedStorageLookup(keccak256(abi.encodePacked(tempOwner, allowedSelector)));
SessionInfo storage sessionInfo = _castPtrToStruct(ptr);
sessionInfo.validAfter = _validAfter;
sessionInfo.validUntil = _validUntil;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sessionInfo.validAfter = _validAfter;
sessionInfo.validUntil = _validUntil;
sessionInfo.validAfter = validAfter;
sessionInfo.validUntil = validUntil;

}

function _removeSessionKey(address account, address tempOwner, bytes4 allowedSelector) internal {
bytes memory key = account.allocateAssociatedStorageKey(0, 1);
StoragePointer ptr = key.associatedStorageLookup(keccak256(abi.encodePacked(tempOwner, allowedSelector)));
SessionInfo storage sessionInfo = _castPtrToStruct(ptr);
sessionInfo.validAfter = 0;
sessionInfo.validUntil = 0;
}

function _castPtrToStruct(StoragePointer ptr) internal pure returns (SessionInfo storage val) {
assembly ("memory-safe") {
val.slot := ptr
}
}

function _packValidationData(bool sigFailed, uint48 validUntil, uint48 validAfter)
internal
pure
returns (uint256)
{
return (sigFailed ? 1 : 0) | (uint256(validUntil) << 160) | (uint256(validAfter) << (160 + 48));
}
}
Loading