mirror of
https://github.com/placeholder-soft/tokenbound.git
synced 2026-01-12 08:44:20 +08:00
283 lines
7.9 KiB
Solidity
283 lines
7.9 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.13;
|
|
|
|
import "forge-std/Test.sol";
|
|
|
|
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
import "@openzeppelin/contracts/proxy/Clones.sol";
|
|
|
|
import "account-abstraction/core/EntryPoint.sol";
|
|
|
|
import "erc6551/ERC6551Registry.sol";
|
|
import "erc6551/interfaces/IERC6551Account.sol";
|
|
|
|
import "../src/Account.sol";
|
|
import "../src/AccountGuardian.sol";
|
|
|
|
import "./mocks/MockERC721.sol";
|
|
|
|
contract AccountERC4337Test is Test {
|
|
using ECDSA for bytes32;
|
|
|
|
Account implementation;
|
|
AccountGuardian public guardian;
|
|
ERC6551Registry public registry;
|
|
IEntryPoint public entryPoint;
|
|
|
|
MockERC721 public tokenCollection;
|
|
|
|
function setUp() public {
|
|
entryPoint = new EntryPoint();
|
|
guardian = new AccountGuardian();
|
|
implementation = new Account(address(guardian), address(entryPoint));
|
|
registry = new ERC6551Registry();
|
|
|
|
tokenCollection = new MockERC721();
|
|
}
|
|
|
|
function testReturnsEntryPoint() public {
|
|
address accountAddress = registry.createAccount(
|
|
address(implementation),
|
|
block.chainid,
|
|
address(tokenCollection),
|
|
1,
|
|
0,
|
|
""
|
|
);
|
|
|
|
assertEq(
|
|
address(Account(payable(accountAddress)).entryPoint()),
|
|
address(entryPoint)
|
|
);
|
|
}
|
|
|
|
function testNonceIncrementsOnDirectCall(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);
|
|
|
|
Account account = Account(payable(accountAddress));
|
|
|
|
uint256 nonce = account.getNonce();
|
|
assertEq(nonce, 0);
|
|
|
|
// user1 executes transaction to send ETH from account
|
|
vm.prank(user1);
|
|
account.execute(payable(user1), 0.1 ether, "", 0);
|
|
|
|
assertEq(account.getNonce(), nonce + 1);
|
|
assertEq(account.getNonce(), entryPoint.getNonce(accountAddress, 0));
|
|
|
|
// success!
|
|
assertEq(accountAddress.balance, 0.9 ether);
|
|
assertEq(user1.balance, 0.1 ether);
|
|
}
|
|
|
|
function test4337CallCreateAccount() public {
|
|
uint256 tokenId = 1;
|
|
address user1 = vm.addr(1);
|
|
address user2 = vm.addr(2);
|
|
|
|
tokenCollection.mint(user1, tokenId);
|
|
assertEq(tokenCollection.ownerOf(tokenId), user1);
|
|
|
|
address accountAddress = registry.account(
|
|
address(implementation),
|
|
block.chainid,
|
|
address(tokenCollection),
|
|
tokenId,
|
|
0
|
|
);
|
|
|
|
bytes memory initCode = abi.encodePacked(
|
|
address(registry),
|
|
abi.encodeWithSignature(
|
|
"createAccount(address,uint256,address,uint256,uint256,bytes)",
|
|
address(implementation),
|
|
block.chainid,
|
|
address(tokenCollection),
|
|
tokenId,
|
|
0,
|
|
""
|
|
)
|
|
);
|
|
|
|
bytes memory callData = abi.encodeWithSignature(
|
|
"execute(address,uint256,bytes,uint256)",
|
|
user2,
|
|
0.1 ether,
|
|
"",
|
|
0
|
|
);
|
|
|
|
UserOperation memory op = UserOperation({
|
|
sender: accountAddress,
|
|
nonce: 0,
|
|
initCode: initCode,
|
|
callData: callData,
|
|
callGasLimit: 1000000,
|
|
verificationGasLimit: 1000000,
|
|
preVerificationGas: 1000000,
|
|
maxFeePerGas: block.basefee + 10,
|
|
maxPriorityFeePerGas: 10,
|
|
paymasterAndData: "",
|
|
signature: ""
|
|
});
|
|
|
|
bytes32 opHash = entryPoint.getUserOpHash(op);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
|
1,
|
|
opHash.toEthSignedMessageHash()
|
|
);
|
|
|
|
bytes memory signature = abi.encodePacked(r, s, v);
|
|
op.signature = signature;
|
|
|
|
vm.deal(accountAddress, 1 ether);
|
|
|
|
UserOperation[] memory ops = new UserOperation[](1);
|
|
ops[0] = op;
|
|
|
|
assertEq(entryPoint.getNonce(accountAddress, 0), 0);
|
|
entryPoint.handleOps(ops, payable(user1));
|
|
assertEq(entryPoint.getNonce(accountAddress, 0), 1);
|
|
|
|
assertEq(user2.balance, 0.1 ether);
|
|
assertTrue(accountAddress.balance < 0.9 ether);
|
|
}
|
|
|
|
function test4337CallExistingAccount() public {
|
|
uint256 tokenId = 1;
|
|
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,
|
|
""
|
|
);
|
|
|
|
bytes memory callData = abi.encodeWithSignature(
|
|
"execute(address,uint256,bytes,uint256)",
|
|
user2,
|
|
0.1 ether,
|
|
"",
|
|
0
|
|
);
|
|
|
|
UserOperation memory op = UserOperation({
|
|
sender: accountAddress,
|
|
nonce: 0,
|
|
initCode: "",
|
|
callData: callData,
|
|
callGasLimit: 1000000,
|
|
verificationGasLimit: 1000000,
|
|
preVerificationGas: 1000000,
|
|
maxFeePerGas: block.basefee + 10,
|
|
maxPriorityFeePerGas: 10,
|
|
paymasterAndData: "",
|
|
signature: ""
|
|
});
|
|
|
|
bytes32 opHash = entryPoint.getUserOpHash(op);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
|
1,
|
|
opHash.toEthSignedMessageHash()
|
|
);
|
|
|
|
bytes memory signature = abi.encodePacked(r, s, v);
|
|
op.signature = signature;
|
|
|
|
vm.deal(accountAddress, 1 ether);
|
|
|
|
UserOperation[] memory ops = new UserOperation[](1);
|
|
ops[0] = op;
|
|
|
|
assertEq(entryPoint.getNonce(accountAddress, 0), 0);
|
|
entryPoint.handleOps(ops, payable(user1));
|
|
assertEq(entryPoint.getNonce(accountAddress, 0), 1);
|
|
|
|
assertEq(user2.balance, 0.1 ether);
|
|
assertTrue(accountAddress.balance < 0.9 ether);
|
|
}
|
|
|
|
function test4337CallRevertsInvalidSignature() public {
|
|
uint256 tokenId = 1;
|
|
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,
|
|
""
|
|
);
|
|
|
|
bytes memory callData = abi.encodeWithSignature(
|
|
"execute(address,uint256,bytes,uint256)",
|
|
user2,
|
|
0.1 ether,
|
|
"",
|
|
0
|
|
);
|
|
|
|
UserOperation memory op = UserOperation({
|
|
sender: accountAddress,
|
|
nonce: 0,
|
|
initCode: "",
|
|
callData: callData,
|
|
callGasLimit: 1000000,
|
|
verificationGasLimit: 1000000,
|
|
preVerificationGas: 1000000,
|
|
maxFeePerGas: block.basefee + 10,
|
|
maxPriorityFeePerGas: 10,
|
|
paymasterAndData: "",
|
|
signature: ""
|
|
});
|
|
|
|
bytes32 opHash = entryPoint.getUserOpHash(op);
|
|
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
|
|
1,
|
|
opHash.toEthSignedMessageHash()
|
|
);
|
|
|
|
// invalidate signature
|
|
bytes memory signature = abi.encodePacked(r, s, v + 1);
|
|
op.signature = signature;
|
|
|
|
vm.deal(accountAddress, 1 ether);
|
|
|
|
UserOperation[] memory ops = new UserOperation[](1);
|
|
ops[0] = op;
|
|
|
|
vm.expectRevert();
|
|
entryPoint.handleOps(ops, payable(user1));
|
|
|
|
assertEq(accountAddress.balance, 1 ether);
|
|
}
|
|
}
|