Skip to content

Commit

Permalink
Integration with Solday's ERC6551 Implementation (storyprotocol#133)
Browse files Browse the repository at this point in the history
* forge install: solady v0.0.192

* integration with Solady 6551
  • Loading branch information
kingster-will authored Apr 25, 2024
1 parent 972c201 commit 330b8b8
Show file tree
Hide file tree
Showing 13 changed files with 437 additions and 107 deletions.
144 changes: 89 additions & 55 deletions contracts/IPAccountImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
pragma solidity 0.8.23;

import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { IERC6551Account } from "erc6551/interfaces/IERC6551Account.sol";
import { ERC6551, Receiver } from "@solady/src/accounts/ERC6551.sol";

import { IAccessController } from "./interfaces/access/IAccessController.sol";
import { IIPAccount } from "./interfaces/IIPAccount.sol";
Expand All @@ -17,13 +17,10 @@ import { IPAccountStorage } from "./IPAccountStorage.sol";

/// @title IPAccountImpl
/// @notice The Story Protocol's implementation of the IPAccount.
contract IPAccountImpl is IPAccountStorage, IIPAccount {
contract IPAccountImpl is ERC6551, IPAccountStorage, IIPAccount {
address public immutable ACCESS_CONTROLLER;

/// @notice Returns the IPAccount's internal nonce for transaction ordering.
uint256 public state;

receive() external payable override(IERC6551Account) {}
receive() external payable override(Receiver, IIPAccount) {}

/// @notice Creates a new IPAccountImpl contract instance
/// @dev Initializes the IPAccountImpl with an AccessController address which is stored
Expand All @@ -44,7 +41,9 @@ contract IPAccountImpl is IPAccountStorage, IIPAccount {
/// @notice Checks if the contract supports a specific interface
/// @param interfaceId The interface identifier, as specified in ERC-165
/// @return bool is true if the contract supports the interface, false otherwise
function supportsInterface(bytes4 interfaceId) public view override(IPAccountStorage, IERC165) returns (bool) {
function supportsInterface(
bytes4 interfaceId
) public view override(ERC6551, IPAccountStorage, IERC165) returns (bool) {
return (interfaceId == type(IIPAccount).interfaceId ||
interfaceId == type(IERC6551Account).interfaceId ||
interfaceId == type(IERC1155Receiver).interfaceId ||
Expand All @@ -56,42 +55,30 @@ contract IPAccountImpl is IPAccountStorage, IIPAccount {
/// @return chainId The EIP-155 ID of the chain the token exists on
/// @return tokenContract The contract address of the token
/// @return tokenId The ID of the token
function token() public view override returns (uint256, address, uint256) {
bytes memory footer = new bytes(0x60);
// 0x4d = 77 bytes (ERC-1167 Header, address, ERC-1167 Footer, salt)
// 0x60 = 96 bytes (chainId, tokenContract, tokenId)
// ERC-1167 Header (10 bytes)
// <implementation (address)> (20 bytes)
// ERC-1167 Footer (15 bytes)
// <salt (uint256)> (32 bytes)
// <chainId (uint256)> (32 bytes)
// <tokenContract (address)> (32 bytes)
// <tokenId (uint256)> (32 bytes)
assembly {
extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60)
}

return abi.decode(footer, (uint256, address, uint256));
function token() public view override(ERC6551, IIPAccount) returns (uint256, address, uint256) {
return super.token();
}

/// @notice Checks if the signer is valid for the given data
/// @param signer The signer to check
/// @param data The data to check against
/// @return The function selector if the signer is valid, 0 otherwise
function isValidSigner(address signer, bytes calldata data) external view returns (bytes4) {
if (_isValidSigner(signer, address(0), data)) {
return IERC6551Account.isValidSigner.selector;
}

return bytes4(0);
function isValidSigner(
address signer,
bytes calldata data
) public view override(ERC6551, IIPAccount) returns (bytes4) {
return super.isValidSigner(signer, data);
}

/// @notice Returns the owner of the IP Account.
/// @return The address of the owner.
function owner() public view returns (address) {
(uint256 chainId, address contractAddress, uint256 tokenId) = token();
if (chainId != block.chainid) return address(0);
return IERC721(contractAddress).ownerOf(tokenId);
function owner() public view override(ERC6551, IIPAccount) returns (address) {
return super.owner();
}

/// @notice Returns the IPAccount's internal nonce for transaction ordering.
function state() public view override(ERC6551, IIPAccount) returns (bytes32 result) {
return super.state();
}

/// @dev Checks if the signer is valid for the given data and recipient via the AccessController permission system.
Expand Down Expand Up @@ -135,12 +122,12 @@ contract IPAccountImpl is IPAccountStorage, IIPAccount {
revert Errors.IPAccount__ExpiredSignature();
}

++state;
_updateStateForExecute(to, value, data);

bytes32 digest = MessageHashUtils.toTypedDataHash(
MetaTx.calculateDomainSeparator(),
MetaTx.getExecuteStructHash(
MetaTx.Execute({ to: to, value: value, data: data, nonce: state, deadline: deadline })
MetaTx.Execute({ to: to, value: value, data: data, nonce: state(), deadline: deadline })
)
);

Expand All @@ -149,7 +136,7 @@ contract IPAccountImpl is IPAccountStorage, IIPAccount {
}

result = _execute(signer, to, value, data);
emit ExecutedWithSig(to, value, data, state, deadline, signer, signature);
emit ExecutedWithSig(to, value, data, state(), deadline, signer, signature);
}

/// @notice Executes a transaction from the IP Account.
Expand All @@ -158,30 +145,45 @@ contract IPAccountImpl is IPAccountStorage, IIPAccount {
/// @param data The data to send along with the transaction.
/// @return result The return data from the transaction.
function execute(address to, uint256 value, bytes calldata data) external payable returns (bytes memory result) {
++state;
_updateStateForExecute(to, value, data);
result = _execute(msg.sender, to, value, data);
emit Executed(to, value, data, state);
emit Executed(to, value, data, state());
}

/// @inheritdoc IERC721Receiver
function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) {
return this.onERC721Received.selector;
}

/// @inheritdoc IERC1155Receiver
function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure returns (bytes4) {
return this.onERC1155Received.selector;
/// @dev Override 6551 execute function.
/// Only "CALL" operation is supported.
/// @param to The recipient of the transaction.
/// @param value The amount of Ether to send.
/// @param data The data to send along with the transaction.
/// @param operation The operation type to perform, only 0 - CALL is supported.
/// @return result The return data from the transaction.
function execute(
address to,
uint256 value,
bytes calldata data,
uint8 operation
) public payable override returns (bytes memory result) {
// Only "CALL" operation is supported.
if (operation != 0) {
revert Errors.IPAccount__InvalidOperation();
}
_updateStateForExecute(to, value, data);
result = _execute(msg.sender, to, value, data);
emit Executed(to, value, data, state());
}

/// @inheritdoc IERC1155Receiver
function onERC1155BatchReceived(
address,
address,
uint256[] memory,
uint256[] memory,
bytes memory
) public pure returns (bytes4) {
return this.onERC1155BatchReceived.selector;
/// @notice Executes a batch of transactions from the IP Account.
/// @param calls The array of calls to execute.
/// @param operation The operation type to perform, only 0 - CALL is supported.
/// @return results The return data from the transactions.
function executeBatch(
Call[] calldata calls,
uint8 operation
) public payable override returns (bytes[] memory results) {
results = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
results[i] = execute(calls[i].target, calls[i].value, calls[i].data, operation);
}
}

/// @dev Executes a transaction from the IP Account.
Expand All @@ -202,4 +204,36 @@ contract IPAccountImpl is IPAccountStorage, IIPAccount {
}
}
}

/// @dev Updates the IP Account's state all execute transactions.
/// @param to The "target" of the execute transactions.
/// @param value The amount of Ether to send.
/// @param data The data to send along with the transaction.
function _updateStateForExecute(address to, uint256 value, bytes calldata data) internal {
bytes32 newState = keccak256(
abi.encode(state(), abi.encodeWithSignature("execute(address,uint256,bytes)", to, value, data))
);
assembly {
sstore(_ERC6551_STATE_SLOT, newState)
}
}

/// @dev Override Solady 6551 _isValidSigner function.
/// @param signer The signer to check
/// @param extraData The extra data to check against, it should bethe address of the recipient for IPAccount
/// @param context The context for validating the signer
/// @return bool is true if the signer is valid, false otherwise
function _isValidSigner(
address signer,
bytes32 extraData,
bytes calldata context
) internal view override returns (bool) {
return _isValidSigner(signer, address(uint160(uint256(extraData))), context);
}

/// @dev Override Solady EIP712 function and return EIP712 domain name for IPAccount.
function _domainNameAndVersion() internal view override returns (string memory name, string memory version) {
name = "Story Protocol IP Account";
version = "1";
}
}
4 changes: 2 additions & 2 deletions contracts/IPAccountStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.23;
import { IIPAccountStorage } from "./interfaces/IIPAccountStorage.sol";
import { IModuleRegistry } from "./interfaces/registries/IModuleRegistry.sol";
import { Errors } from "./lib/Errors.sol";
import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { ERC165, IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol";
/// @title IPAccount Storage
/// @dev Implements the IIPAccountStorage interface for managing IPAccount's state using a namespaced storage pattern.
Expand Down Expand Up @@ -66,7 +66,7 @@ contract IPAccountStorage is ERC165, IIPAccountStorage {
}

/// @notice ERC165 interface identifier for IIPAccountStorage
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) {
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IIPAccountStorage).interfaceId || super.supportsInterface(interfaceId);
}

Expand Down
13 changes: 6 additions & 7 deletions contracts/interfaces/IIPAccount.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import { IERC6551Account } from "erc6551/interfaces/IERC6551Account.sol";
import { IIPAccountStorage } from "./IIPAccountStorage.sol";

/// @title IIPAccount
Expand All @@ -14,13 +11,13 @@ import { IIPAccountStorage } from "./IIPAccountStorage.sol";
/// IPAccount can interact with modules by making calls as a normal transaction sender.
/// This allows for seamless operations on the state and data of IP.
/// IPAccount is core identity for all actions.
interface IIPAccount is IERC6551Account, IERC721Receiver, IERC1155Receiver, IIPAccountStorage {
interface IIPAccount is IIPAccountStorage {
/// @notice Emitted when a transaction is executed.
/// @param to The recipient of the transaction.
/// @param value The amount of Ether sent.
/// @param data The data sent along with the transaction.
/// @param nonce The nonce of the transaction.
event Executed(address indexed to, uint256 value, bytes data, uint256 nonce);
event Executed(address indexed to, uint256 value, bytes data, bytes32 nonce);

/// @notice Emitted when a transaction is executed on behalf of the signer.
/// @param to The recipient of the transaction.
Expand All @@ -34,14 +31,16 @@ interface IIPAccount is IERC6551Account, IERC721Receiver, IERC1155Receiver, IIPA
address indexed to,
uint256 value,
bytes data,
uint256 nonce,
bytes32 nonce,
uint256 deadline,
address indexed signer,
bytes signature
);

receive() external payable;

/// @notice Returns the IPAccount's internal nonce for transaction ordering.
function state() external view returns (uint256);
function state() external view returns (bytes32);

/// @notice Returns the identifier of the non-fungible token which owns the account
/// @return chainId The EIP-155 ID of the chain the token exists on
Expand Down
4 changes: 3 additions & 1 deletion contracts/interfaces/IIPAccountStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf
pragma solidity ^0.8.23;

import { IERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";

/// @title IPAccount Namespaced Storage Interface
/// @dev Provides a structured way to store IPAccount's state using a namespaced storage pattern.
/// This interface facilitates conflict-free data writing by different Modules into the same IPAccount
Expand All @@ -15,7 +17,7 @@ pragma solidity ^0.8.23;
/// - Every Module can read data from any namespace.
/// - Only the owning Module (i.e., the Module whose address is used as the namespace) can write data into
/// its respective namespace.
interface IIPAccountStorage {
interface IIPAccountStorage is IERC165 {
/// @dev Sets a bytes value under a given key within the default namespace, determined by `msg.sender`.
/// @param key The key under which to store the value.
/// @param value The bytes value to be stored.
Expand Down
3 changes: 3 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ library Errors {
/// @notice Provided calldata is invalid.
error IPAccount__InvalidCalldata();

/// @notice Execute operation type is not supported.
error IPAccount__InvalidOperation();

////////////////////////////////////////////////////////////////////////////
// IP Account Storage //
////////////////////////////////////////////////////////////////////////////
Expand Down
2 changes: 1 addition & 1 deletion contracts/lib/MetaTx.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ library MetaTx {
address to;
uint256 value;
bytes data;
uint256 nonce;
bytes32 nonce;
uint256 deadline;
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@openzeppelin/contracts": "5.0.2",
"@openzeppelin/contracts-upgradeable": "5.0.2",
"@openzeppelin/contracts-upgradeable-v4": "npm:@openzeppelin/[email protected]",
"erc6551": "^0.3.1"
"erc6551": "^0.3.1",
"solady": "^0.0.192"
}
}
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ ds-test/=node_modules/ds-test/src/
forge-std/=node_modules/forge-std/src/
@openzeppelin/=node_modules/@openzeppelin/
@openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades
@create3-deployer/=lib/create3-deployer
@create3-deployer/=lib/create3-deployer
@solady/=node_modules/solady/
Loading

0 comments on commit 330b8b8

Please sign in to comment.