This repository has been archived by the owner on Nov 19, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathERC1155Action.sol
477 lines (417 loc) · 20.8 KB
/
ERC1155Action.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
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity =0.7.6;
pragma abicoder v2;
import {
AccountContext,
PortfolioAsset
} from '../../global/Types.sol';
import {StorageLayoutV1} from "../../global/StorageLayoutV1.sol";
import {Constants} from "../../global/Constants.sol";
import {SafeInt256} from "../../math/SafeInt256.sol";
import {Emitter} from "../../internal/Emitter.sol";
import {AccountContextHandler} from "../../internal/AccountContextHandler.sol";
import {DateTime} from "../../internal/markets/DateTime.sol";
import {CashGroup} from "../../internal/markets/CashGroup.sol";
import {TransferAssets} from "../../internal/portfolio/TransferAssets.sol";
import {PortfolioHandler} from "../../internal/portfolio/PortfolioHandler.sol";
import {BitmapAssetsHandler} from "../../internal/portfolio/BitmapAssetsHandler.sol";
import {FreeCollateralExternal} from "../FreeCollateralExternal.sol";
import {SettleAssetsExternal} from "../SettleAssetsExternal.sol";
import {ActionGuards} from "./ActionGuards.sol";
import {NotionalProxy} from "../../../interfaces/notional/NotionalProxy.sol";
import {IERC1155TokenReceiver} from "../../../interfaces/IERC1155TokenReceiver.sol";
import {nERC1155Interface} from "../../../interfaces/notional/nERC1155Interface.sol";
import {IVaultAccountHealth} from "../../../interfaces/notional/IVaultController.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "@openzeppelin/contracts/utils/Address.sol";
contract ERC1155Action is nERC1155Interface, ActionGuards {
using SafeInt256 for int256;
using AccountContextHandler for AccountContext;
bytes4 internal constant ERC1155_ACCEPTED = bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"));
bytes4 internal constant ERC1155_BATCH_ACCEPTED = bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"));
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IERC1155).interfaceId;
}
/// @notice Returns the balance of an ERC1155 id on an account.
/// @param account account to get the id for
/// @param id the ERC1155 id
/// @return Balance of the ERC1155 id as an unsigned integer (negative fCash balances return zero)
function balanceOf(address account, uint256 id) public view override returns (uint256) {
int256 notional = signedBalanceOf(account, id);
return notional < 0 ? 0 : notional.toUint();
}
/// @notice Returns the balance of an ERC1155 id on an account.
/// @param account account to get the id for
/// @param id the ERC1155 id
/// @return notional balance of the ERC1155 id as a signed integer
function signedBalanceOf(address account, uint256 id) public view override returns (int256 notional) {
if (Emitter.isfCash(id)) {
(uint16 currencyId, uint256 maturity, bool isfCashDebt) = Emitter.decodefCashId(id);
AccountContext memory accountContext = AccountContextHandler.getAccountContext(account);
if (accountContext.isBitmapEnabled()) {
notional = _balanceInBitmap(account, accountContext.bitmapCurrencyId, currencyId, maturity);
} else {
notional = _balanceInArray(
PortfolioHandler.getSortedPortfolio(account, accountContext.assetArrayLength),
currencyId,
maturity
);
}
// If asking for the fCash debt id, then return the positive amount or zero if it is not debt
if (isfCashDebt) return notional < 0 ? notional.neg() : 0;
return notional;
} else {
// In this case, the id is referencing a vault asset and we make a call back to retrieve the relevant
// data. This is pretty inefficient for on chain calls but will work fine for off chain calls
IVaultAccountHealth(address(this)).signedBalanceOfVaultTokenId(account, id);
}
}
/// @notice Returns the balance of a batch of accounts and ids.
/// @param accounts array of accounts to get balances for
/// @param ids array of ids to get balances for
/// @return Returns an array of signed balances
function signedBalanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
external
view
override
returns (int256[] memory)
{
require(accounts.length == ids.length);
int256[] memory amounts = new int256[](accounts.length);
for (uint256 i; i < accounts.length; i++) {
// This is pretty inefficient but gets the job done
amounts[i] = signedBalanceOf(accounts[i], ids[i]);
}
return amounts;
}
/// @notice Returns the balance of a batch of accounts and ids. WARNING: negative fCash balances are represented
/// as zero balances in the array.
/// @param accounts array of accounts to get balances for
/// @param ids array of ids to get balances for
/// @return Returns an array of unsigned balances
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
external
view
override
returns (uint256[] memory)
{
require(accounts.length == ids.length);
uint256[] memory amounts = new uint256[](accounts.length);
for (uint256 i; i < accounts.length; i++) {
// This is pretty inefficient but gets the job done
amounts[i] = balanceOf(accounts[i], ids[i]);
}
return amounts;
}
/// @dev Returns the balance from a bitmap given the id
function _balanceInBitmap(
address account,
uint256 bitmapCurrencyId,
uint16 currencyId,
uint256 maturity
) internal view returns (int256) {
if (currencyId == 0 || currencyId != bitmapCurrencyId) {
return 0;
} else {
return BitmapAssetsHandler.getifCashNotional(account, currencyId, maturity);
}
}
/// @dev Searches an array for the matching asset
function _balanceInArray(
PortfolioAsset[] memory portfolio, uint16 currencyId, uint256 maturity
) internal pure returns (int256) {
for (uint256 i; i < portfolio.length; i++) {
PortfolioAsset memory asset = portfolio[i];
if (
asset.currencyId == currencyId &&
asset.maturity == maturity &&
asset.assetType == Constants.FCASH_ASSET_TYPE
) return asset.notional;
}
return 0;
}
/// @notice Transfer of a single fCash or liquidity token asset between accounts. Allows `from` account to transfer more fCash
/// than they have as long as they pass a subsequent free collateral check. This enables OTC trading of fCash assets.
/// @param from account to transfer from
/// @param to account to transfer to
/// @param id ERC1155 id of the asset
/// @param amount amount to transfer
/// @param data arbitrary data passed to ERC1155Receiver (if contract) and if properly specified can be used to initiate
/// a trading action on Notional for the `from` address
/// @dev emit:TransferSingle, emit:AccountContextUpdate, emit:AccountSettled
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes calldata data
) external payable override {
// NOTE: there is no re-entrancy guard on this method because that would prevent a callback in
// _checkPostTransferEvent. The external call to the receiver is done at the very end after all stateful
// updates have occurred.
_validateAccounts(from, to);
// When amount is set to zero this method can be used as a way to execute trades via a transfer operator
AccountContext memory fromContext;
if (amount > 0) {
PortfolioAsset[] memory assets = new PortfolioAsset[](1);
PortfolioAsset memory asset = assets[0];
// Only Positive fCash is supported in ERC1155 transfers
_decodeTofCashAsset(id, asset);
// This ensures that asset.notional is always a positive amount
asset.notional = SafeInt256.toInt(amount);
_requireValidMaturity(asset.currencyId, asset.maturity, block.timestamp);
// prettier-ignore
(fromContext, /* toContext */) = _transfer(from, to, assets);
emit TransferSingle(msg.sender, from, to, id, amount);
} else {
fromContext = AccountContextHandler.getAccountContext(from);
}
// toContext is always empty here because we cannot have bidirectional transfers in `safeTransferFrom`
AccountContext memory toContext;
_checkPostTransferEvent(from, to, fromContext, toContext, data, false);
// Do this external call at the end to prevent re-entrancy
if (Address.isContract(to)) {
require(
IERC1155TokenReceiver(to).onERC1155Received(msg.sender, from, id, amount, data) ==
ERC1155_ACCEPTED,
"Not accepted"
);
}
}
/// @notice Transfer of a batch of fCash or liquidity token assets between accounts. Allows `from` account to transfer more fCash
/// than they have as long as they pass a subsequent free collateral check. This enables OTC trading of fCash assets.
/// @param from account to transfer from
/// @param to account to transfer to
/// @param ids ERC1155 ids of the assets
/// @param amounts amounts to transfer
/// @param data arbitrary data passed to ERC1155Receiver (if contract) and if properly specified can be used to initiate
/// a trading action on Notional for the `from` address
/// @dev emit:TransferBatch, emit:AccountContextUpdate, emit:AccountSettled
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata amounts,
bytes calldata data
) external payable override {
// NOTE: there is no re-entrancy guard on this method because that would prevent a callback in
// _checkPostTransferEvent. The external call to the receiver is done at the very end.
_validateAccounts(from, to);
(PortfolioAsset[] memory assets, bool toTransferNegative) = _decodeToAssets(ids, amounts);
// When doing a bidirectional transfer must ensure that the `to` account has given approval
// to msg.sender as well.
if (toTransferNegative) require(isApprovedForAll(to, msg.sender), "Unauthorized");
(AccountContext memory fromContext, AccountContext memory toContext) = _transfer(
from,
to,
assets
);
_checkPostTransferEvent(from, to, fromContext, toContext, data, toTransferNegative);
emit TransferBatch(msg.sender, from, to, ids, amounts);
// Do this at the end to prevent re-entrancy
if (Address.isContract(to)) {
require(
IERC1155TokenReceiver(to).onERC1155BatchReceived(
msg.sender,
from,
ids,
amounts,
data
) == ERC1155_BATCH_ACCEPTED,
"Not accepted"
);
}
}
/// @dev Validates accounts on transfer
function _validateAccounts(address from, address to) private view {
// Cannot transfer to self, cannot transfer to zero address
require(from != to && to != address(0) && to != address(this), "Invalid address");
// Authentication is valid
require(msg.sender == from || isApprovedForAll(from, msg.sender), "Unauthorized");
// nTokens will not accept transfers because they do not implement the ERC1155
// receive method
// Defensive check to ensure that an authorized operator does not call these methods
// with an invalid `from` account
requireValidAccount(from);
}
/// @notice Decodes ids and amounts to PortfolioAsset objects
/// @param ids array of ERC1155 ids
/// @param amounts amounts to transfer
/// @return array of portfolio asset objects
function decodeToAssets(uint256[] calldata ids, uint256[] calldata amounts)
external
view
override
returns (PortfolioAsset[] memory)
{
// prettier-ignore
(PortfolioAsset[] memory assets, /* */) = _decodeToAssets(ids, amounts);
return assets;
}
function _decodeTofCashAsset(uint256 id, PortfolioAsset memory asset) private pure {
require(Emitter.isfCash(id), "Only fCash Transfer");
bool isfCashDebt;
(asset.currencyId, asset.maturity, isfCashDebt) = Emitter.decodefCashId(id);
// Technically debt is transferrable inside this method, but for clarity and backwards compatibility
// this restriction is applied here.
require(!isfCashDebt, "No Debt Transfer");
asset.assetType = Constants.FCASH_ASSET_TYPE;
}
function _decodeToAssets(uint256[] calldata ids, uint256[] calldata amounts)
private
view
returns (PortfolioAsset[] memory, bool)
{
require(ids.length == amounts.length);
bool toTransferNegative = false;
PortfolioAsset[] memory assets = new PortfolioAsset[](ids.length);
for (uint256 i; i < ids.length; i++) {
// Require that ids are not duplicated, there is no valid reason to have duplicate ids
if (i > 0) require(ids[i] > ids[i - 1], "IDs must be sorted");
PortfolioAsset memory asset = assets[i];
_decodeTofCashAsset(ids[i], assets[i]);
_requireValidMaturity(asset.currencyId, asset.maturity, block.timestamp);
// Although amounts is encoded as uint256 we allow it to be negative here. This will
// allow for bidirectional transfers of fCash. Internally fCash assets are always stored
// as int128 (for bitmap portfolio) or int88 (for array portfolio) so there is no potential
// that a uint256 value that is greater than type(int256).max would actually valid.
asset.notional = int256(amounts[i]);
// If there is a negative transfer we mark it as such, this will force us to do a free collateral
// check on the `to` address as well.
if (asset.notional < 0) toTransferNegative = true;
}
return (assets, toTransferNegative);
}
/// @notice Encodes parameters into an ERC1155 id, this method always returns an fCash id
/// @param currencyId currency id of the asset
/// @param maturity timestamp of the maturity
/// @return ERC1155 id
function encodeToId(
uint16 currencyId,
uint40 maturity,
uint8 /* assetType */
) external pure override returns (uint256) {
return Emitter.encodefCashId(currencyId, maturity, 0);
}
/// @dev Ensures that all maturities specified are valid for the currency id (i.e. they do not
/// go past the max maturity date)
function _requireValidMaturity(
uint256 currencyId,
uint256 maturity,
uint256 blockTime
) private view {
require(
DateTime.isValidMaturity(CashGroup.getMaxMarketIndex(currencyId), maturity, blockTime),
"Invalid maturity"
);
}
/// @dev Internal asset transfer event between accounts
function _transfer(
address from,
address to,
PortfolioAsset[] memory assets
) internal returns (AccountContext memory, AccountContext memory) {
AccountContext memory toContext = AccountContextHandler.getAccountContext(to);
AccountContext memory fromContext = AccountContextHandler.getAccountContext(from);
// NOTE: context returned are in different memory locations
(fromContext, toContext) = SettleAssetsExternal.transferAssets(
from, to, fromContext, toContext, assets
);
fromContext.setAccountContext(from);
toContext.setAccountContext(to);
return (fromContext, toContext);
}
/// @dev Checks post transfer events which will either be initiating one of the batch trading events or a free collateral
/// check if required.
function _checkPostTransferEvent(
address from,
address to,
AccountContext memory fromContext,
AccountContext memory toContext,
bytes calldata data,
bool toTransferNegative
) internal {
bytes4 sig = 0;
address transactedAccount = address(0);
if (data.length >= 32) {
// Method signature is not abi encoded so decode to bytes32 first and take the first 4 bytes. This works
// because all the methods we want to call below require more than 32 bytes in the calldata
bytes32 tmp = abi.decode(data, (bytes32));
sig = bytes4(tmp);
}
// These are the only four methods allowed to occur in a post transfer event. These actions allow `from`
// accounts to take any sort of trading action as a result of their transfer. All of these actions will
// handle checking free collateral so no additional check is necessary here.
if (
sig == NotionalProxy.nTokenRedeem.selector ||
sig == NotionalProxy.batchLend.selector ||
sig == NotionalProxy.batchBalanceAction.selector ||
sig == NotionalProxy.batchBalanceAndTradeAction.selector
) {
transactedAccount = abi.decode(data[4:36], (address));
// Ensure that the "transactedAccount" parameter of the call is set to the from address or the
// to address. If it is the "to" address then ensure that the msg.sender has approval to
// execute operations
require(
transactedAccount == from ||
(transactedAccount == to && isApprovedForAll(to, msg.sender)),
"Unauthorized call"
);
// We can only call back to Notional itself at this point, account context is already
// stored and all three of the whitelisted methods above will check free collateral.
(bool status, bytes memory result) = address(this).call{value: msg.value}(data);
require(status, _getRevertMsg(result));
}
// The transacted account will have its free collateral checked above so there is
// no need to recheck here.
// If transactedAccount == 0 then will check fc
// If transactedAccount == to then will check fc
// If transactedAccount == from then will skip, prefer call above
if (transactedAccount != from && fromContext.hasDebt != 0x00) {
FreeCollateralExternal.checkFreeCollateralAndRevert(from);
}
// Check free collateral if the `to` account has taken on a negative fCash amount
// If toTransferNegative is false then will not check
// If transactedAccount == 0 then will check fc
// If transactedAccount == from then will check fc
// If transactedAccount == to then will skip, prefer call above
if (toTransferNegative && transactedAccount != to && toContext.hasDebt != 0x00) {
FreeCollateralExternal.checkFreeCollateralAndRevert(to);
}
}
function _getRevertMsg(bytes memory _returnData) internal pure returns (string memory) {
// If the _res length is less than 68, then the transaction failed silently (without a revert message)
if (_returnData.length < 68) return "Transaction reverted silently";
assembly {
// Slice the sighash.
_returnData := add(_returnData, 0x04)
}
return abi.decode(_returnData, (string)); // All that remains is the revert string
}
/// @notice Allows an account to set approval for an operator
/// @param operator address of the operator
/// @param approved state of the approval
/// @dev emit:ApprovalForAll
function setApprovalForAll(address operator, bool approved) external override {
accountAuthorizedTransferOperator[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
/// @notice Checks approval state for an account, will first check if global transfer operator is enabled
/// before falling through to an account specific transfer operator.
/// @param account address of the account
/// @param operator address of the operator
/// @return true for approved
function isApprovedForAll(address account, address operator)
public
view
override
returns (bool)
{
if (globalTransferOperator[operator]) return true;
return accountAuthorizedTransferOperator[account][operator];
}
/// @notice Get a list of deployed library addresses (sorted by library name)
function getLibInfo() external pure returns (address, address) {
return (address(FreeCollateralExternal), address(SettleAssetsExternal));
}
}