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
Expand all
Hide 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
This diff is collapsed.
Click to expand it.
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