-
Notifications
You must be signed in to change notification settings - Fork 0
/
Game.sol
657 lines (574 loc) · 23.4 KB
/
Game.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
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";
import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol";
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {IBank} from "../bank/IBank.sol";
// import "hardhat/console.sol";
interface IVRFCoordinatorV2 is VRFCoordinatorV2Interface {
function getFeeConfig()
external
view
returns (
uint32 fulfillmentFlatFeeLinkPPMTier1,
uint32 fulfillmentFlatFeeLinkPPMTier2,
uint32 fulfillmentFlatFeeLinkPPMTier3,
uint32 fulfillmentFlatFeeLinkPPMTier4,
uint32 fulfillmentFlatFeeLinkPPMTier5,
uint24 reqsForTier2,
uint24 reqsForTier3,
uint24 reqsForTier4,
uint24 reqsForTier5
);
}
/// @title Game base contract
/// @author Romuald Hog
/// @notice This should be parent contract of each games.
/// It defines all the games common functions and state variables.
/// @dev All rates are in basis point. Chainlink VRF v2 is used.
abstract contract Game is
Ownable,
Pausable,
Multicall,
VRFConsumerBaseV2,
ReentrancyGuard
{
using SafeERC20 for IERC20;
/// @notice Bet information struct.
/// @param resolved Whether the bet has been resolved.
/// @param user Address of the gamer.
/// @param token Address of the token.
/// @param id Bet ID generated by Chainlink VRF.
/// @param amount The bet amount.
/// @param blockNumber Block number of the bet used to refund in case Chainlink's callback fail.
/// @param payout The payout amount.
/// @param vrfCost The Chainlink VRF cost paid by player.
struct Bet {
bool resolved;
address user;
address token;
uint256 id;
uint256 amount;
uint256 blockNumber;
uint256 payout;
uint256 vrfCost;
}
/// @notice Token struct.
/// @param houseEdge House edge rate.
/// @param pendingCount Number of pending bets.
/// @param VRFCallbackGasLimit How much gas is needed in the Chainlink VRF callback.
/// @param VRFFees Chainlink's VRF collected fees amount.
struct Token {
uint16 houseEdge;
uint64 pendingCount;
uint32 VRFCallbackGasLimit;
uint256 VRFFees;
}
/// @notice Chainlink VRF configuration struct.
/// @param requestConfirmations How many confirmations the Chainlink node should wait before responding.
/// @param numRandomWords How many random words is needed to resolve a game's bet.
/// @param keyHash Hash of the public key used to verify the VRF proof.
/// @param chainlinkCoordinator Reference to the VRFCoordinatorV2 deployed contract.
/// @param gasAfterCalculation Gas to be added for VRF cost refund.
struct ChainlinkConfig {
uint16 requestConfirmations;
uint16 numRandomWords;
bytes32 keyHash;
IVRFCoordinatorV2 chainlinkCoordinator;
uint256 gasAfterCalculation;
}
/// @notice Chainlink VRF configuration state.
ChainlinkConfig private _chainlinkConfig;
/// @notice Chainlink price feed.
AggregatorV3Interface private immutable _LINK_ETH_feed;
/// @notice Maps bets IDs to Bet information.
mapping(uint256 => Bet) public bets;
/// @notice Maps users addresses to bets IDs
mapping(address => uint256[]) internal _userBets;
/// @notice Maps tokens addresses to token configuration.
mapping(address => Token) public tokens;
/// @notice Maps user addresses to VRF overcharged cost.
mapping(address => uint256) public userOverchargedVRFCost;
/// @notice The bank that manage to payout a won bet and collect a loss bet.
IBank public immutable bank;
/// @notice Emitted after the house edge is set for a token.
/// @param token Address of the token.
/// @param houseEdge House edge rate.
event SetHouseEdge(address indexed token, uint16 houseEdge);
/// @notice Emitted after the Chainlink callback gas limit is set for a token.
/// @param token Address of the token.
/// @param callbackGasLimit New Chainlink VRF callback gas limit.
event SetVRFCallbackGasLimit(
address indexed token,
uint32 callbackGasLimit
);
/// @notice Emitted after the Chainlink config is set.
/// @param requestConfirmations How many confirmations the Chainlink node should wait before responding.
/// @param keyHash Hash of the public key used to verify the VRF proof.
/// @param gasAfterCalculation Gas to be added for VRF cost refund.
event SetChainlinkConfig(
uint16 requestConfirmations,
bytes32 keyHash,
uint256 gasAfterCalculation
);
/// @notice Emitted after the bet amount is transfered to the user.
/// @param id The bet ID.
/// @param user Address of the gamer.
/// @param amount Number of tokens refunded.
/// @param chainlinkVRFCost The Chainlink VRF cost refunded to player.
event BetRefunded(
uint256 id,
address user,
uint256 amount,
uint256 chainlinkVRFCost
);
/// @notice Emitted after the token's VRF fees amount is transfered to the user.
/// @param token Address of the token.
/// @param amount Number of tokens refunded.
event DistributeTokenVRFFees(address indexed token, uint256 amount);
/// @notice Emitted after the user's overcharged VRF cost amount is transfered.
/// @param user Address of the user.
/// @param overchargedVRFCost Number of tokens refunded.
event DistributeOverchargedVRFCost(
address indexed user,
uint256 overchargedVRFCost
);
/// @notice Emitted after the overcharged VRF cost amount is accounted.
/// @param user Address of the user.
/// @param overchargedVRFCost Number of tokens overcharged.
event AccountOverchargedVRFCost(
address indexed user,
uint256 overchargedVRFCost
);
/// @notice No user's overcharged Chainlink fee.
error NoOverchargedVRFCost();
/// @notice Insufficient bet amount.
/// @param minBetAmount Bet amount.
error UnderMinBetAmount(uint256 minBetAmount);
/// @notice Bet provided doesn't exist or was already resolved.
error NotPendingBet();
/// @notice Bet isn't resolved yet.
error NotFulfilled();
/// @notice House edge is capped at 4%.
error ExcessiveHouseEdge();
/// @notice Token is not allowed.
error ForbiddenToken();
/// @notice Chainlink price feed not working
/// @param linkWei LINK/ETH price returned.
error InvalidLinkWeiPrice(int256 linkWei);
/// @notice The msg.value is not enough to cover Chainlink's fee.
error WrongGasValueToCoverFee();
/// @notice Reverting error when sender isn't allowed.
error AccessDenied();
/// @notice Reverting error when provided address isn't valid.
error InvalidAddress();
/// @notice Reverting error when token has pending bets.
error TokenHasPendingBets();
/// @notice Initialize contract's state variables and VRF Consumer.
/// @param bankAddress The address of the bank.
/// @param chainlinkCoordinatorAddress Address of the Chainlink VRF Coordinator.
/// @param numRandomWords How many random words is needed to resolve a game's bet.
/// @param LINK_ETH_feedAddress Address of the Chainlink LINK/ETH price feed.
constructor(
address bankAddress,
address chainlinkCoordinatorAddress,
uint16 numRandomWords,
address LINK_ETH_feedAddress
) VRFConsumerBaseV2(chainlinkCoordinatorAddress) {
if (
LINK_ETH_feedAddress == address(0) ||
chainlinkCoordinatorAddress == address(0) ||
bankAddress == address(0)
) {
revert InvalidAddress();
}
require(
numRandomWords != 0 && numRandomWords <= 500,
"Wrong Chainlink NumRandomWords"
);
bank = IBank(bankAddress);
_chainlinkConfig.chainlinkCoordinator = IVRFCoordinatorV2(
chainlinkCoordinatorAddress
);
_chainlinkConfig.numRandomWords = numRandomWords;
_LINK_ETH_feed = AggregatorV3Interface(LINK_ETH_feedAddress);
}
/// @notice Calculates the amount's fee based on the house edge.
/// @param token Address of the token.
/// @param amount From which the fee amount will be calculated.
/// @return The fee amount.
function _getFees(address token, uint256 amount)
private
view
returns (uint256)
{
return (tokens[token].houseEdge * amount) / 10000;
}
/// @notice Creates a new bet and request randomness to Chainlink,
/// transfer the ERC20 tokens to the contract or refund the bet amount overflow if the bet amount exceed the maxBetAmount.
/// @param tokenAddress Address of the token.
/// @param tokenAmount The number of tokens bet.
/// @param multiplier The bet amount leverage determines the user's profit amount. 10000 = 100% = no profit.
/// @return A new Bet struct information.
function _newBet(
address tokenAddress,
uint256 tokenAmount,
uint256 multiplier
) internal whenNotPaused nonReentrant returns (Bet memory) {
Token storage token = tokens[tokenAddress];
if (
bank.isAllowedToken(tokenAddress) == false || token.houseEdge == 0
) {
revert ForbiddenToken();
}
address user = msg.sender;
bool isGasToken = tokenAddress == address(0);
uint256 fee = isGasToken ? (msg.value - tokenAmount) : msg.value;
uint256 betAmount = isGasToken ? msg.value - fee : tokenAmount;
// Charge user for Chainlink VRF fee.
{
uint256 chainlinkVRFCost = getChainlinkVRFCost(tokenAddress);
if (fee < (chainlinkVRFCost - ((10 * chainlinkVRFCost) / 100))) {
// 10% slippage.
revert WrongGasValueToCoverFee();
}
}
// Bet amount is capped.
{
uint256 minBetAmount = bank.getMinBetAmount(tokenAddress);
if (betAmount < minBetAmount) {
revert UnderMinBetAmount(minBetAmount);
}
uint256 maxBetAmount = bank.getMaxBetAmount(
tokenAddress,
multiplier
);
if (betAmount > maxBetAmount) {
if (isGasToken) {
Address.sendValue(payable(user), betAmount - maxBetAmount);
}
betAmount = maxBetAmount;
}
}
// Create bet
uint256 id = _chainlinkConfig.chainlinkCoordinator.requestRandomWords(
_chainlinkConfig.keyHash,
bank.getVRFSubId(tokenAddress),
_chainlinkConfig.requestConfirmations,
token.VRFCallbackGasLimit,
_chainlinkConfig.numRandomWords
);
Bet memory newBet = Bet(
false,
user,
tokenAddress,
id,
betAmount,
block.number,
0,
fee
);
_userBets[user].push(id);
bets[id] = newBet;
token.pendingCount++;
// If ERC20, transfer the tokens
if (!isGasToken) {
IERC20(tokenAddress).safeTransferFrom(
user,
address(this),
betAmount
);
}
return newBet;
}
/// @notice Calculates the overcharged VRF cost based on the gas consumed.
/// @param bet The Bet struct information.
/// @param startGas Gas amount at start.
function _accountVRFCost(Bet storage bet, uint256 startGas) internal {
(, int256 weiPerUnitLink, , , ) = _LINK_ETH_feed.latestRoundData();
if (weiPerUnitLink < 0) {
weiPerUnitLink = 0;
}
// Get Chainlink VRF v2 fee amount.
(
uint32 fulfillmentFlatFeeLinkPPMTier1,
,
,
,
,
,
,
,
) = _chainlinkConfig.chainlinkCoordinator.getFeeConfig();
// Calculates the VRF premium fee in ETH
uint256 chainlinkPremium = ((1e12 *
uint256(fulfillmentFlatFeeLinkPPMTier1) *
uint256(weiPerUnitLink)) / 1e18);
// Calculate the gas fee (adding the estimated gas spent after this calculation) + premium
uint256 actualVRFCost = (tx.gasprice *
(startGas - gasleft() + _chainlinkConfig.gasAfterCalculation)) +
chainlinkPremium;
// If the actual VRF cost is higher than what the player paid.
if (actualVRFCost > bet.vrfCost) {
actualVRFCost = bet.vrfCost;
} else {
// Otherwise credits it to his account.
uint256 overchargedVRFCost = bet.vrfCost - actualVRFCost;
userOverchargedVRFCost[bet.user] += overchargedVRFCost;
bet.vrfCost = actualVRFCost;
emit AccountOverchargedVRFCost(bet.user, overchargedVRFCost);
}
// Credits the actual VRF cost to fund the VRF subscription.
tokens[bet.token].VRFFees += actualVRFCost;
}
/// @notice Resolves the bet based on the game child contract result.
/// In case bet is won, the bet amount minus the house edge is transfered to user from the game contract, and the profit is transfered to the user from the Bank.
/// In case bet is lost, the bet amount is transfered to the Bank from the game contract.
/// @param bet The Bet struct information.
/// @param payout What should be sent to the user in case of a won bet. Payout = bet amount + profit amount.
/// @return The payout amount.
/// @dev Should not revert as it resolves the bet with the randomness.
function _resolveBet(Bet storage bet, uint256 payout)
internal
returns (uint256)
{
if (bet.resolved == true || bet.id == 0) {
revert NotPendingBet();
}
bet.resolved = true;
address token = bet.token;
tokens[token].pendingCount--;
uint256 betAmount = bet.amount;
bool isGasToken = bet.token == address(0);
if (payout > betAmount) {
// The user has won more than his bet
address user = bet.user;
uint256 profit = payout - betAmount;
uint256 betAmountFee = _getFees(token, betAmount);
uint256 profitFee = _getFees(token, profit);
uint256 fee = betAmountFee + profitFee;
payout -= fee;
uint256 betAmountPayout = betAmount - betAmountFee;
uint256 profitPayout = profit - profitFee;
// Transfer the bet amount payout to the player
if (isGasToken) {
Address.sendValue(payable(user), betAmountPayout);
} else {
IERC20(token).safeTransfer(user, betAmountPayout);
// Transfer the bet amount fee to the bank.
IERC20(token).safeTransfer(address(bank), betAmountFee);
}
// Transfer the payout from the bank, the bet amount fee to the bank, and account fees.
bank.payout{value: isGasToken ? betAmountFee : 0}(
user,
token,
profitPayout,
fee
);
} else if (payout > 0) {
// The user has won something smaller than his bet
address user = bet.user;
uint256 fee = _getFees(token, payout);
payout -= fee;
uint256 bankCashIn = betAmount - payout;
// Transfer the bet amount payout to the player
if (isGasToken) {
Address.sendValue(payable(user), payout);
} else {
IERC20(token).safeTransfer(user, payout);
// Transfer the lost bet amount and fee to the bank
IERC20(token).safeTransfer(address(bank), bankCashIn);
}
bank.cashIn{value: isGasToken ? bankCashIn : 0}(
token,
bankCashIn,
fee
);
} else {
// The user did not win anything
if (!isGasToken) {
IERC20(token).safeTransfer(address(bank), betAmount);
}
bank.cashIn{value: isGasToken ? betAmount : 0}(token, betAmount, 0);
}
bet.payout = payout;
return payout;
}
/// @notice Gets the list of the last user bets.
/// @param user Address of the gamer.
/// @param dataLength The amount of bets to return.
/// @return A list of Bet.
function _getLastUserBets(address user, uint256 dataLength)
internal
view
returns (Bet[] memory)
{
uint256[] memory userBetsIds = _userBets[user];
uint256 betsLength = userBetsIds.length;
if (betsLength < dataLength) {
dataLength = betsLength;
}
Bet[] memory userBets = new Bet[](dataLength);
if (dataLength != 0) {
uint256 userBetsIndex;
for (uint256 i = betsLength; i > betsLength - dataLength; i--) {
userBets[userBetsIndex] = bets[userBetsIds[i - 1]];
userBetsIndex++;
}
}
return userBets;
}
/// @notice Sets the game house edge rate for a specific token.
/// @param token Address of the token.
/// @param houseEdge House edge rate.
/// @dev The house edge rate couldn't exceed 4%.
function setHouseEdge(address token, uint16 houseEdge) external onlyOwner {
if (houseEdge > 400) {
revert ExcessiveHouseEdge();
}
if (hasPendingBets(token)) {
revert TokenHasPendingBets();
}
tokens[token].houseEdge = houseEdge;
emit SetHouseEdge(token, houseEdge);
}
/// @notice Pauses the contract to disable new bets.
function pause() external onlyOwner {
if (paused()) {
_unpause();
} else {
_pause();
}
}
/// @notice Sets the Chainlink VRF V2 configuration.
/// @param requestConfirmations How many confirmations the Chainlink node should wait before responding.
/// @param keyHash Hash of the public key used to verify the VRF proof.
/// @param gasAfterCalculation Gas to be added for VRF cost refund.
function setChainlinkConfig(
uint16 requestConfirmations,
bytes32 keyHash,
uint256 gasAfterCalculation
) external onlyOwner {
_chainlinkConfig.requestConfirmations = requestConfirmations;
_chainlinkConfig.keyHash = keyHash;
_chainlinkConfig.gasAfterCalculation = gasAfterCalculation;
emit SetChainlinkConfig(
requestConfirmations,
keyHash,
gasAfterCalculation
);
}
/// @notice Sets the Chainlink VRF V2 configuration.
/// @param callbackGasLimit How much gas is needed in the Chainlink VRF callback.
function setVRFCallbackGasLimit(address token, uint32 callbackGasLimit)
external
onlyOwner
{
tokens[token].VRFCallbackGasLimit = callbackGasLimit;
emit SetVRFCallbackGasLimit(token, callbackGasLimit);
}
/// @notice Distributes the token's collected Chainlink fees.
/// @param token Address of the token.
function withdrawTokensVRFFees(address token) external {
uint256 tokenChainlinkFees = tokens[token].VRFFees;
if (tokenChainlinkFees != 0) {
delete tokens[token].VRFFees;
Address.sendValue(
payable(bank.getTokenOwner(token)),
tokenChainlinkFees
);
emit DistributeTokenVRFFees(token, tokenChainlinkFees);
}
}
/// @notice Withdraw user's overcharged Chainlink fees.
function withdrawOverchargedVRFCost(address user) external {
uint256 overchargedVRFCost = userOverchargedVRFCost[user];
if (overchargedVRFCost == 0) {
revert NoOverchargedVRFCost();
}
delete userOverchargedVRFCost[user];
Address.sendValue(payable(user), overchargedVRFCost);
emit DistributeOverchargedVRFCost(user, overchargedVRFCost);
}
/// @notice Refunds the bet to the user if the Chainlink VRF callback failed.
/// @param id The Bet ID.
function refundBet(uint256 id) external {
Bet storage bet = bets[id];
if (bet.resolved == true || bet.id == 0) {
revert NotPendingBet();
} else if (block.number < bet.blockNumber + 30) {
revert NotFulfilled();
}
Token storage token = tokens[bet.token];
token.pendingCount--;
bet.resolved = true;
bet.payout = bet.amount;
uint256 chainlinkVRFCost = bet.vrfCost;
if (bet.token == address(0)) {
Address.sendValue(payable(bet.user), bet.amount + chainlinkVRFCost);
} else {
IERC20(bet.token).safeTransfer(bet.user, bet.amount);
Address.sendValue(payable(bet.user), chainlinkVRFCost);
}
emit BetRefunded(id, bet.user, bet.amount, chainlinkVRFCost);
}
/// @notice Returns the Chainlink VRF config.
/// @param requestConfirmations How many confirmations the Chainlink node should wait before responding.
/// @param keyHash Hash of the public key used to verify the VRF proof.
/// @param chainlinkCoordinator Reference to the VRFCoordinatorV2 deployed contract.
/// @param gasAfterCalculation Gas to be added for VRF cost refund.
function getChainlinkConfig()
external
view
returns (
uint16 requestConfirmations,
bytes32 keyHash,
IVRFCoordinatorV2 chainlinkCoordinator,
uint256 gasAfterCalculation
)
{
return (
_chainlinkConfig.requestConfirmations,
_chainlinkConfig.keyHash,
_chainlinkConfig.chainlinkCoordinator,
_chainlinkConfig.gasAfterCalculation
);
}
/// @notice Returns whether the token has pending bets.
/// @return Whether the token has pending bets.
function hasPendingBets(address token) public view returns (bool) {
return tokens[token].pendingCount != 0;
}
/// @notice Returns the amount of ETH that should be passed to the wager transaction.
/// to cover Chainlink VRF fee.
/// @return The bet resolution cost amount.
function getChainlinkVRFCost(address token) public view returns (uint256) {
(, int256 weiPerUnitLink, , , ) = _LINK_ETH_feed.latestRoundData();
if (weiPerUnitLink <= 0) {
revert InvalidLinkWeiPrice(weiPerUnitLink);
}
// Get Chainlink VRF v2 fee amount.
(
uint32 fulfillmentFlatFeeLinkPPMTier1,
,
,
,
,
,
,
,
) = _chainlinkConfig.chainlinkCoordinator.getFeeConfig();
// 115000 gas is the average Verification gas of Chainlink VRF.
return
(tx.gasprice * (115000 + tokens[token].VRFCallbackGasLimit)) +
((1e12 *
uint256(fulfillmentFlatFeeLinkPPMTier1) *
uint256(weiPerUnitLink)) / 1e18);
}
}