Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
96c8f41d
Commit
96c8f41d
authored
Mar 27, 2023
by
James Kim
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
create OptimistInviter contract
parent
7354398f
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
940 additions
and
0 deletions
+940
-0
OptimistInviter.t.sol
...s-periphery/contracts/foundry-tests/OptimistInviter.t.sol
+689
-0
TestERC1271Wallet.sol
...periphery/contracts/testing/helpers/TestERC1271Wallet.sol
+26
-0
OptimistInviter.sol
...-periphery/contracts/universal/op-nft/OptimistInviter.sol
+225
-0
No files found.
packages/contracts-periphery/contracts/foundry-tests/OptimistInviter.t.sol
0 → 100644
View file @
96c8f41d
//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";
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 CLAIMABLE_INVITE_TYPEHASH;
bytes32 EIP712_DOMAIN_TYPEHASH;
address constant alice_inviteGranter = address(128);
address constant sally = address(512);
address constant ted = address(1024);
address constant eve = address(2048);
address internal bob;
uint256 internal bobPrivateKey;
address internal carol;
uint256 internal carolPrivateKey;
uint256 currentNonce;
TestERC1271Wallet carolERC1271Wallet;
AttestationStation attestationStation;
OptimistInviter optimistInviter;
function setUp() public {
currentNonce = 0;
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(eve, 1 ether);
vm.label(alice_inviteGranter, "alice_inviteGranter");
vm.label(bob, "bob");
vm.label(sally, "sally");
vm.label(carol, "carol");
CLAIMABLE_INVITE_TYPEHASH = keccak256("ClaimableInvite(address issuer,bytes32 nonce)");
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();
vm.expectEmit(false, false, false, false);
emit Initialized(1);
optimistInviter = new OptimistInviter(alice_inviteGranter, attestationStation);
optimistInviter.initialize("OptimistInviter");
}
/**
* @notice Returns a bytes32 nonce that should change everytime. In practice, people should use
* pseudorandom nonces.
*/
function _consumeNonce() internal returns (bytes32) {
return bytes32(keccak256(abi.encode(currentNonce++)));
}
/**
* @notice Returns a user's current invite count, as stored in the AttestationStation.
*/
function _getInviteCount(address _issuer) internal view returns (uint256) {
bytes memory attestation = attestationStation.attestations(
address(optimistInviter),
_issuer,
bytes32("optimist.can-invite")
);
return abi.decode(attestation, (uint256));
}
/**
* @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,
bytes32("optimist.can-mint-from-invite")
);
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.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) {
bytes32 nonce = _consumeNonce();
address issuer = vm.addr(_issuerPrivateKey);
OptimistInviter.ClaimableInvite memory claimableInvite = OptimistInviter.ClaimableInvite(
issuer,
nonce
);
return (
claimableInvite,
_getSignature(
_issuerPrivateKey,
_getEIP712Digest(
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
assertTrue(optimistInviter.commitments(hashedSignature));
}
/**
* @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.commitments(keccak256(abi.encode(_claimer, signature))), true);
// OptimistInviter should issue a new attestation allowing claimer to mint
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
_claimer,
bytes32("optimist.can-mint-from-invite"),
abi.encode(issuer)
);
// OptimistInviter should issue a new attestation with updated invite count
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
issuer,
bytes32("optimist.can-invite"),
abi.encode(prevInviteCount - 1)
);
// 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,
bytes32("optimist.can-invite"),
abi.encode(3)
);
vm.prank(alice_inviteGranter);
optimistInviter.setInviteCounts(addresses, 3);
assertEq(_getInviteCount(_to), 3);
}
/**
* @notice Gets the hashed typed object for a EIP712 signature.
*/
function _getStructHash(OptimistInviter.ClaimableInvite memory _claimableInvite)
internal
view
returns (bytes32)
{
return
keccak256(
abi.encode(
CLAIMABLE_INVITE_TYPEHASH,
_claimableInvite.issuer,
_claimableInvite.nonce
)
);
}
/**
* @notice Gets the signable digest for a EIP712 signature.
*/
function _getEIP712Digest(
OptimistInviter.ClaimableInvite memory _claimableInvite,
bytes memory _name,
bytes memory _version,
uint256 _chainid,
address _verifyingContract
) internal view returns (bytes32) {
bytes32 domainSeparator = keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(_name),
keccak256(_version),
_chainid,
_verifyingContract
)
);
return
keccak256(
abi.encodePacked("\x19\x01", domainSeparator, _getStructHash(_claimableInvite))
);
}
}
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 create invites as '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,
bytes32("optimist.can-invite"),
abi.encode(3)
);
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
sally,
bytes32("optimist.can-invite"),
abi.encode(3)
);
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
address(carolERC1271Wallet),
bytes32("optimist.can-invite"),
abi.encode(3)
);
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);
assertTrue(optimistInviter.commitments(hashedSignature));
}
/**
* @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);
assertTrue(optimistInviter.commitments(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);
_commitInviteAs(sally, signature);
vm.prank(ted);
optimistInviter.commitInvite(keccak256(abi.encode(sally, signature)));
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
sally,
bytes32("optimist.can-mint-from-invite"),
abi.encode(bob)
);
// OptimistInviter should issue a new attestation with updated invite count
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
bob,
bytes32("optimist.can-invite"),
abi.encode(2)
);
// Should emit an event indicating that the invite was claimed
vm.expectEmit(true, false, false, false, address(optimistInviter));
emit InviteClaimed(bob, sally);
vm.prank(eve);
optimistInviter.claimInvite(sally, claimableInvite, signature);
assertTrue(_hasMintAttestation(sally));
assertFalse(_hasMintAttestation(eve));
}
/**
* @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);
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.version()),
1,
address(optimistInviter)
);
_commitInviteAs(sally, signature);
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.version()),
block.chainid,
address(0xBEEF)
);
_commitInviteAs(sally, signature);
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
_commitInviteAs(sally, 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);
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));
bytes32 nonce = _consumeNonce();
OptimistInviter.ClaimableInvite memory claimableInvite = OptimistInviter.ClaimableInvite(
address(carolERC1271Wallet),
nonce
);
bytes memory signature = _getSignature(
carolPrivateKey,
_getEIP712Digest(
claimableInvite,
bytes("OptimistInviter"),
bytes(optimistInviter.version()),
block.chainid,
address(optimistInviter)
)
);
// Sally tries to claim the invite
_commitInviteAs(sally, signature);
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
sally,
bytes32("optimist.can-mint-from-invite"),
abi.encode(address(carolERC1271Wallet))
);
// OptimistInviter should issue a new attestation with updated invite count
vm.expectEmit(true, true, true, true, address(attestationStation));
emit AttestationCreated(
address(optimistInviter),
address(carolERC1271Wallet),
bytes32("optimist.can-invite"),
abi.encode(2)
);
vm.prank(sally);
optimistInviter.claimInvite(sally, claimableInvite, signature);
}
/**
* @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);
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);
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);
vm.expectRevert("OptimistInviter: issuer has no invites");
vm.prank(sally);
optimistInviter.claimInvite(eve, claimableInvite4, signature4);
assertEq(_getInviteCount(bob), 0);
}
}
packages/contracts-periphery/contracts/testing/helpers/TestERC1271Wallet.sol
0 → 100644
View file @
96c8f41d
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/**
* Simple ERC1271 wallet that can be used to test the ERC1271 signature checker.
*/
contract TestERC1271Wallet is Ownable, IERC1271 {
constructor(address originalOwner) {
transferOwnership(originalOwner);
}
function isValidSignature(bytes32 hash, bytes memory signature)
public
view
override
returns (bytes4 magicValue)
{
return
ECDSA.recover(hash, signature) == owner() ? this.isValidSignature.selector : bytes4(0);
}
}
packages/contracts-periphery/contracts/universal/op-nft/OptimistInviter.sol
0 → 100644
View file @
96c8f41d
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { Semver } from "@eth-optimism/contracts-bedrock/contracts/universal/Semver.sol";
import { AttestationStation } from "./AttestationStation.sol";
import {
SignatureCheckerUpgradeable
} from "@openzeppelin/contracts-upgradeable/utils/cryptography/SignatureCheckerUpgradeable.sol";
import {
EIP712Upgradeable
} from "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol";
/**
* @title OptimistInviter
* @notice OptimistInviter is a contract that allows a user to issue invites as a signature,
* allowing the invitee to claim the invite to an address of their choosing. The invitee
* can claim the invite using a commit and reveal flow.
*/
contract OptimistInviter is Semver, EIP712Upgradeable {
/**
* @notice Emitted when an invite is claimed.
*
* @param issuer Address that issued the signature.
* @param claimer Address that claimed the invite.
*/
event InviteClaimed(address indexed issuer, address indexed claimer);
/**
* @notice Version identifier, used for upgrades.
*/
uint8 public constant VERSION = 1;
/**
* @notice EIP712 typehash for the ClaimableInvite type.
* keccak256("ClaimableInvite(address issuer,bytes32 nonce)")
*/
bytes32 public immutable CLAIMABLE_INVITE_TYPEHASH =
0x6529fd129351e725d7bcbc468b0b0b4675477e56b58514e69ab7e66ddfd20fce;
/**
* @notice Granter who can set accounts' invite counts.
*/
address public immutable INVITE_GRANTER;
/**
* @notice Address of the AttestationStation contract.
*/
AttestationStation public immutable ATTESTATION_STATION;
/**
* @notice Struct that represents a claimable invite.
*
* @custom:field issuer Address that issued the signature. Reason this is explicitly included,
* and not implicitly assumed to be the recovered address from the
* signature is that the issuer may be using a ERC-1271 compatible
* contract wallet, where the recovered address is not the same as the
* issuer, or the signature is not an ECDSA signature at all.
* @custom:field nonce Pseudorandom nonce to prevent replay attacks.
*/
struct ClaimableInvite {
address issuer;
bytes32 nonce;
}
/**
* @notice Maps from hashes to whether or not they have been committed.
*/
mapping(bytes32 => bool) public commitments;
/**
* @notice Maps from addresses to nonces to whether or not they have been used.
*/
mapping(address => mapping(bytes32 => bool)) public usedNonces;
/**
* @custom:semver 1.0.0
* @param _inviteGranter Address of the invite granter.
* @param _attestationStation Address of the AttestationStation contract.
*/
constructor(address _inviteGranter, AttestationStation _attestationStation) Semver(1, 0, 0) {
INVITE_GRANTER = _inviteGranter;
ATTESTATION_STATION = _attestationStation;
}
/**
* @notice Initializes the OptimistInviter contract, setting the EIP712 context.
*
* @param _name Contract name
*/
function initialize(string memory _name) public reinitializer(VERSION) {
__EIP712_init(_name, version());
}
/**
* @notice Allows invite granter to set the number of invites an address has.
*
* @param _accounts An array of accounts to update the invite counts of.
* @param _inviteCount Number of invites to set to.
*/
function setInviteCounts(address[] calldata _accounts, uint256 _inviteCount) public {
// Only invite granter can grant invites
require(
msg.sender == INVITE_GRANTER,
"OptimistInviter: only invite granter can grant invites"
);
uint256 length = _accounts.length;
for (uint256 i; i < length; ) {
// The granted invites are stored as an attestation from this contract on the
// AttestationStation contract. Number of invites is stored as a encoded uint256 in the
// data field of the attetation.
ATTESTATION_STATION.attest(
_accounts[i],
bytes32("optimist.can-invite"),
abi.encode(_inviteCount)
);
unchecked {
i++;
}
}
}
/**
* @notice Allows anyone to commit a received signature along with the address to claim to.
* This is necessary to prevent front-running when the invitee is claiming the invite.
*
* @param _commitment A hash of the claimer and signature concatenated.
keccak256(abi.encode(_claimer, _signature))
*/
function commitInvite(bytes32 _commitment) public {
commitments[_commitment] = true;
}
/**
* @notice Allows anyone to reveal a commitment and claim an invite.
* The claimer ++ signature pair should have been previously committed using commitInvite.
* Doesn't require that the claimer is calling this function.
*
* @param _claimer Address that will be granted the invite. This should should be committed.
* @param _claimableInvite ClaimableInvite struct containing the issuer and nonce.
* @param _signature Signature signed over the claimable invite. This should have been committed.
*/
function claimInvite(
address _claimer,
ClaimableInvite calldata _claimableInvite,
bytes memory _signature
) public {
// Make sure the claimer and signature have been committed.
require(
commitments[keccak256(abi.encode(_claimer, _signature))],
"OptimistInviter: claimer and signature have not been committed yet"
);
// Generate a EIP712 typed data hash to compare against the signature.
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(
CLAIMABLE_INVITE_TYPEHASH,
_claimableInvite.issuer,
_claimableInvite.nonce
)
)
);
// Uses SignatureChecker, which supports both regular ECDSA signatures from EOAs as well as
// ERC-1271 signatures from contract wallets or multi-sigs. This means that if the issuer
// wants to revoke a signature, they can use a smart contract wallet to issue the signature,
// then invalidate the signature after issuing it.
require(
SignatureCheckerUpgradeable.isValidSignatureNow(
_claimableInvite.issuer,
digest,
_signature
),
"OptimistInviter: invalid signature"
);
// The issuer includes a pseudorandom nonce in the signature to prevent replay attacks.
// This checks that the nonce has not been used for this issuer before. The nonces are
// scoped to the issuer address, so the same nonce can be used by different issuers without
// clashing.
require(
!usedNonces[_claimableInvite.issuer][_claimableInvite.nonce],
"OptimistInviter: nonce has already been used"
);
// Set the nonce as used for the issuer so that it cannot be replayed.
usedNonces[_claimableInvite.issuer][_claimableInvite.nonce] = true;
// Check the AttestationStation contract to see how many invites the issuer has left.
bytes memory attestation = ATTESTATION_STATION.attestations(
address(this),
_claimableInvite.issuer,
bytes32("optimist.can-invite")
);
// Failing this check means that the issuer was never granted any invites to begin with.
require(attestation.length > 0, "OptimistInviter: issuer has no invites");
uint256 count = abi.decode(attestation, (uint256));
// Failing this check means that the issuer has used up all of their existing invites.
require(count > 0, "OptimistInviter: issuer has no invites");
// Create the attestation that the claimer can mint from the issuer's invite.
// The invite issuer is included in the data of the attestation.
ATTESTATION_STATION.attest(
_claimer,
bytes32("optimist.can-mint-from-invite"),
abi.encode(_claimableInvite.issuer)
);
// Reduce the issuer's invite count by 1 by re-attesting the optimist.can-invite attestation
// with the new count.
count--;
ATTESTATION_STATION.attest(
_claimableInvite.issuer,
bytes32("optimist.can-invite"),
abi.encode(count)
);
emit InviteClaimed(_claimableInvite.issuer, _claimer);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment