Removed old implementation + tests, migrated all asset transfer tests to v2 implementation

This commit is contained in:
Jayden Windle
2023-04-09 17:36:28 -04:00
parent dcf7de85e1
commit 36840f5070
17 changed files with 532 additions and 1787 deletions

View File

@@ -1,324 +1,331 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/interfaces/IERC1271.sol";
import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol";
import "erc6551/interfaces/IERC6551Account.sol";
import "openzeppelin-contracts/utils/introspection/IERC165.sol";
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/token/ERC721/IERC721Receiver.sol";
import "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol";
import "openzeppelin-contracts/interfaces/IERC1271.sol";
import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol";
import "openzeppelin-contracts/proxy/utils/UUPSUpgradeable.sol";
import "./CrossChainExecutorList.sol";
import "./MinimalReceiver.sol";
import "./interfaces/IAccount.sol";
import "./lib/MinimalProxyStore.sol";
import "sstore2/utils/Bytecode.sol";
import {BaseAccount as BaseERC4337Account, IEntryPoint, UserOperation, IAccount as IERC4337Account} from "account-abstraction/core/BaseAccount.sol";
import "./interfaces/IAccountGuardian.sol";
error NotAuthorized();
error InvalidInput();
error AccountLocked();
error ExceedsMaxLockTime();
error InvalidNonce();
error UntrustedImplementation();
/**
* @title A smart contract wallet owned by a single ERC721 token
* @author Jayden Windle (jaydenwindle)
*/
contract Account is IERC165, IERC1271, IAccount, MinimalReceiver {
error NotAuthorized();
error AccountLocked();
error ExceedsMaxLockTime();
contract Account is
IERC165,
IERC1271,
IERC6551Account,
IERC721Receiver,
IERC1155Receiver,
UUPSUpgradeable,
BaseERC4337Account
{
// @dev ERC-4337 entry point
address immutable _entryPoint;
CrossChainExecutorList public immutable crossChainExecutorList;
// @dev AccountGuardian contract
address public immutable guardian;
/**
* @dev Timestamp at which Account will unlock
*/
uint256 public unlockTimestamp;
// @dev Updated on each transaction
uint256 _nonce;
/**
* @dev Mapping from owner address to executor address
*/
mapping(address => address) public executor;
// @dev timestamp at which this account will be unlocked
uint256 public lockedUntil;
/**
* @dev Emitted whenever the lock status of a account is updated
*/
event LockUpdated(uint256 timestamp);
// @dev mapping from owner => selector => implementation
mapping(address => mapping(bytes4 => address)) public overrides;
/**
* @dev Emitted whenever the executor for a account is updated
*/
event ExecutorUpdated(address owner, address executor);
// @dev mapping from owner => caller => selector => has permissions
mapping(address => mapping(address => mapping(bytes4 => bool)))
public permissions;
constructor(address _crossChainExecutorList) {
crossChainExecutorList = CrossChainExecutorList(
_crossChainExecutorList
);
}
/**
* @dev Ensures execution can only continue if the account is not locked
*/
modifier onlyUnlocked() {
if (unlockTimestamp > block.timestamp) revert AccountLocked();
modifier onlyOwner() {
if (msg.sender != owner()) revert NotAuthorized();
_;
}
/**
* @dev If account is unlocked and an executor is set, pass call to executor
*/
fallback(bytes calldata data)
external
payable
onlyUnlocked
returns (bytes memory result)
{
address _owner = owner();
address _executor = executor[_owner];
// accept funds if executor is undefined or cannot be called
if (_executor.code.length == 0) return "";
return _call(_executor, 0, data);
modifier onlyAuthorized() {
if (!isAuthorized(msg.sender, msg.sig)) revert NotAuthorized();
_;
}
modifier onlyUnlocked() {
if (isLocked()) revert AccountLocked();
_;
}
constructor(address _guardian, address entryPoint_) {
_entryPoint = entryPoint_;
guardian = _guardian;
}
receive() external payable {
_handleOverride();
}
fallback() external payable {
_handleOverride();
}
/**
* @dev Executes a transaction from the Account. Must be called by an account owner.
*
* @param to Destination address of the transaction
* @param value Ether value of the transaction
* @param data Encoded payload of the transaction
*/
function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable onlyUnlocked returns (bytes memory result) {
)
external
payable
onlyAuthorized
onlyUnlocked
returns (bytes memory result)
{
++_nonce;
_handleOverride();
result = _call(to, value, data);
emit TransactionExecuted(to, value, data);
}
function setOverrides(
bytes4[] calldata selectors,
address[] calldata implementations
) external onlyUnlocked {
address _owner = owner();
if (msg.sender != _owner) revert NotAuthorized();
return _call(to, value, data);
}
if (selectors.length != implementations.length) revert InvalidInput();
/**
* @dev Executes a transaction from the Account. Must be called by an authorized executor.
*
* @param to Destination address of the transaction
* @param value Ether value of the transaction
* @param data Encoded payload of the transaction
*/
function executeTrustedCall(
address to,
uint256 value,
bytes calldata data
) external payable onlyUnlocked returns (bytes memory result) {
address _executor = executor[owner()];
if (msg.sender != _executor) revert NotAuthorized();
++_nonce;
return _call(to, value, data);
}
/**
* @dev Executes a transaction from the Account. Must be called by a trusted cross-chain executor.
* Can only be called if account is owned by a token on another chain.
*
* @param to Destination address of the transaction
* @param value Ether value of the transaction
* @param data Encoded payload of the transaction
*/
function executeCrossChainCall(
address to,
uint256 value,
bytes calldata data
) external payable onlyUnlocked returns (bytes memory result) {
(uint256 chainId, , ) = context();
if (chainId == block.chainid) {
revert NotAuthorized();
for (uint256 i = 0; i < selectors.length; i++) {
overrides[_owner][selectors[i]] = implementations[i];
}
if (!crossChainExecutorList.isCrossChainExecutor(chainId, msg.sender)) {
revert NotAuthorized();
}
return _call(to, value, data);
}
/**
* @dev Sets executor address for Account, allowing owner to use a custom implementation if they choose to.
* When the token controlling the account is transferred, the implementation address will reset
*
* @param _executionModule the address of the execution module
*/
function setExecutor(address _executionModule) external onlyUnlocked {
function setPermissions(
bytes4[] calldata selectors,
address[] calldata implementations
) external onlyUnlocked {
address _owner = owner();
if (_owner != msg.sender) revert NotAuthorized();
if (msg.sender != _owner) revert NotAuthorized();
executor[_owner] = _executionModule;
if (selectors.length != implementations.length) revert InvalidInput();
emit ExecutorUpdated(_owner, _executionModule);
++_nonce;
for (uint256 i = 0; i < selectors.length; i++) {
permissions[_owner][implementations[i]][selectors[i]] = true;
}
}
/**
* @dev Locks Account, preventing transactions from being executed until a certain time
*
* @param _unlockTimestamp timestamp when the account will become unlocked
*/
function lock(uint256 _unlockTimestamp) external onlyUnlocked {
if (_unlockTimestamp > block.timestamp + 365 days)
function lock(uint256 _lockedUntil) external onlyOwner onlyUnlocked {
if (_lockedUntil > block.timestamp + 365 days)
revert ExceedsMaxLockTime();
address _owner = owner();
if (_owner != msg.sender) revert NotAuthorized();
++_nonce;
unlockTimestamp = _unlockTimestamp;
emit LockUpdated(_unlockTimestamp);
lockedUntil = _lockedUntil;
}
/**
* @dev Returns Account lock status
*
* @return true if Account is locked, false otherwise
*/
function isLocked() external view returns (bool) {
return unlockTimestamp > block.timestamp;
function isLocked() public view returns (bool) {
return lockedUntil > block.timestamp;
}
/**
* @dev Returns true if caller is authorized to execute actions on this account
*
* @param caller the address to query authorization for
* @return true if caller is authorized, false otherwise
*/
function isAuthorized(address caller) external view returns (bool) {
(uint256 chainId, address tokenCollection, uint256 tokenId) = context();
if (chainId != block.chainid) {
return crossChainExecutorList.isCrossChainExecutor(chainId, caller);
}
address _owner = IERC721(tokenCollection).ownerOf(tokenId);
if (caller == _owner) return true;
address _executor = executor[_owner];
if (caller == _executor) return true;
return false;
}
/**
* @dev Implements EIP-1271 signature validation
*
* @param hash Hash of the signed data
* @param signature Signature to validate
*/
function isValidSignature(bytes32 hash, bytes memory signature)
external
view
returns (bytes4 magicValue)
{
// If account is locked, disable signing
if (unlockTimestamp > block.timestamp) return "";
_handleOverrideStatic();
// If account has an executor, check if executor signature is valid
address _owner = owner();
address _executor = executor[_owner];
bool isValid = SignatureChecker.isValidSignatureNow(
owner(),
hash,
signature
);
if (
_executor != address(0) &&
SignatureChecker.isValidSignatureNow(_executor, hash, signature)
) {
return IERC1271.isValidSignature.selector;
}
// Default - check if signature is valid for account owner
if (SignatureChecker.isValidSignatureNow(_owner, hash, signature)) {
if (isValid) {
return IERC1271.isValidSignature.selector;
}
return "";
}
/**
* @dev Implements EIP-165 standard interface detection
*
* @param interfaceId the interfaceId to check support for
* @return true if the interface is supported, false otherwise
*/
function token()
external
view
returns (
uint256 chainId,
address tokenContract,
uint256 tokenId
)
{
address self = address(this);
uint256 length = self.code.length;
if (length < 0x60) return (0, address(0), 0);
return
abi.decode(
Bytecode.codeAt(self, length - 0x60, length),
(uint256, address, uint256)
);
}
function nonce()
public
view
override(BaseERC4337Account, IERC6551Account)
returns (uint256)
{
return _nonce;
}
function entryPoint() public view override returns (IEntryPoint) {
return IEntryPoint(_entryPoint);
}
function owner() public view returns (address) {
(uint256 chainId, address tokenContract, uint256 tokenId) = this
.token();
if (chainId != block.chainid) return address(0);
return IERC721(tokenContract).ownerOf(tokenId);
}
function isAuthorized(address caller, bytes4 selector)
public
view
returns (bool)
{
(uint256 chainId, address tokenContract, uint256 tokenId) = this
.token();
address _owner = IERC721(tokenContract).ownerOf(tokenId);
// authorize token owner
if (caller == _owner) return true;
// authorize entrypoint for 4337 transactions
if (caller == _entryPoint) return true;
// authorize caller if owner has granted permissions for function call
if (permissions[_owner][caller][selector]) return true;
// authorize trusted cross-chain executors if not on native chain
if (
chainId != block.chainid &&
IAccountGuardian(guardian).isTrustedExecutor(caller)
) return true;
return false;
}
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(IERC165, ERC1155Receiver)
override
returns (bool)
{
// default interface support
if (
interfaceId == type(IAccount).interfaceId ||
bool defaultSupport = interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC1155Receiver).interfaceId ||
interfaceId == type(IERC165).interfaceId
) {
return true;
}
interfaceId == type(IERC6551Account).interfaceId;
address _executor = executor[owner()];
if (defaultSupport) return true;
if (_executor == address(0) || _executor.code.length == 0) {
return false;
}
// if not supported by default, check override
_handleOverrideStatic();
// if interface is not supported by default, check executor
try IERC165(_executor).supportsInterface(interfaceId) returns (
bool _supportsInterface
) {
return _supportsInterface;
} catch {
return false;
}
return false;
}
/**
* @dev Returns the owner of the token that controls this Account (public for Ownable compatibility)
*
* @return the address of the Account owner
*/
function owner() public view returns (address) {
(uint256 chainId, address tokenCollection, uint256 tokenId) = context();
function onERC721Received(
address,
address,
uint256,
bytes memory
) public view override returns (bytes4) {
_handleOverrideStatic();
if (chainId != block.chainid) {
return address(0);
}
return IERC721(tokenCollection).ownerOf(tokenId);
return this.onERC721Received.selector;
}
/**
* @dev Returns information about the token that owns this account
*
* @return tokenCollection the contract address of the ERC721 token which owns this account
* @return tokenId the tokenId of the ERC721 token which owns this account
*/
function token()
public
view
returns (address tokenCollection, uint256 tokenId)
{
(, tokenCollection, tokenId) = context();
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes memory
) public view override returns (bytes4) {
_handleOverrideStatic();
return this.onERC1155Received.selector;
}
function context()
function onERC1155BatchReceived(
address,
address,
uint256[] memory,
uint256[] memory,
bytes memory
) public view override returns (bytes4) {
_handleOverrideStatic();
return this.onERC1155BatchReceived.selector;
}
function _authorizeUpgrade(address newImplementation)
internal
view
returns (
uint256,
address,
uint256
)
override
onlyOwner
{
bytes memory rawContext = MinimalProxyStore.getContext(address(this));
if (rawContext.length == 0) return (0, address(0), 0);
return abi.decode(rawContext, (uint256, address, uint256));
bool isTrusted = IAccountGuardian(guardian).isTrustedImplementation(
newImplementation
);
if (!isTrusted) revert UntrustedImplementation();
}
function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal view override returns (uint256 validationData) {
bool isValid = SignatureChecker.isValidSignatureNow(
owner(),
userOpHash,
userOp.signature
);
if (isValid) {
return 0;
}
return 1;
}
function _validateAndUpdateNonce(UserOperation calldata userOp)
internal
override
{
if (_nonce++ != userOp.nonce) revert InvalidNonce();
}
/**
* @dev Executes a low-level call
*/
function _call(
address to,
uint256 value,
@@ -333,4 +340,41 @@ contract Account is IERC165, IERC1271, IAccount, MinimalReceiver {
}
}
}
function _handleOverride() internal {
address implementation = overrides[owner()][msg.sig];
if (implementation != address(0)) {
bytes memory result = _call(implementation, msg.value, msg.data);
assembly {
return(add(result, 32), mload(result))
}
}
}
function _callStatic(address to, bytes calldata data)
internal
view
returns (bytes memory result)
{
bool success;
(success, result) = to.staticcall(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
function _handleOverrideStatic() internal view {
address implementation = overrides[owner()][msg.sig];
if (implementation != address(0)) {
bytes memory result = _callStatic(implementation, msg.data);
assembly {
return(add(result, 32), mload(result))
}
}
}
}

View File

@@ -1,128 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./interfaces/IRegistry.sol";
import "./lib/MinimalProxyStore.sol";
import "./Account.sol";
/**
* @title A registry for token bound accounts
* @dev Determines the address for each token bound account and performs deployment of accounts
* @author Jayden Windle (jaydenwindle)
*/
contract AccountRegistry is IRegistry {
/**
* @dev Address of the account implementation
*/
address public immutable implementation;
constructor(address _implementation) {
implementation = _implementation;
}
/**
* @dev Creates the account for an ERC721 token. Will revert if account has already been deployed
*
* @param chainId the chainid of the network the ERC721 token exists on
* @param tokenCollection the contract address of the ERC721 token which will control the deployed account
* @param tokenId the token ID of the ERC721 token which will control the deployed account
* @return The address of the deployed ccount
*/
function createAccount(
uint256 chainId,
address tokenCollection,
uint256 tokenId
) external returns (address) {
return _createAccount(chainId, tokenCollection, tokenId);
}
/**
* @dev Deploys the account for an ERC721 token. Will revert if account has already been deployed
*
* @param tokenCollection the contract address of the ERC721 token which will control the deployed account
* @param tokenId the token ID of the ERC721 token which will control the deployed account
* @return The address of the deployed account
*/
function createAccount(address tokenCollection, uint256 tokenId)
external
returns (address)
{
return _createAccount(block.chainid, tokenCollection, tokenId);
}
/**
* @dev Gets the address of the account for an ERC721 token. If account is
* not yet deployed, returns the address it will be deployed to
*
* @param chainId the chainid of the network the ERC721 token exists on
* @param tokenCollection the address of the ERC721 token contract
* @param tokenId the tokenId of the ERC721 token that controls the account
* @return The account address
*/
function account(
uint256 chainId,
address tokenCollection,
uint256 tokenId
) external view returns (address) {
return _account(chainId, tokenCollection, tokenId);
}
/**
* @dev Gets the address of the account for an ERC721 token. If account is
* not yet deployed, returns the address it will be deployed to
*
* @param tokenCollection the address of the ERC721 token contract
* @param tokenId the tokenId of the ERC721 token that controls the account
* @return The account address
*/
function account(address tokenCollection, uint256 tokenId)
external
view
returns (address)
{
return _account(block.chainid, tokenCollection, tokenId);
}
function _createAccount(
uint256 chainId,
address tokenCollection,
uint256 tokenId
) internal returns (address) {
bytes memory encodedTokenData = abi.encode(
chainId,
tokenCollection,
tokenId
);
bytes32 salt = keccak256(encodedTokenData);
address accountProxy = MinimalProxyStore.cloneDeterministic(
implementation,
encodedTokenData,
salt
);
emit AccountCreated(accountProxy, tokenCollection, tokenId);
return accountProxy;
}
function _account(
uint256 chainId,
address tokenCollection,
uint256 tokenId
) internal view returns (address) {
bytes memory encodedTokenData = abi.encode(
chainId,
tokenCollection,
tokenId
);
bytes32 salt = keccak256(encodedTokenData);
address accountProxy = MinimalProxyStore.predictDeterministicAddress(
implementation,
encodedTokenData,
salt
);
return accountProxy;
}
}

View File

@@ -1,368 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "erc6551/interfaces/IERC6551Account.sol";
import "openzeppelin-contracts/utils/introspection/IERC165.sol";
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/token/ERC721/IERC721Receiver.sol";
import "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol";
import "openzeppelin-contracts/interfaces/IERC1271.sol";
import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol";
import "openzeppelin-contracts/proxy/utils/UUPSUpgradeable.sol";
import "sstore2/utils/Bytecode.sol";
import {BaseAccount as BaseERC4337Account, IEntryPoint, UserOperation, IAccount as IERC4337Account} from "account-abstraction/core/BaseAccount.sol";
import "./interfaces/IAccountGuardian.sol";
error NotAuthorized();
error InvalidInput();
error AccountLocked();
error ExceedsMaxLockTime();
error InvalidNonce();
error UntrustedImplementation();
contract AccountV2 is
IERC165,
IERC1271,
IERC6551Account,
IERC721Receiver,
IERC1155Receiver,
BaseERC4337Account
{
// @dev ERC-4337 entry point
address immutable _entryPoint;
// @dev AccountGuardian contract
address public immutable guardian;
// @dev Updated on each transaction
uint256 _nonce;
// @dev timestamp at which this account will be unlocked
uint256 public lockedUntil;
// @dev mapping from owner => selector => implementation
mapping(address => mapping(bytes4 => address)) public overrides;
// @dev mapping from owner => caller => selector => has permissions
mapping(address => mapping(address => mapping(bytes4 => bool)))
public permissions;
modifier onlyOwner() {
if (msg.sender != owner()) revert NotAuthorized();
_;
}
modifier onlyAuthorized() {
if (!isAuthorized(msg.sender, msg.sig)) revert NotAuthorized();
_;
}
modifier onlyUnlocked() {
if (isLocked()) revert AccountLocked();
_;
}
constructor(address _guardian, address entryPoint_) {
_entryPoint = entryPoint_;
guardian = _guardian;
}
receive() external payable {
_handleOverride();
}
fallback() external payable {
_handleOverride();
}
function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable onlyAuthorized onlyUnlocked returns (bytes memory) {
++_nonce;
_handleOverride();
return _call(to, value, data);
}
function setOverrides(
bytes4[] calldata selectors,
address[] calldata implementations
) external onlyUnlocked {
address _owner = owner();
if (msg.sender != _owner) revert NotAuthorized();
if (selectors.length != implementations.length) revert InvalidInput();
++_nonce;
for (uint256 i = 0; i < selectors.length; i++) {
overrides[_owner][selectors[i]] = implementations[i];
}
}
function setPermissions(
bytes4[] calldata selectors,
address[] calldata implementations
) external onlyUnlocked {
address _owner = owner();
if (msg.sender != _owner) revert NotAuthorized();
if (selectors.length != implementations.length) revert InvalidInput();
++_nonce;
for (uint256 i = 0; i < selectors.length; i++) {
permissions[_owner][implementations[i]][selectors[i]] = true;
}
}
function lock(uint256 _lockedUntil) external onlyOwner onlyUnlocked {
if (_lockedUntil > block.timestamp + 365 days)
revert ExceedsMaxLockTime();
++_nonce;
lockedUntil = _lockedUntil;
}
function isLocked() public view returns (bool) {
return lockedUntil > block.timestamp;
}
function isValidSignature(bytes32 hash, bytes memory signature)
external
view
returns (bytes4 magicValue)
{
_handleOverrideStatic();
bool isValid = SignatureChecker.isValidSignatureNow(
owner(),
hash,
signature
);
if (isValid) {
return IERC1271.isValidSignature.selector;
}
return "";
}
function token()
external
view
returns (
uint256 chainId,
address tokenContract,
uint256 tokenId
)
{
address self = address(this);
uint256 length = self.code.length;
if (length < 0x60) return (0, address(0), 0);
return
abi.decode(
Bytecode.codeAt(self, length - 0x60, length),
(uint256, address, uint256)
);
}
function nonce()
public
view
override(BaseERC4337Account, IERC6551Account)
returns (uint256)
{
return _nonce;
}
function entryPoint() public view override returns (IEntryPoint) {
return IEntryPoint(_entryPoint);
}
function owner() public view returns (address) {
(uint256 chainId, address tokenContract, uint256 tokenId) = this
.token();
if (chainId != block.chainid) return address(0);
return IERC721(tokenContract).ownerOf(tokenId);
}
function isAuthorized(address caller, bytes4 selector)
public
view
returns (bool)
{
(uint256 chainId, address tokenContract, uint256 tokenId) = this
.token();
address _owner = IERC721(tokenContract).ownerOf(tokenId);
// authorize token owner
if (caller == _owner) return true;
// authorize entrypoint for 4337 transactions
if (caller == _entryPoint) return true;
// authorize caller if owner has granted permissions for function call
if (permissions[_owner][caller][selector]) return true;
// authorize trusted cross-chain executors if not on native chain
if (
chainId != block.chainid &&
IAccountGuardian(guardian).isTrustedExecutor(caller)
) return true;
return false;
}
function supportsInterface(bytes4 interfaceId)
public
view
override
returns (bool)
{
bool defaultSupport = interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC1155Receiver).interfaceId ||
interfaceId == type(IERC6551Account).interfaceId ||
interfaceId == type(IERC4337Account).interfaceId;
if (defaultSupport) return true;
// if not supported by default, check override
_handleOverrideStatic();
return false;
}
function onERC721Received(
address,
address,
uint256,
bytes memory
) public view override returns (bytes4) {
_handleOverrideStatic();
return this.onERC721Received.selector;
}
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes memory
) public view override returns (bytes4) {
_handleOverrideStatic();
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived(
address,
address,
uint256[] memory,
uint256[] memory,
bytes memory
) public view override returns (bytes4) {
_handleOverrideStatic();
return this.onERC1155BatchReceived.selector;
}
function _authorizeUpgrade(address newImplementation)
internal
view
onlyOwner
{
bool isTrusted = IAccountGuardian(guardian).isTrustedImplementation(
newImplementation
);
if (!isTrusted) revert UntrustedImplementation();
}
function _validateSignature(
UserOperation calldata userOp,
bytes32 userOpHash
) internal view override returns (uint256 validationData) {
bool isValid = SignatureChecker.isValidSignatureNow(
owner(),
userOpHash,
userOp.signature
);
if (isValid) {
return 0;
}
return 1;
}
function _validateAndUpdateNonce(UserOperation calldata userOp)
internal
override
{
if (_nonce++ != userOp.nonce) revert InvalidNonce();
}
function _call(
address to,
uint256 value,
bytes calldata data
) internal returns (bytes memory result) {
bool success;
(success, result) = to.call{value: value}(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
function _handleOverride() internal {
address implementation = overrides[owner()][msg.sig];
if (implementation != address(0)) {
bytes memory result = _call(implementation, msg.value, msg.data);
assembly {
return(add(result, 32), mload(result))
}
}
}
function _callStatic(address to, bytes calldata data)
internal
view
returns (bytes memory result)
{
bool success;
(success, result) = to.staticcall(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
function _handleOverrideStatic() internal view {
address implementation = overrides[owner()][msg.sig];
if (implementation != address(0)) {
bytes memory result = _callStatic(implementation, msg.data);
assembly {
return(add(result, 32), mload(result))
}
}
}
}

View File

@@ -1,23 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "openzeppelin-contracts/access/Ownable2Step.sol";
contract CrossChainExecutorList is Ownable2Step {
mapping(uint256 => mapping(address => bool)) public isCrossChainExecutor;
/**
* @dev Enables or disables a trusted cross-chain executor.
*
* @param chainId the chainid of the network the executor exists on
* @param executor the address of the executor
* @param enabled true if executor should be enabled, false otherwise
*/
function setCrossChainExecutor(
uint256 chainId,
address executor,
bool enabled
) external onlyOwner {
isCrossChainExecutor[chainId][executor] = enabled;
}
}

View File

@@ -1,12 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "openzeppelin-contracts/token/ERC721/utils/ERC721Holder.sol";
import "openzeppelin-contracts/token/ERC1155/utils/ERC1155Holder.sol";
contract MinimalReceiver is ERC721Holder, ERC1155Holder {
/**
* @dev Allows all Ether transfers
*/
receive() external payable virtual {}
}

View File

@@ -1,17 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface IAccount {
function owner() external view returns (address);
function token()
external
view
returns (address tokenContract, uint256 tokenId);
function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable returns (bytes memory);
}

View File

@@ -1,19 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface IRegistry {
event AccountCreated(
address account,
address indexed tokenContract,
uint256 indexed tokenId
);
function createAccount(address tokenContract, uint256 tokenId)
external
returns (address);
function account(address tokenContract, uint256 tokenId)
external
view
returns (address);
}

View File

@@ -1,135 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "openzeppelin-contracts/utils/Create2.sol";
import "sstore2/utils/Bytecode.sol";
/**
* @title A library for deploying EIP-1167 minimal proxy contracts with embedded constant data
* @author Jayden Windle (jaydenwindle)
*/
library MinimalProxyStore {
error CreateError();
error ContextOverflow();
/**
* @dev Returns bytecode for a minmal proxy with additional context data appended to it
*
* @param implementation the implementation this proxy will delegate to
* @param context the data to be appended to the proxy
* @return the generated bytecode
*/
function getBytecode(address implementation, bytes memory context)
internal
pure
returns (bytes memory)
{
return
abi.encodePacked(
hex"3d61", // RETURNDATASIZE, PUSH2
uint16(0x2d + context.length + 1), // size of minimal proxy (45 bytes) + size of context + stop byte
hex"8060", // DUP1, PUSH1
uint8(0x0a + 1), // default offset (0x0a) + 1 byte because we increased size from uint8 to uint16
hex"3d3981f3363d3d373d3d3d363d73", // standard EIP1167 implementation
implementation, // implementation address
hex"5af43d82803e903d91602b57fd5bf3", // standard EIP1167 implementation
hex"00", // stop byte (prevents context from executing as code)
context // appended context data
);
}
/**
* @dev Fetches the context data stored in a deployed proxy
*
* @param instance the proxy to query context data for
* @return the queried context data
*/
function getContext(address instance) internal view returns (bytes memory) {
uint256 instanceCodeLength = instance.code.length;
return Bytecode.codeAt(instance, 46, instanceCodeLength);
}
/**
* @dev Deploys and returns the address of a clone with stored context data that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*
* @param implementation the implementation to delegate to
* @param context context data to be stored in the proxy
* @return instance the address of the deployed proxy
*/
function clone(address implementation, bytes memory context)
internal
returns (address instance)
{
// Generate bytecode for proxy
bytes memory code = getBytecode(implementation, context);
// Deploy contract using create
assembly {
instance := create(0, add(code, 32), mload(code))
}
// If address is zero, deployment failed
if (instance == address(0)) revert CreateError();
}
/**
* @dev Deploys and returns the address of a clone with stored context data that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple time will revert, since
* the clones cannot be deployed twice at the same address.
*
* @param implementation the implementation to delegate to
* @param context context data to be stored in the proxy
* @return instance the address of the deployed proxy
*/
function cloneDeterministic(
address implementation,
bytes memory context,
bytes32 salt
) internal returns (address instance) {
bytes memory code = getBytecode(implementation, context);
// Deploy contract using create2
assembly {
instance := create2(0, add(code, 32), mload(code), salt)
}
// If address is zero, deployment failed
if (instance == address(0)) revert CreateError();
}
/**
* @dev Computes the address of a clone deployed using {MinimalProxyStore-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes memory context,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
bytes memory code = getBytecode(implementation, context);
return Create2.computeAddress(salt, keccak256(code), deployer);
}
/**
* @dev Computes the address of a clone deployed using {MinimalProxyStore-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes memory context,
bytes32 salt
) internal view returns (address predicted) {
return
predictDeterministicAddress(
implementation,
context,
salt,
address(this)
);
}
}

View File

@@ -6,25 +6,28 @@ import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "../src/CrossChainExecutorList.sol";
import "erc6551/ERC6551Registry.sol";
import "erc6551/interfaces/IERC6551Account.sol";
import "../src/Account.sol";
import "../src/AccountRegistry.sol";
import "../src/AccountGuardian.sol";
import "./mocks/MockERC721.sol";
import "./mocks/MockExecutor.sol";
import "./mocks/MockReverter.sol";
contract AccountTest is Test {
CrossChainExecutorList ccExecutorList;
Account implementation;
AccountRegistry public accountRegistry;
AccountGuardian public guardian;
ERC6551Registry public registry;
MockERC721 public tokenCollection;
function setUp() public {
ccExecutorList = new CrossChainExecutorList();
implementation = new Account(address(ccExecutorList));
accountRegistry = new AccountRegistry(address(implementation));
guardian = new AccountGuardian();
implementation = new Account(address(guardian), address(0));
registry = new ERC6551Registry();
tokenCollection = new MockERC721();
}
@@ -36,9 +39,13 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
@@ -47,17 +54,21 @@ contract AccountTest is Test {
// should fail if user2 tries to use account
vm.prank(user2);
vm.expectRevert(Account.NotAuthorized.selector);
vm.expectRevert(NotAuthorized.selector);
account.executeCall(payable(user2), 0.1 ether, "");
// should fail if user2 tries to set executor
// should fail if user2 tries to set override
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = Account.executeCall.selector;
address[] memory implementations = new address[](1);
implementations[0] = vm.addr(1337);
vm.prank(user2);
vm.expectRevert(Account.NotAuthorized.selector);
account.setExecutor(vm.addr(1337));
vm.expectRevert(NotAuthorized.selector);
account.setPermissions(selectors, implementations);
// should fail if user2 tries to lock account
vm.prank(user2);
vm.expectRevert(Account.NotAuthorized.selector);
vm.expectRevert(NotAuthorized.selector);
account.lock(364 days);
}
@@ -68,9 +79,13 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
@@ -79,7 +94,7 @@ contract AccountTest is Test {
// should fail if user2 tries to use account
vm.prank(user2);
vm.expectRevert(Account.NotAuthorized.selector);
vm.expectRevert(NotAuthorized.selector);
account.executeCall(payable(user2), 0.1 ether, "");
vm.prank(user1);
@@ -98,9 +113,13 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
Account account = Account(payable(accountAddress));
@@ -123,9 +142,13 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
Account account = Account(payable(accountAddress));
@@ -146,9 +169,13 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
@@ -157,7 +184,7 @@ contract AccountTest is Test {
// cannot be locked for more than 365 days
vm.prank(user1);
vm.expectRevert(Account.ExceedsMaxLockTime.selector);
vm.expectRevert(ExceedsMaxLockTime.selector);
account.lock(366 days);
// lock account for 10 days
@@ -169,12 +196,12 @@ contract AccountTest is Test {
// transaction should revert if account is locked
vm.prank(user1);
vm.expectRevert(Account.AccountLocked.selector);
vm.expectRevert(AccountLocked.selector);
account.executeCall(payable(user1), 1 ether, "");
// fallback calls should revert if account is locked
vm.prank(user1);
vm.expectRevert(Account.AccountLocked.selector);
vm.expectRevert(AccountLocked.selector);
(bool success, bytes memory result) = accountAddress.call(
abi.encodeWithSignature("customFunction()")
);
@@ -183,14 +210,20 @@ contract AccountTest is Test {
success;
result;
// setExecutor calls should revert if account is locked
vm.prank(user1);
vm.expectRevert(Account.AccountLocked.selector);
account.setExecutor(vm.addr(1337));
// setOverrides calls should revert if account is locked
{
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = Account.executeCall.selector;
address[] memory implementations = new address[](1);
implementations[0] = vm.addr(1337);
vm.prank(user1);
vm.expectRevert(AccountLocked.selector);
account.setOverrides(selectors, implementations);
}
// lock calls should revert if account is locked
vm.prank(user1);
vm.expectRevert(Account.AccountLocked.selector);
vm.expectRevert(AccountLocked.selector);
account.lock(0);
// signing should fail if account is locked
@@ -219,15 +252,19 @@ contract AccountTest is Test {
assertEq(returnValue1, IERC1271.isValidSignature.selector);
}
function testCustomExecutorFallback(uint256 tokenId) public {
function testCustomOverridesFallback(uint256 tokenId) public {
address user1 = vm.addr(1);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
@@ -236,35 +273,22 @@ contract AccountTest is Test {
MockExecutor mockExecutor = new MockExecutor();
// calls succeed with noop if executor is undefined
// calls succeed with noop if override is undefined
(bool success, bytes memory result) = accountAddress.call(
abi.encodeWithSignature("customFunction()")
);
assertEq(success, true);
assertEq(result, "");
// calls succeed with noop if executor is EOA
// set overrides on account
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = bytes4(abi.encodeWithSignature("customFunction()"));
selectors[1] = bytes4(abi.encodeWithSignature("fail()"));
address[] memory implementations = new address[](2);
implementations[0] = address(mockExecutor);
implementations[1] = address(mockExecutor);
vm.prank(user1);
account.setExecutor(vm.addr(1337));
(bool success1, bytes memory result1) = accountAddress.call(
abi.encodeWithSignature("customFunction()")
);
assertEq(success1, true);
assertEq(result1, "");
assertEq(account.isAuthorized(user1), true);
assertEq(account.isAuthorized(address(mockExecutor)), false);
vm.prank(user1);
account.setExecutor(address(mockExecutor));
assertEq(account.isAuthorized(user1), true);
assertEq(account.isAuthorized(address(mockExecutor)), true);
assertEq(
account.isValidSignature(bytes32(0), ""),
IERC1271.isValidSignature.selector
);
account.setOverrides(selectors, implementations);
// execution module handles fallback calls
assertEq(MockExecutor(accountAddress).customFunction(), 12345);
@@ -274,31 +298,44 @@ contract AccountTest is Test {
MockExecutor(accountAddress).fail();
}
function testCustomExecutorCalls(uint256 tokenId) public {
/**/
function testCustomPermissions(uint256 tokenId) public {
address user1 = vm.addr(1);
address user2 = vm.addr(2);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
Account account = Account(payable(accountAddress));
assertEq(account.isAuthorized(user2), false);
bytes4 selector = bytes4(
abi.encodeWithSignature("executeCall(address,uint256,bytes)")
);
assertEq(account.isAuthorized(user2, selector), false);
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = selector;
address[] memory implementations = new address[](1);
implementations[0] = address(user2);
vm.prank(user1);
account.setExecutor(user2);
account.setPermissions(selectors, implementations);
assertEq(account.isAuthorized(user2), true);
assertEq(account.isAuthorized(user2, selector), true);
vm.prank(user2);
account.executeTrustedCall(user2, 0.1 ether, "");
account.executeCall(user2, 0.1 ether, "");
assertEq(user2.balance, 0.1 ether);
}
@@ -313,47 +350,53 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
chainId,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
Account account = Account(payable(accountAddress));
assertEq(account.isAuthorized(crossChainExecutor), false);
CrossChainExecutorList(ccExecutorList).setCrossChainExecutor(
chainId,
crossChainExecutor,
true
bytes4 selector = bytes4(
abi.encodeWithSignature("executeCall(address,uint256,bytes)")
);
assertEq(account.isAuthorized(crossChainExecutor), true);
assertEq(account.isAuthorized(crossChainExecutor, selector), false);
guardian.setTrustedExecutor(crossChainExecutor, true);
assertEq(account.isAuthorized(crossChainExecutor, selector), true);
vm.prank(crossChainExecutor);
account.executeCrossChainCall(user1, 0.1 ether, "");
account.executeCall(user1, 0.1 ether, "");
assertEq(user1.balance, 0.1 ether);
address notCrossChainExecutor = vm.addr(3);
vm.prank(notCrossChainExecutor);
vm.expectRevert(Account.NotAuthorized.selector);
Account(payable(account)).executeCrossChainCall(user1, 0.1 ether, "");
vm.expectRevert(NotAuthorized.selector);
Account(payable(account)).executeCall(user1, 0.1 ether, "");
assertEq(user1.balance, 0.1 ether);
address nativeAccountAddress = accountRegistry.createAccount(
address nativeAccountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.prank(crossChainExecutor);
vm.expectRevert(Account.NotAuthorized.selector);
Account(payable(nativeAccountAddress)).executeCrossChainCall(
vm.expectRevert(NotAuthorized.selector);
Account(payable(nativeAccountAddress)).executeCall(
user1,
0.1 ether,
""
@@ -368,9 +411,13 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
@@ -389,7 +436,7 @@ contract AccountTest is Test {
}
function testAccountOwnerIsNullIfContextNotSet() public {
address accountClone = Clones.clone(accountRegistry.implementation());
address accountClone = Clones.clone(address(implementation));
assertEq(Account(payable(accountClone)).owner(), address(0));
}
@@ -401,34 +448,27 @@ contract AccountTest is Test {
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
Account account = Account(payable(accountAddress));
assertEq(account.supportsInterface(type(IAccount).interfaceId), true);
assertEq(
account.supportsInterface(type(IERC6551Account).interfaceId),
true
);
assertEq(
account.supportsInterface(type(IERC1155Receiver).interfaceId),
true
);
assertEq(account.supportsInterface(type(IERC165).interfaceId), true);
assertEq(
account.supportsInterface(IERC1271.isValidSignature.selector),
false
);
MockExecutor mockExecutor = new MockExecutor();
vm.prank(user1);
account.setExecutor(address(mockExecutor));
assertEq(
account.supportsInterface(IERC1271.isValidSignature.selector),
true
);
}
}

View File

@@ -6,28 +6,30 @@ import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "../src/CrossChainExecutorList.sol";
import "erc6551/ERC6551Registry.sol";
import "erc6551/interfaces/IERC6551Account.sol";
import "../src/Account.sol";
import "../src/AccountRegistry.sol";
import "../src/AccountGuardian.sol";
import "./mocks/MockERC721.sol";
import "./mocks/MockERC1155.sol";
contract AccountTest is Test {
contract AccountERC1155Test is Test {
MockERC1155 public dummyERC1155;
CrossChainExecutorList ccExecutorList;
Account implementation;
AccountRegistry public accountRegistry;
AccountGuardian public guardian;
ERC6551Registry public registry;
MockERC721 public tokenCollection;
function setUp() public {
dummyERC1155 = new MockERC1155();
ccExecutorList = new CrossChainExecutorList();
implementation = new Account(address(ccExecutorList));
accountRegistry = new AccountRegistry(address(implementation));
guardian = new AccountGuardian();
implementation = new Account(address(guardian), address(0));
registry = new ERC6551Registry();
tokenCollection = new MockERC721();
}
@@ -35,9 +37,12 @@ contract AccountTest is Test {
function testTransferERC1155PreDeploy(uint256 tokenId) public {
address user1 = vm.addr(1);
address computedAccountInstance = accountRegistry.account(
address computedAccountInstance = registry.account(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0
);
tokenCollection.mint(user1, tokenId);
@@ -47,9 +52,13 @@ contract AccountTest is Test {
assertEq(dummyERC1155.balanceOf(computedAccountInstance, 1), 10);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
Account account = Account(payable(accountAddress));
@@ -76,9 +85,13 @@ contract AccountTest is Test {
function testTransferERC1155PostDeploy(uint256 tokenId) public {
address user1 = vm.addr(1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
tokenCollection.mint(user1, tokenId);

View File

@@ -6,28 +6,30 @@ import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "../src/CrossChainExecutorList.sol";
import "erc6551/ERC6551Registry.sol";
import "erc6551/interfaces/IERC6551Account.sol";
import "../src/Account.sol";
import "../src/AccountRegistry.sol";
import "../src/AccountGuardian.sol";
import "./mocks/MockERC721.sol";
import "./mocks/MockERC20.sol";
contract AccountTest is Test {
contract AccountERC20Test is Test {
MockERC20 public dummyERC20;
CrossChainExecutorList ccExecutorList;
Account implementation;
AccountRegistry public accountRegistry;
AccountGuardian public guardian;
ERC6551Registry public registry;
MockERC721 public tokenCollection;
function setUp() public {
dummyERC20 = new MockERC20();
ccExecutorList = new CrossChainExecutorList();
implementation = new Account(address(ccExecutorList));
accountRegistry = new AccountRegistry(address(implementation));
guardian = new AccountGuardian();
implementation = new Account(address(guardian), address(0));
registry = new ERC6551Registry();
tokenCollection = new MockERC721();
}
@@ -35,9 +37,12 @@ contract AccountTest is Test {
function testTransferERC20PreDeploy(uint256 tokenId) public {
address user1 = vm.addr(1);
address computedAccountInstance = accountRegistry.account(
address computedAccountInstance = registry.account(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0
);
tokenCollection.mint(user1, tokenId);
@@ -47,9 +52,13 @@ contract AccountTest is Test {
assertEq(dummyERC20.balanceOf(computedAccountInstance), 1 ether);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
Account account = Account(payable(accountAddress));
@@ -69,9 +78,13 @@ contract AccountTest is Test {
function testTransferERC20PostDeploy(uint256 tokenId) public {
address user1 = vm.addr(1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
tokenCollection.mint(user1, tokenId);

View File

@@ -6,27 +6,29 @@ import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "../src/CrossChainExecutorList.sol";
import "erc6551/ERC6551Registry.sol";
import "erc6551/interfaces/IERC6551Account.sol";
import "../src/Account.sol";
import "../src/AccountRegistry.sol";
import "../src/AccountGuardian.sol";
import "./mocks/MockERC721.sol";
contract AccountTest is Test {
contract AccountERC721Test is Test {
MockERC721 public dummyERC721;
CrossChainExecutorList ccExecutorList;
Account implementation;
AccountRegistry public accountRegistry;
AccountGuardian public guardian;
ERC6551Registry public registry;
MockERC721 public tokenCollection;
function setUp() public {
dummyERC721 = new MockERC721();
ccExecutorList = new CrossChainExecutorList();
implementation = new Account(address(ccExecutorList));
accountRegistry = new AccountRegistry(address(implementation));
guardian = new AccountGuardian();
implementation = new Account(address(guardian), address(0));
registry = new ERC6551Registry();
tokenCollection = new MockERC721();
}
@@ -34,9 +36,12 @@ contract AccountTest is Test {
function testTransferERC721PreDeploy(uint256 tokenId) public {
address user1 = vm.addr(1);
address computedAccountInstance = accountRegistry.account(
address computedAccountInstance = registry.account(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0
);
tokenCollection.mint(user1, tokenId);
@@ -47,9 +52,13 @@ contract AccountTest is Test {
assertEq(dummyERC721.balanceOf(computedAccountInstance), 1);
assertEq(dummyERC721.ownerOf(1), computedAccountInstance);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
Account account = Account(payable(accountAddress));
@@ -75,9 +84,13 @@ contract AccountTest is Test {
function testTransferERC721PostDeploy(uint256 tokenId) public {
address user1 = vm.addr(1);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
tokenCollection.mint(user1, tokenId);

View File

@@ -6,23 +6,25 @@ import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "../src/CrossChainExecutorList.sol";
import "erc6551/ERC6551Registry.sol";
import "erc6551/interfaces/IERC6551Account.sol";
import "../src/Account.sol";
import "../src/AccountRegistry.sol";
import "../src/AccountGuardian.sol";
import "./mocks/MockERC721.sol";
contract AccountTest is Test {
CrossChainExecutorList ccExecutorList;
contract AccountETHTest is Test {
Account implementation;
AccountRegistry public accountRegistry;
AccountGuardian public guardian;
ERC6551Registry public registry;
MockERC721 public tokenCollection;
function setUp() public {
ccExecutorList = new CrossChainExecutorList();
implementation = new Account(address(ccExecutorList));
accountRegistry = new AccountRegistry(address(implementation));
guardian = new AccountGuardian();
implementation = new Account(address(guardian), address(0));
registry = new ERC6551Registry();
tokenCollection = new MockERC721();
}
@@ -33,9 +35,12 @@ contract AccountTest is Test {
vm.deal(user1, 0.2 ether);
// get address that account will be deployed to (before token is minted)
address accountAddress = accountRegistry.account(
address accountAddress = registry.account(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0
);
// mint token for account to user1
@@ -51,9 +56,13 @@ contract AccountTest is Test {
assertEq(accountAddress.balance, 0.2 ether);
// deploy account contract (from a different wallet)
address createdAccountInstance = accountRegistry.createAccount(
address createdAccountInstance = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
assertEq(accountAddress, createdAccountInstance);
@@ -73,9 +82,13 @@ contract AccountTest is Test {
address user1 = vm.addr(1);
vm.deal(user1, 0.2 ether);
address accountAddress = accountRegistry.createAccount(
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId
tokenId,
0,
""
);
tokenCollection.mint(user1, tokenId);

View File

@@ -1,53 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/interfaces/IRegistry.sol";
import "../src/lib/MinimalProxyStore.sol";
import "../src/CrossChainExecutorList.sol";
import "../src/Account.sol";
import "../src/AccountRegistry.sol";
contract AccountRegistryTest is Test {
CrossChainExecutorList ccExecutorList;
Account implementation;
AccountRegistry public accountRegistry;
event AccountCreated(
address account,
address indexed tokenContract,
uint256 indexed tokenId
);
function setUp() public {
ccExecutorList = new CrossChainExecutorList();
implementation = new Account(address(ccExecutorList));
accountRegistry = new AccountRegistry(address(implementation));
}
function testDeployAccount(address tokenCollection, uint256 tokenId)
public
{
assertTrue(address(accountRegistry) != address(0));
address predictedAccountAddress = accountRegistry.account(
tokenCollection,
tokenId
);
vm.expectEmit(true, true, true, true);
emit AccountCreated(predictedAccountAddress, tokenCollection, tokenId);
address accountAddress = accountRegistry.createAccount(
tokenCollection,
tokenId
);
assertTrue(accountAddress != address(0));
assertTrue(accountAddress == predictedAccountAddress);
assertEq(
MinimalProxyStore.getContext(accountAddress),
abi.encode(block.chainid, tokenCollection, tokenId)
);
}
}

View File

@@ -1,477 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "openzeppelin-contracts/proxy/Clones.sol";
import "erc6551/ERC6551Registry.sol";
import "erc6551/interfaces/IERC6551Account.sol";
import "../src/CrossChainExecutorList.sol";
import "../src/Account.sol";
import "../src/AccountV2.sol";
import "../src/AccountGuardian.sol";
import "../src/AccountRegistry.sol";
import "./mocks/MockERC721.sol";
import "./mocks/MockExecutor.sol";
import "./mocks/MockReverter.sol";
contract AccountV2Test is Test {
AccountV2 implementation;
AccountGuardian public guardian;
ERC6551Registry public registry;
MockERC721 public tokenCollection;
function setUp() public {
guardian = new AccountGuardian();
implementation = new AccountV2(address(guardian), address(0));
registry = new ERC6551Registry();
tokenCollection = new MockERC721();
}
function testNonOwnerCallsFail(uint256 tokenId) public {
address user1 = vm.addr(1);
address user2 = vm.addr(2);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
// should fail if user2 tries to use account
vm.prank(user2);
vm.expectRevert(NotAuthorized.selector);
account.executeCall(payable(user2), 0.1 ether, "");
// should fail if user2 tries to set override
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = AccountV2.executeCall.selector;
address[] memory implementations = new address[](1);
implementations[0] = vm.addr(1337);
vm.prank(user2);
vm.expectRevert(NotAuthorized.selector);
account.setPermissions(selectors, implementations);
// should fail if user2 tries to lock account
vm.prank(user2);
vm.expectRevert(NotAuthorized.selector);
account.lock(364 days);
}
function testAccountOwnershipTransfer(uint256 tokenId) public {
address user1 = vm.addr(1);
address user2 = vm.addr(2);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
// should fail if user2 tries to use account
vm.prank(user2);
vm.expectRevert(NotAuthorized.selector);
account.executeCall(payable(user2), 0.1 ether, "");
vm.prank(user1);
tokenCollection.safeTransferFrom(user1, user2, tokenId);
// should succeed now that user2 is owner
vm.prank(user2);
account.executeCall(payable(user2), 0.1 ether, "");
assertEq(user2.balance, 0.1 ether);
}
function testMessageVerification(uint256 tokenId) public {
address user1 = vm.addr(1);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
AccountV2 account = AccountV2(payable(accountAddress));
bytes32 hash = keccak256("This is a signed message");
(uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(1, hash);
bytes memory signature1 = abi.encodePacked(r1, s1, v1);
bytes4 returnValue1 = account.isValidSignature(hash, signature1);
assertEq(returnValue1, IERC1271.isValidSignature.selector);
}
function testMessageVerificationForUnauthorizedUser(uint256 tokenId)
public
{
address user1 = vm.addr(1);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
AccountV2 account = AccountV2(payable(accountAddress));
bytes32 hash = keccak256("This is a signed message");
(uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(2, hash);
bytes memory signature2 = abi.encodePacked(r2, s2, v2);
bytes4 returnValue2 = account.isValidSignature(hash, signature2);
assertEq(returnValue2, 0);
}
function testAccountLocksAndUnlocks(uint256 tokenId) public {
address user1 = vm.addr(1);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
// cannot be locked for more than 365 days
vm.prank(user1);
vm.expectRevert(ExceedsMaxLockTime.selector);
account.lock(366 days);
// lock account for 10 days
uint256 unlockTimestamp = block.timestamp + 10 days;
vm.prank(user1);
account.lock(unlockTimestamp);
assertEq(account.isLocked(), true);
// transaction should revert if account is locked
vm.prank(user1);
vm.expectRevert(AccountLocked.selector);
account.executeCall(payable(user1), 1 ether, "");
// fallback calls should revert if account is locked
vm.prank(user1);
vm.expectRevert(AccountLocked.selector);
(bool success, bytes memory result) = accountAddress.call(
abi.encodeWithSignature("customFunction()")
);
// silence unused variable compiler warnings
success;
result;
// setOverrides calls should revert if account is locked
{
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = AccountV2.executeCall.selector;
address[] memory implementations = new address[](1);
implementations[0] = vm.addr(1337);
vm.prank(user1);
vm.expectRevert(AccountLocked.selector);
account.setOverrides(selectors, implementations);
}
// lock calls should revert if account is locked
vm.prank(user1);
vm.expectRevert(Account.AccountLocked.selector);
account.lock(0);
// signing should fail if account is locked
bytes32 hash = keccak256("This is a signed message");
(uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(2, hash);
bytes memory signature1 = abi.encodePacked(r1, s1, v1);
bytes4 returnValue = account.isValidSignature(hash, signature1);
assertEq(returnValue, 0);
// warp to timestamp after account is unlocked
vm.warp(unlockTimestamp + 1 days);
// transaction succeed now that account lock has expired
vm.prank(user1);
account.executeCall(payable(user1), 1 ether, "");
assertEq(user1.balance, 1 ether);
// signing should now that account lock has expired
bytes32 hashAfterUnlock = keccak256("This is a signed message");
(uint8 v2, bytes32 r2, bytes32 s2) = vm.sign(1, hashAfterUnlock);
bytes memory signature2 = abi.encodePacked(r2, s2, v2);
bytes4 returnValue1 = account.isValidSignature(
hashAfterUnlock,
signature2
);
assertEq(returnValue1, IERC1271.isValidSignature.selector);
}
function testCustomOverridesFallback(uint256 tokenId) public {
address user1 = vm.addr(1);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
MockExecutor mockExecutor = new MockExecutor();
// calls succeed with noop if override is undefined
(bool success, bytes memory result) = accountAddress.call(
abi.encodeWithSignature("customFunction()")
);
assertEq(success, true);
assertEq(result, "");
// set overrides on account
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = bytes4(abi.encodeWithSignature("customFunction()"));
selectors[1] = bytes4(abi.encodeWithSignature("fail()"));
address[] memory implementations = new address[](2);
implementations[0] = address(mockExecutor);
implementations[1] = address(mockExecutor);
vm.prank(user1);
account.setOverrides(selectors, implementations);
// execution module handles fallback calls
assertEq(MockExecutor(accountAddress).customFunction(), 12345);
// execution bubbles up errors on revert
vm.expectRevert(MockReverter.MockError.selector);
MockExecutor(accountAddress).fail();
}
/**/
function testCustomPermissions(uint256 tokenId) public {
address user1 = vm.addr(1);
address user2 = vm.addr(2);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
bytes4 selector = bytes4(
abi.encodeWithSignature("executeCall(address,uint256,bytes)")
);
assertEq(account.isAuthorized(user2, selector), false);
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = selector;
address[] memory implementations = new address[](1);
implementations[0] = address(user2);
vm.prank(user1);
account.setPermissions(selectors, implementations);
assertEq(account.isAuthorized(user2, selector), true);
vm.prank(user2);
account.executeCall(user2, 0.1 ether, "");
assertEq(user2.balance, 0.1 ether);
}
function testCrossChainCalls() public {
uint256 tokenId = 1;
address user1 = vm.addr(1);
address crossChainExecutor = vm.addr(2);
uint256 chainId = block.chainid + 1;
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
chainId,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
bytes4 selector = bytes4(
abi.encodeWithSignature("executeCall(address,uint256,bytes)")
);
assertEq(account.isAuthorized(crossChainExecutor, selector), false);
guardian.setTrustedExecutor(crossChainExecutor, true);
assertEq(account.isAuthorized(crossChainExecutor, selector), true);
vm.prank(crossChainExecutor);
account.executeCall(user1, 0.1 ether, "");
assertEq(user1.balance, 0.1 ether);
address notCrossChainExecutor = vm.addr(3);
vm.prank(notCrossChainExecutor);
vm.expectRevert(NotAuthorized.selector);
AccountV2(payable(account)).executeCall(user1, 0.1 ether, "");
assertEq(user1.balance, 0.1 ether);
address nativeAccountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.prank(crossChainExecutor);
vm.expectRevert(NotAuthorized.selector);
AccountV2(payable(nativeAccountAddress)).executeCall(
user1,
0.1 ether,
""
);
assertEq(user1.balance, 0.1 ether);
}
function testExecuteCallRevert(uint256 tokenId) public {
address user1 = vm.addr(1);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
MockReverter mockReverter = new MockReverter();
vm.prank(user1);
vm.expectRevert(MockReverter.MockError.selector);
account.executeCall(
payable(address(mockReverter)),
0,
abi.encodeWithSignature("fail()")
);
}
function testAccountOwnerIsNullIfContextNotSet() public {
address accountClone = Clones.clone(address(implementation));
assertEq(AccountV2(payable(accountClone)).owner(), address(0));
}
function testEIP165Support() public {
uint256 tokenId = 1;
address user1 = vm.addr(1);
tokenCollection.mint(user1, tokenId);
assertEq(tokenCollection.ownerOf(tokenId), user1);
address accountAddress = registry.createAccount(
address(implementation),
block.chainid,
address(tokenCollection),
tokenId,
0,
""
);
vm.deal(accountAddress, 1 ether);
AccountV2 account = AccountV2(payable(accountAddress));
assertEq(
account.supportsInterface(type(IERC6551Account).interfaceId),
true
);
assertEq(
account.supportsInterface(type(IERC1155Receiver).interfaceId),
true
);
assertEq(account.supportsInterface(type(IERC165).interfaceId), true);
}
}

View File

@@ -1,53 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/CrossChainExecutorList.sol";
import "../src/lib/MinimalProxyStore.sol";
contract AccountRegistryTest is Test {
CrossChainExecutorList public crossChainExecutorList;
function setUp() public {
crossChainExecutorList = new CrossChainExecutorList();
}
function testSetCrossChainExecutor() public {
address crossChainExecutor = vm.addr(1);
address notCrossChainExecutor = vm.addr(2);
crossChainExecutorList.setCrossChainExecutor(
block.chainid,
crossChainExecutor,
true
);
assertTrue(
crossChainExecutorList.isCrossChainExecutor(
block.chainid,
crossChainExecutor
)
);
assertEq(
crossChainExecutorList.isCrossChainExecutor(
block.chainid,
notCrossChainExecutor
),
false
);
crossChainExecutorList.setCrossChainExecutor(
block.chainid,
crossChainExecutor,
false
);
assertEq(
crossChainExecutorList.isCrossChainExecutor(
block.chainid,
crossChainExecutor
),
false
);
}
}

View File

@@ -1,106 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/lib/MinimalProxyStore.sol";
contract TestContract {
error Failed();
function test() public pure returns (uint256) {
return 123;
}
function fails() public pure {
revert Failed();
}
}
contract MinimalProxyStoreTest is Test {
function testDeploymentSucceeds() public {
TestContract testContract = new TestContract();
address clone = MinimalProxyStore.clone(
address(testContract),
abi.encode("hello")
);
assertTrue(clone != address(0));
assertEq(TestContract(clone).test(), 123);
}
function testReverts() public {
TestContract testContract = new TestContract();
address clone = MinimalProxyStore.clone(
address(testContract),
abi.encode("hello")
);
assertTrue(clone != address(0));
vm.expectRevert(TestContract.Failed.selector);
TestContract(clone).fails();
}
function testGetContext() public {
TestContract testContract = new TestContract();
bytes memory context = abi.encode("hello");
address clone = MinimalProxyStore.clone(address(testContract), context);
assertTrue(clone != address(0));
assertEq(TestContract(clone).test(), 123);
bytes memory recoveredContext = MinimalProxyStore.getContext(clone);
assertEq(recoveredContext, context);
}
function testCreate2() public {
TestContract testContract = new TestContract();
bytes memory context = abi.encode("hello");
address clone = MinimalProxyStore.cloneDeterministic(
address(testContract),
context,
keccak256("hello")
);
assertTrue(clone != address(0));
assertEq(TestContract(clone).test(), 123);
bytes memory recoveredContext = MinimalProxyStore.getContext(clone);
assertEq(recoveredContext, context);
address predictedAddress = MinimalProxyStore
.predictDeterministicAddress(
address(testContract),
context,
keccak256("hello")
);
assertEq(clone, predictedAddress);
}
function testRedeploymentFails() public {
TestContract testContract = new TestContract();
bytes memory context = abi.encode("hello");
MinimalProxyStore.cloneDeterministic(
address(testContract),
context,
keccak256("hello")
);
vm.expectRevert(MinimalProxyStore.CreateError.selector);
MinimalProxyStore.cloneDeterministic(
address(testContract),
context,
keccak256("hello")
);
}
}