github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/periphery/op-nft/OptimistInviter.t.sol (about) 1 // SPDX-License-Identifier: MIT 2 pragma solidity 0.8.15; 3 4 // Testing utilities 5 import { Test } from "forge-std/Test.sol"; 6 import { AttestationStation } from "src/periphery/op-nft/AttestationStation.sol"; 7 import { OptimistInviter } from "src/periphery/op-nft/OptimistInviter.sol"; 8 import { Optimist } from "src/periphery/op-nft/Optimist.sol"; 9 import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; 10 import { TestERC1271Wallet } from "test/mocks/TestERC1271Wallet.sol"; 11 import { OptimistInviterHelper } from "test/mocks/OptimistInviterHelper.sol"; 12 import { OptimistConstants } from "src/periphery/op-nft/libraries/OptimistConstants.sol"; 13 14 contract OptimistInviter_Initializer is Test { 15 event InviteClaimed(address indexed issuer, address indexed claimer); 16 event Initialized(uint8 version); 17 event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 18 event AttestationCreated(address indexed creator, address indexed about, bytes32 indexed key, bytes val); 19 20 bytes32 EIP712_DOMAIN_TYPEHASH; 21 22 address internal alice_inviteGranter; 23 address internal sally; 24 address internal ted; 25 address internal eve; 26 27 address internal bob; 28 uint256 internal bobPrivateKey; 29 address internal carol; 30 uint256 internal carolPrivateKey; 31 32 TestERC1271Wallet carolERC1271Wallet; 33 34 AttestationStation attestationStation; 35 OptimistInviter optimistInviter; 36 37 OptimistInviterHelper optimistInviterHelper; 38 39 function setUp() public { 40 alice_inviteGranter = makeAddr("alice_inviteGranter"); 41 sally = makeAddr("sally"); 42 ted = makeAddr("ted"); 43 eve = makeAddr("eve"); 44 45 bobPrivateKey = 0xB0B0B0B0; 46 bob = vm.addr(bobPrivateKey); 47 48 carolPrivateKey = 0xC0C0C0C0; 49 carol = vm.addr(carolPrivateKey); 50 51 carolERC1271Wallet = new TestERC1271Wallet(carol); 52 53 // Give alice and bob and sally some ETH 54 vm.deal(alice_inviteGranter, 1 ether); 55 vm.deal(bob, 1 ether); 56 vm.deal(sally, 1 ether); 57 vm.deal(ted, 1 ether); 58 vm.deal(eve, 1 ether); 59 60 EIP712_DOMAIN_TYPEHASH = 61 keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 62 63 _initializeContracts(); 64 } 65 66 /// @notice Instantiates an AttestationStation, and an OptimistInviter. 67 function _initializeContracts() internal { 68 attestationStation = new AttestationStation(); 69 70 optimistInviter = new OptimistInviter(alice_inviteGranter, attestationStation); 71 72 vm.expectEmit(true, true, true, true, address(optimistInviter)); 73 emit Initialized(1); 74 optimistInviter.initialize("OptimistInviter"); 75 76 optimistInviterHelper = new OptimistInviterHelper(optimistInviter, "OptimistInviter"); 77 } 78 79 function _passMinCommitmentPeriod() internal { 80 vm.warp(optimistInviter.MIN_COMMITMENT_PERIOD() + block.timestamp); 81 } 82 83 /// @notice Returns a user's current invite count, as stored in the AttestationStation. 84 function _getInviteCount(address _issuer) internal view returns (uint256) { 85 return optimistInviter.inviteCounts(_issuer); 86 } 87 88 /// @notice Returns true if claimer has the proper attestation from OptimistInviter to mint. 89 function _hasMintAttestation(address _claimer) internal view returns (bool) { 90 bytes memory attestation = attestationStation.attestations( 91 address(optimistInviter), _claimer, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY 92 ); 93 return attestation.length > 0; 94 } 95 96 /// @notice Get signature as a bytes blob, since SignatureChecker takes arbitrary signature blobs. 97 function _getSignature(uint256 _signingPrivateKey, bytes32 _digest) internal pure returns (bytes memory) { 98 (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signingPrivateKey, _digest); 99 100 bytes memory signature = abi.encodePacked(r, s, v); 101 return signature; 102 } 103 104 /// @notice Signs a claimable invite with the given private key and returns the signature using 105 /// correct EIP712 domain separator. 106 function _issueInviteAs(uint256 _privateKey) 107 internal 108 returns (OptimistInviter.ClaimableInvite memory, bytes memory) 109 { 110 return _issueInviteWithEIP712Domain( 111 _privateKey, 112 bytes("OptimistInviter"), 113 bytes(optimistInviter.EIP712_VERSION()), 114 block.chainid, 115 address(optimistInviter) 116 ); 117 } 118 119 /// @notice Signs a claimable invite with the given private key and returns the signature using 120 /// the given EIP712 domain separator. This assumes that the issuer's address is the 121 /// corresponding public key to _issuerPrivateKey. 122 function _issueInviteWithEIP712Domain( 123 uint256 _issuerPrivateKey, 124 bytes memory _eip712Name, 125 bytes memory _eip712Version, 126 uint256 _eip712Chainid, 127 address _eip712VerifyingContract 128 ) 129 internal 130 returns (OptimistInviter.ClaimableInvite memory, bytes memory) 131 { 132 address issuer = vm.addr(_issuerPrivateKey); 133 OptimistInviter.ClaimableInvite memory claimableInvite = 134 optimistInviterHelper.getClaimableInviteWithNewNonce(issuer); 135 return ( 136 claimableInvite, 137 _getSignature( 138 _issuerPrivateKey, 139 optimistInviterHelper.getDigestWithEIP712Domain( 140 claimableInvite, _eip712Name, _eip712Version, _eip712Chainid, _eip712VerifyingContract 141 ) 142 ) 143 ); 144 } 145 146 /// @notice Commits a signature and claimer address to the OptimistInviter contract. 147 function _commitInviteAs(address _as, bytes memory _signature) internal { 148 vm.prank(_as); 149 bytes32 hashedSignature = keccak256(abi.encode(_as, _signature)); 150 optimistInviter.commitInvite(hashedSignature); 151 152 // Check that the commitment was stored correctly 153 assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); 154 } 155 156 /// @notice Signs a claimable invite with the given private key. The claimer commits then claims 157 /// the invite. Checks that all expected events are emitted and that state is updated 158 /// correctly. Returns the signature and invite for use in tests. 159 function _issueThenClaimShouldSucceed( 160 uint256 _issuerPrivateKey, 161 address _claimer 162 ) 163 internal 164 returns (OptimistInviter.ClaimableInvite memory, bytes memory) 165 { 166 address issuer = vm.addr(_issuerPrivateKey); 167 uint256 prevInviteCount = _getInviteCount(issuer); 168 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = 169 _issueInviteAs(_issuerPrivateKey); 170 171 _commitInviteAs(_claimer, signature); 172 173 // The hash(claimer ++ signature) should be committed 174 assertEq(optimistInviter.commitmentTimestamps(keccak256(abi.encode(_claimer, signature))), block.timestamp); 175 176 _passMinCommitmentPeriod(); 177 178 // OptimistInviter should issue a new attestation allowing claimer to mint 179 vm.expectEmit(true, true, true, true, address(attestationStation)); 180 emit AttestationCreated( 181 address(optimistInviter), 182 _claimer, 183 OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY, 184 abi.encode(issuer) 185 ); 186 187 // Should emit an event indicating that the invite was claimed 188 vm.expectEmit(true, false, false, false, address(optimistInviter)); 189 emit InviteClaimed(issuer, _claimer); 190 191 vm.prank(_claimer); 192 optimistInviter.claimInvite(_claimer, claimableInvite, signature); 193 194 // The nonce that issuer used should be marked as used 195 assertTrue(optimistInviter.usedNonces(issuer, claimableInvite.nonce)); 196 197 // Issuer should have one less invite 198 assertEq(prevInviteCount - 1, _getInviteCount(issuer)); 199 200 // Claimer should have the mint attestation from the OptimistInviter contract 201 assertTrue(_hasMintAttestation(_claimer)); 202 203 return (claimableInvite, signature); 204 } 205 206 /// @notice Issues 3 invites to the given address. Checks that all expected events are emitted 207 /// and that state is updated correctly. 208 function _grantInvitesTo(address _to) internal { 209 address[] memory addresses = new address[](1); 210 addresses[0] = _to; 211 212 vm.expectEmit(true, true, true, true, address(attestationStation)); 213 emit AttestationCreated( 214 address(optimistInviter), _to, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true") 215 ); 216 217 vm.prank(alice_inviteGranter); 218 optimistInviter.setInviteCounts(addresses, 3); 219 220 assertEq(_getInviteCount(_to), 3); 221 } 222 } 223 224 contract OptimistInviterTest is OptimistInviter_Initializer { 225 function test_initialize_succeeds() external { 226 // expect attestationStation to be set 227 assertEq(address(optimistInviter.ATTESTATION_STATION()), address(attestationStation)); 228 assertEq(optimistInviter.INVITE_GRANTER(), alice_inviteGranter); 229 } 230 231 /// @notice Alice the admin should be able to give Bob, Sally, and Carol 3 invites, and the 232 /// OptimistInviter contract should increment invite counts on inviteCounts and issue 233 /// 'optimist.can-invite' attestations. 234 function test_grantInvites_adminAddingInvites_succeeds() external { 235 address[] memory addresses = new address[](3); 236 addresses[0] = bob; 237 addresses[1] = sally; 238 addresses[2] = address(carolERC1271Wallet); 239 240 vm.expectEmit(true, true, true, true, address(attestationStation)); 241 emit AttestationCreated( 242 address(optimistInviter), bob, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true") 243 ); 244 245 vm.expectEmit(true, true, true, true, address(attestationStation)); 246 emit AttestationCreated( 247 address(optimistInviter), sally, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true") 248 ); 249 250 vm.expectEmit(true, true, true, true, address(attestationStation)); 251 emit AttestationCreated( 252 address(optimistInviter), 253 address(carolERC1271Wallet), 254 optimistInviter.CAN_INVITE_ATTESTATION_KEY(), 255 bytes("true") 256 ); 257 258 vm.prank(alice_inviteGranter); 259 optimistInviter.setInviteCounts(addresses, 3); 260 261 assertEq(_getInviteCount(bob), 3); 262 assertEq(_getInviteCount(sally), 3); 263 assertEq(_getInviteCount(address(carolERC1271Wallet)), 3); 264 } 265 266 /// @notice Bob, who is not the invite granter, should not be able to issue invites. 267 function test_grantInvites_nonAdminAddingInvites_reverts() external { 268 address[] memory addresses = new address[](2); 269 addresses[0] = bob; 270 addresses[1] = sally; 271 272 vm.expectRevert("OptimistInviter: only invite granter can grant invites"); 273 vm.prank(bob); 274 optimistInviter.setInviteCounts(addresses, 3); 275 } 276 277 /// @notice Sally should be able to commit an invite given by by Bob. 278 function test_commitInvite_committingForYourself_succeeds() external { 279 _grantInvitesTo(bob); 280 (, bytes memory signature) = _issueInviteAs(bobPrivateKey); 281 282 vm.prank(sally); 283 bytes32 hashedSignature = keccak256(abi.encode(sally, signature)); 284 optimistInviter.commitInvite(hashedSignature); 285 286 assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); 287 } 288 289 /// @notice Sally should be able to Bob's for a different claimer, Eve. 290 function test_commitInvite_committingForSomeoneElse_succeeds() external { 291 _grantInvitesTo(bob); 292 (, bytes memory signature) = _issueInviteAs(bobPrivateKey); 293 294 vm.prank(sally); 295 bytes32 hashedSignature = keccak256(abi.encode(eve, signature)); 296 optimistInviter.commitInvite(hashedSignature); 297 298 assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); 299 } 300 301 /// @notice Attempting to commit the same hash twice should revert. This prevents griefing. 302 function test_commitInvite_committingSameHashTwice_reverts() external { 303 _grantInvitesTo(bob); 304 (, bytes memory signature) = _issueInviteAs(bobPrivateKey); 305 306 vm.prank(sally); 307 bytes32 hashedSignature = keccak256(abi.encode(eve, signature)); 308 optimistInviter.commitInvite(hashedSignature); 309 310 assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); 311 312 vm.expectRevert("OptimistInviter: commitment already made"); 313 optimistInviter.commitInvite(hashedSignature); 314 } 315 316 /// @notice Bob issues signature, and Sally claims the invite. Bob's invite count should be 317 /// decremented, and Sally should be able to mint. 318 function test_claimInvite_succeeds() external { 319 _grantInvitesTo(bob); 320 _issueThenClaimShouldSucceed(bobPrivateKey, sally); 321 } 322 323 /// @notice Bob issues signature, and Ted commits the invite for Sally. Eve claims for Sally. 324 function test_claimInvite_claimForSomeoneElse_succeeds() external { 325 _grantInvitesTo(bob); 326 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey); 327 328 vm.prank(ted); 329 optimistInviter.commitInvite(keccak256(abi.encode(sally, signature))); 330 _passMinCommitmentPeriod(); 331 332 vm.expectEmit(true, true, true, true, address(attestationStation)); 333 emit AttestationCreated( 334 address(optimistInviter), 335 sally, 336 OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY, 337 abi.encode(bob) 338 ); 339 340 // Should emit an event indicating that the invite was claimed 341 vm.expectEmit(true, true, true, true, address(optimistInviter)); 342 emit InviteClaimed(bob, sally); 343 344 vm.prank(eve); 345 optimistInviter.claimInvite(sally, claimableInvite, signature); 346 347 assertEq(_getInviteCount(bob), 2); 348 assertTrue(_hasMintAttestation(sally)); 349 assertFalse(_hasMintAttestation(eve)); 350 } 351 352 function test_claimInvite_claimBeforeMinCommitmentPeriod_reverts() external { 353 _grantInvitesTo(bob); 354 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey); 355 356 _commitInviteAs(sally, signature); 357 358 // Some time passes, but not enough to meet the minimum commitment period 359 vm.warp(block.timestamp + 10); 360 361 vm.expectRevert("OptimistInviter: minimum commitment period has not elapsed yet"); 362 vm.prank(sally); 363 optimistInviter.claimInvite(sally, claimableInvite, signature); 364 } 365 366 /// @notice Signature issued for previous versions of the contract should fail. 367 function test_claimInvite_usingSignatureIssuedForDifferentVersion_reverts() external { 368 _grantInvitesTo(bob); 369 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteWithEIP712Domain( 370 bobPrivateKey, "OptimismInviter", "0.9.1", block.chainid, address(optimistInviter) 371 ); 372 373 _commitInviteAs(sally, signature); 374 _passMinCommitmentPeriod(); 375 376 vm.expectRevert("OptimistInviter: invalid signature"); 377 vm.prank(sally); 378 optimistInviter.claimInvite(sally, claimableInvite, signature); 379 } 380 381 /// @notice Replay attack for signature issued for contract on different chain (ie. mainnet) 382 /// should fail. 383 function test_claimInvite_usingSignatureIssuedForDifferentChain_reverts() external { 384 _grantInvitesTo(bob); 385 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteWithEIP712Domain( 386 bobPrivateKey, "OptimismInviter", bytes(optimistInviter.EIP712_VERSION()), 1, address(optimistInviter) 387 ); 388 389 _commitInviteAs(sally, signature); 390 _passMinCommitmentPeriod(); 391 392 vm.expectRevert("OptimistInviter: invalid signature"); 393 vm.prank(sally); 394 optimistInviter.claimInvite(sally, claimableInvite, signature); 395 } 396 397 /// @notice Replay attack for signature issued for instantiation of the OptimistInviter contract 398 /// on a different address should fail. 399 function test_claimInvite_usingSignatureIssuedForDifferentContract_reverts() external { 400 _grantInvitesTo(bob); 401 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteWithEIP712Domain( 402 bobPrivateKey, "OptimismInviter", bytes(optimistInviter.EIP712_VERSION()), block.chainid, address(0xBEEF) 403 ); 404 405 _commitInviteAs(sally, signature); 406 _passMinCommitmentPeriod(); 407 408 vm.expectRevert("OptimistInviter: invalid signature"); 409 vm.prank(sally); 410 optimistInviter.claimInvite(sally, claimableInvite, signature); 411 } 412 413 /// @notice Attempting to claim again using the same signature again should fail. 414 function test_claimInvite_replayingUsedNonce_reverts() external { 415 _grantInvitesTo(bob); 416 417 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = 418 _issueThenClaimShouldSucceed(bobPrivateKey, sally); 419 420 // Sally tries to claim the invite using the same signature 421 vm.expectRevert("OptimistInviter: nonce has already been used"); 422 vm.prank(sally); 423 optimistInviter.claimInvite(sally, claimableInvite, signature); 424 425 // Carol tries to claim the invite using the same signature 426 _commitInviteAs(carol, signature); 427 _passMinCommitmentPeriod(); 428 429 vm.expectRevert("OptimistInviter: nonce has already been used"); 430 vm.prank(carol); 431 optimistInviter.claimInvite(carol, claimableInvite, signature); 432 } 433 434 /// @notice Issuing signatures through a contract that implements ERC1271 should succeed (ie. 435 /// Gnosis Safe or other smart contract wallets). Carol is using a ERC1271 contract 436 /// wallet that is simply backed by her private key. 437 function test_claimInvite_usingERC1271Wallet_succeeds() external { 438 _grantInvitesTo(address(carolERC1271Wallet)); 439 440 OptimistInviter.ClaimableInvite memory claimableInvite = 441 optimistInviterHelper.getClaimableInviteWithNewNonce(address(carolERC1271Wallet)); 442 443 bytes memory signature = _getSignature(carolPrivateKey, optimistInviterHelper.getDigest(claimableInvite)); 444 445 // Sally tries to claim the invite 446 _commitInviteAs(sally, signature); 447 _passMinCommitmentPeriod(); 448 449 vm.expectEmit(true, true, true, true, address(attestationStation)); 450 emit AttestationCreated( 451 address(optimistInviter), 452 sally, 453 OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY, 454 abi.encode(address(carolERC1271Wallet)) 455 ); 456 457 vm.prank(sally); 458 optimistInviter.claimInvite(sally, claimableInvite, signature); 459 assertEq(_getInviteCount(address(carolERC1271Wallet)), 2); 460 } 461 462 /// @notice Claimer must commit the signature before claiming the invite. Sally attempts to 463 /// claim the Bob's invite without committing the signature first. 464 function test_claimInvite_withoutCommittingHash_reverts() external { 465 _grantInvitesTo(bob); 466 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey); 467 468 vm.expectRevert("OptimistInviter: claimer and signature have not been committed yet"); 469 vm.prank(sally); 470 optimistInviter.claimInvite(sally, claimableInvite, signature); 471 } 472 473 /// @notice Using a signature that doesn't correspond to the claimable invite should fail. 474 function test_claimInvite_withIncorrectSignature_reverts() external { 475 _grantInvitesTo(carol); 476 _grantInvitesTo(bob); 477 (OptimistInviter.ClaimableInvite memory bobClaimableInvite, bytes memory bobSignature) = 478 _issueInviteAs(bobPrivateKey); 479 (, bytes memory carolSignature) = _issueInviteAs(carolPrivateKey); 480 481 _commitInviteAs(sally, bobSignature); 482 _commitInviteAs(sally, carolSignature); 483 484 _passMinCommitmentPeriod(); 485 486 vm.expectRevert("OptimistInviter: invalid signature"); 487 vm.prank(sally); 488 optimistInviter.claimInvite(sally, bobClaimableInvite, carolSignature); 489 } 490 491 /// @notice Attempting to use a signature from a issuer who never was granted invites should 492 /// fail. 493 function test_claimInvite_whenIssuerNeverReceivedInvites_reverts() external { 494 // Bob was never granted any invites, but issues an invite for Eve 495 (OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature) = _issueInviteAs(bobPrivateKey); 496 497 _commitInviteAs(sally, signature); 498 _passMinCommitmentPeriod(); 499 500 vm.expectRevert("OptimistInviter: issuer has no invites"); 501 vm.prank(sally); 502 optimistInviter.claimInvite(sally, claimableInvite, signature); 503 } 504 505 /// @notice Attempting to use a signature from a issuer who has no more invites should fail. 506 /// Bob has 3 invites, but issues 4 invites for Sally, Carol, Ted, and Eve. Only the 507 /// first 3 invites should be claimable. The last claimer, Eve, should not be able to 508 /// claim the invite. 509 function test_claimInvite_whenIssuerHasNoInvitesLeft_reverts() external { 510 _grantInvitesTo(bob); 511 512 _issueThenClaimShouldSucceed(bobPrivateKey, sally); 513 _issueThenClaimShouldSucceed(bobPrivateKey, carol); 514 _issueThenClaimShouldSucceed(bobPrivateKey, ted); 515 516 assertEq(_getInviteCount(bob), 0); 517 518 (OptimistInviter.ClaimableInvite memory claimableInvite4, bytes memory signature4) = 519 _issueInviteAs(bobPrivateKey); 520 521 _commitInviteAs(eve, signature4); 522 _passMinCommitmentPeriod(); 523 524 vm.expectRevert("OptimistInviter: issuer has no invites"); 525 vm.prank(eve); 526 optimistInviter.claimInvite(eve, claimableInvite4, signature4); 527 528 assertEq(_getInviteCount(bob), 0); 529 } 530 }