mirror of
https://github.com/placeholder-soft/tokenbound.git
synced 2026-01-12 17:02:56 +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:
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