Files
tokenbound/test/AccountV2.t.sol

478 lines
14 KiB
Solidity

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