From c05baa7d0b1a1961ec7f943a00b8358736ce1e7a Mon Sep 17 00:00:00 2001 From: Peter Kohl-Landgraf <32535675+pekola@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:49:08 +0200 Subject: [PATCH] Website: Update ERC-6123 - Streamlined Reference Implementation, added Unit Tests Merged by EIP-Bot. --- ERCS/erc-6123.md | 17 +- assets/erc-6123/contracts/ERC20Settlement.sol | 37 +- assets/erc-6123/contracts/ISDC.sol | 61 ++-- assets/erc-6123/contracts/SDC.sol | 168 ++++++---- .../erc-6123/contracts/SDCPledgedBalance.sol | 201 +++++------ assets/erc-6123/doc/sequence.puml | 4 +- assets/erc-6123/package.json | 2 +- assets/erc-6123/test/SDCTests.js | 316 +++++++++++++----- 8 files changed, 500 insertions(+), 306 deletions(-) diff --git a/ERCS/erc-6123.md b/ERCS/erc-6123.md index 6625fe55fa..591bd4dcef 100644 --- a/ERCS/erc-6123.md +++ b/ERCS/erc-6123.md @@ -65,7 +65,7 @@ The following methods specify a Smart Derivative Contract's trade initiation and A party can initiate a trade by providing the party address to trade with, trade data, trade position, payment amount for the trade and initial settlement data. Only registered counterparties are allowed to use that function. ```solidity -function inceptTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; +function inceptTrade(address withParty, string memory tradeData, int position, int256 paymentAmount, string memory initialSettlementData) external; ``` #### Trade Initiation Phase: `confirmTrade` @@ -73,7 +73,7 @@ function inceptTrade(address _withParty, string memory _tradeData, int _position A counterparty can confirm a trade by providing its trade specification data, which then gets matched against the data stored from `inceptTrade` call. ```solidity -function confirmTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; +function confirmTrade(address withParty, string memory tradeData, int position, int256 paymentAmount, string memory initialSettlementData) external; ``` #### Trade Initiation Phase: `cancelTrade` @@ -81,7 +81,7 @@ function confirmTrade(address _withParty, string memory _tradeData, int _positio The counterparty that called `inceptTrade` has the option to cancel the trade, e.g., in the case where the trade is not confirmed in a timely manner. ```solidity -function cancelTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; +function cancelTrade(address withParty, string memory tradeData, int position, int256 paymentAmount, string memory initialSettlementData) external; ``` #### Trade Settlement Phase: `initiateSettlement` @@ -106,18 +106,19 @@ function performSettlement(int256 settlementAmount, string memory settlementData This method - either called back from the provided settlement token directly or from an eligible address - completes the settlement transfer. This might result in a termination or start of the next settlement phase, depending on the provided success flag. +The transactionData is emitted as part of the corresponding event: `TradeSettled` or `TradeTerminated` ```solidity -function afterTransfer(uint256 transactionHash, bool success) external; +function afterTransfer(bool success, uint256 transactionData) external; ``` #### Trade Termination: `requestTermination` -Allows an eligible party to request a mutual termination with a termination amount she is willing to pay and provide further termination terms (e.g. an XML) +Allows an eligible party to request a mutual termination of the trade with the correspondig `tradeData` with a termination amount she is willing to pay and provide further termination terms (e.g. an XML) ```solidity -function requestTradeTermination(string memory tradeId, int256 _terminationPayment, string memory terminationTerms) external; +function requestTradeTermination(string memory tradeData, int256 terminationPayment, string memory terminationTerms) external; ``` #### Trade Termination: `confirmTradeTermination` @@ -125,7 +126,7 @@ function requestTradeTermination(string memory tradeId, int256 _terminationPayme Allows an eligible party to confirm a previously requested (mutual) trade termination, including termination payment value and termination terms ```solidity -function confirmTradeTermination(string memory tradeId, int256 _terminationPayment, string memory terminationTerms) external; +function confirmTradeTermination(string memory tradeData, int256 terminationPayment, string memory terminationTerms) external; ``` #### Trade Termination: `cancelTradeTermination` @@ -133,7 +134,7 @@ function confirmTradeTermination(string memory tradeId, int256 _terminationPayme The party that initiated `requestTradeTermination` has the option to withdraw the request, e.g., in the case where the termination is not confirmed in a timely manner. ```solidity -function cancelTradeTermination(string memory tradeId, int256 _terminationPayment, string memory terminationTerms) external; +function cancelTradeTermination(string memory tradeData, int256 terminationPayment, string memory terminationTerms) external; ``` ### Trade Events diff --git a/assets/erc-6123/contracts/ERC20Settlement.sol b/assets/erc-6123/contracts/ERC20Settlement.sol index c6fedc9e5f..5aedb06d25 100644 --- a/assets/erc-6123/contracts/ERC20Settlement.sol +++ b/assets/erc-6123/contracts/ERC20Settlement.sol @@ -38,25 +38,14 @@ contract ERC20Settlement is ERC20, IERC20Settlement{ } function checkedTransfer(address to, uint256 value, uint256 transactionID) public onlySDC{ - try this.transfer(to,value) returns (bool transferSuccessFlag) { - ISDC(sdcAddress).afterTransfer(transactionID, transferSuccessFlag); - } - catch{ - ISDC(sdcAddress).afterTransfer(transactionID, false); - } + if ( balanceOf(sdcAddress) < value) + ISDC(sdcAddress).afterTransfer(false, transactionID); + else + ISDC(sdcAddress).afterTransfer(true, transactionID); } - function checkedTransferFrom(address from, address to, uint256 value, uint256 transactionID) external onlySDC { - // TODO: Bug - reason="Error: Transaction reverted: contract call run out of gas and made the transaction revert", method="estimateGas", - if (this.balanceOf(from)< value || this.allowance(from,address(msg.sender)) < value ) - ISDC(sdcAddress).afterTransfer(transactionID, false); - try this.transfer(to,value) returns (bool transferSuccessFlag) { - ISDC(sdcAddress).afterTransfer(transactionID, transferSuccessFlag); - } - catch{ - ISDC(sdcAddress).afterTransfer(transactionID, false); - } - // address owner = _msgSender(); // currently not used + function checkedTransferFrom(address from, address to, uint256 value, uint256 transactionID) external view onlySDC { + revert("not implemented"); } function checkedBatchTransfer(address[] memory to, uint256[] memory values, uint256 transactionID ) public onlySDC{ @@ -65,14 +54,14 @@ contract ERC20Settlement is ERC20, IERC20Settlement{ for(uint256 i = 0; i < values.length; i++) requiredBalance += values[i]; if (balanceOf(msg.sender) < requiredBalance){ - ISDC(sdcAddress).afterTransfer(transactionID, false); + ISDC(sdcAddress).afterTransfer(false, transactionID); return; } else{ for(uint256 i = 0; i < to.length; i++){ - transfer(to[i],values[i]); + _transfer(sdcAddress,to[i],values[i]); } - ISDC(sdcAddress).afterTransfer(transactionID, true); + ISDC(sdcAddress).afterTransfer(true, transactionID); } } @@ -88,15 +77,15 @@ contract ERC20Settlement is ERC20, IERC20Settlement{ totalRequiredBalance += values[j]; } if (balanceOf(fromAddress) < totalRequiredBalance){ - ISDC(sdcAddress).afterTransfer(transactionID, false); - break; + ISDC(sdcAddress).afterTransfer(false, transactionID); + return; } } for(uint256 i = 0; i < to.length; i++){ - transferFrom(from[i],to[i],values[i]); + _transfer(from[i],to[i],values[i]); } - ISDC(sdcAddress).afterTransfer(transactionID, true); + ISDC(sdcAddress).afterTransfer(true, transactionID); } } \ No newline at end of file diff --git a/assets/erc-6123/contracts/ISDC.sol b/assets/erc-6123/contracts/ISDC.sol index 88804d1fc4..e82b4d3a8c 100644 --- a/assets/erc-6123/contracts/ISDC.sol +++ b/assets/erc-6123/contracts/ISDC.sol @@ -93,7 +93,7 @@ interface ISDC { /** * @dev Emitted when an active trade is terminated - * @param cause string holding the cause of the termination + * @param cause string holding data associated with the termination, e.g. transactionData upon a failed transaction */ event TradeTerminated(string cause); @@ -105,7 +105,7 @@ interface ISDC { /** * @dev Emitted when settlement process has been finished */ - event TradeSettled(); + event TradeSettled(string transactionData); /** * @dev Emitted when a settlement gets requested @@ -152,35 +152,35 @@ interface ISDC { /** * @notice Incepts a trade, stores trade data * @dev emits a {TradeIncepted} event - * @param _withParty is the party the inceptor wants to trade with - * @param _tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml - * @param _position is the position the inceptor has in that trade - * @param _paymentAmount is the payment amount which can be positive or negative (viewed from the inceptor) - * @param _initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) + * @param withParty is the party the inceptor wants to trade with + * @param tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @param position is the position the inceptor has in that trade + * @param paymentAmount is the payment amount which can be positive or negative (viewed from the inceptor) + * @param initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) */ - function inceptTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; + function inceptTrade(address withParty, string memory tradeData, int position, int256 paymentAmount, string memory initialSettlementData) external; /** * @notice Performs a matching of provided trade data and settlement data of a previous trade inception * @dev emits a {TradeConfirmed} event if trade data match - * @param _withParty is the party the confirmer wants to trade with - * @param _tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml - * @param _position is the position the confirmer has in that trade (negative of the position the inceptor has in the trade) - * @param _paymentAmount is the payment amount which can be positive or negative (viewed from the confirmer, negative of the inceptor's view) - * @param _initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) + * @param withParty is the party the confirmer wants to trade with + * @param tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @param position is the position the confirmer has in that trade (negative of the position the inceptor has in the trade) + * @param paymentAmount is the payment amount which can be positive or negative (viewed from the confirmer, negative of the inceptor's view) + * @param initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) */ - function confirmTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; + function confirmTrade(address withParty, string memory tradeData, int position, int256 paymentAmount, string memory initialSettlementData) external; /** * @notice Performs a matching of provided trade data and settlement data of a previous trade inception. Required to be called by inceptor. * @dev emits a {TradeCanceled} event if trade data match and msg.sender agrees with the party that incepted the trade. - * @param _withParty is the party the inceptor wants to trade with - * @param _tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml - * @param _position is the position the inceptor has in that trade - * @param _paymentAmount is the payment amount which can be positive or negative (viewed from the inceptor) - * @param _initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) + * @param withParty is the party the inceptor wants to trade with + * @param tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @param position is the position the inceptor has in that trade + * @param paymentAmount is the payment amount which can be positive or negative (viewed from the inceptor) + * @param initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) */ - function cancelTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; + function cancelTrade(address withParty, string memory tradeData, int position, int256 paymentAmount, string memory initialSettlementData) external; /// Settlement Cycle: Settlement @@ -202,9 +202,10 @@ interface ISDC { /** * @notice May get called from outside to to finish a transfer (callback). The trade decides on how to proceed based on success flag * @param success tells the protocol whether transfer was successful + * @param transactionData data associtated with the transfer, will be emitted via the events. * @dev may emit a {TradeSettled} event or a {TradeTerminated} event */ - function afterTransfer(uint256 transactionHash, bool success) external; + function afterTransfer(bool success, uint256 transactionData) external; /// Trade termination @@ -212,25 +213,27 @@ interface ISDC { /** * @notice Called from a counterparty to request a mutual termination * @dev emits a {TradeTerminationRequest} - * @param tradeId the trade identifier which is supposed to be terminated - * @param terminationTerms the termination terms + * @param tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @param terminationPayment an agreed termination amount (viewed from the requester) + * @param terminationTerms the termination terms to be stored on chain. */ - function requestTradeTermination(string memory tradeId, int256 _terminationPayment, string memory terminationTerms) external; + function requestTradeTermination(string memory tradeData, int256 terminationPayment, string memory terminationTerms) external; /** * @notice Called from a party to confirm an incepted termination, which might trigger a final settlement before trade gets closed * @dev emits a {TradeTerminationConfirmed} - * @param tradeId the trade identifier of the trade which is supposed to be terminated - * @param terminationTerms the termination terms + * @param tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @param terminationPayment an agreed termination amount (viewed from the confirmer, negative of the value provided by the requester) + * @param terminationTerms the termination terms to be stored on chain. */ - function confirmTradeTermination(string memory tradeId, int256 _terminationPayment, string memory terminationTerms) external; + function confirmTradeTermination(string memory tradeData, int256 terminationPayment, string memory terminationTerms) external; /** * @notice Called from a party to confirm an incepted termination, which might trigger a final settlement before trade gets closed * @dev emits a {TradeTerminationConfirmed} - * @param tradeId the trade identifier of the trade which is supposed to be terminated + * @param tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml * @param terminationTerms the termination terms */ - function cancelTradeTermination(string memory tradeId, int256 _terminationPayment, string memory terminationTerms) external; + function cancelTradeTermination(string memory tradeData, int256 terminationPayment, string memory terminationTerms) external; } diff --git a/assets/erc-6123/contracts/SDC.sol b/assets/erc-6123/contracts/SDC.sol index 7d61ef9a41..71c3eba737 100644 --- a/assets/erc-6123/contracts/SDC.sol +++ b/assets/erc-6123/contracts/SDC.sol @@ -41,20 +41,24 @@ abstract contract SDC is ISDC { Confirmed, /* - * Valuation Phase + * Valuation Phase: The contract is awaiting a valuation for the next settlement. */ Valuation, /* - * A Token-based Transfer is in Progress + * Token-based Transfer is in Progress. Contracts awaits termination of token transfer (allows async transfers). */ InTransfer, /* - * Settlement is Completed + * Settlement is Completed. */ Settled, + /* + * Termination is in Progress. + */ + InTermination, /* * Terminated. */ @@ -65,29 +69,32 @@ abstract contract SDC is ISDC { * Modifiers serve as guards whether at a specific process state a specific function can be called */ - modifier onlyWhenTradeInactive() { require(tradeState == TradeState.Inactive, "Trade state is not 'Inactive'."); _; } + modifier onlyWhenTradeIncepted() { require(tradeState == TradeState.Incepted, "Trade state is not 'Incepted'."); _; } + modifier onlyWhenSettled() { require(tradeState == TradeState.Settled, "Trade state is not 'Settled'."); _; } + modifier onlyWhenValuation() { require(tradeState == TradeState.Valuation, "Trade state is not 'Valuation'."); _; } - modifier onlyWhenInTransfer() { - require(tradeState == TradeState.InTransfer, "Trade state is not 'InTransfer'."); _; - } - TradeState internal tradeState; + modifier onlyWhenInTermination () { + require(tradeState == TradeState.InTermination, "Trade state is not 'InTermination'."); _; + } modifier onlyCounterparty() { require(msg.sender == party1 || msg.sender == party2, "You are not a counterparty."); _; } + TradeState private tradeState; + address internal party1; address internal party2; address internal receivingParty; @@ -95,12 +102,8 @@ abstract contract SDC is ISDC { string internal tradeID; string internal tradeData; mapping(uint256 => address) internal pendingRequests; // Stores open request hashes for several requests: initiation, update and termination - bool internal mutuallyTerminated = false; int256 terminationPayment; - - int256[] internal settlementAmounts; - string[] internal settlementData; - + int256 upfrontPayment; /* * SettlementToken holds: @@ -116,12 +119,15 @@ abstract contract SDC is ISDC { address _party2, address _settlementToken ) { + terminationPayment = 0; + upfrontPayment = 0; party1 = _party1; party2 = _party2; settlementToken = ERC20Settlement(_settlementToken); settlementToken.setSDCAddress(address(this)); tradeState = TradeState.Inactive; } + /* * generates a hash from tradeData and generates a map entry in openRequests * emits a TradeIncepted @@ -134,6 +140,7 @@ abstract contract SDC is ISDC { uint256 transactionHash = uint256(keccak256(abi.encode(msg.sender,_withParty,_tradeData,_position, _paymentAmount,_initialSettlementData))); pendingRequests[transactionHash] = msg.sender; receivingParty = _position == 1 ? msg.sender : _withParty; + upfrontPayment = _position == 1 ? _paymentAmount : -_paymentAmount; // upfrontPayment is saved with view on the receiving party tradeID = Strings.toString(transactionHash); tradeData = _tradeData; // Set trade data to enable querying already in inception state emit TradeIncepted(msg.sender, tradeID, _tradeData); @@ -152,18 +159,9 @@ abstract contract SDC is ISDC { delete pendingRequests[transactionHash]; // Delete Pending Request tradeState = TradeState.Confirmed; emit TradeConfirmed(msg.sender, tradeID); - address upfrontPayer; - if (_position==1 && _paymentAmount < 0) // payment amount negative means from a long position : party has to pay - upfrontPayer = msg.sender; - else if (_position==1 && _paymentAmount > 0) - upfrontPayer = _withParty; - else if (_position==-1 && _paymentAmount < 0) // payment amount negative means from a short position : party has to pay - upfrontPayer = msg.sender; - else - upfrontPayer = _withParty; - settlementData.push(_initialSettlementData); - uint256 absPaymentAmount = uint256(abs(_paymentAmount)); - processTradeAfterConfirmation(upfrontPayer, absPaymentAmount); + address upfrontPayer = upfrontPayment > 0 ? otherParty(receivingParty) : receivingParty; + uint256 upfrontTransferAmount = uint256(abs(_paymentAmount)); + processTradeAfterConfirmation(upfrontPayer, upfrontTransferAmount,_initialSettlementData); } /* @@ -191,8 +189,8 @@ abstract contract SDC is ISDC { require(keccak256(abi.encodePacked(tradeID)) == keccak256(abi.encodePacked(_tradeId)), "Trade ID mismatch"); uint256 hash = uint256(keccak256(abi.encode(_tradeId, "terminate", _terminationPayment, terminationTerms))); pendingRequests[hash] = msg.sender; - - emit TradeTerminationRequest(msg.sender, _tradeId, _terminationPayment, terminationTerms); + terminationPayment = _terminationPayment; // termination payment will be provided in view of receiving party + emit TradeTerminationRequest(msg.sender, _tradeId, terminationPayment, terminationTerms); } /* @@ -205,13 +203,13 @@ abstract contract SDC is ISDC { uint256 hashConfirm = uint256(keccak256(abi.encode(_tradeId, "terminate", -_terminationPayment, terminationTerms))); require(pendingRequests[hashConfirm] == pendingRequestParty, "Confirmation of termination failed due to wrong party or missing request"); delete pendingRequests[hashConfirm]; - mutuallyTerminated = true; - terminationPayment = _terminationPayment; - emit TradeTerminationConfirmed(msg.sender, _tradeId, -_terminationPayment, terminationTerms); - /* Trigger final Settlement */ - address initiator = msg.sender; - tradeState = TradeState.Valuation; - emit TradeSettlementRequest(initiator, tradeData, settlementData[settlementData.length - 1]); + emit TradeTerminationConfirmed(msg.sender, _tradeId, terminationPayment, terminationTerms); + /* Trigger Termination Payment Amount */ + address payerAddress = terminationPayment > 0 ? otherParty(receivingParty) : receivingParty; + uint256 absPaymentAmount = uint256(abs(_terminationPayment)); + setTradeState(TradeState.InTermination); + processTradeAfterMutualTermination(payerAddress,absPaymentAmount,terminationTerms); + } /* @@ -227,49 +225,63 @@ abstract contract SDC is ISDC { emit TradeTerminationCanceled(msg.sender, _tradeId, terminationTerms); } - function processTradeAfterConfirmation(address upfrontPayer, uint256 upfrontPayment) virtual internal; - /* - * Utilities - */ + * Booking of the upfrontPayment and implementation specific setups of margin buffers / wallets. + */ + function processTradeAfterConfirmation(address upfrontPayer, uint256 upfrontPayment, string memory initialSettlementData) virtual internal; - /** - * Absolute value of an integer + /* + * Booking of the terminationAmount and implementation specific cleanup of margin buffers / wallets. */ - function abs(int x) internal pure returns (int256) { - return x >= 0 ? x : -x; - } + function processTradeAfterMutualTermination(address terminationFeePayer, uint256 terminationAmount, string memory terminationData) virtual internal; - /** - * Maximum value of two integers + /* + * Management of Trade States */ - function max(int a, int b) internal pure returns (int256) { - return a > b ? a : b; + function inStateIncepted() public view returns (bool) { return tradeState == TradeState.Incepted; } + function inStateConfirmed() public view returns (bool) { return tradeState == TradeState.Confirmed; } + function inStateSettled() public view returns (bool) { return tradeState == TradeState.Settled; } + function inStateTransfer() public view returns (bool) { return tradeState == TradeState.InTransfer; } + function inStateTermination() public view returns (bool) { return tradeState == TradeState.InTermination; } + function inStateTerminated() public view returns (bool) { return tradeState == TradeState.Terminated; } + + function getTradeState() public view returns (TradeState) { + return tradeState; } - /** - * Minimum value of two integers - */ - function min(int a, int b) internal pure returns (int256) { - return a < b ? a : b; + function setTradeState(TradeState newState) internal { + if ( newState == TradeState.Incepted && tradeState != TradeState.Inactive) + revert("Provided Trade state is not allowed"); + if ( newState == TradeState.Confirmed && tradeState != TradeState.Incepted) + revert("Provided Trade state is not allowed"); + if ( newState == TradeState.InTransfer && !(tradeState == TradeState.Confirmed || tradeState == TradeState.Valuation) ) + revert("Provided Trade state is not allowed"); + if ( newState == TradeState.Valuation && tradeState != TradeState.Settled) + revert("Provided Trade state is not allowed"); + if ( newState == TradeState.InTermination && !(tradeState == TradeState.InTransfer || tradeState == TradeState.Settled ) ) + revert("Provided Trade state is not allowed"); + tradeState = newState; } + /* + * Upfront and termination payments. + */ - function getTokenAddress() public view returns(address) { - return address(settlementToken); + function getReceivingParty() public view returns (address) { + return receivingParty; } - function getTradeState() public view returns (TradeState) { - return tradeState; + function getUpfrontPayment() public view returns (int) { + return upfrontPayment; } - /** - * Other party - */ - function otherParty(address party) internal view returns (address) { - return (party == party1 ? party2 : party1); + function getTerminationPayment() public view returns (int) { + return terminationPayment; } + /* + * Trade Specification (ID, Token, Data) + */ function getTradeID() public view returns (string memory) { return tradeID; @@ -279,9 +291,43 @@ abstract contract SDC is ISDC { tradeID= _tradeID; } + function getTokenAddress() public view returns(address) { + return address(settlementToken); + } + function getTradeData() public view returns (string memory) { return tradeData; } + /* + * Utilities (internal) + */ + + /** + * Other party + */ + function otherParty(address party) internal view returns (address) { + return (party == party1 ? party2 : party1); + } + /** + * Maximum value of two integers + */ + function max(int a, int b) internal pure returns (int256) { + return a > b ? a : b; + } + + /** + * Minimum value of two integers + */ + function min(int a, int b) internal pure returns (int256) { + return a < b ? a : b; + } + + /** + * Absolute value of an integer + */ + function abs(int x) internal pure returns (int256) { + return x >= 0 ? x : -x; + } } \ No newline at end of file diff --git a/assets/erc-6123/contracts/SDCPledgedBalance.sol b/assets/erc-6123/contracts/SDCPledgedBalance.sol index 17bd7edfe0..357ba86f1a 100644 --- a/assets/erc-6123/contracts/SDCPledgedBalance.sol +++ b/assets/erc-6123/contracts/SDCPledgedBalance.sol @@ -37,50 +37,29 @@ import "./ERC20Settlement.sol"; contract SDCPledgedBalance is SDC { + struct MarginRequirement { uint256 buffer; uint256 terminationFee; } - mapping(address => MarginRequirement) private marginRequirements; // Storage of M and P per counterparty address + int256[] private settlementAmounts; + string[] private settlementData; + constructor( address _party1, address _party2, address _settlementToken, - uint256 _initialBuffer, // m - uint256 _initalTerminationFee // p + uint256 _initialBuffer, // m + uint256 _initalTerminationFee // p ) SDC(_party1,_party2,_settlementToken) { marginRequirements[party1] = MarginRequirement(_initialBuffer, _initalTerminationFee); marginRequirements[party2] = MarginRequirement(_initialBuffer, _initalTerminationFee); } - function processTradeAfterConfirmation(address upfrontPayer, uint256 upfrontPayment) override internal{ - uint256 marginRequirementParty1 = uint(marginRequirements[party1].buffer + marginRequirements[party1].terminationFee ); - uint256 marginRequirementParty2 = uint(marginRequirements[party2].buffer + marginRequirements[party2].terminationFee ); - uint256 requiredBalanceParty1 = marginRequirementParty1 + (upfrontPayer==party1 ? upfrontPayment : 0); - uint256 requiredBalanceParty2 = marginRequirementParty2 + (upfrontPayer==party2 ? upfrontPayment : 0); - bool isAvailableParty1 = (settlementToken.balanceOf(party1) >= requiredBalanceParty1) && (settlementToken.allowance(party1, address(this)) >= requiredBalanceParty1); - bool isAvailableParty2 = (settlementToken.balanceOf(party2) >= requiredBalanceParty2) && (settlementToken.allowance(party2, address(this)) >= requiredBalanceParty2); - if (isAvailableParty1 && isAvailableParty2){ // Pre-Conditions: M + P needs to be locked (i.e. pledged) - address[] memory from = new address[](3); - address[] memory to = new address[](3); - uint256[] memory amounts = new uint256[](3); - from[0] = party1; to[0] = address(this); amounts[0] = marginRequirementParty1; - from[1] = party2; to[1] = address(this); amounts[1] = marginRequirementParty2; - from[2] = upfrontPayer; to[2] = otherParty(upfrontPayer); amounts[2] = upfrontPayment; - uint256 transactionID = uint256(keccak256(abi.encodePacked(from,to,amounts))); - tradeState = TradeState.InTransfer; - settlementToken.checkedBatchTransferFrom(from,to,amounts,transactionID); // Atomic Transfer - } - else { - tradeState = TradeState.Inactive; - emit TradeTerminated("Insufficient Balance or Allowance"); - } - } - /* * Settlement can be initiated when margin accounts are locked, a valuation request event is emitted containing tradeData and valuationViewParty * Changes Process State to Valuation&Settlement @@ -88,7 +67,7 @@ contract SDCPledgedBalance is SDC { */ function initiateSettlement() external override onlyCounterparty onlyWhenSettled { address initiator = msg.sender; - tradeState = TradeState.Valuation; + setTradeState(TradeState.Valuation); emit TradeSettlementRequest(initiator, tradeData, settlementData[settlementData.length - 1]); } @@ -98,92 +77,124 @@ contract SDCPledgedBalance is SDC { * Checks Settlement amount according to valuationViewParty: If SettlementAmount is > 0, valuationViewParty receives * can be called only when ProcessState = ValuationAndSettlement */ - function performSettlement(int256 settlementAmount, string memory _settlementData) onlyWhenValuation external override { - - if (mutuallyTerminated){ - settlementAmount = settlementAmount + terminationPayment; - } - + (address settlementPayer,uint256 transferAmount) = determineTransferAmountAndPayerAddress(settlementAmount); + int cappedSettlementAmount = settlementPayer == receivingParty ? -int256(transferAmount) : int256(transferAmount); settlementData.push(_settlementData); - settlementAmounts.push(settlementAmount); + settlementAmounts.push(cappedSettlementAmount); // save the capped settlement amount + uint256 transactionID = uint256(keccak256(abi.encodePacked(settlementPayer,otherParty(settlementPayer), transferAmount, block.timestamp))); + address[] memory from = new address[](1); + address[] memory to = new address[](1); + uint256[] memory amounts = new uint256[](1); + from[0] = settlementPayer; to[0] = otherParty(settlementPayer); amounts[0] = transferAmount; + emit TradeSettlementPhase(); + setTradeState(TradeState.InTransfer); + settlementToken.checkedBatchTransferFrom(from,to,amounts,transactionID); + } - uint256 transferAmount; - address settlementPayer; - (settlementPayer, transferAmount) = determineTransferAmountAndPayerAddress(settlementAmount); - - if (settlementToken.balanceOf(settlementPayer) >= transferAmount && - settlementToken.allowance(settlementPayer,address(this)) >= transferAmount) { /* Good case: Balances are sufficient and token has enough approval */ - uint256 transactionID = uint256(keccak256(abi.encodePacked(settlementPayer,otherParty(settlementPayer), transferAmount))); - emit TradeSettlementPhase(); - tradeState = TradeState.InTransfer; - address[] memory from = new address[](1); - address[] memory to = new address[](1); - uint256[] memory amounts = new uint256[](1); - from[0] = settlementPayer; to[0] = otherParty(settlementPayer); amounts[0] = transferAmount; - tradeState = TradeState.InTransfer; - settlementToken.checkedBatchTransferFrom(from,to,amounts,transactionID); + /* + * afterTransfer processes SDC depending on success of the respective payment and depending on the current trade state + * Good Case: state will be settled, failed settlement will trigger the pledge balance transfer and termination + */ + function afterTransfer(bool success, uint256 transactionHash) external override { + if ( inStateConfirmed()){ + if (success){ + setTradeState(TradeState.Settled); + emit TradeActivated(getTradeID()); + } + else{ + setTradeState(TradeState.Terminated); + emit TradeTerminated("Upfront Transfer Failure"); + } } - else { /* Bad Case: Process termination by booking from own balance */ - tradeState = TradeState.InTransfer; - _processAfterTransfer(false); + else if ( inStateTransfer() ){ + if (success){ + setTradeState(TradeState.Settled); + emit TradeSettled("Settlement Settled - Pledge Transfer"); + } + else{ // Settlement & Pledge Case: transferAmount is transferred from SDC balance (i.e. pledged balance). + int256 settlementAmount = settlementAmounts[settlementAmounts.length-1]; + setTradeState(TradeState.InTermination); + processTerminationWithPledge(settlementAmount); + emit TradeTerminated("Settlement Failed - Pledge Transfer"); + } } + else if( inStateTermination() ){ + if (success){ + setTradeState(TradeState.Terminated); + emit TradeTerminated("Trade terminated sucessfully"); + } + else{ + emit TradeTerminated("Mutual Termination failed - Pledge Transfer"); + processTerminationWithPledge(getTerminationPayment()); + } + } + else + revert("Trade State does not allow to call 'afterTransfer'"); } + /* + * internal function which determines the capped settlement amount and poyer address + */ function determineTransferAmountAndPayerAddress(int256 settlementAmount) internal view returns(address, uint256) { address settlementReceiver = settlementAmount > 0 ? receivingParty : otherParty(receivingParty); address settlementPayer = otherParty(settlementReceiver); uint256 transferAmount; if (settlementAmount > 0) - transferAmount = uint256(abs(min( settlementAmount, int(marginRequirements[settlementPayer].buffer)))); + transferAmount = uint256(abs(min( settlementAmount, int256(marginRequirements[settlementPayer].buffer)))); else - transferAmount = uint256(abs(max( settlementAmount, -int(marginRequirements[settlementReceiver].buffer)))); + transferAmount = uint256(abs(max( settlementAmount, -int256(marginRequirements[settlementReceiver].buffer)))); return (settlementPayer,transferAmount); } - function afterTransfer(uint256 /* transactionHash */, bool success) external override onlyWhenInTransfer { - // Note: parameter transactionHash currenty unused - emit TradeSettled(); - _processAfterTransfer(success); + /* + * internal function which pepares the settlement tranfer after confirmation. + * Batched Transfer consists of Upfront Payment and Initial Prefunding to SDC Address + */ + + function processTradeAfterConfirmation(address upfrontPayer, uint256 upfrontPayment, string memory initialSettlementData) override internal{ + settlementAmounts.push(0); + settlementData.push(initialSettlementData); + address[] memory from = new address[](3); + address[] memory to = new address[](3); + uint256[] memory amounts = new uint256[](3); + from[0] = party1; to[0] = address(this); amounts[0] = uint(marginRequirements[party1].buffer + marginRequirements[party1].terminationFee ); + from[1] = party2; to[1] = address(this); amounts[1] = uint(marginRequirements[party2].buffer + marginRequirements[party2].terminationFee ); + from[2] = upfrontPayer; to[2] = otherParty(upfrontPayer); amounts[2] = upfrontPayment; + uint256 transactionID = uint256(keccak256(abi.encodePacked(from,to,amounts))); + settlementToken.checkedBatchTransferFrom(from,to,amounts,transactionID); // Batched Transfer } - function _processAfterTransfer(bool success) internal{ - if(success){ - emit TradeSettled(); - if (tradeState == TradeState.Terminated || mutuallyTerminated){ - tradeState = TradeState.Inactive; - } - else{ - tradeState = TradeState.Settled; - } - } - else{ // TRANSFER HAS FAILED - if (settlementData.length == 1){ // Case after confirmTrade where Transfer of upfront has failed - tradeState = TradeState.Inactive; - emit TradeTerminated("Initial Upfront Transfer fail - Trade Inactive"); - } - else{ - // Settlement & Pledge Case: transferAmount is transferred from SDC balance (i.e. pledged balance). - int256 settlementAmount = settlementAmounts[settlementAmounts.length-1]; - uint256 transferAmount; - address settlementPayer; - (settlementPayer, transferAmount) = determineTransferAmountAndPayerAddress(settlementAmount); - address settlementReceiver = otherParty(settlementPayer); - settlementToken.approve(settlementPayer,uint256(marginRequirements[settlementPayer].buffer - transferAmount)); // Release Buffers - settlementToken.approve(settlementReceiver,uint256(marginRequirements[settlementReceiver].buffer)); // Release Buffers - - // Do Pledge Transfer from own balances including termination fee - tradeState = TradeState.Terminated; - emit TradeTerminated("Trade terminated due to regular settlement failure"); - address[] memory to = new address[](2); - uint256[] memory amounts = new uint256[](2); - to[0] = settlementReceiver; amounts[0] = uint256(transferAmount); - to[1] = settlementReceiver; amounts[1] = uint256(marginRequirements[settlementPayer].terminationFee); - uint256 transactionID = uint256(keccak256(abi.encodePacked(to,amounts))); - settlementToken.checkedBatchTransfer(to,amounts,transactionID); - } - } + /* + * internal function which processes mutual termination, transfers termination payment and releases pledged balances from sdc address + */ + function processTradeAfterMutualTermination(address terminationFeePayer, uint256 terminationAmount, string memory terminationData) override internal{ + settlementAmounts.push(0); // termination payment is saved separately + settlementData.push(terminationData); + address[] memory from = new address[](3); + address[] memory to = new address[](3); + uint256[] memory amounts = new uint256[](3); + from[0] = address(this); to[0] = party1; amounts[0] = uint(marginRequirements[party1].buffer + marginRequirements[party1].terminationFee ); // Release buffers + from[1] = address(this); to[1] = party2; amounts[1] = uint(marginRequirements[party2].buffer + marginRequirements[party2].terminationFee ); // Release buffers + from[2] = terminationFeePayer; to[2] = otherParty(terminationFeePayer); amounts[2] = terminationAmount; + uint256 transactionID = uint256(keccak256(abi.encodePacked(from,to,amounts))); + settlementToken.checkedBatchTransferFrom(from,to,amounts,transactionID); // Batched Transfer } + + /* function which perfoms the "Pledged Booking" in case of failed settlement, transferring open settlement amount as well as termination fee from sdc's own balance + */ + function processTerminationWithPledge(int256 settlementAmount) internal{ + (address settlementPayer, uint256 transferAmount) = determineTransferAmountAndPayerAddress(settlementAmount); + address settlementReceiver = otherParty(settlementPayer); + address[] memory to = new address[](3); + uint256[] memory amounts = new uint256[](3); + to[0] = settlementReceiver; amounts[0] = transferAmount+marginRequirements[settlementPayer].terminationFee; // Settlement from Own Balance + to[1] = settlementReceiver; amounts[1] = marginRequirements[settlementReceiver].terminationFee + marginRequirements[settlementReceiver].buffer; // Release + to[2] = settlementPayer; amounts[2] = marginRequirements[settlementPayer].buffer-transferAmount; // Release of Buffer + uint256 transactionID = uint256(keccak256(abi.encodePacked(to,amounts))); + settlementToken.checkedBatchTransfer(to,amounts,transactionID); + } + } diff --git a/assets/erc-6123/doc/sequence.puml b/assets/erc-6123/doc/sequence.puml index 5ff19b5c7a..ea04005b0e 100644 --- a/assets/erc-6123/doc/sequence.puml +++ b/assets/erc-6123/doc/sequence.puml @@ -27,8 +27,6 @@ CP1 ->SettlementToken: allocate balances CP2 ->SettlementToken: allocate balances CP1 ->SDC: tx 'deploy' a SDC with token address activate SDC -CP1 ->SettlementToken: tx set 'allowance' for SDC address -CP2 ->SettlementToken: tx set 'allowance' for SDC address CP1 ->SDC: tx 'inceptTrade' SDC-->EventHandler: emit TradeIncepted @@ -71,7 +69,7 @@ else success else fail SDC->SettlementToken: tx 'transfer' Settlement Amount from SDC Balance to Receiving Party SDC->SettlementToken: tx 'transfer' Termination Fee from SDC Balance to Receiving Party - SDC->SettlementToken: tx 'approve' - Unlock remaing Party Balances + SDC->SettlementToken: tx 'transfer' - Release remainigBalances to parties == TradeState 'Terminated' == end diff --git a/assets/erc-6123/package.json b/assets/erc-6123/package.json index 24d9974fb7..19e85871df 100644 --- a/assets/erc-6123/package.json +++ b/assets/erc-6123/package.json @@ -1,6 +1,6 @@ { "name": "@finmath.net/sdc", - "version": "0.3.1", + "version": "0.4.0", "description": "Solidity Smart Derivative Contracts", "author": "Christian Fries, Peter Kohl-Landgraf, Alexandros Korpis", "license": "ISC", diff --git a/assets/erc-6123/test/SDCTests.js b/assets/erc-6123/test/SDCTests.js index b21ff704c4..dececef6c7 100644 --- a/assets/erc-6123/test/SDCTests.js +++ b/assets/erc-6123/test/SDCTests.js @@ -13,18 +13,19 @@ describe("Livecycle Unit-Tests for SDC Plege Balance", () => { Valuation: 3, InTransfer: 4, Settled: 5, - Terminated: 6 + InTermination: 6, + Terminated: 7 }; const abiCoder = new AbiCoder(); const trade_data = "here are the trade specification { counterparty2 = _counterparty2; ERC20Factory = await ethers.getContractFactory("ERC20Settlement"); SDCFactory = await ethers.getContractFactory("SDCPledgedBalance"); - token = await ERC20Factory.deploy(); - await token.deployed(); + //oken = await ERC20Factory.deploy(); + // await token.deployed(); }); - it("Initial minting and approvals for SDC", async () => { - await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); - await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); - }); - it("Counterparties incept and confirm a trade successfully, upfront is transferred", async () => { + + it("1. Counterparties incept and confirm a trade successfully, Upfront is transferred from CP1 to CP2", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount+upfront); let trade_id =""; - const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, -upfront, "initialMarketData"); await expect(incept_call).to.emit(sdc, "TradeIncepted"); - const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, -upfront, "initialMarketData"); + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, upfront, "initialMarketData"); await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); let trade_state = await sdc.connect(counterparty1).getTradeState(); await expect(trade_state).equal(TradeState.Settled); + let sdc_balance = await token.connect(counterparty1).balanceOf(sdc.address); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + console.log("Balance for SDC-Address: %s", sdc_balance); + await expect(sdc_balance).equal(2*(terminationFee+marginBufferAmount)); + await expect(cp1_balance).equal(initialLiquidityBalance-upfront-terminationFee-marginBufferAmount); + await expect(cp2_balance).equal(initialLiquidityBalance+upfront-terminationFee-marginBufferAmount); }); - it("Counterparty incepts and cancels trade successfully", async () => { + it("2a. CP1 is receiving party and pays initial Upfront (no buffers)", async () => { + let token = await ERC20Factory.deploy(); + let upfront1 = 150; + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,0,0); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, -upfront1, "initialMarketData"); + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, upfront1, "initialMarketData"); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(cp1_balance).equal(initialLiquidityBalance-upfront1); + await expect(cp2_balance).equal(initialLiquidityBalance+upfront1); + }); + + it("2b. CP1 is paying party and receives initial Upfront (no buffers)", async () => { + let token = await ERC20Factory.deploy(); + let upfront1 = 150; + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,0,0); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, -1, upfront1, "initialMarketData"); + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, 1, -upfront1, "initialMarketData"); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(cp1_balance).equal(initialLiquidityBalance+upfront1); + await expect(cp2_balance).equal(initialLiquidityBalance-upfront1); + }); + + it("2c. CP2 is paying party and pays initial Upfront (no buffers)", async () => { + let token = await ERC20Factory.deploy(); + let upfront1 = 150; + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,0,0); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront1, "initialMarketData"); + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, -upfront1, "initialMarketData"); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(cp1_balance).equal(initialLiquidityBalance+upfront1); + await expect(cp2_balance).equal(initialLiquidityBalance-upfront1); + }); + + it("3. Counterparty incepts and cancels trade successfully", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount+upfront); let trade_id =""; const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); await expect(incept_call).to.emit(sdc, "TradeIncepted"); @@ -80,87 +125,126 @@ describe("Livecycle Unit-Tests for SDC Plege Balance", () => { await expect(trade_state).equal(TradeState.Inactive); }); - it("Not enough approval to transfer upfront payment", async () => { - let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount); - const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); - await expect(incept_call).to.emit(sdc, "TradeIncepted"); - const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, -upfront, "initialMarketData"); - await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); - let trade_state = await sdc.connect(counterparty1).getTradeState(); - await expect(trade_state).equal(TradeState.Inactive); - }); + it("4. Not enough balance to transfer upfront payment", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - it("Trade Matching fails", async () => { - let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount); - const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); - await expect(incept_call).to.emit(sdc, "TradeIncepted"); - const confirm_call = sdc.connect(counterparty2).confirmTrade(counterparty1.address, "none", -1, -upfront, "initialMarketData23"); - await expect(confirm_call).to.be.revertedWith("Confirmation fails due to inconsistent trade data or wrong party address"); - }); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, 2*initialLiquidityBalance, "initialMarketData"); + await expect(incept_call).to.emit(sdc, "TradeIncepted"); + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, -2*initialLiquidityBalance, "initialMarketData"); + await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); + let trade_state = await sdc.connect(counterparty1).getTradeState(); + await expect(trade_state).equal(TradeState.Terminated); + let sdc_balance = await token.connect(counterparty1).balanceOf(sdc.address); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(sdc_balance).equal(0); + await expect(cp1_balance).equal(initialLiquidityBalance); + await expect(cp2_balance).equal(initialLiquidityBalance); + }); - it("Trade cancellation fails due to wrong party calling cancel", async () => { - let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount); - const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); - await expect(incept_call).to.emit(sdc, "TradeIncepted"); - const confirm_call = sdc.connect(counterparty2).cancelTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); - await expect(confirm_call).to.be.revertedWith("Cancellation fails due to inconsistent trade data or wrong party address"); - }); + it("5. Trade Matching fails", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); + await expect(incept_call).to.emit(sdc, "TradeIncepted"); + const confirm_call = sdc.connect(counterparty2).confirmTrade(counterparty1.address, "none", -1, -upfront, "initialMarketData23"); + await expect(confirm_call).to.be.revertedWith("Confirmation fails due to inconsistent trade data or wrong party address"); + }); - it("Trade cancellation fails due to wrong arguments", async () => { - let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount); - const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); - await expect(incept_call).to.emit(sdc, "TradeIncepted"); - const confirm_call = sdc.connect(counterparty1).cancelTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData23"); - await expect(confirm_call).to.be.revertedWith("Cancellation fails due to inconsistent trade data or wrong party address"); - }); + it("6. Trade cancellation fails due to wrong party calling cancel", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); + await expect(incept_call).to.emit(sdc, "TradeIncepted"); + const confirm_call = sdc.connect(counterparty2).cancelTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); + await expect(confirm_call).to.be.revertedWith("Cancellation fails due to inconsistent trade data or wrong party address"); + }); - it("Counterparties incept and confirm a trade successfully, upfront is transferred, trade is terminated", async () => { + it("7. Trade cancellation fails due to wrong arguments", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount+upfront); - // Incept trade (and fetch tradeId) + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); + await expect(incept_call).to.emit(sdc, "TradeIncepted"); + const confirm_call = sdc.connect(counterparty1).cancelTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData23"); + await expect(confirm_call).to.be.revertedWith("Cancellation fails due to inconsistent trade data or wrong party address"); + }); + + it("8. Counterparties incept and confirm, upfront is transferred from CP2 to CP1, Trade is terminated with Payment from CP2 to CP1", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); const receipt = await incept_call.wait(); const event = receipt.events.find(event => event.event === 'TradeIncepted'); const trade_id = event.args[1]; - const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, -upfront, "initialMarketData"); await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); - const terminate_call = await sdc.connect(counterparty1).requestTradeTermination(trade_id, terminationPayment, "terminationTerms"); await expect(terminate_call).to.emit(sdc, "TradeTerminationRequest"); - const confirm_terminate_call = await sdc.connect(counterparty2).confirmTradeTermination(trade_id, -terminationPayment, "terminationTerms"); await expect(confirm_terminate_call).to.emit(sdc, "TradeTerminationConfirmed"); let trade_state = await sdc.connect(counterparty1).getTradeState(); - await expect(trade_state).equal(TradeState.Valuation); + await expect(trade_state).equal(TradeState.Terminated); + let sdc_balance = await token.connect(counterparty1).balanceOf(sdc.address); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(sdc_balance).equal(0); + await expect(cp1_balance).equal(initialLiquidityBalance+upfront+terminationPayment); + await expect(cp2_balance).equal(initialLiquidityBalance-upfront-terminationPayment); }); - it("Successful Settlement", async () => { + it("9a. CP1 is Receiving Party, Trade-Termination is incepted by CP2 which receives the termination payment from CP1", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, 0, "initialMarketData"); + const receipt = await incept_call.wait(); + const event = receipt.events.find(event => event.event === 'TradeIncepted'); + const trade_id = event.args[1]; + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, 0, "initialMarketData"); + const terminate_call = await sdc.connect(counterparty2).requestTradeTermination(trade_id, -terminationPayment, "terminationTerms"); + const confirm_terminate_call = await sdc.connect(counterparty1).confirmTradeTermination(trade_id, +terminationPayment, "terminationTerms"); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(cp1_balance).equal(initialLiquidityBalance-terminationPayment); + await expect(cp2_balance).equal(initialLiquidityBalance+terminationPayment); + }); + it("9b. CP1 is Receiving Party, Trade-Termination is incepted by CP1 which pays the termination payment to CP2", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, 0, "initialMarketData"); + const receipt = await incept_call.wait(); + const event = receipt.events.find(event => event.event === 'TradeIncepted'); + const trade_id = event.args[1]; + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, 0, "initialMarketData"); + const terminate_call = await sdc.connect(counterparty1).requestTradeTermination(trade_id, -terminationPayment, "terminationTerms"); + const confirm_terminate_call = await sdc.connect(counterparty2).confirmTradeTermination(trade_id, +terminationPayment, "terminationTerms"); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(cp1_balance).equal(initialLiquidityBalance-terminationPayment); + await expect(cp2_balance).equal(initialLiquidityBalance+terminationPayment); + }); + + it("10. Successful Inception with Upfront transferred from CP2 to CP1 + successful settlement transferred from CP1 to CP2", async () => { + let settlementAmount = -245; + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); -// console.log("SDC Address: %s", sdc.address); - await token.connect(counterparty1).approve(sdc.address,terminationFee+10*marginBufferAmount); //Approve for 10*margin amount - await token.connect(counterparty2).approve(sdc.address,terminationFee+10*marginBufferAmount+upfront); let trade_id =""; const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, upfront, "initialMarketData"); await expect(incept_call).to.emit(sdc, "TradeIncepted"); @@ -168,11 +252,73 @@ describe("Livecycle Unit-Tests for SDC Plege Balance", () => { await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); const initSettlementPhase = sdc.connect(counterparty2).initiateSettlement(); await expect(initSettlementPhase).to.emit(sdc, "TradeSettlementRequest"); - const balance_call = await token.connect(counterparty2).balanceOf(counterparty2.address); -// console.log("Balance: %s", balance_call); - const performSettlementCall = sdc.connect(counterparty1).performSettlement(1,"settlementData"); + + const performSettlementCall = sdc.connect(counterparty1).performSettlement(settlementAmount,"settlementData"); await expect(performSettlementCall).to.emit(sdc, "TradeSettlementPhase"); let trade_state = await sdc.connect(counterparty1).getTradeState(); await expect(trade_state).equal(TradeState.Settled); + let sdc_balance = await token.connect(counterparty1).balanceOf(sdc.address); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(sdc_balance).equal(2*(terminationFee+marginBufferAmount)); + await expect(cp1_balance).equal(initialLiquidityBalance-terminationFee-marginBufferAmount+upfront+settlementAmount); + await expect(cp2_balance).equal(initialLiquidityBalance-terminationFee-marginBufferAmount-upfront-settlementAmount); + }); + + it("11. Failed settlement followed by Termination with Pledge Case", async () => { + let settlementAmount = -500; + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); + let trade_id =""; + let upfront_max = initialLiquidityBalance - marginBufferAmount - terminationFee; + + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, -upfront_max, "initialMarketData"); + await expect(incept_call).to.emit(sdc, "TradeIncepted"); + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, +upfront_max, "initialMarketData"); + await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); + + const initSettlementPhase = sdc.connect(counterparty2).initiateSettlement(); + await expect(initSettlementPhase).to.emit(sdc, "TradeSettlementRequest"); + + const performSettlementCall = sdc.connect(counterparty1).performSettlement(settlementAmount,"settlementData"); + await expect(performSettlementCall).to.emit(sdc, "TradeSettlementPhase"); + let trade_state = await sdc.connect(counterparty1).getTradeState(); + let sdc_balance = await token.connect(counterparty1).balanceOf(sdc.address); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + + await expect(trade_state).equal(TradeState.Terminated); + await expect(sdc_balance).equal(0); + await expect(cp1_balance).equal(marginBufferAmount+settlementAmount); + await expect(cp2_balance).equal(initialLiquidityBalance+upfront_max-settlementAmount+terminationFee); + + }); + + it("12. Failed Mutual Termination: Payment from CP1 to CP2 results in pledge case with capped termination fee amount being transferred", async () => { + let token = await ERC20Factory.deploy(); + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + let sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,token.address,marginBufferAmount,terminationFee); + const incept_call = await sdc.connect(counterparty1).inceptTrade(counterparty2.address, trade_data, 1, 0, "initialMarketData"); + const receipt = await incept_call.wait(); + const event = receipt.events.find(event => event.event === 'TradeIncepted'); + const trade_id = event.args[1]; + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, 0, "initialMarketData"); + await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); + const terminate_call = await sdc.connect(counterparty1).requestTradeTermination(trade_id, 10000000, "terminationTerms"); + await expect(terminate_call).to.emit(sdc, "TradeTerminationRequest"); + const confirm_terminate_call = await sdc.connect(counterparty2).confirmTradeTermination(trade_id, -10000000, "terminationTerms"); + await expect(confirm_terminate_call).to.emit(sdc, "TradeTerminationConfirmed"); + let trade_state = await sdc.connect(counterparty1).getTradeState(); + await expect(trade_state).equal(TradeState.Terminated); + let sdc_balance = await token.connect(counterparty1).balanceOf(sdc.address); + let cp1_balance = await token.connect(counterparty1).balanceOf(counterparty1.address); + let cp2_balance = await token.connect(counterparty1).balanceOf(counterparty2.address); + await expect(sdc_balance).equal(0); + await expect(cp1_balance).equal(initialLiquidityBalance+marginBufferAmount+terminationFee); + await expect(cp2_balance).equal(initialLiquidityBalance-marginBufferAmount-terminationFee); + }); }); \ No newline at end of file