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:
Jayden Windle
2023-04-11 14:23:26 -04:00
committed by GitHub
parent 6a9ebf78de
commit 03a730c2c0
19 changed files with 1488 additions and 938 deletions

336
src/Account.sol Normal file
View 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
View 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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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);
}

View 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);
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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);
}
}

View 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)
);
}
}

View 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
);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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)
);
}
}

View File

@@ -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;
}
}