-
Notifications
You must be signed in to change notification settings - Fork 51
/
Copy pathNttManager.sol
601 lines (504 loc) · 21.5 KB
/
NttManager.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
// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "wormhole-solidity-sdk/Utils.sol";
import "wormhole-solidity-sdk/libraries/BytesParsing.sol";
import "../libraries/RateLimiter.sol";
import "../interfaces/INttManager.sol";
import "../interfaces/INttToken.sol";
import "../interfaces/ITransceiver.sol";
import {ManagerBase} from "./ManagerBase.sol";
/// @title NttManager
/// @author Wormhole Project Contributors.
/// @notice The NttManager contract is responsible for managing the token
/// and associated transceivers.
///
/// @dev Each NttManager contract is associated with a single token but
/// can be responsible for multiple transceivers.
///
/// @dev When transferring tokens, the NttManager contract will either
/// lock the tokens or burn them, depending on the mode.
///
/// @dev To initiate a transfer, the user calls the transfer function with:
/// - the amount
/// - the recipient chain
/// - the recipient address
/// - the refund address: the address to which refunds are issued for any unused gas
/// for attestations on a given transfer. If the gas limit is configured
/// to be too high, users will be refunded the difference.
/// - (optional) a flag to indicate whether the transfer should be queued
/// if the rate limit is exceeded
contract NttManager is INttManager, RateLimiter, ManagerBase {
using BytesParsing for bytes;
using SafeERC20 for IERC20;
using TrimmedAmountLib for uint256;
using TrimmedAmountLib for TrimmedAmount;
string public constant NTT_MANAGER_VERSION = "0.1.0";
// =============== Setup =================================================================
constructor(
address _token,
Mode _mode,
uint16 _chainId,
uint64 _rateLimitDuration,
bool _skipRateLimiting
) RateLimiter(_rateLimitDuration, _skipRateLimiting) ManagerBase(_token, _mode, _chainId) {}
function __NttManager_init() internal onlyInitializing {
// check if the owner is the deployer of this contract
if (msg.sender != deployer) {
revert UnexpectedDeployer(deployer, msg.sender);
}
if (msg.value != 0) {
revert UnexpectedMsgValue();
}
__PausedOwnable_init(msg.sender, msg.sender);
__ReentrancyGuard_init();
_setOutboundLimit(TrimmedAmountLib.max(tokenDecimals()));
_getUnilateralPauseStorage().inbound = true;
_getUnilateralPauseStorage().outbound = true;
}
function _initialize() internal virtual override {
__NttManager_init();
_checkThresholdInvariants();
_checkTransceiversInvariants();
}
// =============== Storage ==============================================================
bytes32 private constant PEERS_SLOT = bytes32(uint256(keccak256("ntt.peers")) - 1);
// =============== Storage Getters/Setters ==============================================
function _getPeersStorage()
internal
pure
returns (mapping(uint16 => NttManagerPeer) storage $)
{
uint256 slot = uint256(PEERS_SLOT);
assembly ("memory-safe") {
$.slot := slot
}
}
// =============== Public Getters ========================================================
/// @inheritdoc INttManager
function getPeer(uint16 chainId_) external view returns (NttManagerPeer memory) {
return _getPeersStorage()[chainId_];
}
// =============== Admin ==============================================================
/// @inheritdoc INttManager
function setPeer(
uint16 peerChainId,
bytes32 peerContract,
uint8 decimals,
uint256 inboundLimit
) public onlyOwner outboundWhenPaused {
if (peerChainId == 0) {
revert InvalidPeerChainIdZero();
}
if (peerContract == bytes32(0)) {
revert InvalidPeerZeroAddress();
}
if (decimals == 0) {
revert InvalidPeerDecimals();
}
if (peerChainId == chainId) {
revert InvalidPeerSameChainId();
}
NttManagerPeer memory oldPeer = _getPeersStorage()[peerChainId];
_getPeersStorage()[peerChainId].peerAddress = peerContract;
_getPeersStorage()[peerChainId].tokenDecimals = decimals;
uint8 toDecimals = tokenDecimals();
_setInboundLimit(inboundLimit.trim(toDecimals, toDecimals), peerChainId);
emit PeerUpdated(
peerChainId, oldPeer.peerAddress, oldPeer.tokenDecimals, peerContract, decimals
);
}
/// @inheritdoc INttManager
function setOutboundLimit(uint256 limit) external onlyOwner {
uint8 toDecimals = tokenDecimals();
_setOutboundLimit(limit.trim(toDecimals, toDecimals));
}
/// @inheritdoc INttManager
function setInboundLimit(uint256 limit, uint16 chainId_) external onlyOwner {
uint8 toDecimals = tokenDecimals();
_setInboundLimit(limit.trim(toDecimals, toDecimals), chainId_);
}
/// ============== Invariants =============================================
/// @dev When we add new immutables, this function should be updated
function _checkImmutables() internal view override {
super._checkImmutables();
assert(this.rateLimitDuration() == rateLimitDuration);
}
// ==================== External Interface ===============================================
/// @inheritdoc INttManager
function transfer(
uint256 amount,
uint16 recipientChain,
bytes32 recipient
) external payable nonReentrant whenNotPaused outboundNotPaused returns (uint64) {
return
_transferEntryPoint(amount, recipientChain, recipient, recipient, false, new bytes(1));
}
/// @inheritdoc INttManager
function transfer(
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
bytes32 refundAddress,
bool shouldQueue,
bytes memory transceiverInstructions
) external payable nonReentrant whenNotPaused outboundNotPaused returns (uint64) {
return _transferEntryPoint(
amount, recipientChain, recipient, refundAddress, shouldQueue, transceiverInstructions
);
}
/// @inheritdoc INttManager
function attestationReceived(
uint16 sourceChainId,
bytes32 sourceNttManagerAddress,
TransceiverStructs.NttManagerMessage memory payload
) external onlyTransceiver whenNotPaused inboundNotPaused {
_verifyPeer(sourceChainId, sourceNttManagerAddress);
// Compute manager message digest and record transceiver attestation.
bytes32 nttManagerMessageHash = _recordTransceiverAttestation(sourceChainId, payload);
if (isMessageApproved(nttManagerMessageHash)) {
executeMsg(sourceChainId, sourceNttManagerAddress, payload);
}
}
/// @inheritdoc INttManager
function executeMsg(
uint16 sourceChainId,
bytes32 sourceNttManagerAddress,
TransceiverStructs.NttManagerMessage memory message
) public whenNotPaused inboundNotPaused {
(bytes32 digest, bool alreadyExecuted) =
_isMessageExecuted(sourceChainId, sourceNttManagerAddress, message);
if (alreadyExecuted) {
return;
}
TransceiverStructs.NativeTokenTransfer memory nativeTokenTransfer =
TransceiverStructs.parseNativeTokenTransfer(message.payload);
// verify that the destination chain is valid
if (nativeTokenTransfer.toChain != chainId) {
revert InvalidTargetChain(nativeTokenTransfer.toChain, chainId);
}
uint8 toDecimals = tokenDecimals();
TrimmedAmount nativeTransferAmount =
(nativeTokenTransfer.amount.untrim(toDecimals)).trim(toDecimals, toDecimals);
address transferRecipient = fromWormholeFormat(nativeTokenTransfer.to);
{
// Check inbound rate limits
bool isRateLimited = _isInboundAmountRateLimited(nativeTransferAmount, sourceChainId);
if (isRateLimited) {
// queue up the transfer
_enqueueInboundTransfer(digest, nativeTransferAmount, transferRecipient);
// end execution early
return;
}
}
// consume the amount for the inbound rate limit
_consumeInboundAmount(nativeTransferAmount, sourceChainId);
// When receiving a transfer, we refill the outbound rate limit
// by the same amount (we call this "backflow")
_backfillOutboundAmount(nativeTransferAmount);
_mintOrUnlockToRecipient(digest, transferRecipient, nativeTransferAmount, false);
}
/// @inheritdoc INttManager
function completeInboundQueuedTransfer(bytes32 digest)
external
nonReentrant
whenNotPaused
inboundNotPaused
{
// find the message in the queue
InboundQueuedTransfer memory queuedTransfer = getInboundQueuedTransfer(digest);
if (queuedTransfer.txTimestamp == 0) {
revert InboundQueuedTransferNotFound(digest);
}
// check that > RATE_LIMIT_DURATION has elapsed
if (block.timestamp - queuedTransfer.txTimestamp < rateLimitDuration) {
revert InboundQueuedTransferStillQueued(digest, queuedTransfer.txTimestamp);
}
// remove transfer from the queue
delete _getInboundQueueStorage()[digest];
// run it through the mint/unlock logic
_mintOrUnlockToRecipient(digest, queuedTransfer.recipient, queuedTransfer.amount, false);
}
/// @inheritdoc INttManager
function completeOutboundQueuedTransfer(uint64 messageSequence)
external
payable
nonReentrant
whenNotPaused
outboundNotPaused
returns (uint64)
{
// find the message in the queue
OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence];
if (queuedTransfer.txTimestamp == 0) {
revert OutboundQueuedTransferNotFound(messageSequence);
}
// check that > RATE_LIMIT_DURATION has elapsed
if (block.timestamp - queuedTransfer.txTimestamp < rateLimitDuration) {
revert OutboundQueuedTransferStillQueued(messageSequence, queuedTransfer.txTimestamp);
}
// remove transfer from the queue
delete _getOutboundQueueStorage()[messageSequence];
// run it through the transfer logic and skip the rate limit
return _transfer(
messageSequence,
queuedTransfer.amount,
queuedTransfer.recipientChain,
queuedTransfer.recipient,
queuedTransfer.refundAddress,
queuedTransfer.sender,
queuedTransfer.transceiverInstructions
);
}
/// @inheritdoc INttManager
function cancelOutboundQueuedTransfer(uint64 messageSequence)
external
nonReentrant
whenNotPaused
{
// find the message in the queue
OutboundQueuedTransfer memory queuedTransfer = _getOutboundQueueStorage()[messageSequence];
if (queuedTransfer.txTimestamp == 0) {
revert OutboundQueuedTransferNotFound(messageSequence);
}
// check msg.sender initiated the transfer
if (queuedTransfer.sender != msg.sender) {
revert CancellerNotSender(msg.sender, queuedTransfer.sender);
}
// remove transfer from the queue
delete _getOutboundQueueStorage()[messageSequence];
// return the queued funds to the sender
_mintOrUnlockToRecipient(
bytes32(uint256(messageSequence)), msg.sender, queuedTransfer.amount, true
);
}
// ==================== Internal Business Logic =========================================
function _transferEntryPoint(
uint256 amount,
uint16 recipientChain,
bytes32 recipient,
bytes32 refundAddress,
bool shouldQueue,
bytes memory transceiverInstructions
) internal returns (uint64) {
if (amount == 0) {
revert ZeroAmount();
}
if (recipient == bytes32(0)) {
revert InvalidRecipient();
}
if (refundAddress == bytes32(0)) {
revert InvalidRefundAddress();
}
{
// Lock/burn tokens before checking rate limits
// use transferFrom to pull tokens from the user and lock them
// query own token balance before transfer
uint256 balanceBefore = _getTokenBalanceOf(token, address(this));
// transfer tokens
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
// query own token balance after transfer
uint256 balanceAfter = _getTokenBalanceOf(token, address(this));
// correct amount for potential transfer fees
amount = balanceAfter - balanceBefore;
if (mode == Mode.BURNING) {
{
// NOTE: We don't account for burn fees in this code path.
// We verify that the user's change in balance is equal to the amount that's burned.
// Accounting for burn fees can be non-trivial, since there
// is no standard way to account for the fee if the fee amount
// is taken out of the burn amount.
// For example, if there's a fee of 1 which is taken out of the
// amount, then burning 20 tokens would result in a transfer of only 19 tokens.
// However, the difference in the user's balance would only show 20.
// Since there is no standard way to query for burn fee amounts with burnable tokens,
// and NTT would be used on a per-token basis, implementing this functionality
// is left to integrating projects who may need to account for burn fees on their tokens.
ERC20Burnable(token).burn(amount);
// tokens held by the contract after the operation should be the same as before
uint256 balanceAfterBurn = _getTokenBalanceOf(token, address(this));
if (balanceBefore != balanceAfterBurn) {
revert BurnAmountDifferentThanBalanceDiff(balanceBefore, balanceAfterBurn);
}
}
}
}
// trim amount after burning to ensure transfer amount matches (amount - fee)
TrimmedAmount trimmedAmount = _trimTransferAmount(amount, recipientChain);
TrimmedAmount internalAmount = trimmedAmount.shift(tokenDecimals());
// get the sequence for this transfer
uint64 sequence = _useMessageSequence();
{
// now check rate limits
bool isAmountRateLimited = _isOutboundAmountRateLimited(internalAmount);
if (!shouldQueue && isAmountRateLimited) {
revert NotEnoughCapacity(getCurrentOutboundCapacity(), amount);
}
if (shouldQueue && isAmountRateLimited) {
// verify chain has not forked
checkFork(evmChainId);
// emit an event to notify the user that the transfer is rate limited
emit OutboundTransferRateLimited(
msg.sender, sequence, amount, getCurrentOutboundCapacity()
);
// queue up and return
_enqueueOutboundTransfer(
sequence,
trimmedAmount,
recipientChain,
recipient,
refundAddress,
msg.sender,
transceiverInstructions
);
// refund price quote back to sender
_refundToSender(msg.value);
// return the sequence in the queue
return sequence;
}
}
// otherwise, consume the outbound amount
_consumeOutboundAmount(internalAmount);
// When sending a transfer, we refill the inbound rate limit for
// that chain by the same amount (we call this "backflow")
_backfillInboundAmount(internalAmount, recipientChain);
return _transfer(
sequence,
trimmedAmount,
recipientChain,
recipient,
refundAddress,
msg.sender,
transceiverInstructions
);
}
function _transfer(
uint64 sequence,
TrimmedAmount amount,
uint16 recipientChain,
bytes32 recipient,
bytes32 refundAddress,
address sender,
bytes memory transceiverInstructions
) internal returns (uint64 msgSequence) {
// verify chain has not forked
checkFork(evmChainId);
(
address[] memory enabledTransceivers,
TransceiverStructs.TransceiverInstruction[] memory instructions,
uint256[] memory priceQuotes,
uint256 totalPriceQuote
) = _prepareForTransfer(recipientChain, transceiverInstructions);
// push it on the stack again to avoid a stack too deep error
uint64 seq = sequence;
TransceiverStructs.NativeTokenTransfer memory ntt = TransceiverStructs.NativeTokenTransfer(
amount, toWormholeFormat(token), recipient, recipientChain
);
// construct the NttManagerMessage payload
bytes memory encodedNttManagerPayload = TransceiverStructs.encodeNttManagerMessage(
TransceiverStructs.NttManagerMessage(
bytes32(uint256(seq)),
toWormholeFormat(sender),
TransceiverStructs.encodeNativeTokenTransfer(ntt)
)
);
// push onto the stack again to avoid stack too deep error
uint16 destinationChain = recipientChain;
// send the message
_sendMessageToTransceivers(
recipientChain,
refundAddress,
_getPeersStorage()[destinationChain].peerAddress,
priceQuotes,
instructions,
enabledTransceivers,
encodedNttManagerPayload
);
// push it on the stack again to avoid a stack too deep error
TrimmedAmount amt = amount;
emit TransferSent(
recipient,
refundAddress,
amt.untrim(tokenDecimals()),
totalPriceQuote,
destinationChain,
seq
);
// return the sequence number
return seq;
}
function _mintOrUnlockToRecipient(
bytes32 digest,
address recipient,
TrimmedAmount amount,
bool cancelled
) internal {
// verify chain has not forked
checkFork(evmChainId);
// calculate proper amount of tokens to unlock/mint to recipient
// untrim the amount
uint256 untrimmedAmount = amount.untrim(tokenDecimals());
if (cancelled) {
emit OutboundTransferCancelled(uint256(digest), recipient, untrimmedAmount);
} else {
emit TransferRedeemed(digest);
}
if (mode == Mode.LOCKING) {
// unlock tokens to the specified recipient
IERC20(token).safeTransfer(recipient, untrimmedAmount);
} else if (mode == Mode.BURNING) {
// mint tokens to the specified recipient
INttToken(token).mint(recipient, untrimmedAmount);
} else {
revert InvalidMode(uint8(mode));
}
}
function tokenDecimals() public view override(INttManager, RateLimiter) returns (uint8) {
(bool success, bytes memory queriedDecimals) =
token.staticcall(abi.encodeWithSignature("decimals()"));
if (!success) {
revert StaticcallFailed();
}
return abi.decode(queriedDecimals, (uint8));
}
// ==================== Internal Helpers ===============================================
/// @dev Verify that the peer address saved for `sourceChainId` matches the `peerAddress`.
function _verifyPeer(uint16 sourceChainId, bytes32 peerAddress) internal view {
if (_getPeersStorage()[sourceChainId].peerAddress != peerAddress) {
revert InvalidPeer(sourceChainId, peerAddress);
}
}
function _trimTransferAmount(
uint256 amount,
uint16 toChain
) internal view returns (TrimmedAmount) {
uint8 toDecimals = _getPeersStorage()[toChain].tokenDecimals;
if (toDecimals == 0) {
revert InvalidPeerDecimals();
}
TrimmedAmount trimmedAmount;
{
uint8 fromDecimals = tokenDecimals();
trimmedAmount = amount.trim(fromDecimals, toDecimals);
// don't deposit dust that can not be bridged due to the decimal shift
uint256 newAmount = trimmedAmount.untrim(fromDecimals);
if (amount != newAmount) {
revert TransferAmountHasDust(amount, amount - newAmount);
}
}
return trimmedAmount;
}
function _getTokenBalanceOf(
address tokenAddr,
address accountAddr
) internal view returns (uint256) {
(bool success, bytes memory queriedBalance) =
tokenAddr.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, accountAddr));
if (!success) {
revert StaticcallFailed();
}
return abi.decode(queriedBalance, (uint256));
}
}