mirror of
https://github.com/placeholder-soft/tokenbound.git
synced 2026-01-12 17:02:56 +08:00
Removed old implementation + tests, migrated all asset transfer tests to v2 implementation
This commit is contained in:
526
src/Account.sol
526
src/Account.sol
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user