|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +pragma solidity ^0.8.0; |
| 3 | + |
| 4 | +// Test utils |
| 5 | +import "../utils/BaseTest.sol"; |
| 6 | +// Account Abstraction setup for smart wallets. |
| 7 | +import { EntryPoint, IEntryPoint } from "contracts/smart-wallet/utils/Entrypoint.sol"; |
| 8 | +import { UserOperation } from "contracts/smart-wallet/utils/UserOperation.sol"; |
| 9 | + |
| 10 | +// Target |
| 11 | +import { IAccountPermissions } from "contracts/extension/interface/IAccountPermissions.sol"; |
| 12 | +import { AccountFactory, Account } from "contracts/smart-wallet/non-upgradeable/AccountFactory.sol"; |
| 13 | + |
| 14 | +library GPv2EIP1271 { |
| 15 | + bytes4 internal constant MAGICVALUE = 0x1626ba7e; |
| 16 | +} |
| 17 | + |
| 18 | +interface EIP1271Verifier { |
| 19 | + function isValidSignature(bytes32 _hash, bytes memory _signature) external view returns (bytes4 magicValue); |
| 20 | +} |
| 21 | + |
| 22 | +/// @dev This is a dummy contract to test contract interactions with Account. |
| 23 | +contract Number { |
| 24 | + uint256 public num; |
| 25 | + |
| 26 | + function setNum(uint256 _num) public { |
| 27 | + num = _num; |
| 28 | + } |
| 29 | + |
| 30 | + function doubleNum() public { |
| 31 | + num *= 2; |
| 32 | + } |
| 33 | + |
| 34 | + function incrementNum() public { |
| 35 | + num += 1; |
| 36 | + } |
| 37 | + |
| 38 | + function setNumBySignature( |
| 39 | + address owner, |
| 40 | + uint256 newNum, |
| 41 | + bytes calldata signature |
| 42 | + ) public { |
| 43 | + if (owner.code.length == 0) { |
| 44 | + // Signature verification by ECDSA |
| 45 | + } else { |
| 46 | + // Signature verfication by EIP1271 |
| 47 | + bytes32 digest = keccak256(abi.encode(newNum)); |
| 48 | + require( |
| 49 | + EIP1271Verifier(owner).isValidSignature(digest, signature) == GPv2EIP1271.MAGICVALUE, |
| 50 | + "GPv2: invalid eip1271 signature" |
| 51 | + ); |
| 52 | + num = newNum; |
| 53 | + } |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +contract SimpleAccountVulnPOCTest is BaseTest { |
| 58 | + // Target contracts |
| 59 | + EntryPoint private entrypoint; |
| 60 | + AccountFactory private accountFactory; |
| 61 | + |
| 62 | + // Mocks |
| 63 | + Number internal numberContract; |
| 64 | + |
| 65 | + // Test params |
| 66 | + uint256 private accountAdminPKey = 100; |
| 67 | + address private accountAdmin; |
| 68 | + |
| 69 | + uint256 private accountSignerPKey = 200; |
| 70 | + address private accountSigner; |
| 71 | + |
| 72 | + uint256 private nonSignerPKey = 300; |
| 73 | + address private nonSigner; |
| 74 | + |
| 75 | + // UserOp terminology: `sender` is the smart wallet. |
| 76 | + address private sender = 0xBB956D56140CA3f3060986586A2631922a4B347E; |
| 77 | + address payable private beneficiary = payable(address(0x45654)); |
| 78 | + |
| 79 | + bytes32 private uidCache = bytes32("random uid"); |
| 80 | + |
| 81 | + event AccountCreated(address indexed account, address indexed accountAdmin); |
| 82 | + |
| 83 | + function _signSignerPermissionRequest(IAccountPermissions.SignerPermissionRequest memory _req) |
| 84 | + internal |
| 85 | + view |
| 86 | + returns (bytes memory signature) |
| 87 | + { |
| 88 | + bytes32 typehashSignerPermissionRequest = keccak256( |
| 89 | + "SignerPermissionRequest(address signer,address[] approvedTargets,uint256 nativeTokenLimitPerTransaction,uint128 permissionStartTimestamp,uint128 permissionEndTimestamp,uint128 reqValidityStartTimestamp,uint128 reqValidityEndTimestamp,bytes32 uid)" |
| 90 | + ); |
| 91 | + bytes32 nameHash = keccak256(bytes("Account")); |
| 92 | + bytes32 versionHash = keccak256(bytes("1")); |
| 93 | + bytes32 typehashEip712 = keccak256( |
| 94 | + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" |
| 95 | + ); |
| 96 | + bytes32 domainSeparator = keccak256(abi.encode(typehashEip712, nameHash, versionHash, block.chainid, sender)); |
| 97 | + |
| 98 | + bytes memory encodedRequest = abi.encode( |
| 99 | + typehashSignerPermissionRequest, |
| 100 | + _req.signer, |
| 101 | + keccak256(abi.encodePacked(_req.approvedTargets)), |
| 102 | + _req.nativeTokenLimitPerTransaction, |
| 103 | + _req.permissionStartTimestamp, |
| 104 | + _req.permissionEndTimestamp, |
| 105 | + _req.reqValidityStartTimestamp, |
| 106 | + _req.reqValidityEndTimestamp, |
| 107 | + _req.uid |
| 108 | + ); |
| 109 | + bytes32 structHash = keccak256(encodedRequest); |
| 110 | + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); |
| 111 | + |
| 112 | + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountAdminPKey, typedDataHash); |
| 113 | + signature = abi.encodePacked(r, s, v); |
| 114 | + } |
| 115 | + |
| 116 | + function _setupUserOp( |
| 117 | + uint256 _signerPKey, |
| 118 | + bytes memory _initCode, |
| 119 | + bytes memory _callDataForEntrypoint |
| 120 | + ) internal returns (UserOperation[] memory ops) { |
| 121 | + uint256 nonce = entrypoint.getNonce(sender, 0); |
| 122 | + |
| 123 | + // Get user op fields |
| 124 | + UserOperation memory op = UserOperation({ |
| 125 | + sender: sender, |
| 126 | + nonce: nonce, |
| 127 | + initCode: _initCode, |
| 128 | + callData: _callDataForEntrypoint, |
| 129 | + callGasLimit: 500_000, |
| 130 | + verificationGasLimit: 500_000, |
| 131 | + preVerificationGas: 500_000, |
| 132 | + maxFeePerGas: 0, |
| 133 | + maxPriorityFeePerGas: 0, |
| 134 | + paymasterAndData: bytes(""), |
| 135 | + signature: bytes("") |
| 136 | + }); |
| 137 | + |
| 138 | + // Sign UserOp |
| 139 | + bytes32 opHash = EntryPoint(entrypoint).getUserOpHash(op); |
| 140 | + bytes32 msgHash = ECDSA.toEthSignedMessageHash(opHash); |
| 141 | + |
| 142 | + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPKey, msgHash); |
| 143 | + bytes memory userOpSignature = abi.encodePacked(r, s, v); |
| 144 | + |
| 145 | + address recoveredSigner = ECDSA.recover(msgHash, v, r, s); |
| 146 | + address expectedSigner = vm.addr(_signerPKey); |
| 147 | + assertEq(recoveredSigner, expectedSigner); |
| 148 | + |
| 149 | + op.signature = userOpSignature; |
| 150 | + |
| 151 | + // Store UserOp |
| 152 | + ops = new UserOperation[](1); |
| 153 | + ops[0] = op; |
| 154 | + } |
| 155 | + |
| 156 | + function _setupUserOpExecute( |
| 157 | + uint256 _signerPKey, |
| 158 | + bytes memory _initCode, |
| 159 | + address _target, |
| 160 | + uint256 _value, |
| 161 | + bytes memory _callData |
| 162 | + ) internal returns (UserOperation[] memory) { |
| 163 | + bytes memory callDataForEntrypoint = abi.encodeWithSignature( |
| 164 | + "execute(address,uint256,bytes)", |
| 165 | + _target, |
| 166 | + _value, |
| 167 | + _callData |
| 168 | + ); |
| 169 | + |
| 170 | + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); |
| 171 | + } |
| 172 | + |
| 173 | + function _setupUserOpExecuteBatch( |
| 174 | + uint256 _signerPKey, |
| 175 | + bytes memory _initCode, |
| 176 | + address[] memory _target, |
| 177 | + uint256[] memory _value, |
| 178 | + bytes[] memory _callData |
| 179 | + ) internal returns (UserOperation[] memory) { |
| 180 | + bytes memory callDataForEntrypoint = abi.encodeWithSignature( |
| 181 | + "executeBatch(address[],uint256[],bytes[])", |
| 182 | + _target, |
| 183 | + _value, |
| 184 | + _callData |
| 185 | + ); |
| 186 | + |
| 187 | + return _setupUserOp(_signerPKey, _initCode, callDataForEntrypoint); |
| 188 | + } |
| 189 | + |
| 190 | + function setUp() public override { |
| 191 | + super.setUp(); |
| 192 | + |
| 193 | + // Setup signers. |
| 194 | + accountAdmin = vm.addr(accountAdminPKey); |
| 195 | + vm.deal(accountAdmin, 100 ether); |
| 196 | + |
| 197 | + accountSigner = vm.addr(accountSignerPKey); |
| 198 | + nonSigner = vm.addr(nonSignerPKey); |
| 199 | + |
| 200 | + // Setup contracts |
| 201 | + entrypoint = new EntryPoint(); |
| 202 | + // deploy account factory |
| 203 | + accountFactory = new AccountFactory(IEntryPoint(payable(address(entrypoint)))); |
| 204 | + // deploy dummy contract |
| 205 | + numberContract = new Number(); |
| 206 | + } |
| 207 | + |
| 208 | + /*////////////////////////////////////////////////////////// |
| 209 | + Test: performing a contract call |
| 210 | + //////////////////////////////////////////////////////////////*/ |
| 211 | + |
| 212 | + function _setup_executeTransaction() internal { |
| 213 | + bytes memory initCallData = abi.encodeWithSignature("createAccount(address,bytes)", accountAdmin, bytes("")); |
| 214 | + bytes memory initCode = abi.encodePacked(abi.encodePacked(address(accountFactory)), initCallData); |
| 215 | + |
| 216 | + UserOperation[] memory userOpCreateAccount = _setupUserOpExecute( |
| 217 | + accountAdminPKey, |
| 218 | + initCode, |
| 219 | + address(0), |
| 220 | + 0, |
| 221 | + bytes("") |
| 222 | + ); |
| 223 | + |
| 224 | + EntryPoint(entrypoint).handleOps(userOpCreateAccount, beneficiary); |
| 225 | + } |
| 226 | + |
| 227 | + function test_POC() public { |
| 228 | + _setup_executeTransaction(); |
| 229 | + |
| 230 | + /*////////////////////////////////////////////////////////// |
| 231 | + Setup |
| 232 | + //////////////////////////////////////////////////////////////*/ |
| 233 | + address[] memory approvedTargets = new address[](1); |
| 234 | + approvedTargets[0] = address(0); // allowing accountSigner permissions for some random contract, consider it as 0 address here |
| 235 | + |
| 236 | + IAccountPermissions.SignerPermissionRequest memory permissionsReq = IAccountPermissions.SignerPermissionRequest( |
| 237 | + accountSigner, |
| 238 | + approvedTargets, |
| 239 | + 1 ether, |
| 240 | + 0, |
| 241 | + type(uint128).max, |
| 242 | + 0, |
| 243 | + type(uint128).max, |
| 244 | + uidCache |
| 245 | + ); |
| 246 | + |
| 247 | + vm.prank(accountAdmin); |
| 248 | + bytes memory sig = _signSignerPermissionRequest(permissionsReq); |
| 249 | + address account = accountFactory.getAddress(accountAdmin, bytes("")); |
| 250 | + IAccountPermissions(payable(account)).setPermissionsForSigner(permissionsReq, sig); |
| 251 | + |
| 252 | + // As expected, Account Signer is not be able to call setNum on numberContract since it doesnt have numberContract as approved target |
| 253 | + assertEq(numberContract.num(), 0); |
| 254 | + |
| 255 | + vm.prank(accountSigner); |
| 256 | + UserOperation[] memory userOp = _setupUserOpExecute( |
| 257 | + accountSignerPKey, |
| 258 | + bytes(""), |
| 259 | + address(numberContract), |
| 260 | + 0, |
| 261 | + abi.encodeWithSignature("setNum(uint256)", 42) |
| 262 | + ); |
| 263 | + |
| 264 | + vm.expectRevert(); |
| 265 | + EntryPoint(entrypoint).handleOps(userOp, beneficiary); |
| 266 | + |
| 267 | + /*////////////////////////////////////////////////////////// |
| 268 | + Attack |
| 269 | + //////////////////////////////////////////////////////////////*/ |
| 270 | + |
| 271 | + //However they can bypass this by using signature verification on number contract instead |
| 272 | + vm.prank(accountSigner); |
| 273 | + bytes32 digest = keccak256(abi.encode(42)); |
| 274 | + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountSignerPKey, digest); |
| 275 | + bytes memory signature = abi.encodePacked(r, s, v); |
| 276 | + |
| 277 | + vm.expectRevert("Account: caller not approved target."); |
| 278 | + numberContract.setNumBySignature(account, 42, signature); |
| 279 | + assertEq(numberContract.num(), 0); |
| 280 | + } |
| 281 | +} |
0 commit comments