mirror of
https://github.com/placeholder-soft/tokenbound.git
synced 2026-01-12 17:02:56 +08:00
478 lines
14 KiB
Solidity
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);
|
|
}
|
|
}
|