-
Notifications
You must be signed in to change notification settings - Fork 0
/
RetirementCertificates.sol
389 lines (347 loc) · 15 KB
/
RetirementCertificates.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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
// SPDX-FileCopyrightText: 2021 Toucan Labs
//
// SPDX-License-Identifier: UNLICENSED
// If you encounter a vulnerability or an issue, please contact <[email protected]> or visit security.toucan.earth
pragma solidity 0.8.14;
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol';
import '@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol';
import './interfaces/IToucanContractRegistry.sol';
import './RetirementCertificatesStorage.sol';
/// @notice The `RetirementCertificates` contract lets users mint NFTs that act as proof-of-retirement.
/// These Retirement Certificate NFTs display how many TCO2s a user has burnt
/// @dev The amount of RetirementEvents is denominated in the 18-decimal form
/// @dev Getters in this contract return the corresponding amount in tonnes or kilos
contract RetirementCertificates is
ERC721Upgradeable,
OwnableUpgradeable,
UUPSUpgradeable,
RetirementCertificatesStorageV1,
ReentrancyGuardUpgradeable,
RetirementCertificatesStorage
{
// ----------------------------------------
// Libraries
// ----------------------------------------
using AddressUpgradeable for address;
// ----------------------------------------
// Constants
// ----------------------------------------
/// @dev Version-related parameters. VERSION keeps track of production
/// releases. VERSION_RELEASE_CANDIDATE keeps track of iterations
/// of a VERSION in our staging environment.
string public constant VERSION = '1.0.1';
uint256 public constant VERSION_RELEASE_CANDIDATE = 1;
/// @dev dividers to round carbon in human-readable denominations
uint256 public constant tonneDenomination = 1e18;
uint256 public constant kiloDenomination = 1e15;
// ----------------------------------------
// Events
// ----------------------------------------
event CertificateMinted(uint256 tokenId);
event CertificateUpdated(uint256 tokenId);
event ToucanRegistrySet(address ContractRegistry);
event BaseURISet(string baseURI);
event MinValidAmountSet(uint256 previousAmount, uint256 newAmount);
event EventsAttached(uint256 tokenId, uint256[] eventIds);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
// ----------------------------------------
// Upgradable related functions
// ----------------------------------------
function initialize(address _contractRegistry, string memory _baseURI)
external
virtual
initializer
{
__Context_init_unchained();
__ERC721_init_unchained(
'Toucan Protocol: Retirement Certificates for Tokenized Carbon Offsets',
'TOUCAN-CERT'
);
__Ownable_init_unchained();
__ReentrancyGuard_init_unchained();
__UUPSUpgradeable_init_unchained();
contractRegistry = _contractRegistry;
baseURI = _baseURI;
}
function _authorizeUpgrade(address newImplementation)
internal
virtual
override
onlyOwner
{}
// ------------------------
// Admin functions
// ------------------------
function setToucanContractRegistry(address _address)
external
virtual
onlyOwner
{
contractRegistry = _address;
emit ToucanRegistrySet(_address);
}
function setBaseURI(string memory baseURI_) external virtual onlyOwner {
baseURI = baseURI_;
emit BaseURISet(baseURI_);
}
function setMinValidRetirementAmount(uint256 amount) external onlyOwner {
uint256 previousAmount = minValidRetirementAmount;
require(previousAmount != amount, 'Already set');
minValidRetirementAmount = amount;
emit MinValidAmountSet(previousAmount, amount);
}
// ----------------------------------
// Permissionless functions
// ----------------------------------
/// @notice Register retirement events. This function can only be called by a TC02 contract
/// to register retirement events so they can be directly linked to an NFT mint.
/// @param retiringEntity The entity that has retired TCO2 and is eligible to mint an NFT.
/// @param projectVintageTokenId The vintage id of the TCO2 that is retired.
/// @param amount The amount of the TCO2 that is retired.
/// @param isLegacy Whether this event registration was executed by using the legacy retired
/// amount in the TCO2 contract or utilizes the new retirement event design.
/// @dev The function can either be only called by a valid TCO2 contract.
function registerEvent(
address retiringEntity,
uint256 projectVintageTokenId,
uint256 amount,
bool isLegacy
) external returns (uint256) {
// Logic requires that minting can only originate from a project-vintage ERC20 contract
require(
IToucanContractRegistry(contractRegistry).checkERC20(msg.sender),
'Caller not a TCO2'
);
require(
amount != 0 && amount >= minValidRetirementAmount,
'Invalid amount'
);
/// Read from storage once, then use everywhere by reading
/// from memory.
uint256 eventCounter = retireEventCounter;
unchecked {
/// Realistically, the counter will never overflow
++eventCounter;
}
/// Store counter back in storage
retireEventCounter = eventCounter;
// Track all events of a user
eventsOfUser[retiringEntity].push(eventCounter);
// Track retirements
if (!isLegacy) {
// Avoid tracking timestamps for legacy retirements since these
// are inaccurate.
retirements[eventCounter].createdAt = block.timestamp;
}
retirements[eventCounter].retiringEntity = retiringEntity;
retirements[eventCounter].amount = amount;
retirements[eventCounter].projectVintageTokenId = projectVintageTokenId;
return eventCounter;
}
/// @notice Attach retirement events to an NFT.
/// @param tokenId The id of the NFT to attach events to.
/// @param retirementEventIds An array of event ids to associate with the NFT.
function attachRetirementEvents(
uint256 tokenId,
uint256[] calldata retirementEventIds
) external {
address tokenOwner = ownerOf(tokenId);
require(tokenOwner == msg.sender, 'Unauthorized');
_attachRetirementEvents(tokenId, tokenOwner, retirementEventIds);
}
/// @notice Attach retirement events to an NFT.
/// @param tokenId The id of the NFT to attach events to.
/// @param retiringEntity The entity that has retired TCO2 and is eligible to mint an NFT.
/// @param retirementEventIds An array of event ids to associate with the NFT.
function _attachRetirementEvents(
uint256 tokenId,
address retiringEntity,
uint256[] calldata retirementEventIds
) internal {
// 0. Check whether retirementEventIds is empty
// 1. Check whether event belongs to user (retiring entity)
// 2. Check whether the event has previously been attached
require(retirementEventIds.length != 0, 'Empty event array');
//slither-disable-next-line uninitialized-local
for (uint256 i; i < retirementEventIds.length; ++i) {
uint256 eventId = retirementEventIds[i];
require(
retirements[eventId].retiringEntity == retiringEntity,
'Invalid event to be claimed'
);
require(!claimedEvents[eventId], 'Already claimed event');
claimedEvents[eventId] = true;
certificates[tokenId].retirementEventIds.push(eventId);
}
emit EventsAttached(tokenId, retirementEventIds);
}
/// @notice Mint new Retirement Certificate NFT that shows how many TCO2s have been retired.
/// @param retiringEntity The entity that has retired TCO2 and is eligible to mint an NFT.
/// @param retiringEntityString An identifiable string for the retiring entity, eg. their name.
/// @param beneficiary The beneficiary address for whom the TCO2 amount was retired.
/// @param beneficiaryString An identifiable string for the beneficiary, eg. their name.
/// @param retirementMessage A message to accompany the retirement.
/// @param retirementEventIds An array of event ids to associate with the NFT.
/// @dev The function can either be called by a valid TCO2 contract or by someone who
/// owns retirement events.
function mintCertificate(
address retiringEntity,
string calldata retiringEntityString,
address beneficiary,
string calldata beneficiaryString,
string calldata retirementMessage,
uint256[] calldata retirementEventIds
) external virtual nonReentrant {
// If the provided retiring entity is not the caller, then
// ensure the caller is at least a TCO2 contract. This is to
// allow TCO2 contracts to call retireAndMintCertificate.
require(
retiringEntity == msg.sender ||
IToucanContractRegistry(contractRegistry).checkERC20(
msg.sender
) ==
true,
'Invalid caller'
);
uint256 newItemId = _tokenIds;
unchecked {
++newItemId;
}
_tokenIds = newItemId;
_safeMint(retiringEntity, newItemId);
// Attach retirement events to the newly minted NFT
_attachRetirementEvents(newItemId, retiringEntity, retirementEventIds);
certificates[newItemId].createdAt = block.timestamp;
certificates[newItemId].beneficiary = beneficiary;
certificates[newItemId].beneficiaryString = beneficiaryString;
certificates[newItemId].retiringEntity = retiringEntity;
certificates[newItemId].retiringEntityString = retiringEntityString;
certificates[newItemId].retirementMessage = retirementMessage;
emit CertificateMinted(newItemId);
}
/// @param tokenId The id of the NFT to get the URI.
/// @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
/// based on the ERC721URIStorage implementation
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
require(
_exists(tokenId),
'ERC721URIStorage: URI query for nonexistent token'
);
return
string(
abi.encodePacked(baseURI, StringsUpgradeable.toString(tokenId))
);
}
/// @notice Update retirementMessage, beneficiary, and beneficiaryString of a NFT
/// within 24h of creation. Empty values are ignored, ie., will not overwrite the
/// existing stored values in the NFT.
/// @param tokenId The id of the NFT to update.
/// @param retiringEntityString An identifiable string for the retiring entity, eg. their name.
/// @param beneficiary The new beneficiary to set in the NFT.
/// @param beneficiaryString An identifiable string for the beneficiary, eg. their name.
/// @param retirementMessage The new retirementMessage to set in the NFT.
function updateCertificate(
uint256 tokenId,
string calldata retiringEntityString,
address beneficiary,
string calldata beneficiaryString,
string calldata retirementMessage
) external virtual {
require(msg.sender == ownerOf(tokenId), 'Sender is not owner');
require(
block.timestamp < certificates[tokenId].createdAt + 24 hours,
'24 hours elapsed'
);
if (bytes(retiringEntityString).length != 0) {
certificates[tokenId].retiringEntityString = retiringEntityString;
}
if (beneficiary != address(0)) {
certificates[tokenId].beneficiary = beneficiary;
}
if (bytes(beneficiaryString).length != 0) {
certificates[tokenId].beneficiaryString = beneficiaryString;
}
if (bytes(retirementMessage).length != 0) {
certificates[tokenId].retirementMessage = retirementMessage;
}
emit CertificateUpdated(tokenId);
}
/// @notice Get certificate data for an NFT.
/// @param tokenId The id of the NFT to get data for.
function getData(uint256 tokenId) external view returns (Data memory) {
return certificates[tokenId];
}
/// @notice Get all events for a user.
/// @param user The user for whom to fetch all events.
function getUserEvents(address user)
external
view
returns (uint256[] memory)
{
return eventsOfUser[user];
}
/// @notice Get total retired amount for an NFT.
/// @param tokenId The id of the NFT to update.
/// @return amount Total retired amount for an NFT.
/// @dev The return amount is denominated in 18 decimals, similar to amounts
/// as they are read in TCO2 contracts.
/// For example, 1000000000000000000 means 1 tonne.
function getRetiredAmount(uint256 tokenId)
external
view
returns (uint256 amount)
{
uint256[] memory eventIds = certificates[tokenId].retirementEventIds;
//slither-disable-next-line uninitialized-local
for (uint256 i; i < eventIds.length; ++i) {
amount += retirements[eventIds[i]].amount;
}
}
/// @notice Get total retired amount for an NFT in tonnes.
/// @param tokenId The id of the NFT to update.
/// @return amount Total retired amount for an NFT in tonnes.
function getRetiredAmountInTonnes(uint256 tokenId)
external
view
returns (uint256)
{
//slither-disable-next-line uninitialized-local
uint256 amount;
uint256[] memory eventIds = certificates[tokenId].retirementEventIds;
//slither-disable-next-line uninitialized-local
for (uint256 i; i < eventIds.length; ++i) {
amount += retirements[eventIds[i]].amount;
}
return amount / tonneDenomination;
}
/// @notice Get total retired amount for an NFT in kilos.
/// @param tokenId The id of the NFT to update.
/// @return amount Total retired amount for an NFT in kilos.
function getRetiredAmountInKilos(uint256 tokenId)
external
view
returns (uint256)
{
//slither-disable-next-line uninitialized-local
uint256 amount;
uint256[] memory eventIds = certificates[tokenId].retirementEventIds;
//slither-disable-next-line uninitialized-local
for (uint256 i; i < eventIds.length; ++i) {
amount += retirements[eventIds[i]].amount;
}
return amount / kiloDenomination;
}
}