-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathACOToken.sol
996 lines (880 loc) · 39.6 KB
/
ACOToken.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
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
pragma solidity ^0.6.6;
import "./ERC20.sol";
import "../libs/Address.sol";
import "../libs/ACONameFormatter.sol";
/**
* @title ACOToken
* @dev The implementation of the ACO token.
* The token is ERC20 compliant.
*/
contract ACOToken is ERC20 {
using Address for address;
/**
* @dev Struct to store the accounts that generated tokens with a collateral deposit.
*/
struct TokenCollateralized {
/**
* @dev Current amount of tokens.
*/
uint256 amount;
/**
* @dev Index on the collateral owners array.
*/
uint256 index;
}
/**
* @dev Emitted when collateral is deposited on the contract.
* @param account Address of the collateral owner.
* @param amount Amount of collateral deposited.
*/
event CollateralDeposit(address indexed account, uint256 amount);
/**
* @dev Emitted when collateral is withdrawn from the contract.
* @param account Address of the account.
* @param recipient Address of the collateral destination.
* @param amount Amount of collateral withdrawn.
* @param fee The fee amount charged on the withdrawal.
*/
event CollateralWithdraw(address indexed account, address indexed recipient, uint256 amount, uint256 fee);
/**
* @dev Emitted when the collateral is used on an assignment.
* @param from Address of the account of the collateral owner.
* @param to Address of the account that exercises tokens to get the collateral.
* @param paidAmount Amount paid to the collateral owner.
* @param tokenAmount Amount of tokens used to exercise.
*/
event Assigned(address indexed from, address indexed to, uint256 paidAmount, uint256 tokenAmount);
/**
* @dev Emitted when the collateralized token is transferred.
* @param from Address of the account of the collateral owner.
* @param to Address of the account to get the collateralized tokens.
* @param tokenCollateralizedAmount Amount of collateralized tokens transferred.
*/
event TransferCollateralOwnership(address indexed from, address indexed to, uint256 tokenCollateralizedAmount);
/**
* @dev The ERC20 token address for the underlying asset (0x0 for Ethereum).
*/
address public underlying;
/**
* @dev The ERC20 token address for the strike asset (0x0 for Ethereum).
*/
address public strikeAsset;
/**
* @dev Address of the fee destination charged on the exercise.
*/
address payable public feeDestination;
/**
* @dev True if the type is CALL, false for PUT.
*/
bool public isCall;
/**
* @dev The strike price for the token with the strike asset precision.
*/
uint256 public strikePrice;
/**
* @dev The UNIX time for the token expiration.
*/
uint256 public expiryTime;
/**
* @dev The total amount of collateral on the contract.
*/
uint256 public totalCollateral;
/**
* @dev The fee value. It is a percentage value (100000 is 100%).
*/
uint256 public acoFee;
/**
* @dev Symbol of the underlying asset.
*/
string public underlyingSymbol;
/**
* @dev Symbol of the strike asset.
*/
string public strikeAssetSymbol;
/**
* @dev Decimals for the underlying asset.
*/
uint8 public underlyingDecimals;
/**
* @dev Decimals for the strike asset.
*/
uint8 public strikeAssetDecimals;
/**
* @dev The maximum number of accounts that can be exercised by transaction.
*/
uint256 public maxExercisedAccounts;
/**
* @dev Underlying precision. (10 ^ underlyingDecimals)
*/
uint256 internal underlyingPrecision;
/**
* @dev Accounts that generated tokens with a collateral deposit.
*/
mapping(address => TokenCollateralized) internal tokenData;
/**
* @dev Array with all accounts with collateral deposited.
*/
address[] internal _collateralOwners;
/**
* @dev Internal data to control the reentrancy.
*/
bool internal _notEntered;
/**
* @dev Selector for ERC20 transfer function.
*/
bytes4 internal _transferSelector;
/**
* @dev Selector for ERC20 transfer from function.
*/
bytes4 internal _transferFromSelector;
/**
* @dev Modifier to check if the token is not expired.
* It is executed only while the token is not expired.
*/
modifier notExpired() {
require(_notExpired(), "ACOToken::Expired");
_;
}
/**
* @dev Modifier to prevent a contract from calling itself during the function execution.
*/
modifier nonReentrant() {
require(_notEntered, "ACOToken::Reentry");
_notEntered = false;
_;
_notEntered = true;
}
/**
* @dev Function to initialize the contract.
* It should be called when creating the token.
* It must be called only once. The first `require` is to guarantee that behavior.
* @param _underlying Address of the underlying asset (0x0 for Ethereum).
* @param _strikeAsset Address of the strike asset (0x0 for Ethereum).
* @param _isCall True if the type is CALL, false for PUT.
* @param _strikePrice The strike price with the strike asset precision.
* @param _expiryTime The UNIX time for the token expiration.
* @param _acoFee Value of the ACO fee. It is a percentage value (100000 is 100%).
* @param _feeDestination Address of the fee destination charged on the exercise.
* @param _maxExercisedAccounts The maximum number of accounts that can be exercised by transaction.
*/
function init(
address _underlying,
address _strikeAsset,
bool _isCall,
uint256 _strikePrice,
uint256 _expiryTime,
uint256 _acoFee,
address payable _feeDestination,
uint256 _maxExercisedAccounts
) public {
require(underlying == address(0) && strikeAsset == address(0) && strikePrice == 0, "ACOToken::init: Already initialized");
require(_expiryTime > now, "ACOToken::init: Invalid expiry");
require(_strikePrice > 0, "ACOToken::init: Invalid strike price");
require(_underlying != _strikeAsset, "ACOToken::init: Same assets");
require(_acoFee <= 500, "ACOToken::init: Invalid ACO fee"); // Maximum is 0.5%
require(_isEther(_underlying) || _underlying.isContract(), "ACOToken::init: Invalid underlying");
require(_isEther(_strikeAsset) || _strikeAsset.isContract(), "ACOToken::init: Invalid strike asset");
require(_maxExercisedAccounts >= 25 && _maxExercisedAccounts <= 150, "ACOToken::init: Invalid number to max exercised accounts");
underlying = _underlying;
strikeAsset = _strikeAsset;
isCall = _isCall;
strikePrice = _strikePrice;
expiryTime = _expiryTime;
acoFee = _acoFee;
feeDestination = _feeDestination;
maxExercisedAccounts = _maxExercisedAccounts;
underlyingDecimals = _getAssetDecimals(_underlying);
require(underlyingDecimals < 78, "ACOToken::init: Invalid underlying decimals");
strikeAssetDecimals = _getAssetDecimals(_strikeAsset);
underlyingSymbol = _getAssetSymbol(_underlying);
strikeAssetSymbol = _getAssetSymbol(_strikeAsset);
underlyingPrecision = 10 ** uint256(underlyingDecimals);
_transferSelector = bytes4(keccak256(bytes("transfer(address,uint256)")));
_transferFromSelector = bytes4(keccak256(bytes("transferFrom(address,address,uint256)")));
_notEntered = true;
}
/**
* @dev Function to guarantee that the contract will not receive ether directly.
*/
receive() external payable {
revert();
}
/**
* @dev Function to get the token name.
*/
function name() public view override returns(string memory) {
return _name();
}
/**
* @dev Function to get the token symbol, that it is equal to the name.
*/
function symbol() public view override returns(string memory) {
return _name();
}
/**
* @dev Function to get the token decimals, that it is equal to the underlying asset decimals.
*/
function decimals() public view override returns(uint8) {
return underlyingDecimals;
}
/**
* @dev Function to get the current amount of collateral for an account.
* @param account Address of the account.
* @return The current amount of collateral.
*/
function currentCollateral(address account) public view returns(uint256) {
return getCollateralAmount(currentCollateralizedTokens(account));
}
/**
* @dev Function to get the current amount of unassignable collateral for an account.
* After expiration, the unassignable collateral is equal to the account's collateral balance.
* @param account Address of the account.
* @return The respective amount of unassignable collateral.
*/
function unassignableCollateral(address account) public view returns(uint256) {
return getCollateralAmount(unassignableTokens(account));
}
/**
* @dev Function to get the current amount of assignable collateral for an account.
* After expiration, the assignable collateral is zero.
* @param account Address of the account.
* @return The respective amount of assignable collateral.
*/
function assignableCollateral(address account) public view returns(uint256) {
return getCollateralAmount(assignableTokens(account));
}
/**
* @dev Function to get the current amount of collateralized tokens for an account.
* @param account Address of the account.
* @return The current amount of collateralized tokens.
*/
function currentCollateralizedTokens(address account) public view returns(uint256) {
return tokenData[account].amount;
}
/**
* @dev Function to get the current amount of unassignable tokens for an account.
* After expiration, the unassignable tokens is equal to the account's collateralized tokens.
* @param account Address of the account.
* @return The respective amount of unassignable tokens.
*/
function unassignableTokens(address account) public view returns(uint256) {
if (balanceOf(account) > tokenData[account].amount || !_notExpired()) {
return tokenData[account].amount;
} else {
return balanceOf(account);
}
}
/**
* @dev Function to get the current amount of assignable tokens for an account.
* After expiration, the assignable tokens is zero.
* @param account Address of the account.
* @return The respective amount of assignable tokens.
*/
function assignableTokens(address account) public view returns(uint256) {
if (_notExpired()) {
return _getAssignableAmount(account);
} else {
return 0;
}
}
/**
* @dev Function to get the equivalent collateral amount for a token amount.
* @param tokenAmount Amount of tokens.
* @return The respective amount of collateral.
*/
function getCollateralAmount(uint256 tokenAmount) public view returns(uint256) {
if (isCall) {
return tokenAmount;
} else if (tokenAmount > 0) {
return _getTokenStrikePriceRelation(tokenAmount);
} else {
return 0;
}
}
/**
* @dev Function to get the equivalent token amount for a collateral amount.
* @param collateralAmount Amount of collateral.
* @return The respective amount of tokens.
*/
function getTokenAmount(uint256 collateralAmount) public view returns(uint256) {
if (isCall) {
return collateralAmount;
} else if (collateralAmount > 0) {
return collateralAmount.mul(underlyingPrecision).div(strikePrice);
} else {
return 0;
}
}
/**
* @dev Function to get the number of addresses that have collateral deposited.
* @return The number of addresses.
*/
function numberOfAccountsWithCollateral() public view returns(uint256) {
return _collateralOwners.length;
}
/**
* @dev Function to get the base data for exercise of an amount of token.
* To call the exercise the value returned must be added by the number of accounts that could be exercised:
* - using the ´exercise´ or ´exerciseFrom´ functions it will be equal to `maxExercisedAccounts`.
* - using the ´exerciseAccounts´ or `exerciseAccountsFrom` functions it will be equal to the number of accounts sent as function argument.
* @param tokenAmount Amount of tokens.
* @return The asset and the respective base amount that should be sent to get the collateral.
*/
function getBaseExerciseData(uint256 tokenAmount) public view returns(address, uint256) {
if (isCall) {
return (strikeAsset, _getTokenStrikePriceRelation(tokenAmount));
} else {
return (underlying, tokenAmount);
}
}
/**
* @dev Function to get the collateral to be received on an exercise and the respective fee.
* @param tokenAmount Amount of tokens.
* @return The collateral to be received and the respective fee.
*/
function getCollateralOnExercise(uint256 tokenAmount) public view returns(uint256, uint256) {
uint256 collateralAmount = getCollateralAmount(tokenAmount);
uint256 fee = collateralAmount.mul(acoFee).div(100000);
collateralAmount = collateralAmount.sub(fee);
return (collateralAmount, fee);
}
/**
* @dev Function to get the collateral asset.
* @return The address of the collateral asset.
*/
function collateral() public view returns(address) {
if (isCall) {
return underlying;
} else {
return strikeAsset;
}
}
/**
* @dev Function to mint tokens with Ether deposited as collateral.
* NOTE: The function only works when the token is NOT expired yet.
* @return The amount of tokens minted.
*/
function mintPayable() external payable returns(uint256) {
require(_isEther(collateral()), "ACOToken::mintPayable: Invalid call");
return _mintToken(msg.sender, msg.value);
}
/**
* @dev Function to mint tokens with Ether deposited as collateral to an informed account.
* However, the minted tokens are assigned to the transaction sender.
* NOTE: The function only works when the token is NOT expired yet.
* @param account Address of the account that will be the collateral owner.
* @return The amount of tokens minted.
*/
function mintToPayable(address account) external payable returns(uint256) {
require(_isEther(collateral()), "ACOToken::mintToPayable: Invalid call");
return _mintToken(account, msg.value);
}
/**
* @dev Function to mint tokens with ERC20 deposited as collateral.
* NOTE: The function only works when the token is NOT expired yet.
* @param collateralAmount Amount of collateral deposited.
* @return The amount of tokens minted.
*/
function mint(uint256 collateralAmount) external returns(uint256) {
address _collateral = collateral();
require(!_isEther(_collateral), "ACOToken::mint: Invalid call");
_transferFromERC20(_collateral, msg.sender, address(this), collateralAmount);
return _mintToken(msg.sender, collateralAmount);
}
/**
* @dev Function to mint tokens with ERC20 deposited as collateral to an informed account.
* However, the minted tokens are assigned to the transaction sender.
* NOTE: The function only works when the token is NOT expired yet.
* @param account Address of the account that will be the collateral owner.
* @param collateralAmount Amount of collateral deposited.
* @return The amount of tokens minted.
*/
function mintTo(address account, uint256 collateralAmount) external returns(uint256) {
address _collateral = collateral();
require(!_isEther(_collateral), "ACOToken::mintTo: Invalid call");
_transferFromERC20(_collateral, msg.sender, address(this), collateralAmount);
return _mintToken(account, collateralAmount);
}
/**
* @dev Function to burn tokens and get the collateral, not assigned, back.
* NOTE: The function only works when the token is NOT expired yet.
* @param tokenAmount Amount of tokens to be burned.
* @return The amount of collateral transferred.
*/
function burn(uint256 tokenAmount) external returns(uint256) {
return _burn(msg.sender, tokenAmount);
}
/**
* @dev Function to burn tokens from a specific account and send the collateral to its address.
* The token allowance must be respected.
* The collateral is sent to the transaction sender.
* NOTE: The function only works when the token is NOT expired yet.
* @param account Address of the account.
* @param tokenAmount Amount of tokens to be burned.
* @return The amount of collateral transferred.
*/
function burnFrom(address account, uint256 tokenAmount) external returns(uint256) {
return _burn(account, tokenAmount);
}
/**
* @dev Function to get the collateral, not assigned, back.
* NOTE: The function only works when the token IS expired.
* @return The amount of collateral transferred.
*/
function redeem() external returns(uint256) {
return _redeem(msg.sender);
}
/**
* @dev Function to get the collateral from a specific account sent back to its address .
* The token allowance must be respected.
* The collateral is sent to the transaction sender.
* NOTE: The function only works when the token IS expired.
* @param account Address of the account.
* @return The amount of collateral transferred.
*/
function redeemFrom(address account) external returns(uint256) {
require(tokenData[account].amount <= allowance(account, msg.sender), "ACOToken::redeemFrom: Allowance too low");
return _redeem(account);
}
/**
* @dev Function to exercise the tokens, paying to get the equivalent collateral.
* The paid amount is sent to the collateral owners that were assigned.
* NOTE: The function only works when the token is NOT expired.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
* @return The amount of collateral transferred.
*/
function exercise(uint256 tokenAmount, uint256 salt) external payable returns(uint256) {
return _exercise(msg.sender, tokenAmount, salt);
}
/**
* @dev Function to exercise the tokens from an account, paying to get the equivalent collateral.
* The token allowance must be respected.
* The paid amount is sent to the collateral owners that were assigned.
* The collateral is transferred to the transaction sender.
* NOTE: The function only works when the token is NOT expired.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
* @return The amount of collateral transferred.
*/
function exerciseFrom(address account, uint256 tokenAmount, uint256 salt) external payable returns(uint256) {
return _exercise(account, tokenAmount, salt);
}
/**
* @dev Function to exercise the tokens, paying to get the equivalent collateral.
* The paid amount is sent to the collateral owners (on accounts list) that were assigned.
* NOTE: The function only works when the token is NOT expired.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get collateral from.
* @return The amount of collateral transferred.
*/
function exerciseAccounts(uint256 tokenAmount, address[] calldata accounts) external payable returns(uint256) {
return _exerciseFromAccounts(msg.sender, tokenAmount, accounts);
}
/**
* @dev Function to transfer collateralized tokens.
* @param recipient Address of the destination.
* @param tokenCollateralizedAmount Amount of collateralized tokens to be transferred.
*/
function transferCollateralOwnership(address recipient, uint256 tokenCollateralizedAmount) external {
require(recipient != address(0), "ACOToken::transferCollateralOwnership: Invalid recipient");
require(tokenCollateralizedAmount > 0, "ACOToken::transferCollateralOwnership: Invalid amount");
TokenCollateralized storage senderData = tokenData[msg.sender];
senderData.amount = senderData.amount.sub(tokenCollateralizedAmount);
_removeCollateralDataIfNecessary(msg.sender);
TokenCollateralized storage recipientData = tokenData[recipient];
if (_hasCollateral(recipientData)) {
recipientData.amount = recipientData.amount.add(tokenCollateralizedAmount);
} else {
tokenData[recipient] = TokenCollateralized(tokenCollateralizedAmount, _collateralOwners.length);
_collateralOwners.push(recipient);
}
emit TransferCollateralOwnership(msg.sender, recipient, tokenCollateralizedAmount);
}
/**
* @dev Function to exercise the tokens from a specific account, paying to get the equivalent collateral sent to its address.
* The token allowance must be respected.
* The paid amount is sent to the collateral owners (on accounts list) that were assigned.
* The collateral is transferred to the transaction sender.
* NOTE: The function only works when the token is NOT expired.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get the deposited collateral.
* @return The amount of collateral transferred.
*/
function exerciseAccountsFrom(address account, uint256 tokenAmount, address[] calldata accounts) external payable returns(uint256) {
return _exerciseFromAccounts(account, tokenAmount, accounts);
}
/**
* @dev Internal function to redeem respective collateral from an account.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @return The amount of collateral transferred.
*/
function _redeemCollateral(address account, uint256 tokenAmount) internal returns(uint256) {
require(_accountHasCollateral(account), "ACOToken::_redeemCollateral: No collateral available");
require(tokenAmount > 0, "ACOToken::_redeemCollateral: Invalid token amount");
TokenCollateralized storage data = tokenData[account];
data.amount = data.amount.sub(tokenAmount);
_removeCollateralDataIfNecessary(account);
return _transferCollateral(account, getCollateralAmount(tokenAmount), 0);
}
/**
* @dev Internal function to mint tokens.
* The tokens are minted for the transaction sender.
* @param account Address of the account.
* @param collateralAmount Amount of collateral deposited.
* @return The amount of tokens minted.
*/
function _mintToken(address account, uint256 collateralAmount) nonReentrant notExpired internal returns(uint256) {
require(collateralAmount > 0, "ACOToken::_mintToken: Invalid collateral amount");
if (!_accountHasCollateral(account)) {
tokenData[account].index = _collateralOwners.length;
_collateralOwners.push(account);
}
uint256 tokenAmount = getTokenAmount(collateralAmount);
require(tokenAmount != 0, "ACOToken::_mintToken: Invalid token amount");
tokenData[account].amount = tokenData[account].amount.add(tokenAmount);
totalCollateral = totalCollateral.add(collateralAmount);
emit CollateralDeposit(account, collateralAmount);
super._mintAction(msg.sender, tokenAmount);
return tokenAmount;
}
/**
* @dev Internal function to transfer collateral.
* When there is a fee, the calculated fee is also transferred to the destination fee address.
* The collateral destination is always the transaction sender address.
* @param account Address of the account.
* @param collateralAmount Amount of collateral to be transferred.
* @param fee Amount of fee charged.
* @return The amount of collateral transferred.
*/
function _transferCollateral(address account, uint256 collateralAmount, uint256 fee) internal returns(uint256) {
totalCollateral = totalCollateral.sub(collateralAmount.add(fee));
address _collateral = collateral();
if (_isEther(_collateral)) {
payable(msg.sender).transfer(collateralAmount);
if (fee > 0) {
feeDestination.transfer(fee);
}
} else {
_transferERC20(_collateral, msg.sender, collateralAmount);
if (fee > 0) {
_transferERC20(_collateral, feeDestination, fee);
}
}
emit CollateralWithdraw(account, msg.sender, collateralAmount, fee);
return collateralAmount;
}
/**
* @dev Internal function to exercise the tokens from an account.
* @param account Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
* @return The amount of collateral transferred.
*/
function _exercise(address account, uint256 tokenAmount, uint256 salt) nonReentrant internal returns(uint256) {
_validateAndBurn(account, tokenAmount, maxExercisedAccounts);
_exerciseOwners(account, tokenAmount, salt);
(uint256 collateralAmount, uint256 fee) = getCollateralOnExercise(tokenAmount);
return _transferCollateral(account, collateralAmount, fee);
}
/**
* @dev Internal function to exercise the tokens from an account.
* @param account Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get the collateral from.
* @return The amount of collateral transferred.
*/
function _exerciseFromAccounts(address account, uint256 tokenAmount, address[] memory accounts) nonReentrant internal returns(uint256) {
_validateAndBurn(account, tokenAmount, accounts.length);
_exerciseAccounts(account, tokenAmount, accounts);
(uint256 collateralAmount, uint256 fee) = getCollateralOnExercise(tokenAmount);
return _transferCollateral(account, collateralAmount, fee);
}
/**
* @dev Internal function to exercise the assignable tokens from the stored list of collateral owners.
* @param exerciseAccount Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param salt Random number to calculate the start index of the array of accounts to be exercised.
*/
function _exerciseOwners(address exerciseAccount, uint256 tokenAmount, uint256 salt) internal {
uint256 accountsExercised = 0;
uint256 start = salt.mod(_collateralOwners.length);
uint256 index = start;
uint256 count = 0;
while (tokenAmount > 0 && count < _collateralOwners.length) {
uint256 remainingAmount = _exerciseAccount(_collateralOwners[index], tokenAmount, exerciseAccount);
if (remainingAmount < tokenAmount) {
accountsExercised++;
require(accountsExercised < maxExercisedAccounts || remainingAmount == 0, "ACOToken::_exerciseOwners: Too many accounts to exercise");
}
tokenAmount = remainingAmount;
++index;
if (index == _collateralOwners.length) {
index = 0;
}
++count;
}
require(tokenAmount == 0, "ACOToken::_exerciseOwners: Invalid remaining amount");
uint256 indexOnModifyIteration;
bool shouldModifyIteration = false;
if (index == 0) {
index = _collateralOwners.length;
} else if (index <= start) {
indexOnModifyIteration = index - 1;
shouldModifyIteration = true;
index = _collateralOwners.length;
}
for (uint256 i = 0; i < count; ++i) {
--index;
if (shouldModifyIteration && index < start) {
index = indexOnModifyIteration;
shouldModifyIteration = false;
}
_removeCollateralDataIfNecessary(_collateralOwners[index]);
}
}
/**
* @dev Internal function to exercise the assignable tokens from an accounts list.
* @param exerciseAccount Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param accounts The array of addresses to get the collateral from.
*/
function _exerciseAccounts(address exerciseAccount, uint256 tokenAmount, address[] memory accounts) internal {
for (uint256 i = 0; i < accounts.length; ++i) {
if (tokenAmount == 0) {
break;
}
tokenAmount = _exerciseAccount(accounts[i], tokenAmount, exerciseAccount);
_removeCollateralDataIfNecessary(accounts[i]);
}
require(tokenAmount == 0, "ACOToken::_exerciseAccounts: Invalid remaining amount");
}
/**
* @dev Internal function to exercise the assignable tokens from an account and transfer to its address the respective payment.
* @param account Address of the account.
* @param tokenAmount Amount of tokens.
* @param exerciseAccount Address of the account that is exercising.
* @return Remaining amount of tokens.
*/
function _exerciseAccount(address account, uint256 tokenAmount, address exerciseAccount) internal returns(uint256) {
uint256 available = _getAssignableAmount(account);
if (available > 0) {
TokenCollateralized storage data = tokenData[account];
uint256 valueToTransfer;
if (available < tokenAmount) {
valueToTransfer = available;
tokenAmount = tokenAmount.sub(available);
} else {
valueToTransfer = tokenAmount;
tokenAmount = 0;
}
(address exerciseAsset, uint256 amount) = getBaseExerciseData(valueToTransfer);
// To guarantee that the minter will be paid.
amount = amount.add(1);
data.amount = data.amount.sub(valueToTransfer);
if (_isEther(exerciseAsset)) {
payable(account).transfer(amount);
} else {
_transferERC20(exerciseAsset, account, amount);
}
emit Assigned(account, exerciseAccount, amount, valueToTransfer);
}
return tokenAmount;
}
/**
* @dev Internal function to validate the exercise operation and burn the respective tokens.
* @param account Address of the account that is exercising.
* @param tokenAmount Amount of tokens.
* @param maximumNumberOfAccounts The maximum number of accounts that can be exercised.
*/
function _validateAndBurn(address account, uint256 tokenAmount, uint256 maximumNumberOfAccounts) notExpired internal {
require(tokenAmount > 0, "ACOToken::_validateAndBurn: Invalid token amount");
// Whether an account has deposited collateral it only can exercise the extra amount of unassignable tokens.
if (_accountHasCollateral(account)) {
require(tokenAmount <= balanceOf(account).sub(tokenData[account].amount), "ACOToken::_validateAndBurn: Token amount not available");
}
_callBurn(account, tokenAmount);
(address exerciseAsset, uint256 expectedAmount) = getBaseExerciseData(tokenAmount);
expectedAmount = expectedAmount.add(maximumNumberOfAccounts);
if (_isEther(exerciseAsset)) {
require(msg.value == expectedAmount, "ACOToken::_validateAndBurn: Invalid ether amount");
} else {
require(msg.value == 0, "ACOToken::_validateAndBurn: No ether expected");
_transferFromERC20(exerciseAsset, msg.sender, address(this), expectedAmount);
}
}
/**
* @dev Internal function to calculate the token strike price relation.
* @param tokenAmount Amount of tokens.
* @return Calculated value with strike asset precision.
*/
function _getTokenStrikePriceRelation(uint256 tokenAmount) internal view returns(uint256) {
return tokenAmount.mul(strikePrice).div(underlyingPrecision);
}
/**
* @dev Internal function to get the collateral sent back from an account.
* Function to be called when the token IS expired.
* @param account Address of the account.
* @return The amount of collateral transferred.
*/
function _redeem(address account) nonReentrant internal returns(uint256) {
require(!_notExpired(), "ACOToken::_redeem: Token not expired yet");
uint256 collateralAmount = _redeemCollateral(account, tokenData[account].amount);
super._burnAction(account, balanceOf(account));
return collateralAmount;
}
/**
* @dev Internal function to burn tokens from an account and get the collateral, not assigned, back.
* @param account Address of the account.
* @param tokenAmount Amount of tokens to be burned.
* @return The amount of collateral transferred.
*/
function _burn(address account, uint256 tokenAmount) nonReentrant notExpired internal returns(uint256) {
uint256 collateralAmount = _redeemCollateral(account, tokenAmount);
_callBurn(account, tokenAmount);
return collateralAmount;
}
/**
* @dev Internal function to burn tokens.
* @param account Address of the account.
* @param tokenAmount Amount of tokens to be burned.
*/
function _callBurn(address account, uint256 tokenAmount) internal {
if (account == msg.sender) {
super._burnAction(account, tokenAmount);
} else {
super._burnFrom(account, tokenAmount);
}
}
/**
* @dev Internal function to get the amount of assignable token from an account.
* @param account Address of the account.
* @return The assignable amount of tokens.
*/
function _getAssignableAmount(address account) internal view returns(uint256) {
if (tokenData[account].amount > balanceOf(account)) {
return tokenData[account].amount.sub(balanceOf(account));
} else {
return 0;
}
}
/**
* @dev Internal function to remove the token data with collateral if its total amount was assigned.
* @param account Address of account.
*/
function _removeCollateralDataIfNecessary(address account) internal {
TokenCollateralized storage data = tokenData[account];
if (!_hasCollateral(data)) {
uint256 lastIndex = _collateralOwners.length - 1;
if (lastIndex != data.index) {
address last = _collateralOwners[lastIndex];
tokenData[last].index = data.index;
_collateralOwners[data.index] = last;
}
_collateralOwners.pop();
delete tokenData[account];
}
}
/**
* @dev Internal function to get if the token is not expired.
* @return Whether the token is NOT expired.
*/
function _notExpired() internal view returns(bool) {
return now < expiryTime;
}
/**
* @dev Internal function to get if an account has collateral deposited.
* @param account Address of the account.
* @return Whether the account has collateral deposited.
*/
function _accountHasCollateral(address account) internal view returns(bool) {
return _hasCollateral(tokenData[account]);
}
/**
* @dev Internal function to get if an account has collateral deposited.
* @param data Token data from an account.
* @return Whether the account has collateral deposited.
*/
function _hasCollateral(TokenCollateralized storage data) internal view returns(bool) {
return data.amount > 0;
}
/**
* @dev Internal function to get if the address is for Ethereum (0x0).
* @param _address Address to be checked.
* @return Whether the address is for Ethereum.
*/
function _isEther(address _address) internal pure returns(bool) {
return _address == address(0);
}
/**
* @dev Internal function to get the token name.
* The token name is assembled with the token data:
* ACO UNDERLYING_SYMBOL-STRIKE_PRICE_STRIKE_ASSET_SYMBOL-TYPE-EXPIRYTIME
* @return The token name.
*/
function _name() internal view returns(string memory) {
return string(abi.encodePacked(
"ACO ",
underlyingSymbol,
"-",
ACONameFormatter.formatNumber(strikePrice, strikeAssetDecimals),
strikeAssetSymbol,
"-",
ACONameFormatter.formatType(isCall),
"-",
ACONameFormatter.formatTime(expiryTime)
));
}
/**
* @dev Internal function to the asset decimals.
* @param asset Address of the asset.
* @return The asset decimals.
*/
function _getAssetDecimals(address asset) internal view returns(uint8) {
if (_isEther(asset)) {
return uint8(18);
} else {
(bool success, bytes memory returndata) = asset.staticcall(abi.encodeWithSignature("decimals()"));
require(success, "ACOToken::_getAssetDecimals: Invalid asset decimals");
return abi.decode(returndata, (uint8));
}
}
/**
* @dev Internal function to the asset symbol.
* @param asset Address of the asset.
* @return The asset symbol.
*/
function _getAssetSymbol(address asset) internal view returns(string memory) {
if (_isEther(asset)) {
return "ETH";
} else {
(bool success, bytes memory returndata) = asset.staticcall(abi.encodeWithSignature("symbol()"));
require(success, "ACOToken::_getAssetSymbol: Invalid asset symbol");
return abi.decode(returndata, (string));
}
}
/**
* @dev Internal function to transfer ERC20 tokens.
* @param token Address of the token.
* @param recipient Address of the transfer destination.
* @param amount Amount to transfer.
*/
function _transferERC20(address token, address recipient, uint256 amount) internal {
(bool success, bytes memory returndata) = token.call(abi.encodeWithSelector(_transferSelector, recipient, amount));
require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "ACOToken::_transferERC20");
}
/**
* @dev Internal function to call transferFrom on ERC20 tokens.
* @param token Address of the token.
* @param sender Address of the sender.
* @param recipient Address of the transfer destination.
* @param amount Amount to transfer.
*/
function _transferFromERC20(address token, address sender, address recipient, uint256 amount) internal {
(bool success, bytes memory returndata) = token.call(abi.encodeWithSelector(_transferFromSelector, sender, recipient, amount));
require(success && (returndata.length == 0 || abi.decode(returndata, (bool))), "ACOToken::_transferFromERC20");
}
}