-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathCatalystChainInterface.sol
1002 lines (871 loc) · 51.2 KB
/
CatalystChainInterface.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
997
998
999
1000
//SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.19;
import {ERC20} from 'solmate/tokens/ERC20.sol';
import {SafeTransferLib} from 'solmate/utils/SafeTransferLib.sol';
import { IMessageEscrowStructs } from "GeneralisedIncentives/src/interfaces/IMessageEscrowStructs.sol";
import { IIncentivizedMessageEscrow } from "GeneralisedIncentives/src/interfaces/IIncentivizedMessageEscrow.sol";
import { ICatalystReceiver } from "./interfaces/IOnCatalyst.sol";
import { ICatalystV1Vault } from "./ICatalystV1Vault.sol";
import { Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import "./ICatalystV1Vault.sol";
import "./interfaces/ICatalystV1VaultState.sol"; // structs
import "./CatalystPayload.sol";
import { Bytes65 } from "GeneralisedIncentives/src/utils/Bytes65.sol";
import { ICatalystChainInterface } from "./interfaces/ICatalystChainInterface.sol";
/**
* @title Catalyst: Generalised IBC Interface
* @author Cata Labs
* @notice This contract is a generalised proof of concept
* IBC interface using an example ABI.
* It acts as an intermediate between the vault and the router to
* abstract router logic away from the vaults. This simplifies the
* development of the vaults and allows Catalyst to adopt or change
* message routers with more flexibility.
*/
contract CatalystChainInterface is ICatalystChainInterface, Ownable, Bytes65 {
using SafeTransferLib for ERC20;
//--- ERRORS ---//
// Only the message router should be able to deliver messages.
error InvalidCaller(); // 48f5c3ed
error InvalidContext(bytes1 context); // 9f769791
error InvalidAddress(); // e6c4247b
error InvalidSourceApplication(); // 003923e0
error NotEnoughIncentives(uint256 expected, uint256 actual); // 6de78246
error ChainAlreadySetup(); // b8e35614
//-- Underwriting Errors --//
error SwapAlreadyUnderwritten(); // d0c27c9f
error UnderwriteDoesNotExist(bytes32 identifier); // ae029d69
error UnderwriteNotExpired(uint256 blocksUnitilExpiry); // 62141db5
error MaxUnderwriteDurationTooLong(); // 3f6368aa
error MaxUnderwriteDurationTooShort(); // 6229dcd0
error NoVaultConnection(); // ea66ca6d
error SwapRecentlyUnderwritten(); // 695b3a94
//--- Events ---//
event SwapFailed(bytes1 error);
event RemoteImplementationSet(bytes32 chainIdentifier, bytes remoteCCI, bytes remoteGARP);
event MaxUnderwriteDuration(uint256 newMaxUnderwriteDuration);
event MinGasFor(
bytes32 identifier,
uint48 minGas
);
//-- Underwriting Events --//
event SwapUnderwritten(
bytes32 indexed identifier,
address indexed underwriter,
uint96 expiry,
address targetVault,
address toAsset,
uint256 U,
address toAccount,
uint256 outAmount
);
event FulfillUnderwrite(
bytes32 indexed identifier
);
event ExpireUnderwrite(
bytes32 indexed identifier,
address expirer,
uint256 reward
);
//--- Structs ---//
struct UnderwritingStorage {
uint256 tokens; // 1 slot
address refundTo; // 2 slot: 20/32
uint96 expiry; // 2 slot: 32/32
}
//--- Underwrite Duration Frame Parameters ---//
// Underwriters should take on as little risk as possible. One risk associated with underwriting is
// chain haulting or timestamp manipulation / diviation. To combat this risk, time is measured in blocks.
// This implies that there needs to be some configuration if the block time changes over time or to fit it
// to various blockchains.
// The reason behind is if the underwrite duration was timestamp based, say 8 hours. If the chain haulted for
// 10 hours, then the underwriter would risk being expired and lose everything. If a chain doesn't produce any
// blocks during a hault, then it wouldn't be a risk to an underwriter.
// Generally, it is more common for there to be unpredictable block slowdowns rather than unpredictable block speedups.
/// @notice The initial underwrite duration (in blocks).
/// @dev Is 8 hours if the block time is 2 seconds, 48 hours if the block time is 12 seconds. Not a great initial value but better than nothing.
uint256 constant INITIAL_MAX_UNDERWRITE_BLOCK_DURATION = 24 hours / 6 seconds;
/// @notice The maximum configurable underwrite duration (in blocks).
/// @dev Is 9.3 days if the block time is 2 seconds, 56 days if the block time is 12 seconds.
uint256 constant MAX_UNDERWRITE_BLOCK_DURATION = 14 days / 3 seconds;
/// @notice The minimum configurable underwrite duration (in blocks).
/// @dev Is 10 minutes if the block time is 2 seconds, 1 hour if the block time is 12 seconds.
uint256 constant MIN_UNDERWRITE_BLOCK_DURATION = 1 hours / 12 seconds;
//--- Config ---//
IIncentivizedMessageEscrow public immutable GARP; // Set on deployment
//-- Underwriting Config--//
// How many blocks should there be between when an identifier can be underwritten.
uint24 constant BUFFER_BLOCKS = 4;
uint256 constant public UNDERWRITING_COLLATERAL = 35; // 3,5% extra as collateral.
uint256 constant public UNDERWRITING_COLLATERAL_DENOMINATOR = 1000;
uint256 constant public EXPIRE_CALLER_REWARD = 350; // 35% of the 3,5% = 1,225%. Of $1000 = $12,25
uint256 constant public EXPIRE_CALLER_REWARD_DENOMINATOR = 1000;
//--- Storage ---//
/// @notice The destination address on the chain by chain identifier.
mapping(bytes32 => bytes) public chainIdentifierToDestinationAddress;
/// @notice The minimum amount of gas for a specific chain. bytes32(0) indicates ack.
mapping(bytes32 => uint48) public minGasFor;
//-- Underwriting Storage --//
/// @notice Sets the maximum duration for underwriting.
/// @dev Should be set long enough for all swaps to be able to confirm + a small buffer
/// Should also be set long enough to not take up an excess amount of escrow usage.
uint256 public maxUnderwritingDuration = INITIAL_MAX_UNDERWRITE_BLOCK_DURATION;
/// @notice Maps underwriting identifiers to underwriting state.
/// refundTo can be checked to see if the ID has been underwritten.
mapping(bytes32 => UnderwritingStorage) public underwritingStorage;
constructor(address GARP_, address defaultOwner) {
require(address(GARP_) != address(0)); // dev: GARP_ cannot be zero address
GARP = IIncentivizedMessageEscrow(GARP_);
_transferOwnership(defaultOwner);
emit MaxUnderwriteDuration(INITIAL_MAX_UNDERWRITE_BLOCK_DURATION);
}
//-- Admin--//
/// @notice Allow updating of the minimum gas limit.
/// @dev Set chainIdentifier to 0 for gas for ack.
function setMinGasFor(bytes32 chainIdentifier, uint48 minGas) override external onlyOwner {
minGasFor[chainIdentifier] = minGas;
emit MinGasFor(chainIdentifier, minGas);
}
/// @notice Sets the new max underwrite duration, which is the period of time
/// before an underwrite can be expired. When an underwrite is expired, the underwriter
/// loses all capital provided.
/// @dev This function can be exploited by the owner. By setting newMaxUnderwriteDuration to (almost) 0 right before someone calls underwrite and then
/// expiring them before the actual swap arrives. The min protection here is not sufficient since it needs to be well into
/// when a message can be validated. As a result, the owner of this contract should be a timelock which underwriters monitor.
function setMaxUnderwritingDuration(uint256 newMaxUnderwriteDuration) onlyOwner override external {
if (newMaxUnderwriteDuration <= MIN_UNDERWRITE_BLOCK_DURATION) revert MaxUnderwriteDurationTooShort();
// If the underwriting duration is too long, users can freeze up a lot of value for not a lot of cost.
if (newMaxUnderwriteDuration > MAX_UNDERWRITE_BLOCK_DURATION) revert MaxUnderwriteDurationTooLong();
maxUnderwritingDuration = newMaxUnderwriteDuration;
emit MaxUnderwriteDuration(newMaxUnderwriteDuration);
}
modifier checkRouteDescription(ICatalystV1Vault.RouteDescription calldata routeDescription) {
// -- Check incentives -- //
ICatalystV1Vault.IncentiveDescription calldata incentive = routeDescription.incentive;
// 1. Gas limits
if (incentive.maxGasDelivery < minGasFor[routeDescription.chainIdentifier]) revert NotEnoughIncentives(minGasFor[routeDescription.chainIdentifier], incentive.maxGasDelivery);
if (incentive.maxGasAck < minGasFor[bytes32(0)]) revert NotEnoughIncentives(minGasFor[bytes32(0)], incentive.maxGasAck);
// 2. Gas prices
// The gas price of ack has to be 10% higher than the gas price spent on this transaction.
if (incentive.priceOfAckGas < tx.gasprice * 11 / 10) revert NotEnoughIncentives(tx.gasprice * 11 / 10, incentive.priceOfAckGas);
// -- Check Address Lengths -- //
// toAccount
if (!_checkBytes65(routeDescription.toAccount)) revert InvalidBytes65Address();
// toVault
if (!_checkBytes65(routeDescription.toVault)) revert InvalidBytes65Address();
_;
}
modifier onlyGARP() {
if (msg.sender != address(GARP)) revert InvalidCaller();
_;
}
modifier verifySourceChainAddress(bytes32 sourceChainIdentifier, bytes calldata fromApplication) {
if (keccak256(fromApplication) != keccak256(chainIdentifierToDestinationAddress[sourceChainIdentifier])) revert InvalidSourceApplication();
_;
}
//-- Transparent viewer --//
/// @notice Estimate the addition verification cost beyond the
/// cost paid to the relayer.
function estimateAdditionalCost() override external view returns(address asset, uint256 amount) {
(asset, amount) = GARP.estimateAdditionalCost();
}
//-- Functions --//
/// @notice matches the hash of error calldata to common revert functions
/// and then reverts a relevant ack which can be exposed on the origin to provide information
/// about why the transaction didn't execute as expected.
function _handleError(bytes memory err) pure internal returns (bytes1) {
// To only get the error identifier, only use the first 8 bytes. This lets us add additional error
// data for easier debugger on trace.
bytes8 errorIdentifier = bytes8(err);
// We can use memory slices to get better insight into exactly the error which occured.
// This would also allow us to reuse events.
// However, it looks like it will significantly increase gas costs so this works for now.
// It looks like Solidity will improve their error catch implementation which will replace this.
if (bytes8(abi.encodeWithSelector(ExceedsSecurityLimit.selector)) == errorIdentifier) return 0x11;
if (bytes8(abi.encodeWithSelector(ReturnInsufficient.selector)) == errorIdentifier) return 0x12;
if (bytes8(abi.encodeWithSelector(VaultNotConnected.selector)) == errorIdentifier) return 0x13;
return 0x10; // unknown error.
}
/// @notice Connects this CCI with another contract on another chain.
/// @dev To simplify the implementation, each chain can only be setup once. This reduces governance risks.
/// @param remoteCCI The bytes65 encoded address on the destination chain.
/// @param remoteGARP The messaging router encoded address on the destination chain.
function connectNewChain(bytes32 chainIdentifier, bytes calldata remoteCCI, bytes calldata remoteGARP) onlyOwner checkBytes65Address(remoteCCI) override external {
// Check if the chain has already been set.
// If it has, we don't allow setting it as another. This would impact existing pools.
if (chainIdentifierToDestinationAddress[chainIdentifier].length != 0) revert ChainAlreadySetup();
// Set the remote CCI.
chainIdentifierToDestinationAddress[chainIdentifier] = remoteCCI;
emit RemoteImplementationSet(chainIdentifier, remoteCCI, remoteGARP);
// Set the remote messaging router escrow.
GARP.setRemoteImplementation(chainIdentifier, remoteGARP);
}
/**
* @notice Packs cross-chain swap information into a bytearray and sends it to the target vault with IBC.
* @dev Callable by anyone but this cannot be abused since the connection management ensures no
* wrong messages enter a healthy vault.
* @param routeDescription A cross-chain route description which contains the chainIdentifier, toAccount, toVault and relaying incentive.
* @param toAssetIndex The index of the asset the user wants to buy in the target vault.
* @param U The calculated liquidity reference. (Units)
* @param minOut The minimum number output of tokens on the target chain.
* @param fromAmount Escrow related value. The amount returned if the swap fails.
* @param fromAsset Escrow related value. The asset that was sold.
* @param underwriteIncentiveX16 The payment for underwriting the swap (out of type(uint16).max)
* @param calldata_ Data field if a call should be made on the target chain.
* Encoding depends on the target chain, with EVM: abi.encodePacket(bytes20(<address>), <data>).
*/
function sendCrossChainAsset(
ICatalystV1Vault.RouteDescription calldata routeDescription,
uint8 toAssetIndex,
uint256 U,
uint256 minOut,
uint256 fromAmount,
address fromAsset,
uint16 underwriteIncentiveX16,
bytes calldata calldata_
) checkRouteDescription(routeDescription) override external payable {
// We need to ensure that all information is in the correct places. This ensures that calls to this contract
// will always be decoded semi-correctly even if the input is very incorrect. This also checks that the user
// inputs into the swap contracts are correct while making the cross-chain interface flexible for future implementations.
// These checks are done by the modifier.
// Anyone can call this function, but unless someone can also manage to pass the security check on onRecvPacket
// they cannot drain any value. As such, the very worst they can do is waste gas.
// Encode payload. See CatalystPayload.sol for the payload definition
bytes memory data = abi.encodePacked(
CTX0_ASSET_SWAP,
abi.encodePacked(
uint8(20), // EVM addresses are 20 bytes.
bytes32(0), // EVM only uses 20 bytes. abi.encode packs the 20 bytes into 32 then we need to add 32 more
abi.encode(msg.sender) // Use abi.encode to encode address into 32 bytes
),
abi.encodePacked(
routeDescription.toVault, // Length is expected to be pre-encoded.
routeDescription.toAccount, // Length is expected to be pre-encoded.
U,
toAssetIndex,
minOut,
fromAmount
),
abi.encodePacked(
uint8(20), // EVM addresses are 20 bytes.
bytes32(0), // EVM only uses 20 bytes. abi.encode packs the 20 bytes into 32 then we need to add 32 more
abi.encode(fromAsset) // Use abi.encode to encode address into 32 bytes
),
uint32(block.number), // This is the same as block.number mod 2**32-1
uint16(underwriteIncentiveX16),
uint16(calldata_.length), // max length of calldata is 2**16-1 = 65535 bytes which should be more than plenty.
calldata_
);
GARP.submitMessage{value: msg.value}(
routeDescription.chainIdentifier,
chainIdentifierToDestinationAddress[routeDescription.chainIdentifier],
data,
routeDescription.incentive
);
}
/**
* @notice Packs cross-chain swap information into a bytearray and sends it to the target vault with IBC.
* @dev Callable by anyone but this cannot be abused since the connection management ensures no
* wrong messages enter a healthy vault.
* @param routeDescription A cross-chain route description which contains the chainIdentifier, toAccount, toVault and relaying incentive.
* @param U The calculated liquidity reference. (Units)
* @param minOut An array of minout describing: [the minimum number of vault tokens, the minimum number of reference assets]
* @param fromAmount Escrow related value. The amount returned if the swap fails.
* @param calldata_ Data field if a call should be made on the target chain.
* Encoding depends on the target chain, with EVM: abi.encodePacket(bytes20(<address>), <data>).
*/
function sendCrossChainLiquidity(
ICatalystV1Vault.RouteDescription calldata routeDescription,
uint256 U,
uint256[2] calldata minOut,
uint256 fromAmount,
bytes memory calldata_
) checkRouteDescription(routeDescription) override external payable {
// We need to ensure that all information is in the correct places. This ensures that calls to this contract
// will always be decoded semi-correctly even if the input is very incorrect. This also checks that the user
// inputs into the swap contracts are correct while making the cross-chain interface flexible for future implementations.
// These checks are done by the modifier.
// Anyone can call this function, but unless someone can also manage to pass the security check on onRecvPacket
// they cannot drain any value. As such, the very worst they can do is waste gas.
// Encode payload. See CatalystPayload.sol for the payload definition
bytes memory data = abi.encodePacked(
CTX1_LIQUIDITY_SWAP,
abi.encodePacked(
uint8(20), // EVM addresses are 20 bytes.
bytes32(0), // EVM only uses 20 bytes. abi.encode packs the 20 bytes into 32 then we need to add 32 more
abi.encode(msg.sender) // Use abi.encode to encode address into 32 bytes
),
routeDescription.toVault, // Length is expected to be pre-encoded.
routeDescription.toAccount, // Length is expected to be pre-encoded.
U,
minOut[0],
minOut[1],
fromAmount,
uint32(block.number),
uint16(calldata_.length),
calldata_
);
GARP.submitMessage{value: msg.value}(
routeDescription.chainIdentifier,
chainIdentifierToDestinationAddress[routeDescription.chainIdentifier],
data,
routeDescription.incentive
);
}
/**
* @notice Cross-chain message success handler
* @dev Should never revert. (on valid messages)
*/
function _onPacketSuccess(bytes32 destinationIdentifier, bytes calldata data) internal {
bytes1 context = data[CONTEXT_POS];
// Since this is a callback, fromVault must be an EVM address.
address fromVault = address(bytes20(data[ FROM_VAULT_START_EVM : FROM_VAULT_END ]));
if (context == CTX0_ASSET_SWAP) {
ICatalystV1Vault(fromVault).onSendAssetSuccess(
destinationIdentifier, // connectionId
data[ TO_ACCOUNT_LENGTH_POS : TO_ACCOUNT_END ], // toAccount
uint256(bytes32(data[ UNITS_START : UNITS_END ])), // units
uint256(bytes32(data[ CTX0_FROM_AMOUNT_START : CTX0_FROM_AMOUNT_END ])), // fromAmount
address(bytes20(data[ CTX0_FROM_ASSET_START_EVM : CTX0_FROM_ASSET_END ])), // fromAsset
uint32(bytes4(data[ CTX0_BLOCK_NUMBER_START : CTX0_BLOCK_NUMBER_END ])) // block number
);
return;
}
if (context == CTX1_LIQUIDITY_SWAP) {
ICatalystV1Vault(fromVault).onSendLiquiditySuccess(
destinationIdentifier, // connectionId
data[ TO_ACCOUNT_LENGTH_POS : TO_ACCOUNT_END ], // toAccount
uint256(bytes32(data[ UNITS_START : UNITS_END ])), // units
uint256(bytes32(data[ CTX1_FROM_AMOUNT_START : CTX1_FROM_AMOUNT_END ])), // fromAmount
uint32(bytes4(data[ CTX1_BLOCK_NUMBER_START : CTX1_BLOCK_NUMBER_END ])) // block number
);
return;
}
// A proper message should never get here. If the message got here, we are never going to be able to properly process it.
revert InvalidContext(context);
}
/**
* @notice Cross-chain message failure handler
* @dev Should never revert. (on valid messages)
*/
function _onPacketFailure(bytes32 destinationIdentifier, bytes calldata data) internal {
bytes1 context = data[CONTEXT_POS];
// Since this is a callback, fromVault must be an EVM address.
address fromVault = address(bytes20(data[ FROM_VAULT_START_EVM : FROM_VAULT_END ]));
if (context == CTX0_ASSET_SWAP) {
ICatalystV1Vault(fromVault).onSendAssetFailure(
destinationIdentifier, // connectionId
data[ TO_ACCOUNT_LENGTH_POS : TO_ACCOUNT_END ], // toAccount
uint256(bytes32(data[ UNITS_START : UNITS_END ])), // units
uint256(bytes32(data[ CTX0_FROM_AMOUNT_START : CTX0_FROM_AMOUNT_END ])), // fromAmount
address(bytes20(data[ CTX0_FROM_ASSET_START_EVM : CTX0_FROM_ASSET_END ])), // fromAsset
uint32(bytes4(data[ CTX0_BLOCK_NUMBER_START : CTX0_BLOCK_NUMBER_END ])) // block number
);
return;
}
if (context == CTX1_LIQUIDITY_SWAP) {
ICatalystV1Vault(fromVault).onSendLiquidityFailure(
destinationIdentifier, // connectionId
data[ TO_ACCOUNT_LENGTH_POS : TO_ACCOUNT_END ], // toAccount
uint256(bytes32(data[ UNITS_START : UNITS_END ])), // units
uint256(bytes32(data[ CTX1_FROM_AMOUNT_START : CTX1_FROM_AMOUNT_END ])), // fromAmount
uint32(bytes4(data[ CTX1_BLOCK_NUMBER_START : CTX1_BLOCK_NUMBER_END ])) // block number
);
return;
}
// A proper message should never get here. If the message got here, we are never going to be able to properly process it.
revert InvalidContext(context);
}
/**
* @notice The Acknowledgement package handler
* @dev Should never revert. (on valid messages)
* @param destinationIdentifier Identifier for the destination chain
* @param acknowledgement The acknowledgement bytes for the cross-chain swap.
*/
function receiveAck(bytes32 destinationIdentifier, bytes32 /* messageIdentifier */, bytes calldata acknowledgement) onlyGARP override external {
// If the transaction executed but some logic failed, an ack is sent back with an error acknowledgement.
// This is known as "fail on ack". The package should be failed.
// The acknowledgement is prepended the message, so we need to fetch it.
// Then, we need to ignore it when passing the data to the handlers.
bytes1 swapStatus = acknowledgement[0];
if (swapStatus != 0x00) {
emit SwapFailed(swapStatus); // The acknowledgement can be mapped to get some information about what happened.
return _onPacketFailure(destinationIdentifier, acknowledgement[1:]);
}
// Otherwise, swapStatus == 0x00 which implies success.
_onPacketSuccess(destinationIdentifier, acknowledgement[1:]);
}
/**
* @notice The receive packet handler
* @param sourceIdentifier Source chain identifier.
* @param fromApplication The bytes65 encoded fromApplication.
* @param message The message sent by the source chain.
* @return acknowledgement The acknowledgement status of the transaction after execution.
*/
function receiveMessage(bytes32 sourceIdentifier, bytes32 /* messageIdentifier */, bytes calldata fromApplication, bytes calldata message) onlyGARP verifySourceChainAddress(sourceIdentifier, fromApplication) override external returns (bytes memory acknowledgement) {
bytes1 swapStatus = _receiveMessage(sourceIdentifier, message);
return acknowledgement = bytes.concat(
swapStatus,
message
);
}
/**
* @notice Message handler
* @param data The IBC packet
* @return acknowledgement The status of the transaction after execution
*/
function _receiveMessage(bytes32 sourceIdentifier, bytes calldata data) internal virtual returns (bytes1 acknowledgement) {
bytes1 context = data[CONTEXT_POS];
// Check that toAccount is the correct length and only contains 0 bytes beyond the address.
if (uint8(data[TO_ACCOUNT_LENGTH_POS]) != 20) revert InvalidAddress(); // Check correct length
if (uint256(bytes32(data[TO_ACCOUNT_START:TO_ACCOUNT_START+32])) != 0) revert InvalidAddress(); // Check first 32 bytes are 0.
if (uint96(bytes12(data[TO_ACCOUNT_START+32:TO_ACCOUNT_START_EVM])) != 0) revert InvalidAddress(); // Check the next 32-20=12 bytes are 0.
// To vault will not be checked. If it is assumed that any error is random, then an incorrect toVault will result in the call failling.
// The reason toAccount is checked is that any to account will be treated as valid. So any random error will result
// in lost funds.
if (context == CTX0_ASSET_SWAP) {
return acknowledgement = _handleReceiveAsset(sourceIdentifier, data);
}
if (context == CTX1_LIQUIDITY_SWAP) {
return acknowledgement = _handleReceiveLiquidity(sourceIdentifier, data);
}
/* revert InvalidContext(context); */
// No return here. Instead, another implementation can override this implementation. It should just keep adding ifs with returns inside:
// acknowledgement = super._receiveMessage(...)
// if (acknowledgement == 0x01) { if (context == CTXX) ...}
return acknowledgement = 0x01;
}
function _handleReceiveAssetFallback(bytes32 sourceIdentifier, bytes calldata data) internal returns (bytes1 status) {
// We don't know how from_vault is encoded. So we load it as bytes. Including the length.
bytes calldata fromVault = data[ FROM_VAULT_LENGTH_POS : FROM_VAULT_END ];
// We know that toVault is an EVM address
address toVault = address(bytes20(data[ TO_VAULT_START_EVM : TO_VAULT_END ]));
try ICatalystV1Vault(toVault).receiveAsset(
sourceIdentifier, // connectionId
fromVault, // fromVault
uint8(data[CTX0_TO_ASSET_INDEX_POS]), // toAssetIndex
address(bytes20(data[ TO_ACCOUNT_START_EVM : TO_ACCOUNT_END ])), // toAccount
uint256(bytes32(data[ UNITS_START : UNITS_END ])), // units
uint256(bytes32(data[ CTX0_MIN_OUT_START : CTX0_MIN_OUT_END ])), // minOut
uint256(bytes32(data[ CTX0_FROM_AMOUNT_START : CTX0_FROM_AMOUNT_END ])), // fromAmount
bytes(data[ CTX0_FROM_ASSET_LENGTH_POS : CTX0_FROM_ASSET_END ]), // fromAsset
uint32(bytes4(data[ CTX0_BLOCK_NUMBER_START : CTX0_BLOCK_NUMBER_END ])) // blocknumber
) returns(uint256 purchasedTokens) {
uint16 dataLength = uint16(bytes2(data[CTX0_DATA_LENGTH_START:CTX0_DATA_LENGTH_END]));
if (dataLength != 0) {
address dataTarget = address(bytes20(data[ CTX0_DATA_START : CTX0_DATA_START+20 ]));
bytes calldata dataArguments = data[ CTX0_DATA_START+20 : CTX0_DATA_START+dataLength ];
// Let users define custom logic which should be executed after the swap.
// The logic is not contained within a try - except so if the logic reverts
// the transaction will timeout and the user gets the input tokens on the sending chain.
// If this is not desired, wrap further logic in a try - except at dataTarget.
ICatalystReceiver(dataTarget).onCatalystCall(purchasedTokens, dataArguments);
// If dataTarget doesn't implement onCatalystCall BUT implements a fallback function, the call will still succeed.
}
return 0x00;
} catch (bytes memory err) {
return _handleError(err);
}
}
function _handleReceiveLiquidity(bytes32 sourceIdentifier, bytes calldata data) internal returns (bytes1 status) {
// We don't know how from_vault is encoded. So we load it as bytes. Including the length.
bytes calldata fromVault = data[ FROM_VAULT_LENGTH_POS : FROM_VAULT_END ];
// We know that toVault is an EVM address
address toVault = address(bytes20(data[ TO_VAULT_START_EVM : TO_VAULT_END ]));
try ICatalystV1Vault(toVault).receiveLiquidity(
sourceIdentifier, // connectionId
fromVault, // fromVault
address(bytes20(data[ TO_ACCOUNT_START_EVM : TO_ACCOUNT_END ])), // toAccount
uint256(bytes32(data[ UNITS_START : UNITS_END ])), // units
uint256(bytes32(data[ CTX1_MIN_VAULT_TOKEN_START : CTX1_MIN_VAULT_TOKEN_END ])), // minOut
uint256(bytes32(data[ CTX1_MIN_REFERENCE_START : CTX1_MIN_REFERENCE_END ])),// minOut
uint256(bytes32(data[ CTX1_FROM_AMOUNT_START : CTX1_FROM_AMOUNT_END ])), // fromAmount
uint32(bytes4(data[ CTX1_BLOCK_NUMBER_START : CTX1_BLOCK_NUMBER_END ])) // blocknumber
) returns (uint256 purchasedVaultTokens) {
uint16 dataLength = uint16(bytes2(data[CTX1_DATA_LENGTH_START:CTX1_DATA_LENGTH_END]));
if (dataLength != 0) {
address dataTarget = address(bytes20(data[ CTX1_DATA_START : CTX1_DATA_START+20 ]));
bytes calldata dataArguments = data[ CTX1_DATA_START+20 : CTX1_DATA_START+dataLength ];
// Let users define custom logic which should be executed after the swap.
// The logic is not contained within a try - except so if the logic reverts
// the transaction will timeout and the user gets the input tokens on the sending chain.
// If this is not desired, wrap further logic in a try - except at dataTarget.
ICatalystReceiver(dataTarget).onCatalystCall(purchasedVaultTokens, dataArguments);
// If dataTarget doesn't implement onCatalystCall BUT implements a fallback function, the call will still succeed.
}
return 0x00;
} catch (bytes memory err) {
return _handleError(err);
}
}
//--- Underwriting ---//
// The following section contains the underwriting module of Catalyst.
// It serves to speedup swap execution by letting a thirdparty take on the confirmation / delivery risk
// by pre-executing the latter part and then reserving the swap result in the escrow.
/**
* @notice Returns the underwriting identifier for a Catalyst swap.
*/
function _getUnderwriteIdentifier(
address targetVault,
address toAsset,
uint256 U,
uint256 minOut,
address toAccount,
uint16 underwriteIncentiveX16,
bytes calldata cdata
) internal pure returns (bytes32 identifier) {
return identifier = keccak256(
abi.encodePacked(
targetVault,
toAsset,
U,
minOut,
toAccount,
underwriteIncentiveX16,
cdata
)
);
}
/**
* @notice Returns the underwriting identifier for a Catalyst swap.
*/
function getUnderwriteIdentifier(
address targetVault,
address toAsset,
uint256 U,
uint256 minOut,
address toAccount,
uint16 underwriteIncentiveX16,
bytes calldata cdata
) override external pure returns (bytes32 identifier) {
return identifier = _getUnderwriteIdentifier(
targetVault,
toAsset,
U,
minOut,
toAccount,
underwriteIncentiveX16,
cdata
);
}
/**
* @notice Underwrites a swap and check if there is a connection.
* @dev sourceIdentifier and fromVault are not verified that they belong to the message.
* As a result, care should be placed on correctly reading these.
*/
function underwriteAndCheckConnection(
bytes32 sourceIdentifier,
bytes calldata fromVault, // -- Conection Check
address targetVault, // -- Swap information
address toAsset,
uint256 U,
uint256 minOut,
address toAccount,
uint16 underwriteIncentiveX16,
bytes calldata cdata
) override external {
if (!ICatalystV1Vault(targetVault)._vaultConnection(sourceIdentifier, fromVault)) revert NoVaultConnection();
underwrite(
targetVault,
toAsset,
U,
minOut,
toAccount,
underwriteIncentiveX16,
cdata
);
}
/**
* @notice Underwrite a swap.
* There are a few important properties that underwriters should be aware of:
* 1. There is nothing that checks if the connection is valid on this call. This check should be done seperately.
* Even if a swap is executed where it appears that it is possible to execute the swap,
* these values should be read and a connection should be checked. (via this function).
* As you can underwrite the swap (where there is no connection) but when the swap arrives it
* does not fill the underwrite but instead fails on ack and releases the input on the source chain.
* You can use the similar function underwriteAndCheckConnection to also check the connection.
*
* 2. You are underwriting the specific instance of the transaction not the inclusion of the transaction.
* What this means for you, is that if the block of the transaction is lost/abandon/re-entered, then
* the underwriting will not be noted unless the transaction is re-executed almost EXACTLY as it was before.
* The most important parameter is U which is volatile and any change to the vault balances on the source chain
* will cause U to be different.
* In other words, if that transaction is re-executed before or after another swap which wasn't the case before
* then it won't fill the underwrite anymore and either be exeucted as an ordinary swap (to the user) or fail
* with an ack and release the original funds back to the user.
*
* 3. The execution of the underwrite is dependent on the correct execution of both
* the minout but also the additional logic. If either fails, then the swap is not
* underwritable. As a result, it is important that the underwrite is simulated before executed.
*/
function underwrite(
address targetVault, // -- Swap information
address toAsset,
uint256 U,
uint256 minOut,
address toAccount,
uint16 underwriteIncentiveX16,
bytes calldata cdata
) public returns(bytes32 identifier) {
// Get the swap identifier.
// For any incoming swap, the swap will have a semi-unique identifier, which can be derived solely based on the
// swap parameters. Assuming that the swap has already been executed, these arguments are constant and known.
// As a result, once the swap arrives (and the underwriter is competent), the same identifier will be computed and matched.
// For other implementations: The arguments the identifier is based on should be the same but the hashing
// algorithm doesn't matter. It is only used and computed on the destination chain.
identifier = _getUnderwriteIdentifier(
targetVault,
toAsset,
U,
minOut,
toAccount,
underwriteIncentiveX16,
cdata
);
// Observation: A swapper can execute multiple swaps which return the same U.
// In these cases, it would not be possible to underwrite the second swap (or further swaps)
// until after the first swap has arrived. This can be counteracted by either:
// 1. Changing U. The tail of U is **volatile**. As a result, to get overlapping identifiers,
// it would have to be done deliberatly.
// 2. Add random noise to minOut or underwriteIncentiveX16. For tokens with 18 decimals, this noise can
// be as small as 1e-12 then the chance that 2 swaps collide would be 1 in a million'th. (literally 1/(1e(18-12)) = 1/1e6)
// 3. Add either a counter or noise to cdata.
// For most implementations, the observation can be ignored because of the strength of point 1.
// Check if the associated underwrite just arrived and has already been matched.
// This is an issue when the swap was JUST underwriten, JUST arrived (and matched), AND someone else JUST underwrote the swap.
// To give the user a bit more protection, we add a buffer of size `BUFFER_BLOCKS`.
// SwapAlreadyUnderwritten vs SwapRecentlyUnderwritten: It is very likely that this block is trigger not because a swap was fulfilled but because it has already been underwritten. That is because (lastTouchBlock + BUFFER_BLOCKS >= uint96(block.number)) WILL ALWAYS be true when it is the case
// and SwapRecentlyUnderwritten will be the error. You might have expected the error "SwapAlreadyUnderwritten". However, we never get there so it
// cannot emit. We also cannot move that check up here, since then an external call would be made between a state check and a state modification. (reentry)
// As a result, SwapRecentlyUnderwritten will be emitted when a swap has already been underwritten EXCEPT when underwriting a swap through reentry.
// Then the reentry will underwrite the swap and the main call will fail with SwapAlreadyUnderwritten.
unchecked {
// Get the last touch block. For most underwrites it is going to be 0.
uint96 lastTouchBlock = underwritingStorage[identifier].expiry;
if (lastTouchBlock != 0) { // implies that the swap has never been underwritten.
// if lastTouchBlock > type(uint96).max + BUFFER_BLOCKS then lastTouchBlock + BUFFER_BLOCKS overflows.
// if lastTouchBlock < BUFFER_BLOCKS then lastTouchBlock - BUFFER_BLOCKS underflows.
if ((lastTouchBlock < type(uint96).max - BUFFER_BLOCKS) && (lastTouchBlock > BUFFER_BLOCKS)) {
// Add a reasonable buffer so if the transaction got into the memory pool and is delayed into the next blocks
// it doesn't underwrite a non-existing swap.
if (lastTouchBlock + BUFFER_BLOCKS >= uint96(block.number)) {
// Check that uint96(block.number) hasn't overflowed and this is an old reference. We don't care about the underflow
// as that will always return false.
// First however, we need to check that this won't underflow.
if (lastTouchBlock - BUFFER_BLOCKS <= uint96(block.number)) revert SwapRecentlyUnderwritten();
}
} else {
if (lastTouchBlock == uint96(block.number)) revert SwapRecentlyUnderwritten();
}
}
}
// Get the number of purchased units from the vault. This uses a custom call which doesn't return
// any assets.
// This calls escrows the purchasedTokens on the vault.
// Importantly! The connection is not checked here. Instead it is checked when the
// message arrives. As a result, the underwriter should verify that a message is good.
uint256 purchasedTokens = ICatalystV1Vault(targetVault).underwriteAsset(
identifier,
toAsset,
U,
minOut * (2 << 16) / ((2 << 16) - uint256(underwriteIncentiveX16)) // minout is checked after underwrite fee.
);
// The following number of lines act as re-entry protection. Do not add any external call inbetween these lines.
// Ensure the swap hasn't already been underwritten by checking if refundTo is set.
// Notice that this is very unlikely to ever get emitted. Instead, read the comment about SwapRecentlyUnderwritten.
if (underwritingStorage[identifier].refundTo != address(0)) revert SwapAlreadyUnderwritten();
// Save the underwriting state.
underwritingStorage[identifier] = UnderwritingStorage({
tokens: purchasedTokens,
refundTo: msg.sender,
expiry: uint96(uint256(block.number) + uint256(maxUnderwritingDuration)) // Should never overflow.
});
// The above combination of lines act as local re-entry protection. Do not add any external call inbetween these lines.
// Collect tokens and collateral from underwriter.
// We still collect the tokens used to incentivise the underwriter as otherwise they could freely reserve liquidity
// in the vaults. Vaults would essentially be a free source of short term options which isn't wanted.
ERC20(toAsset).safeTransferFrom(
msg.sender,
address(this),
purchasedTokens * (
UNDERWRITING_COLLATERAL_DENOMINATOR+UNDERWRITING_COLLATERAL
)/UNDERWRITING_COLLATERAL_DENOMINATOR
);
uint256 underwritingIncentive = (purchasedTokens * uint256(underwriteIncentiveX16)) >> 16;
// Subtract the underwrite incentive from the funds sent to the user.
unchecked {
// underwritingIncentive <= purchasedTokens.
purchasedTokens -= underwritingIncentive;
}
// Send the assets to the user.
ERC20(toAsset).safeTransfer(toAccount, purchasedTokens);
// Figure out if the user wants to execute additional logic.
// Note that this logic is not contained within a try catch. It could fail.
// An underwrite should simulate the tx execution before submitting the transaction as otherwise
// they could be out the associated gas.
uint16 calldataLength = uint16(bytes2(cdata[0:2]));
if (calldataLength != 0) {
address dataTarget = address(bytes20(cdata[2:2+20]));
bytes calldata customCalldata = cdata[2+20:2+calldataLength];
ICatalystReceiver(dataTarget).onCatalystCall(purchasedTokens, customCalldata);
}
emit SwapUnderwritten(
identifier,
msg.sender,
uint96(uint256(block.number) + uint256(maxUnderwritingDuration)),
targetVault,
toAsset,
U,
toAccount,
purchasedTokens
);
}
/**
* @notice Resolves unexpired underwrites so that the escrowed value can be freed.
* @dev The underwrite owner can expire it at any time. Other callers needs to wait until after expiry.
*/
function expireUnderwrite(
address targetVault,
address toAsset,
uint256 U,
uint256 minOut,
address toAccount,
uint16 underwriteIncentiveX16,
bytes calldata cdata
) override external {
bytes32 identifier = _getUnderwriteIdentifier(
targetVault,
toAsset,
U,
minOut,
toAccount,
underwriteIncentiveX16,
cdata
);
UnderwritingStorage storage underwriteState = underwritingStorage[identifier];
// Check that the refundTo address is set. (Indicates that the underwrite exists.)
if (underwriteState.refundTo == address(0)) revert UnderwriteDoesNotExist(identifier);
// Check that the underwriting can be expired. If the msg.sender is the refundTo address, then it can be expired at any time.
// This lets the underwriter reclaim *some* of the collateral they provided if they change their mind or observed an issue.
if (msg.sender != underwriteState.refundTo) {
// Otherwise, the expiry time must have been passed.
if (underwriteState.expiry > block.number) revert UnderwriteNotExpired(underwriteState.expiry - block.number);
}
uint256 underWrittenTokens = underwriteState.tokens;
// The next line acts as reentry protection. When the storage is deleted underwriteState.refundTo == address(0) will be true.
delete underwritingStorage[identifier];
// Delete the escrow
ICatalystV1Vault(targetVault).deleteUnderwriteAsset(identifier, U, underWrittenTokens, toAsset);
unchecked {
// Compute the underwriting incentive.
// Notice the parts that we only have: incentive + collateral to work with
// The incentive was never sent to the user, neither was the underwriting incentive.
uint256 underwritingIncentive = (underWrittenTokens * uint256(underwriteIncentiveX16)) >> 16;
// This computation has been done before.
// Get the collateral.
// A larger computation has already been done when the swap was initially underwritten.
uint256 refundAmount = underWrittenTokens * (
UNDERWRITING_COLLATERAL
)/UNDERWRITING_COLLATERAL_DENOMINATOR + underwritingIncentive;
// collateral + underwritingIncentive must be less than the full amount.
// Send the coded shares of the collateral to the expirer the rest to the vault.
// This following logic might overflow but we would rather have it overflow (which reduces expireShare)
// than to never be able to expire an underwrite.
uint256 expireShare = refundAmount * EXPIRE_CALLER_REWARD / EXPIRE_CALLER_REWARD_DENOMINATOR;
ERC20(toAsset).safeTransfer(msg.sender, expireShare);
// refundAmount > expireShare, and specially when expireShare overflows.
uint256 vaultShare = refundAmount - expireShare;
ERC20(toAsset).safeTransfer(targetVault, vaultShare);
emit ExpireUnderwrite(
identifier,
msg.sender,
expireShare
);
}
// The underwriting storage has already been deleted.
}
// It is important that any call to this functions has pre-checked the vault connection.
function _matchUnderwrite(
bytes32 identifier,
address toAsset,
address vault,
bytes32 sourceIdentifier,
bytes calldata fromVault,
uint16 underwriteIncentiveX16
) internal returns (bool swapUnderwritten) {
UnderwritingStorage storage underwriteState = underwritingStorage[identifier];
// Load number of tokens from storage.
uint256 underwrittenTokenAmount = underwriteState.tokens;
// Check if the swap was underwritten => refundTo != address(0)
address refundTo = underwriteState.refundTo;
// if refundTo == address(0) then the swap hasn't been underwritten.
if (refundTo == address(0)) return swapUnderwritten = false;
// Reentry protection. No external calls are allowed before this line. The line 'if (refundTo == address(0)) ...' will always be true.
delete underwritingStorage[identifier];
// Set the last touch block so someone doesn't underwrite this swap again.
underwritingStorage[identifier].expiry = uint96(block.number);
// Delete escrow information and send swap tokens directly to the underwriter.
ICatalystV1Vault(vault).releaseUnderwriteAsset(refundTo, identifier, underwrittenTokenAmount, toAsset, sourceIdentifier, fromVault);
// We know only need to handle the collateral and underwriting incentive.
// We also don't have to check that the vault didn't lie to us about underwriting.
// Also refund the collateral.
uint256 refundAmount = underwrittenTokenAmount * (
UNDERWRITING_COLLATERAL
)/UNDERWRITING_COLLATERAL_DENOMINATOR;
// add the underwriting incentive as well. Notice that 2x refundAmount are in play.
// 1. The first part comes from the underwriter + collateral.
// + 1. The second part comes from the vault after the matching message arrives.
// = 2 parts
// 1 - incentive has been sent to the user.
// That leaves us with part (1 - (1 - incentive)) + (1) = incentive + 1
uint256 underwritingIncentive = (underwrittenTokenAmount * uint256(underwriteIncentiveX16)) >> 16;
refundAmount += underwritingIncentive;
ERC20(toAsset).safeTransfer(refundTo, refundAmount);
emit FulfillUnderwrite(
identifier
);
return swapUnderwritten = true;
}
function _handleReceiveAsset(bytes32 sourceIdentifier, bytes calldata data) internal returns (bytes1 acknowledgement) {
// We don't know how from_vault is encoded. So we load it as bytes. Including the length.
bytes calldata fromVault = data[ FROM_VAULT_LENGTH_POS : FROM_VAULT_END ];
// We know that toVault is an EVM address
address toVault = address(bytes20(data[ TO_VAULT_START_EVM : TO_VAULT_END ]));
// Select excess calldata. Excess calldata is not decoded.
bytes calldata cdata = data[CTX0_DATA_LENGTH_START:];
// Get the toAsset
uint8 toAssetIndex = uint8(data[CTX0_TO_ASSET_INDEX_POS]);
address toAsset = ICatalystV1Vault(toVault)._tokenIndexing(toAssetIndex);
// Get the rest of the swap parameters.
address toAccount = address(bytes20(data[ TO_ACCOUNT_START_EVM : TO_ACCOUNT_END ]));
uint256 U = uint256(bytes32(data[ UNITS_START : UNITS_END ]));
uint256 minOut = uint256(bytes32(data[ CTX0_MIN_OUT_START : CTX0_MIN_OUT_END ]));
uint16 underwriteIncentiveX16 = uint16(bytes2(data[CTX0_UW_INCENTIVE_START:CTX0_UW_INCENTIVE_END]));
// Get the underwriting identifier.
bytes32 identifier = _getUnderwriteIdentifier(
toVault,
toAsset,
U,
minOut,
toAccount,
underwriteIncentiveX16,
cdata
);
// Check if the swap has been underwritten. If it has, return funds to underwriter.
bool swapUnderwritten = _matchUnderwrite(
identifier,
toAsset,
toVault,
sourceIdentifier,
fromVault,
underwriteIncentiveX16
);
if (!swapUnderwritten) {
// The swap hasn't been underwritten lets execute the swap properly.
return acknowledgement = _handleReceiveAssetFallback(sourceIdentifier, data);
}
// There is no case where only a subset of the units are filled. As either the complete swap (through the swap identifier)
// is underwritten or it wasn't underwritten.
return acknowledgement = 0x00;