mirror of
https://github.com/placeholder-soft/tokenbound.git
synced 2026-01-12 22:44:58 +08:00
Macro Audit Fixes (#19)
* [H-3]: Added max lock time (#8) * Added max lock time of 1 year * Fixed tests + covered max unlock exceeded scenario * Removed payable castings (#9) * [L-2]: Added event coverage (#10) * [Q-1]: Added onlyUnlocked modifier (#11) * [Q-2] Removed unused imports (#12) * [Q-3]: Added ERC165 support (#14) * [G-3] Made vaultImplementation immutable (#15) * Added chainid to vault salt + context (#16) * Added chainid to vault salt + context to mitigate cross-chain attacks * Added support for deploying cross-chain vaults to VaultRegistry * Updated natspec docs * Added cross-chain executor (#17) * Added support for cross-chain executors and increased test coverage * added natspec coverage * Apply EIP naming conventions (#18) * migrated vault to account in accordance with eip naming * renamed VaultRegistry to AccountRegistry, updated naming in tests * split registry and cross chain executor list, renamed methods * finished migrating to eip naming conventions, split out tests, removed proxy size test * removed chainid from interface * remove console * removed unused imports * fix spelling * fixed registry interface to match eip * fix event * fixed event test * added event indexing
This commit is contained in:
336
src/Account.sol
Normal file
336
src/Account.sol
Normal file
@@ -0,0 +1,336 @@
|
||||
// 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 "openzeppelin-contracts/utils/introspection/IERC165.sol";
|
||||
import "openzeppelin-contracts/token/ERC1155/IERC1155Receiver.sol";
|
||||
|
||||
import "./CrossChainExecutorList.sol";
|
||||
import "./MinimalReceiver.sol";
|
||||
import "./interfaces/IAccount.sol";
|
||||
import "./lib/MinimalProxyStore.sol";
|
||||
|
||||
/**
|
||||
* @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();
|
||||
|
||||
CrossChainExecutorList public immutable crossChainExecutorList;
|
||||
|
||||
/**
|
||||
* @dev Timestamp at which Account will unlock
|
||||
*/
|
||||
uint256 public unlockTimestamp;
|
||||
|
||||
/**
|
||||
* @dev Mapping from owner address to executor address
|
||||
*/
|
||||
mapping(address => address) public executor;
|
||||
|
||||
/**
|
||||
* @dev Emitted whenever the lock status of a account is updated
|
||||
*/
|
||||
event LockUpdated(uint256 timestamp);
|
||||
|
||||
/**
|
||||
* @dev Emitted whenever the executor for a account is updated
|
||||
*/
|
||||
event ExecutorUpdated(address owner, address executor);
|
||||
|
||||
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();
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
address _owner = owner();
|
||||
if (msg.sender != _owner) revert NotAuthorized();
|
||||
|
||||
return _call(to, value, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
address _owner = owner();
|
||||
if (_owner != msg.sender) revert NotAuthorized();
|
||||
|
||||
executor[_owner] = _executionModule;
|
||||
|
||||
emit ExecutorUpdated(_owner, _executionModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
revert ExceedsMaxLockTime();
|
||||
|
||||
address _owner = owner();
|
||||
if (_owner != msg.sender) revert NotAuthorized();
|
||||
|
||||
unlockTimestamp = _unlockTimestamp;
|
||||
|
||||
emit LockUpdated(_unlockTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns Account lock status
|
||||
*
|
||||
* @return true if Account is locked, false otherwise
|
||||
*/
|
||||
function isLocked() external view returns (bool) {
|
||||
return unlockTimestamp > 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 "";
|
||||
|
||||
// If account has an executor, check if executor signature is valid
|
||||
address _owner = owner();
|
||||
address _executor = executor[_owner];
|
||||
|
||||
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)) {
|
||||
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 supportsInterface(bytes4 interfaceId)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override(IERC165, ERC1155Receiver)
|
||||
returns (bool)
|
||||
{
|
||||
// default interface support
|
||||
if (
|
||||
interfaceId == type(IAccount).interfaceId ||
|
||||
interfaceId == type(IERC1155Receiver).interfaceId ||
|
||||
interfaceId == type(IERC165).interfaceId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
address _executor = executor[owner()];
|
||||
|
||||
if (_executor == address(0) || _executor.code.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if interface is not supported by default, check executor
|
||||
try IERC165(_executor).supportsInterface(interfaceId) returns (
|
||||
bool _supportsInterface
|
||||
) {
|
||||
return _supportsInterface;
|
||||
} catch {
|
||||
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();
|
||||
|
||||
if (chainId != block.chainid) {
|
||||
return address(0);
|
||||
}
|
||||
|
||||
return IERC721(tokenCollection).ownerOf(tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 context()
|
||||
internal
|
||||
view
|
||||
returns (
|
||||
uint256,
|
||||
address,
|
||||
uint256
|
||||
)
|
||||
{
|
||||
bytes memory rawContext = MinimalProxyStore.getContext(address(this));
|
||||
if (rawContext.length == 0) return (0, address(0), 0);
|
||||
|
||||
return abi.decode(rawContext, (uint256, address, uint256));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Executes a low-level call
|
||||
*/
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/AccountRegistry.sol
Normal file
128
src/AccountRegistry.sol
Normal file
@@ -0,0 +1,128 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
23
src/CrossChainExecutorList.sol
Normal file
23
src/CrossChainExecutorList.sol
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
199
src/Vault.sol
199
src/Vault.sol
@@ -1,199 +0,0 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
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/utils/Address.sol";
|
||||
|
||||
import "./MinimalReceiver.sol";
|
||||
import "./interfaces/IVault.sol";
|
||||
import "./lib/MinimalProxyStore.sol";
|
||||
|
||||
/**
|
||||
* @title A smart contract wallet owned by a single ERC721 token
|
||||
* @author Jayden Windle (jaydenwindle)
|
||||
*/
|
||||
contract Vault is IVault, MinimalReceiver {
|
||||
error NotAuthorized();
|
||||
error VaultLocked();
|
||||
|
||||
/**
|
||||
* @dev Timestamp at which Vault will unlock
|
||||
*/
|
||||
uint256 public unlockTimestamp;
|
||||
|
||||
/**
|
||||
* @dev Mapping from owner address to executor address
|
||||
*/
|
||||
mapping(address => address) public executor;
|
||||
|
||||
/**
|
||||
* @dev If vault is unlocked and an executor is set, pass call to executor
|
||||
*/
|
||||
fallback(bytes calldata data)
|
||||
external
|
||||
payable
|
||||
returns (bytes memory result)
|
||||
{
|
||||
if (unlockTimestamp > block.timestamp) revert VaultLocked();
|
||||
|
||||
address _owner = owner();
|
||||
address _executor = executor[_owner];
|
||||
|
||||
// accept funds if executor is undefined or cannot be called
|
||||
if (_executor == address(0)) return "";
|
||||
if (_executor.code.length == 0) return "";
|
||||
|
||||
bool success;
|
||||
(success, result) = _executor.call(data);
|
||||
|
||||
if (!success) {
|
||||
assembly {
|
||||
revert(add(result, 32), mload(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Executes a transaction from the Vault. Must be called by an authorized sender.
|
||||
*
|
||||
* @param to Destination address of the transaction
|
||||
* @param value Ether value of the transaction
|
||||
* @param data Encoded payload of the transaction
|
||||
*/
|
||||
function executeCall(
|
||||
address payable to,
|
||||
uint256 value,
|
||||
bytes calldata data
|
||||
) external payable returns (bytes memory result) {
|
||||
if (unlockTimestamp > block.timestamp) revert VaultLocked();
|
||||
if (!isOwnerOrExecutor(msg.sender)) revert NotAuthorized();
|
||||
|
||||
bool success;
|
||||
(success, result) = to.call{value: value}(data);
|
||||
|
||||
if (!success) {
|
||||
assembly {
|
||||
revert(add(result, 32), mload(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Sets executior address for Vault, allowing owner to use a custom implementation if they choose to.
|
||||
* When the token controlling the vault is transferred, the implementation address will reset
|
||||
*
|
||||
* @param _executionModule the address of the execution module
|
||||
*/
|
||||
function setExecutor(address _executionModule) external {
|
||||
if (unlockTimestamp > block.timestamp) revert VaultLocked();
|
||||
|
||||
address _owner = owner();
|
||||
if (_owner != msg.sender) revert NotAuthorized();
|
||||
|
||||
executor[_owner] = _executionModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Locks Vault, preventing transactions from being executed until a certain time
|
||||
*
|
||||
* @param _unlockTimestamp timestamp when the vault will become unlocked
|
||||
*/
|
||||
function lock(uint256 _unlockTimestamp) external {
|
||||
if (unlockTimestamp > block.timestamp) revert VaultLocked();
|
||||
|
||||
address _owner = owner();
|
||||
if (_owner != msg.sender) revert NotAuthorized();
|
||||
|
||||
unlockTimestamp = _unlockTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns Vault lock status
|
||||
*
|
||||
* @return true if Vault is locked, false otherwise
|
||||
*/
|
||||
function isLocked() external view returns (bool) {
|
||||
return unlockTimestamp > block.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns true if caller is authorized to execute actions on this vault
|
||||
*
|
||||
* @param caller the address to query authorization for
|
||||
* @return true if caller is authorized, false otherwise
|
||||
*/
|
||||
function isAuthorized(address caller) external view returns (bool) {
|
||||
return isOwnerOrExecutor(caller);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 vault is locked, disable signing
|
||||
if (unlockTimestamp > block.timestamp) return "";
|
||||
|
||||
// If vault has an executor, check if executor signature is valid
|
||||
address _owner = owner();
|
||||
address _executor = executor[_owner];
|
||||
|
||||
if (
|
||||
_executor != address(0) &&
|
||||
SignatureChecker.isValidSignatureNow(_executor, hash, signature)
|
||||
) {
|
||||
return IERC1271.isValidSignature.selector;
|
||||
}
|
||||
|
||||
// Default - check if signature is valid for vault owner
|
||||
if (SignatureChecker.isValidSignatureNow(_owner, hash, signature)) {
|
||||
return IERC1271.isValidSignature.selector;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the owner of the token that controls this Vault (public for Ownable compatibility)
|
||||
*
|
||||
* @return the address of the Vault owner
|
||||
*/
|
||||
function owner() public view returns (address) {
|
||||
bytes memory context = MinimalProxyStore.getContext(address(this));
|
||||
|
||||
if (context.length == 0) return address(0);
|
||||
|
||||
(address tokenCollection, uint256 tokenId) = abi.decode(
|
||||
context,
|
||||
(address, uint256)
|
||||
);
|
||||
|
||||
return IERC721(tokenCollection).ownerOf(tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns true if caller is owner or ececutor
|
||||
*
|
||||
* @param caller the address to query for
|
||||
* @return true if caller is owner or executor, false otherwise
|
||||
*/
|
||||
function isOwnerOrExecutor(address caller) internal view returns (bool) {
|
||||
address _owner = owner();
|
||||
if (caller == _owner) return true;
|
||||
|
||||
address _executor = executor[_owner];
|
||||
if (caller == _executor) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "openzeppelin-contracts/proxy/Clones.sol";
|
||||
import "openzeppelin-contracts/utils/Create2.sol";
|
||||
import "openzeppelin-contracts/token/ERC721/IERC721.sol";
|
||||
import "openzeppelin-contracts/utils/cryptography/SignatureChecker.sol";
|
||||
|
||||
import "./Vault.sol";
|
||||
import "./lib/MinimalProxyStore.sol";
|
||||
|
||||
/**
|
||||
* @title A registry for tokenbound Vaults
|
||||
* @dev Determines the address for each tokenbound Vault and performs deployment of vault instances
|
||||
* @author Jayden Windle (jaydenwindle)
|
||||
*/
|
||||
contract VaultRegistry {
|
||||
error NotAuthorized();
|
||||
error VaultLocked();
|
||||
|
||||
/**
|
||||
* @dev Address of the default vault implementation
|
||||
*/
|
||||
address public vaultImplementation;
|
||||
|
||||
/**
|
||||
* @dev Deploys the default Vault implementation
|
||||
*/
|
||||
constructor() {
|
||||
vaultImplementation = address(new Vault());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deploys the Vault instance for an ERC721 token. Will revert if Vault has already been deployed
|
||||
*
|
||||
* @param tokenCollection the contract address of the ERC721 token which will control the deployed Vault
|
||||
* @param tokenId the token ID of the ERC721 token which will control the deployed Vault
|
||||
* @return The address of the deployed Vault
|
||||
*/
|
||||
function deployVault(address tokenCollection, uint256 tokenId)
|
||||
external
|
||||
returns (address payable)
|
||||
{
|
||||
bytes memory encodedTokenData = abi.encode(tokenCollection, tokenId);
|
||||
bytes32 salt = keccak256(encodedTokenData);
|
||||
address vaultProxy = MinimalProxyStore.cloneDeterministic(
|
||||
vaultImplementation,
|
||||
encodedTokenData,
|
||||
salt
|
||||
);
|
||||
|
||||
return payable(vaultProxy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Gets the address of the VaultProxy for an ERC721 token. If VaultProxy 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 vault
|
||||
* @return The VaultProxy address
|
||||
*/
|
||||
function vaultAddress(address tokenCollection, uint256 tokenId)
|
||||
external
|
||||
view
|
||||
returns (address payable)
|
||||
{
|
||||
bytes memory encodedTokenData = abi.encode(tokenCollection, tokenId);
|
||||
bytes32 salt = keccak256(encodedTokenData);
|
||||
|
||||
address vaultProxy = MinimalProxyStore.predictDeterministicAddress(
|
||||
vaultImplementation,
|
||||
encodedTokenData,
|
||||
salt
|
||||
);
|
||||
|
||||
return payable(vaultProxy);
|
||||
}
|
||||
}
|
||||
17
src/interfaces/IAccount.sol
Normal file
17
src/interfaces/IAccount.sol
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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);
|
||||
}
|
||||
19
src/interfaces/IRegistry.sol
Normal file
19
src/interfaces/IRegistry.sol
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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,22 +0,0 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "openzeppelin-contracts/interfaces/IERC1271.sol";
|
||||
|
||||
interface IVault is IERC1271 {
|
||||
function executeCall(
|
||||
address payable to,
|
||||
uint256 value,
|
||||
bytes calldata data
|
||||
) external payable returns (bytes memory);
|
||||
|
||||
function executor(address owner) external view returns (address);
|
||||
function setExecutor(address _executionModule) external;
|
||||
|
||||
function isLocked() external view returns (bool);
|
||||
function lock(uint256 _unlockTimestamp) external;
|
||||
|
||||
function isAuthorized(address caller) external view returns (bool);
|
||||
|
||||
function owner() external view returns (address);
|
||||
}
|
||||
434
test/Account.t.sol
Normal file
434
test/Account.t.sol
Normal file
@@ -0,0 +1,434 @@
|
||||
// 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 "../src/CrossChainExecutorList.sol";
|
||||
import "../src/Account.sol";
|
||||
import "../src/AccountRegistry.sol";
|
||||
|
||||
import "./mocks/MockERC721.sol";
|
||||
import "./mocks/MockExecutor.sol";
|
||||
import "./mocks/MockReverter.sol";
|
||||
|
||||
contract AccountTest is Test {
|
||||
CrossChainExecutorList ccExecutorList;
|
||||
Account implementation;
|
||||
AccountRegistry public accountRegistry;
|
||||
|
||||
MockERC721 public tokenCollection;
|
||||
|
||||
function setUp() public {
|
||||
ccExecutorList = new CrossChainExecutorList();
|
||||
implementation = new Account(address(ccExecutorList));
|
||||
accountRegistry = new AccountRegistry(address(implementation));
|
||||
|
||||
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 = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
// should fail if user2 tries to use account
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Account.NotAuthorized.selector);
|
||||
account.executeCall(payable(user2), 0.1 ether, "");
|
||||
|
||||
// should fail if user2 tries to set executor
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Account.NotAuthorized.selector);
|
||||
account.setExecutor(vm.addr(1337));
|
||||
|
||||
// should fail if user2 tries to lock account
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Account.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 = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
// should fail if user2 tries to use account
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Account.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 = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Account account = Account(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 = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Account account = Account(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 = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
// cannot be locked for more than 365 days
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(Account.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(Account.AccountLocked.selector);
|
||||
account.executeCall(payable(user1), 1 ether, "");
|
||||
|
||||
// fallback calls should revert if account is locked
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(Account.AccountLocked.selector);
|
||||
(bool success, bytes memory result) = accountAddress.call(
|
||||
abi.encodeWithSignature("customFunction()")
|
||||
);
|
||||
|
||||
// silence unused variable compiler warnings
|
||||
success;
|
||||
result;
|
||||
|
||||
// setExecutor calls should revert if account is locked
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(Account.AccountLocked.selector);
|
||||
account.setExecutor(vm.addr(1337));
|
||||
|
||||
// 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 testCustomExecutorFallback(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
MockExecutor mockExecutor = new MockExecutor();
|
||||
|
||||
// calls succeed with noop if executor 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
|
||||
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
|
||||
);
|
||||
|
||||
// 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 testCustomExecutorCalls(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(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
assertEq(account.isAuthorized(user2), false);
|
||||
|
||||
vm.prank(user1);
|
||||
account.setExecutor(user2);
|
||||
|
||||
assertEq(account.isAuthorized(user2), true);
|
||||
|
||||
vm.prank(user2);
|
||||
account.executeTrustedCall(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 = accountRegistry.createAccount(
|
||||
chainId,
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
assertEq(account.isAuthorized(crossChainExecutor), false);
|
||||
|
||||
CrossChainExecutorList(ccExecutorList).setCrossChainExecutor(
|
||||
chainId,
|
||||
crossChainExecutor,
|
||||
true
|
||||
);
|
||||
|
||||
assertEq(account.isAuthorized(crossChainExecutor), true);
|
||||
|
||||
vm.prank(crossChainExecutor);
|
||||
account.executeCrossChainCall(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, "");
|
||||
|
||||
assertEq(user1.balance, 0.1 ether);
|
||||
|
||||
address nativeAccountAddress = accountRegistry.createAccount(
|
||||
block.chainid,
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.prank(crossChainExecutor);
|
||||
vm.expectRevert(Account.NotAuthorized.selector);
|
||||
Account(payable(nativeAccountAddress)).executeCrossChainCall(
|
||||
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 = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(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(accountRegistry.implementation());
|
||||
|
||||
assertEq(Account(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 = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(accountAddress, 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
assertEq(account.supportsInterface(type(IAccount).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
|
||||
);
|
||||
}
|
||||
}
|
||||
111
test/AccountERC1155.t.sol
Normal file
111
test/AccountERC1155.t.sol
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 "../src/CrossChainExecutorList.sol";
|
||||
import "../src/Account.sol";
|
||||
import "../src/AccountRegistry.sol";
|
||||
|
||||
import "./mocks/MockERC721.sol";
|
||||
import "./mocks/MockERC1155.sol";
|
||||
|
||||
contract AccountTest is Test {
|
||||
MockERC1155 public dummyERC1155;
|
||||
|
||||
CrossChainExecutorList ccExecutorList;
|
||||
Account implementation;
|
||||
AccountRegistry public accountRegistry;
|
||||
|
||||
MockERC721 public tokenCollection;
|
||||
|
||||
function setUp() public {
|
||||
dummyERC1155 = new MockERC1155();
|
||||
|
||||
ccExecutorList = new CrossChainExecutorList();
|
||||
implementation = new Account(address(ccExecutorList));
|
||||
accountRegistry = new AccountRegistry(address(implementation));
|
||||
|
||||
tokenCollection = new MockERC721();
|
||||
}
|
||||
|
||||
function testTransferERC1155PreDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address computedAccountInstance = accountRegistry.account(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC1155.mint(computedAccountInstance, 1, 10);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(computedAccountInstance, 1), 10);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
bytes memory erc1155TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256,uint256,bytes)",
|
||||
account,
|
||||
user1,
|
||||
1,
|
||||
10,
|
||||
""
|
||||
);
|
||||
vm.prank(user1);
|
||||
account.executeCall(
|
||||
payable(address(dummyERC1155)),
|
||||
0,
|
||||
erc1155TransferCall
|
||||
);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(accountAddress, 1), 0);
|
||||
assertEq(dummyERC1155.balanceOf(user1, 1), 10);
|
||||
}
|
||||
|
||||
function testTransferERC1155PostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC1155.mint(accountAddress, 1, 10);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(accountAddress, 1), 10);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
bytes memory erc1155TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256,uint256,bytes)",
|
||||
account,
|
||||
user1,
|
||||
1,
|
||||
10,
|
||||
""
|
||||
);
|
||||
vm.prank(user1);
|
||||
account.executeCall(
|
||||
payable(address(dummyERC1155)),
|
||||
0,
|
||||
erc1155TransferCall
|
||||
);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(accountAddress, 1), 0);
|
||||
assertEq(dummyERC1155.balanceOf(user1, 1), 10);
|
||||
}
|
||||
}
|
||||
97
test/AccountERC20.t.sol
Normal file
97
test/AccountERC20.t.sol
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 "../src/CrossChainExecutorList.sol";
|
||||
import "../src/Account.sol";
|
||||
import "../src/AccountRegistry.sol";
|
||||
|
||||
import "./mocks/MockERC721.sol";
|
||||
import "./mocks/MockERC20.sol";
|
||||
|
||||
contract AccountTest is Test {
|
||||
MockERC20 public dummyERC20;
|
||||
|
||||
CrossChainExecutorList ccExecutorList;
|
||||
Account implementation;
|
||||
AccountRegistry public accountRegistry;
|
||||
|
||||
MockERC721 public tokenCollection;
|
||||
|
||||
function setUp() public {
|
||||
dummyERC20 = new MockERC20();
|
||||
|
||||
ccExecutorList = new CrossChainExecutorList();
|
||||
implementation = new Account(address(ccExecutorList));
|
||||
accountRegistry = new AccountRegistry(address(implementation));
|
||||
|
||||
tokenCollection = new MockERC721();
|
||||
}
|
||||
|
||||
function testTransferERC20PreDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address computedAccountInstance = accountRegistry.account(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC20.mint(computedAccountInstance, 1 ether);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(computedAccountInstance), 1 ether);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
bytes memory erc20TransferCall = abi.encodeWithSignature(
|
||||
"transfer(address,uint256)",
|
||||
user1,
|
||||
1 ether
|
||||
);
|
||||
vm.prank(user1);
|
||||
account.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(accountAddress), 0);
|
||||
assertEq(dummyERC20.balanceOf(user1), 1 ether);
|
||||
}
|
||||
|
||||
function testTransferERC20PostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC20.mint(accountAddress, 1 ether);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(accountAddress), 1 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
bytes memory erc20TransferCall = abi.encodeWithSignature(
|
||||
"transfer(address,uint256)",
|
||||
user1,
|
||||
1 ether
|
||||
);
|
||||
vm.prank(user1);
|
||||
account.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(accountAddress), 0);
|
||||
assertEq(dummyERC20.balanceOf(user1), 1 ether);
|
||||
}
|
||||
}
|
||||
110
test/AccountERC721.t.sol
Normal file
110
test/AccountERC721.t.sol
Normal file
@@ -0,0 +1,110 @@
|
||||
// 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 "../src/CrossChainExecutorList.sol";
|
||||
import "../src/Account.sol";
|
||||
import "../src/AccountRegistry.sol";
|
||||
|
||||
import "./mocks/MockERC721.sol";
|
||||
|
||||
contract AccountTest is Test {
|
||||
MockERC721 public dummyERC721;
|
||||
|
||||
CrossChainExecutorList ccExecutorList;
|
||||
Account implementation;
|
||||
AccountRegistry public accountRegistry;
|
||||
|
||||
MockERC721 public tokenCollection;
|
||||
|
||||
function setUp() public {
|
||||
dummyERC721 = new MockERC721();
|
||||
|
||||
ccExecutorList = new CrossChainExecutorList();
|
||||
implementation = new Account(address(ccExecutorList));
|
||||
accountRegistry = new AccountRegistry(address(implementation));
|
||||
|
||||
tokenCollection = new MockERC721();
|
||||
}
|
||||
|
||||
function testTransferERC721PreDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address computedAccountInstance = accountRegistry.account(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC721.mint(computedAccountInstance, 1);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(computedAccountInstance), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), computedAccountInstance);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
bytes memory erc721TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256)",
|
||||
accountAddress,
|
||||
user1,
|
||||
1
|
||||
);
|
||||
vm.prank(user1);
|
||||
account.executeCall(
|
||||
payable(address(dummyERC721)),
|
||||
0,
|
||||
erc721TransferCall
|
||||
);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(address(account)), 0);
|
||||
assertEq(dummyERC721.balanceOf(user1), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), user1);
|
||||
}
|
||||
|
||||
function testTransferERC721PostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC721.mint(accountAddress, 1);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(accountAddress), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), accountAddress);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
bytes memory erc721TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256)",
|
||||
account,
|
||||
user1,
|
||||
1
|
||||
);
|
||||
vm.prank(user1);
|
||||
account.executeCall(
|
||||
payable(address(dummyERC721)),
|
||||
0,
|
||||
erc721TransferCall
|
||||
);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(accountAddress), 0);
|
||||
assertEq(dummyERC721.balanceOf(user1), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), user1);
|
||||
}
|
||||
}
|
||||
99
test/AccountETH.t.sol
Normal file
99
test/AccountETH.t.sol
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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 "../src/CrossChainExecutorList.sol";
|
||||
import "../src/Account.sol";
|
||||
import "../src/AccountRegistry.sol";
|
||||
|
||||
import "./mocks/MockERC721.sol";
|
||||
|
||||
contract AccountTest is Test {
|
||||
CrossChainExecutorList ccExecutorList;
|
||||
Account implementation;
|
||||
AccountRegistry public accountRegistry;
|
||||
|
||||
MockERC721 public tokenCollection;
|
||||
|
||||
function setUp() public {
|
||||
ccExecutorList = new CrossChainExecutorList();
|
||||
implementation = new Account(address(ccExecutorList));
|
||||
accountRegistry = new AccountRegistry(address(implementation));
|
||||
|
||||
tokenCollection = new MockERC721();
|
||||
}
|
||||
|
||||
function testTransferETHPreDeploy() public {
|
||||
uint256 tokenId = 1;
|
||||
address user1 = vm.addr(1);
|
||||
vm.deal(user1, 0.2 ether);
|
||||
|
||||
// get address that account will be deployed to (before token is minted)
|
||||
address accountAddress = accountRegistry.account(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
// mint token for account to user1
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
// send ETH from user1 to account (prior to account deployment)
|
||||
vm.prank(user1);
|
||||
(bool sent, ) = accountAddress.call{value: 0.2 ether}("");
|
||||
assertTrue(sent);
|
||||
|
||||
assertEq(accountAddress.balance, 0.2 ether);
|
||||
|
||||
// deploy account contract (from a different wallet)
|
||||
address createdAccountInstance = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
assertEq(accountAddress, createdAccountInstance);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
// user1 executes transaction to send ETH from account
|
||||
vm.prank(user1);
|
||||
account.executeCall(payable(user1), 0.1 ether, "");
|
||||
|
||||
// success!
|
||||
assertEq(accountAddress.balance, 0.1 ether);
|
||||
assertEq(user1.balance, 0.1 ether);
|
||||
}
|
||||
|
||||
function testTransferETHPostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
vm.deal(user1, 0.2 ether);
|
||||
|
||||
address accountAddress = accountRegistry.createAccount(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
vm.prank(user1);
|
||||
(bool sent, ) = accountAddress.call{value: 0.2 ether}("");
|
||||
assertTrue(sent);
|
||||
|
||||
assertEq(accountAddress.balance, 0.2 ether);
|
||||
|
||||
Account account = Account(payable(accountAddress));
|
||||
|
||||
vm.prank(user1);
|
||||
account.executeCall(payable(user1), 0.1 ether, "");
|
||||
|
||||
assertEq(accountAddress.balance, 0.1 ether);
|
||||
assertEq(user1.balance, 0.1 ether);
|
||||
}
|
||||
}
|
||||
53
test/AccountRegistry.t.sol
Normal file
53
test/AccountRegistry.t.sol
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
53
test/CrossChainExecutorList.t.sol
Normal file
53
test/CrossChainExecutorList.t.sol
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -103,16 +103,4 @@ contract MinimalProxyStoreTest is Test {
|
||||
keccak256("hello")
|
||||
);
|
||||
}
|
||||
|
||||
// must run with --code-size-limit 24576
|
||||
function testCannotOverflowContext() public {
|
||||
uint256 maxSize = 0x6000 - 46;
|
||||
bytes memory maxSizeContext = new bytes(maxSize);
|
||||
bytes memory overflowContext = new bytes(maxSize + 1);
|
||||
|
||||
MinimalProxyStore.clone(address(this), maxSizeContext);
|
||||
|
||||
vm.expectRevert(MinimalProxyStore.CreateError.selector);
|
||||
MinimalProxyStore.clone(address(this), overflowContext);
|
||||
}
|
||||
}
|
||||
|
||||
589
test/Vault.t.sol
589
test/Vault.t.sol
@@ -1,589 +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 "../src/Vault.sol";
|
||||
import "../src/VaultRegistry.sol";
|
||||
|
||||
import "./mocks/MockERC721.sol";
|
||||
import "./mocks/MockERC1155.sol";
|
||||
import "./mocks/MockERC20.sol";
|
||||
import "./mocks/MockExecutor.sol";
|
||||
import "./mocks/MockReverter.sol";
|
||||
|
||||
contract VaultTest is Test {
|
||||
MockERC721 public dummyERC721;
|
||||
MockERC1155 public dummyERC1155;
|
||||
MockERC20 public dummyERC20;
|
||||
|
||||
VaultRegistry public vaultRegistry;
|
||||
|
||||
MockERC721 public tokenCollection;
|
||||
|
||||
function setUp() public {
|
||||
dummyERC721 = new MockERC721();
|
||||
dummyERC1155 = new MockERC1155();
|
||||
dummyERC20 = new MockERC20();
|
||||
|
||||
vaultRegistry = new VaultRegistry();
|
||||
|
||||
tokenCollection = new MockERC721();
|
||||
}
|
||||
|
||||
function testTransferETHPreDeploy() public {
|
||||
uint256 tokenId = 1;
|
||||
address user1 = vm.addr(1);
|
||||
vm.deal(user1, 0.2 ether);
|
||||
|
||||
// get address that vault will be deployed to (before token is minted)
|
||||
address payable vaultAddress = vaultRegistry.vaultAddress(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
// mint token for vault to user1
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
// send ETH from user1 to vault (prior to vault deployment)
|
||||
vm.prank(user1);
|
||||
(bool sent, ) = vaultAddress.call{value: 0.2 ether}("");
|
||||
assertTrue(sent);
|
||||
|
||||
assertEq(vaultAddress.balance, 0.2 ether);
|
||||
|
||||
// deploy vault contract (from a different wallet)
|
||||
address payable createdVaultInstance = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
assertEq(vaultAddress, createdVaultInstance);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
// user1 executes transaction to send ETH from vault
|
||||
vm.prank(user1);
|
||||
vault.executeCall(payable(user1), 0.1 ether, "");
|
||||
|
||||
// success!
|
||||
assertEq(vaultAddress.balance, 0.1 ether);
|
||||
assertEq(user1.balance, 0.1 ether);
|
||||
}
|
||||
|
||||
function testTransferETHPostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
vm.deal(user1, 0.2 ether);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
vm.prank(user1);
|
||||
(bool sent, ) = vaultAddress.call{value: 0.2 ether}("");
|
||||
assertTrue(sent);
|
||||
|
||||
assertEq(vaultAddress.balance, 0.2 ether);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
vm.prank(user1);
|
||||
vault.executeCall(payable(user1), 0.1 ether, "");
|
||||
|
||||
assertEq(vaultAddress.balance, 0.1 ether);
|
||||
assertEq(user1.balance, 0.1 ether);
|
||||
}
|
||||
|
||||
function testTransferERC20PreDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address payable computedVaultInstance = vaultRegistry.vaultAddress(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC20.mint(computedVaultInstance, 1 ether);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(computedVaultInstance), 1 ether);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
bytes memory erc20TransferCall = abi.encodeWithSignature(
|
||||
"transfer(address,uint256)",
|
||||
user1,
|
||||
1 ether
|
||||
);
|
||||
vm.prank(user1);
|
||||
vault.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(vaultAddress), 0);
|
||||
assertEq(dummyERC20.balanceOf(user1), 1 ether);
|
||||
}
|
||||
|
||||
function testTransferERC20PostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC20.mint(vaultAddress, 1 ether);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(vaultAddress), 1 ether);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
bytes memory erc20TransferCall = abi.encodeWithSignature(
|
||||
"transfer(address,uint256)",
|
||||
user1,
|
||||
1 ether
|
||||
);
|
||||
vm.prank(user1);
|
||||
vault.executeCall(payable(address(dummyERC20)), 0, erc20TransferCall);
|
||||
|
||||
assertEq(dummyERC20.balanceOf(vaultAddress), 0);
|
||||
assertEq(dummyERC20.balanceOf(user1), 1 ether);
|
||||
}
|
||||
|
||||
function testTransferERC1155PreDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address payable computedVaultInstance = vaultRegistry.vaultAddress(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC1155.mint(computedVaultInstance, 1, 10);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(computedVaultInstance, 1), 10);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
bytes memory erc1155TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256,uint256,bytes)",
|
||||
vaultAddress,
|
||||
user1,
|
||||
1,
|
||||
10,
|
||||
""
|
||||
);
|
||||
vm.prank(user1);
|
||||
vault.executeCall(
|
||||
payable(address(dummyERC1155)),
|
||||
0,
|
||||
erc1155TransferCall
|
||||
);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 0);
|
||||
assertEq(dummyERC1155.balanceOf(user1, 1), 10);
|
||||
}
|
||||
|
||||
function testTransferERC1155PostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC1155.mint(vaultAddress, 1, 10);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 10);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
bytes memory erc1155TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256,uint256,bytes)",
|
||||
vaultAddress,
|
||||
user1,
|
||||
1,
|
||||
10,
|
||||
""
|
||||
);
|
||||
vm.prank(user1);
|
||||
vault.executeCall(
|
||||
payable(address(dummyERC1155)),
|
||||
0,
|
||||
erc1155TransferCall
|
||||
);
|
||||
|
||||
assertEq(dummyERC1155.balanceOf(vaultAddress, 1), 0);
|
||||
assertEq(dummyERC1155.balanceOf(user1, 1), 10);
|
||||
}
|
||||
|
||||
function testTransferERC721PreDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address payable computedVaultInstance = vaultRegistry.vaultAddress(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC721.mint(computedVaultInstance, 1);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(computedVaultInstance), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), computedVaultInstance);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
bytes memory erc721TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256)",
|
||||
address(vaultAddress),
|
||||
user1,
|
||||
1
|
||||
);
|
||||
vm.prank(user1);
|
||||
vault.executeCall(payable(address(dummyERC721)), 0, erc721TransferCall);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(address(vaultAddress)), 0);
|
||||
assertEq(dummyERC721.balanceOf(user1), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), user1);
|
||||
}
|
||||
|
||||
function testTransferERC721PostDeploy(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
dummyERC721.mint(vaultAddress, 1);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(vaultAddress), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), vaultAddress);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
bytes memory erc721TransferCall = abi.encodeWithSignature(
|
||||
"safeTransferFrom(address,address,uint256)",
|
||||
vaultAddress,
|
||||
user1,
|
||||
1
|
||||
);
|
||||
vm.prank(user1);
|
||||
vault.executeCall(payable(address(dummyERC721)), 0, erc721TransferCall);
|
||||
|
||||
assertEq(dummyERC721.balanceOf(vaultAddress), 0);
|
||||
assertEq(dummyERC721.balanceOf(user1), 1);
|
||||
assertEq(dummyERC721.ownerOf(1), user1);
|
||||
}
|
||||
|
||||
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 payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(vaultAddress, 1 ether);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
// should fail if user2 tries to use vault
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Vault.NotAuthorized.selector);
|
||||
vault.executeCall(payable(user2), 0.1 ether, "");
|
||||
|
||||
// should fail if user2 tries to set executor
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Vault.NotAuthorized.selector);
|
||||
vault.setExecutor(vm.addr(1337));
|
||||
|
||||
// should fail if user2 tries to lock vault
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Vault.NotAuthorized.selector);
|
||||
vault.lock(type(uint256).max);
|
||||
}
|
||||
|
||||
function testVaultOwnershipTransfer(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
address user2 = vm.addr(2);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(vaultAddress, 1 ether);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
// should fail if user2 tries to use vault
|
||||
vm.prank(user2);
|
||||
vm.expectRevert(Vault.NotAuthorized.selector);
|
||||
vault.executeCall(payable(user2), 0.1 ether, "");
|
||||
|
||||
vm.prank(user1);
|
||||
tokenCollection.safeTransferFrom(user1, user2, tokenId);
|
||||
|
||||
// should succeed now that user2 is owner
|
||||
vm.prank(user2);
|
||||
vault.executeCall(payable(user2), 0.1 ether, "");
|
||||
|
||||
assertEq(user2.balance, 0.1 ether);
|
||||
}
|
||||
|
||||
function testMessageSigningAndVerificationForAuthorizedUser(uint256 tokenId)
|
||||
public
|
||||
{
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
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 = vault.isValidSignature(hash, signature1);
|
||||
|
||||
assertEq(returnValue1, IERC1271.isValidSignature.selector);
|
||||
}
|
||||
|
||||
function testMessageSigningAndVerificationForUnauthorizedUser(
|
||||
uint256 tokenId
|
||||
) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
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 = vault.isValidSignature(hash, signature2);
|
||||
|
||||
assertEq(returnValue2, 0);
|
||||
}
|
||||
|
||||
function testVaultLocksAndUnlocks(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(vaultAddress, 1 ether);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
// lock vault for 10 days
|
||||
uint256 unlockTimestamp = block.timestamp + 10 days;
|
||||
vm.prank(user1);
|
||||
vault.lock(unlockTimestamp);
|
||||
|
||||
assertEq(vault.isLocked(), true);
|
||||
|
||||
// transaction should revert if vault is locked
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(Vault.VaultLocked.selector);
|
||||
vault.executeCall(payable(user1), 1 ether, "");
|
||||
|
||||
// fallback calls should revert if vault is locked
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(Vault.VaultLocked.selector);
|
||||
(bool success, bytes memory result) = vaultAddress.call(
|
||||
abi.encodeWithSignature("customFunction()")
|
||||
);
|
||||
|
||||
// silence unused variable compiler warnings
|
||||
success;
|
||||
result;
|
||||
|
||||
// setExecutor calls should revert if vault is locked
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(Vault.VaultLocked.selector);
|
||||
vault.setExecutor(vm.addr(1337));
|
||||
|
||||
// lock calls should revert if vault is locked
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(Vault.VaultLocked.selector);
|
||||
vault.lock(0);
|
||||
|
||||
// signing should fail if vault 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 = vault.isValidSignature(hash, signature1);
|
||||
assertEq(returnValue, 0);
|
||||
|
||||
// warp to timestamp after vault is unlocked
|
||||
vm.warp(unlockTimestamp + 1 days);
|
||||
|
||||
// transaction succeed now that vault lock has expired
|
||||
vm.prank(user1);
|
||||
vault.executeCall(payable(user1), 1 ether, "");
|
||||
assertEq(user1.balance, 1 ether);
|
||||
|
||||
// signing should now that vault 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 = vault.isValidSignature(
|
||||
hashAfterUnlock,
|
||||
signature2
|
||||
);
|
||||
assertEq(returnValue1, IERC1271.isValidSignature.selector);
|
||||
}
|
||||
|
||||
function testCustomExecutionModule(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(vaultAddress, 1 ether);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
MockExecutor mockExecutor = new MockExecutor();
|
||||
|
||||
// calls succeed with noop if executor is undefined
|
||||
(bool success, bytes memory result) = vaultAddress.call(
|
||||
abi.encodeWithSignature("customFunction()")
|
||||
);
|
||||
assertEq(success, true);
|
||||
assertEq(result, "");
|
||||
|
||||
// calls succeed with noop if executor is EOA
|
||||
vm.prank(user1);
|
||||
vault.setExecutor(vm.addr(1337));
|
||||
(bool success1, bytes memory result1) = vaultAddress.call(
|
||||
abi.encodeWithSignature("customFunction()")
|
||||
);
|
||||
assertEq(success1, true);
|
||||
assertEq(result1, "");
|
||||
|
||||
assertEq(vault.isAuthorized(user1), true);
|
||||
assertEq(vault.isAuthorized(address(mockExecutor)), false);
|
||||
|
||||
vm.prank(user1);
|
||||
vault.setExecutor(address(mockExecutor));
|
||||
|
||||
assertEq(vault.isAuthorized(user1), true);
|
||||
assertEq(vault.isAuthorized(address(mockExecutor)), true);
|
||||
|
||||
assertEq(
|
||||
vault.isValidSignature(bytes32(0), ""),
|
||||
IERC1271.isValidSignature.selector
|
||||
);
|
||||
|
||||
// execution module handles fallback calls
|
||||
assertEq(MockExecutor(vaultAddress).customFunction(), 12345);
|
||||
|
||||
// execution bubbles up errors on revert
|
||||
vm.expectRevert(MockReverter.MockError.selector);
|
||||
MockExecutor(vaultAddress).fail();
|
||||
}
|
||||
|
||||
function testExecuteCallRevert(uint256 tokenId) public {
|
||||
address user1 = vm.addr(1);
|
||||
|
||||
tokenCollection.mint(user1, tokenId);
|
||||
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
||||
|
||||
address payable vaultAddress = vaultRegistry.deployVault(
|
||||
address(tokenCollection),
|
||||
tokenId
|
||||
);
|
||||
|
||||
vm.deal(vaultAddress, 1 ether);
|
||||
|
||||
Vault vault = Vault(vaultAddress);
|
||||
|
||||
MockReverter mockReverter = new MockReverter();
|
||||
|
||||
vm.prank(user1);
|
||||
vm.expectRevert(MockReverter.MockError.selector);
|
||||
vault.executeCall(
|
||||
payable(address(mockReverter)),
|
||||
0,
|
||||
abi.encodeWithSignature("fail()")
|
||||
);
|
||||
}
|
||||
|
||||
function testVaultOwnerIsNullIfContextNotSet() public {
|
||||
address vaultClone = Clones.clone(vaultRegistry.vaultImplementation());
|
||||
|
||||
assertEq(Vault(payable(vaultClone)).owner(), address(0));
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
import "../src/Vault.sol";
|
||||
import "../src/VaultRegistry.sol";
|
||||
import "../src/lib/MinimalProxyStore.sol";
|
||||
|
||||
contract VaultRegistryTest is Test {
|
||||
VaultRegistry public vaultRegistry;
|
||||
|
||||
function setUp() public {
|
||||
vaultRegistry = new VaultRegistry();
|
||||
}
|
||||
|
||||
function testDeployVault(address tokenCollection, uint256 tokenId) public {
|
||||
assertTrue(address(vaultRegistry) != address(0));
|
||||
|
||||
address predictedVaultAddress = vaultRegistry.vaultAddress(
|
||||
tokenCollection,
|
||||
tokenId
|
||||
);
|
||||
|
||||
address vaultAddress = vaultRegistry.deployVault(
|
||||
tokenCollection,
|
||||
tokenId
|
||||
);
|
||||
|
||||
assertTrue(vaultAddress != address(0));
|
||||
assertTrue(vaultAddress == predictedVaultAddress);
|
||||
assertEq(
|
||||
MinimalProxyStore.getContext(vaultAddress),
|
||||
abi.encode(tokenCollection, tokenId)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,12 @@ contract MockExecutor is MockReverter {
|
||||
function customFunction() external pure returns (uint256) {
|
||||
return 12345;
|
||||
}
|
||||
|
||||
function supportsInterface(bytes4 interfaceId)
|
||||
external
|
||||
pure
|
||||
returns (bool)
|
||||
{
|
||||
return interfaceId == IERC1271.isValidSignature.selector;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user