-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathNftReward.sol
210 lines (184 loc) · 6.75 KB
/
NftReward.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
/**
* @title NFT reward contract
* @notice Allows NFT minter to sign off-chain mint requests for target users
* who can later claim NFTs by minter's signature
*/
contract NftReward is ERC721, Ownable, Pausable, EIP712 {
/// @notice Base URI used for all of the tokens
string public baseUri;
/// @notice Minter address who will sign off-chain mint requests
address public minter;
/// @notice Mapping to check whether nonce is redeemed
mapping (uint256 nonce => bool isRedeemed) public nonceRedeemed;
/// @notice Arbitrary token data
mapping (uint256 tokenId => mapping(bytes32 key => string value)) public tokenData;
/// @notice Array of all arbitraty token data keys (useful for UI)
bytes32[] public tokenDataKeys;
/// @notice Mapping to check whether token data key exists
mapping(bytes32 tokenDataKey => bool isTokenDataKeyExists) public tokenDataKeyExists;
/// @notice Total amount of minted tokens
uint256 public tokenIdCounter;
/// @notice Mint request signed by minter
struct MintRequest {
// address which is eligible for minting NFT
address beneficiary;
// unix timestamp until mint request is valid
uint256 deadline;
// array of arbitrary data keys
bytes32[] keys;
// unique number used to prevent mint request reusage
uint256 nonce;
// array of arbitrary data values
string[] values;
}
/**
* @notice Contract constructor
* @param _tokenName NFT token name
* @param _tokenSymbol NFT token symbol
* @param _initialOwner Initial owner name
* @param _minter Minter address
*/
constructor (
string memory _tokenName,
string memory _tokenSymbol,
address _initialOwner,
address _minter
)
ERC721(_tokenName, _tokenSymbol)
Ownable(_initialOwner)
EIP712("NftReward-Domain", "1")
{
minter = _minter;
}
//==================
// Public methods
//==================
/**
* @notice Returns mint request digest which should be signed by `minter`
* @param _mintRequest Mint request data
* @return Mint request digest which should be signed by `minter`
*/
function getMintRequestDigest(MintRequest calldata _mintRequest) public view returns (bytes32) {
// for `string[]` array type we need to hash all the array values first
bytes32[] memory valuesHashed = new bytes32[](_mintRequest.values.length);
for (uint256 i = 0; i < _mintRequest.values.length; i++) {
valuesHashed[i] = keccak256(bytes(_mintRequest.values[i]));
}
// return final hash
return _hashTypedDataV4(keccak256(abi.encode(
keccak256("MintRequest(address beneficiary,uint256 deadline,bytes32[] keys,uint256 nonce,string[] values)"),
_mintRequest.beneficiary,
_mintRequest.deadline,
keccak256(abi.encodePacked(_mintRequest.keys)),
_mintRequest.nonce,
keccak256(abi.encodePacked(valuesHashed))
)));
}
/**
* @notice Returns all arbitrary token data keys (useful for UI)
* @return Array of all arbitrary token data keys
*/
function getTokenDataKeys() public view returns(bytes32[] memory) {
return tokenDataKeys;
}
/**
* @notice Returns signer of the mint request
* @param _mintRequest Mint request data
* @param _signature Minter signature
* @return Signer of the mint request
*/
function recover(
MintRequest calldata _mintRequest,
bytes calldata _signature
)
public
view
returns (address)
{
bytes32 digest = getMintRequestDigest(_mintRequest);
address signer = ECDSA.recover(digest, _signature);
return signer;
}
/**
* @notice Mints a reward NFT to beneficiary who provided a mint request with valid minter's signature
* @param _mintRequest Mint request data
* @param _signature Minter signature
*/
function safeMint(
MintRequest calldata _mintRequest,
bytes calldata _signature
)
public
whenNotPaused
{
// validation
require(recover(_mintRequest, _signature) == minter, "Signed not by minter");
require(msg.sender == _mintRequest.beneficiary, "Not eligible");
require(block.timestamp < _mintRequest.deadline, "Signature expired");
require(!nonceRedeemed[_mintRequest.nonce], "Already minted");
require(_mintRequest.keys.length == _mintRequest.values.length, "Key/value length mismatch");
// mark nonce as used
nonceRedeemed[_mintRequest.nonce] = true;
// save arbitrary token data
uint256 keysCount = _mintRequest.keys.length;
for (uint256 i = 0; i < keysCount; i++) {
// save data
tokenData[tokenIdCounter][_mintRequest.keys[i]] = _mintRequest.values[i];
// save arbitrary token data key if not saved yet
if (!tokenDataKeyExists[_mintRequest.keys[i]]) {
tokenDataKeys.push(_mintRequest.keys[i]);
tokenDataKeyExists[_mintRequest.keys[i]] = true;
}
}
// mint token to beneficiary
_safeMint(_mintRequest.beneficiary, tokenIdCounter);
// increase token counter
tokenIdCounter++;
}
//=================
// Owner methods
//=================
/**
* @notice Pauses contract operations
*/
function pause() external onlyOwner {
_pause();
}
/**
* @notice Sets new base URI for all of the tokens
* @param _newBaseUri New base URI
*/
function setBaseUri(string memory _newBaseUri) external onlyOwner {
baseUri = _newBaseUri;
}
/**
* @notice Sets new minter address (who can sign off-chain mint requests)
* @param _newMinter New minter address
*/
function setMinter(address _newMinter) external onlyOwner {
minter = _newMinter;
}
/**
* @notice Unpauses contract operations
*/
function unpause() external onlyOwner {
_unpause();
}
//====================
// Internal methods
//====================
/**
* @notice Returns URI used for all of the tokens
* @return URI used for all of the tokens
*/
function _baseURI() internal view override returns (string memory) {
return baseUri;
}
}