//SPDX-License-Identifier: MIT pragma solidity 0.8.15; /* Testing utilities */ import { Test } from "forge-std/Test.sol"; import { AttestationStation } from "../universal/op-nft/AttestationStation.sol"; import { OptimistInviter } from "../universal/op-nft/OptimistInviter.sol"; import { Optimist } from "../universal/op-nft/Optimist.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { TestERC1271Wallet } from "../testing/helpers/TestERC1271Wallet.sol"; import { OptimistInviterHelper } from "../testing/helpers/OptimistInviterHelper.sol"; import { OptimistConstants } from "../universal/op-nft/libraries/OptimistConstants.sol"; contract OptimistInviter_Initializer is Test { event InviteClaimed(address indexed issuer, address indexed claimer); event Initialized(uint8 version); event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); event AttestationCreated( address indexed creator, address indexed about, bytes32 indexed key, bytes val ); bytes32 EIP712_DOMAIN_TYPEHASH; address internal alice_inviteGranter; address internal sally; address internal ted; address internal eve; address internal bob; uint256 internal bobPrivateKey; address internal carol; uint256 internal carolPrivateKey; TestERC1271Wallet carolERC1271Wallet; AttestationStation attestationStation; OptimistInviter optimistInviter; OptimistInviterHelper optimistInviterHelper; function setUp() public { alice_inviteGranter = makeAddr("alice_inviteGranter"); sally = makeAddr("sally"); ted = makeAddr("ted"); eve = makeAddr("eve"); bobPrivateKey = 0xB0B0B0B0; bob = vm.addr(bobPrivateKey); carolPrivateKey = 0xC0C0C0C0; carol = vm.addr(carolPrivateKey); carolERC1271Wallet = new TestERC1271Wallet(carol); // Give alice and bob and sally some ETH vm.deal(alice_inviteGranter, 1 ether); vm.deal(bob, 1 ether); vm.deal(sally, 1 ether); vm.deal(ted, 1 ether); vm.deal(eve, 1 ether); EIP712_DOMAIN_TYPEHASH = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ); _initializeContracts(); } /** * @notice Instantiates an AttestationStation, and an OptimistInviter. */ function _initializeContracts() internal { attestationStation = new AttestationStation(); optimistInviter = new OptimistInviter(alice_inviteGranter, attestationStation); vm.expectEmit(true, true, true, true, address(optimistInviter)); emit Initialized(1); optimistInviter.initialize("OptimistInviter"); optimistInviterHelper = new OptimistInviterHelper(optimistInviter, "OptimistInviter"); } function _passMinCommitmentPeriod() internal { vm.warp(optimistInviter.MIN_COMMITMENT_PERIOD() + block.timestamp); } /** * @notice Returns a user's current invite count, as stored in the AttestationStation. */ function _getInviteCount(address _issuer) internal view returns (uint256) { return optimistInviter.inviteCounts(_issuer); } /** * @notice Returns true if claimer has the proper attestation from OptimistInviter to mint. */ function _hasMintAttestation(address _claimer) internal view returns (bool) { bytes memory attestation = attestationStation.attestations( address(optimistInviter), _claimer, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY ); return attestation.length > 0; } /** * @notice Get signature as a bytes blob, since SignatureChecker takes arbitrary signature blobs. * */ function _getSignature(uint256 _signingPrivateKey, bytes32 _digest) internal pure returns (bytes memory) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signingPrivateKey, _digest); bytes memory signature = abi.encodePacked(r, s, v); return signature; } /** * @notice Signs a claimable invite with the given private key and returns the signature using * correct EIP712 domain separator. */ function _issueInviteAs(uint256 _privateKey) internal returns (OptimistInviter.ClaimableInvite memory, bytes memory) { return _issueInviteWithEIP712Domain( _privateKey, bytes("OptimistInviter"), bytes(optimistInviter.EIP712_VERSION()), block.chainid, address(optimistInviter) ); } /** * @notice Signs a claimable invite with the given private key and returns the signature using * the given EIP712 domain separator. This assumes that the issuer's address is the * corresponding public key to _issuerPrivateKey. */ function _issueInviteWithEIP712Domain( uint256 _issuerPrivateKey, bytes memory _eip712Name, bytes memory _eip712Version, uint256 _eip712Chainid, address _eip712VerifyingContract ) internal returns (OptimistInviter.ClaimableInvite memory, bytes memory) { address issuer = vm.addr(_issuerPrivateKey); OptimistInviter.ClaimableInvite memory claimableInvite = optimistInviterHelper .getClaimableInviteWithNewNonce(issuer); return ( claimableInvite, _getSignature( _issuerPrivateKey, optimistInviterHelper.getDigestWithEIP712Domain( claimableInvite, _eip712Name, _eip712Version, _eip712Chainid, _eip712VerifyingContract ) ) ); } /** * @notice Commits a signature and claimer address to the OptimistInviter contract. */ function _commitInviteAs(address _as, bytes memory _signature) internal { vm.prank(_as); bytes32 hashedSignature = keccak256(abi.encode(_as, _signature)); optimistInviter.commitInvite(hashedSignature); // Check that the commitment was stored correctly assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); } /** * @notice Signs a claimable invite with the given private key. The claimer commits then claims * the invite. Checks that all expected events are emitted and that state is updated * correctly. Returns the signature and invite for use in tests. */ function _issueThenClaimShouldSucceed(uint256 _issuerPrivateKey, address _claimer) internal returns (OptimistInviter.ClaimableInvite memory, bytes memory) { address issuer = vm.addr(_issuerPrivateKey); uint256 prevInviteCount = _getInviteCount(issuer); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteAs(_issuerPrivateKey); _commitInviteAs(_claimer, signature); // The hash(claimer ++ signature) should be committed assertEq( optimistInviter.commitmentTimestamps(keccak256(abi.encode(_claimer, signature))), block.timestamp ); _passMinCommitmentPeriod(); // OptimistInviter should issue a new attestation allowing claimer to mint vm.expectEmit(true, true, true, true, address(attestationStation)); emit AttestationCreated( address(optimistInviter), _claimer, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY, abi.encode(issuer) ); // Should emit an event indicating that the invite was claimed vm.expectEmit(true, false, false, false, address(optimistInviter)); emit InviteClaimed(issuer, _claimer); vm.prank(_claimer); optimistInviter.claimInvite(_claimer, claimableInvite, signature); // The nonce that issuer used should be marked as used assertTrue(optimistInviter.usedNonces(issuer, claimableInvite.nonce)); // Issuer should have one less invite assertEq(prevInviteCount - 1, _getInviteCount(issuer)); // Claimer should have the mint attestation from the OptimistInviter contract assertTrue(_hasMintAttestation(_claimer)); return (claimableInvite, signature); } /** * @notice Issues 3 invites to the given address. Checks that all expected events are emitted * and that state is updated correctly. */ function _grantInvitesTo(address _to) internal { address[] memory addresses = new address[](1); addresses[0] = _to; vm.expectEmit(true, true, true, true, address(attestationStation)); emit AttestationCreated( address(optimistInviter), _to, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true") ); vm.prank(alice_inviteGranter); optimistInviter.setInviteCounts(addresses, 3); assertEq(_getInviteCount(_to), 3); } } contract OptimistInviterTest is OptimistInviter_Initializer { function test_initialize() external { // expect attestationStation to be set assertEq(address(optimistInviter.ATTESTATION_STATION()), address(attestationStation)); assertEq(optimistInviter.INVITE_GRANTER(), alice_inviteGranter); assertEq(optimistInviter.version(), "1.0.0"); } /** * @notice Alice the admin should be able to give Bob, Sally, and Carol 3 invites, and the * OptimistInviter contract should increment invite counts on inviteCounts and issue * 'optimist.can-invite' attestations. */ function test_grantInvites_adminAddingInvites_succeeds() external { address[] memory addresses = new address[](3); addresses[0] = bob; addresses[1] = sally; addresses[2] = address(carolERC1271Wallet); vm.expectEmit(true, true, true, true, address(attestationStation)); emit AttestationCreated( address(optimistInviter), bob, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true") ); vm.expectEmit(true, true, true, true, address(attestationStation)); emit AttestationCreated( address(optimistInviter), sally, optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true") ); vm.expectEmit(true, true, true, true, address(attestationStation)); emit AttestationCreated( address(optimistInviter), address(carolERC1271Wallet), optimistInviter.CAN_INVITE_ATTESTATION_KEY(), bytes("true") ); vm.prank(alice_inviteGranter); optimistInviter.setInviteCounts(addresses, 3); assertEq(_getInviteCount(bob), 3); assertEq(_getInviteCount(sally), 3); assertEq(_getInviteCount(address(carolERC1271Wallet)), 3); } /** * @notice Bob, who is not the invite granter, should not be able to issue invites. */ function test_grantInvites_nonAdminAddingInvites_reverts() external { address[] memory addresses = new address[](2); addresses[0] = bob; addresses[1] = sally; vm.expectRevert("OptimistInviter: only invite granter can grant invites"); vm.prank(bob); optimistInviter.setInviteCounts(addresses, 3); } /** * @notice Sally should be able to commit an invite given by by Bob. */ function test_commitInvite_committingForYourself_succeeds() external { _grantInvitesTo(bob); (, bytes memory signature) = _issueInviteAs(bobPrivateKey); vm.prank(sally); bytes32 hashedSignature = keccak256(abi.encode(sally, signature)); optimistInviter.commitInvite(hashedSignature); assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); } /** * @notice Sally should be able to Bob's for a different claimer, Eve. */ function test_commitInvite_committingForSomeoneElse_succeeds() external { _grantInvitesTo(bob); (, bytes memory signature) = _issueInviteAs(bobPrivateKey); vm.prank(sally); bytes32 hashedSignature = keccak256(abi.encode(eve, signature)); optimistInviter.commitInvite(hashedSignature); assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); } /** * @notice Attempting to commit the same hash twice should revert. This prevents griefing. */ function test_commitInvite_committingSameHashTwice_reverts() external { _grantInvitesTo(bob); (, bytes memory signature) = _issueInviteAs(bobPrivateKey); vm.prank(sally); bytes32 hashedSignature = keccak256(abi.encode(eve, signature)); optimistInviter.commitInvite(hashedSignature); assertEq(optimistInviter.commitmentTimestamps(hashedSignature), block.timestamp); vm.expectRevert("OptimistInviter: commitment already made"); optimistInviter.commitInvite(hashedSignature); } /** * @notice Bob issues signature, and Sally claims the invite. Bob's invite count should be * decremented, and Sally should be able to mint. */ function test_claimInvite_succeeds() external { _grantInvitesTo(bob); _issueThenClaimShouldSucceed(bobPrivateKey, sally); } /** * @notice Bob issues signature, and Ted commits the invite for Sally. Eve claims for Sally. */ function test_claimInvite_claimForSomeoneElse_succeeds() external { _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteAs(bobPrivateKey); vm.prank(ted); optimistInviter.commitInvite(keccak256(abi.encode(sally, signature))); _passMinCommitmentPeriod(); vm.expectEmit(true, true, true, true, address(attestationStation)); emit AttestationCreated( address(optimistInviter), sally, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY, abi.encode(bob) ); // Should emit an event indicating that the invite was claimed vm.expectEmit(true, true, true, true, address(optimistInviter)); emit InviteClaimed(bob, sally); vm.prank(eve); optimistInviter.claimInvite(sally, claimableInvite, signature); assertEq(_getInviteCount(bob), 2); assertTrue(_hasMintAttestation(sally)); assertFalse(_hasMintAttestation(eve)); } function test_claimInvite_claimBeforeMinCommitmentPeriod_reverts() external { _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteAs(bobPrivateKey); _commitInviteAs(sally, signature); // Some time passes, but not enough to meet the minimum commitment period vm.warp(block.timestamp + 10); vm.expectRevert("OptimistInviter: minimum commitment period has not elapsed yet"); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); } /** * @notice Signature issued for previous versions of the contract should fail. */ function test_claimInvite_usingSignatureIssuedForDifferentVersion_reverts() external { _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteWithEIP712Domain( bobPrivateKey, "OptimismInviter", "0.9.1", block.chainid, address(optimistInviter) ); _commitInviteAs(sally, signature); _passMinCommitmentPeriod(); vm.expectRevert("OptimistInviter: invalid signature"); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); } /** * @notice Replay attack for signature issued for contract on different chain (ie. mainnet) * should fail. */ function test_claimInvite_usingSignatureIssuedForDifferentChain_reverts() external { _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteWithEIP712Domain( bobPrivateKey, "OptimismInviter", bytes(optimistInviter.EIP712_VERSION()), 1, address(optimistInviter) ); _commitInviteAs(sally, signature); _passMinCommitmentPeriod(); vm.expectRevert("OptimistInviter: invalid signature"); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); } /** * @notice Replay attack for signature issued for instantiation of the OptimistInviter contract * on a different address should fail. */ function test_claimInvite_usingSignatureIssuedForDifferentContract_reverts() external { _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteWithEIP712Domain( bobPrivateKey, "OptimismInviter", bytes(optimistInviter.EIP712_VERSION()), block.chainid, address(0xBEEF) ); _commitInviteAs(sally, signature); _passMinCommitmentPeriod(); vm.expectRevert("OptimistInviter: invalid signature"); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); } /** * @notice Attempting to claim again using the same signature again should fail. */ function test_claimInvite_replayingUsedNonce_reverts() external { _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueThenClaimShouldSucceed(bobPrivateKey, sally); // Sally tries to claim the invite using the same signature vm.expectRevert("OptimistInviter: nonce has already been used"); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); // Carol tries to claim the invite using the same signature _commitInviteAs(carol, signature); _passMinCommitmentPeriod(); vm.expectRevert("OptimistInviter: nonce has already been used"); vm.prank(carol); optimistInviter.claimInvite(carol, claimableInvite, signature); } /** * @notice Issuing signatures through a contract that implements ERC1271 should succeed (ie. * Gnosis Safe or other smart contract wallets). Carol is using a ERC1271 contract * wallet that is simply backed by her private key. */ function test_claimInvite_usingERC1271Wallet_succeeds() external { _grantInvitesTo(address(carolERC1271Wallet)); OptimistInviter.ClaimableInvite memory claimableInvite = optimistInviterHelper .getClaimableInviteWithNewNonce(address(carolERC1271Wallet)); bytes memory signature = _getSignature( carolPrivateKey, optimistInviterHelper.getDigest(claimableInvite) ); // Sally tries to claim the invite _commitInviteAs(sally, signature); _passMinCommitmentPeriod(); vm.expectEmit(true, true, true, true, address(attestationStation)); emit AttestationCreated( address(optimistInviter), sally, OptimistConstants.OPTIMIST_CAN_MINT_FROM_INVITE_ATTESTATION_KEY, abi.encode(address(carolERC1271Wallet)) ); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); assertEq(_getInviteCount(address(carolERC1271Wallet)), 2); } /** * @notice Claimer must commit the signature before claiming the invite. Sally attempts to * claim the Bob's invite without committing the signature first. */ function test_claimInvite_withoutCommittingHash_reverts() external { _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteAs(bobPrivateKey); vm.expectRevert("OptimistInviter: claimer and signature have not been committed yet"); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); } /** * @notice Using a signature that doesn't correspond to the claimable invite should fail. */ function test_claimInvite_withIncorrectSignature_reverts() external { _grantInvitesTo(carol); _grantInvitesTo(bob); ( OptimistInviter.ClaimableInvite memory bobClaimableInvite, bytes memory bobSignature ) = _issueInviteAs(bobPrivateKey); (, bytes memory carolSignature) = _issueInviteAs(carolPrivateKey); _commitInviteAs(sally, bobSignature); _commitInviteAs(sally, carolSignature); _passMinCommitmentPeriod(); vm.expectRevert("OptimistInviter: invalid signature"); vm.prank(sally); optimistInviter.claimInvite(sally, bobClaimableInvite, carolSignature); } /** * @notice Attempting to use a signature from a issuer who never was granted invites should * fail. */ function test_claimInvite_whenIssuerNeverReceivedInvites_reverts() external { // Bob was never granted any invites, but issues an invite for Eve ( OptimistInviter.ClaimableInvite memory claimableInvite, bytes memory signature ) = _issueInviteAs(bobPrivateKey); _commitInviteAs(sally, signature); _passMinCommitmentPeriod(); vm.expectRevert("OptimistInviter: issuer has no invites"); vm.prank(sally); optimistInviter.claimInvite(sally, claimableInvite, signature); } /** * @notice Attempting to use a signature from a issuer who has no more invites should fail. * Bob has 3 invites, but issues 4 invites for Sally, Carol, Ted, and Eve. Only the * first 3 invites should be claimable. The last claimer, Eve, should not be able to * claim the invite. * */ function test_claimInvite_whenIssuerHasNoInvitesLeft_reverts() external { _grantInvitesTo(bob); _issueThenClaimShouldSucceed(bobPrivateKey, sally); _issueThenClaimShouldSucceed(bobPrivateKey, carol); _issueThenClaimShouldSucceed(bobPrivateKey, ted); assertEq(_getInviteCount(bob), 0); ( OptimistInviter.ClaimableInvite memory claimableInvite4, bytes memory signature4 ) = _issueInviteAs(bobPrivateKey); _commitInviteAs(eve, signature4); _passMinCommitmentPeriod(); vm.expectRevert("OptimistInviter: issuer has no invites"); vm.prank(eve); optimistInviter.claimInvite(eve, claimableInvite4, signature4); assertEq(_getInviteCount(bob), 0); } }