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  }