diff --git a/ERCS/erc-6123.md b/ERCS/erc-6123.md index 9a92aa3ec2f08..4d246008104f2 100644 --- a/ERCS/erc-6123.md +++ b/ERCS/erc-6123.md @@ -1,10 +1,10 @@ --- eip: 6123 title: Smart Derivative Contract -description: A deterministic protocol for frictionless post-trade processing of OTC financial contracts +description: A deterministic protocol for frictionless trade processing of financial contracts author: Christian Fries (@cfries), Peter Kohl-Landgraf (@pekola), Alexandros Korpis (@kourouta) discussions-to: https://ethereum-magicians.org/t/eip-6123-smart-derivative-contract-frictionless-processing-of-financial-derivatives/12134 -status: Stagnant +status: Draft type: Standards Track category: ERC created: 2022-12-13 @@ -12,100 +12,117 @@ created: 2022-12-13 ## Abstract -The Smart Derivative Contract is a deterministic protocol to trade and process -financial derivative contracts frictionless and scalable in a completely automated way. Counterparty credit risk ís removed. -Known operational risks and complexities in post-trade processing are removed by construction as all process states -are fully specified and are known to the counterparties. +The Smart Derivative Contract (SDC) allows fully automizing and securing a financial product's - e.g. a financial derivative or bond - complete trade life cycle. +The SDC leverages the advantages of smart contracts to remove many of the frictions associated with the classical derivative life cycle. Most notably, the protocol allows the removal of counterpart risk essentially. +The SDC can be implemented using a pre-agreed valuation oracle and valuation model, removing ambiguity in the settlement amounts. The SDC provides methods and callbacks to enable fully automated and fully transactional settlements (delivery-versus-payment, payment-vs-payment). +Token-based settlement can be realized by any contract implementation implementing an [ERC-20](./eip-20.md) token. +Proof of concepts in terms of two legally binding digital Interest Rate Swaps were conducted in 2021 and 2022. ## Motivation ### Rethinking Financial Derivatives -By their very nature, so-called "over-the-counter (OTC)" financial contracts are bilateral contractual agreements on the exchange of long-dated cash flow schedules. +By their very nature, so-called "over-the-counter (OTC)" financial contracts are bilateral contractual agreements on exchanging long-dated cash flow schedules. Since these contracts change their intrinsic market value due to changing market environments, they are subject to counterparty credit risk when one counterparty is subject to default. -The initial white paper describes the concept of a Smart Derivative Contract with the central aim -to detach bilateral financial transactions from counterparty credit risk and to remove complexities +The initial white paper describes the concept of a Smart Derivative Contract (SDC) with the central aim to detach bilateral financial transactions from counterparty credit risk and to remove complexities in bilateral post-trade processing by a complete redesign. ### Concept of a Smart Derivative Contract -A Smart Derivative Contract is a deterministic settlement protocol which has the same economic behaviour as a collateralized OTC -Derivative. Every process state is specified; therefore, the entire post-trade process is known in advance. -A Smart Derivative Contract (SDC) settles outstanding net present value of the underlying financial contract on a frequent basis. With each settlement cycle net present value of the underlying contract is -exchanged, and the value of the contract is reset to zero. Pre-Agreed margin buffers are locked at the beginning of each settlement cycle such that settlement will be guaranteed up to a certain amount. -If a counterparty fails to obey contract rules, e.g. not provide sufficient prefunding, SDC will terminate automatically with the guaranteed transfer of a termination fee by the causing party. -These features enable two counterparties to process their financial contract fully decentralized without relying on a third central intermediary agent. -The process logic of SDC can be implemented as a finite state machine on solidity. An [EIP-20](./eip-20.md) token can be used for frictionless decentralized settlement, see reference implementation. -Combined with an appropriate external market data and valuation oracle which calculates net present values, each known OTC derivative contract is able to be processed using this standard protocol. +A Smart Derivative Contract is a deterministic settlement protocol with the same economic behaviour as a Financial Contract - e.g. an OTC-Derivative or a Bond. +Every process state is specified; therefore, the trade and post-trade process is known in advance and is deterministic over the trade's life cycle. An [ERC-20](./eip-20.md) token can be used for frictionless decentralized settlement, see reference implementation. We do provide a separate interface and implementation for a specific "Settlement Token" derived from [ERC-20](./eip-20.md). +These features enable two or multiple trade parties to process their financial contracts fully decentralized without relying on a third central intermediary agent. +The process logic of SDC can be implemented as a finite state machine on solidity. +### Applications + +The interface's life cycle functionality applies to several use cases. + +#### Collateralized OTC Derivative + +In the case of a collateralized OTC derivative, an SDC settles the outstanding net present value of the underlying financial contract on a frequent (e.g. daily) basis. With each settlement cycle, the net present value of the underlying contract is exchanged, and the value of the contract is reset to zero. Pre-agreed margin buffers are locked at the beginning of each settlement cycle so that settlement will be guaranteed up to a certain amount. +If a counterparty fails to obey contract rules, e.g. not providing sufficient pre-funding, SDC will terminate automatically with the guaranteed transfer of a termination fee by the causing party. +We provide a Reference Implementation for this case. + +#### Defaultable OTC Derivative + +A defaultable OTC Derivative has no Collateral Process in place. In that case, a smart derivative will settle the according cash flows as determined in the derivative contract specification. A defaultable OTC derivative might end in +a state 'Failure to Pay' if a settlement cannot be conducted. + +#### Smart Bond Contract + +The life cycle of a bond can also make use of the function catalogue below. The interface enables the issuer to allocate and redeem the bond as well as settle coupon payments. On the other hand, it allows bondholders to interact with each other, conducting secondary market trades. It all boils down to a settlement phase, which needs to be pre-agreed by both parties or triggered by the issuer +which can be processed in a completely frictionless way. ## Specification ### Methods -The following methods specify inception and post-trade live cycle of a Smart Derivative Contract. For futher information also please look at the interface documentation ISDC.sol. +The following methods specify a Smart Derivative Contract's trade initiation and settlement life cycle. For further information, please also look at the interface documentation ISDC.sol. -#### inceptTrade +#### Trade Initiation Phase: `inceptTrade` -A counterparty can initiate a trade by providing trade data as string and calling inceptTrade and initial settlement data. Only registered counteparties are allowed to use that function. +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(string memory _tradeData, string memory _initialSettlementData) external; +function inceptTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; ``` -#### confirmTrade +#### Trade Initiation Phase: `confirmTrade` -A counterparty can confirm a trade by providing the identical trade data and initial settlement information, which are already stored from inceptTrade call. +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(string memory _tradeData, string memory _initialSettlementData) external; +function confirmTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; ``` -#### initiatePrefunding +#### Trade Settlement Phase: `initiateSettlement` -This method checks whether contractual prefunding is provided by both counterparties as agreed in the contract terms. Triggers a contract termination if not. +Allows eligible participants (such as counterparties or a delegated agent) to trigger a settlement phase. ```solidity -function initiatePrefunding() external; +function initiateSettlement() external; ``` -#### initiateSettlement +#### Trade Settlement Phase: `performSettlement` -Allows eligible participants (such as counterparties or a delegated agent) to initiate a settlement. +Valuation may be provided on-chain or off-chain via an external oracle service that calculates the settlement or coupon amounts and uses external market data. +This method serves as a callback called from an external oracle providing settlement amount and used settlement data, which also get stored. +The settlement amount will be checked according to contract terms, resulting in either a regular settlement or a termination of the trade. ```solidity -function initiateSettlement() external; +function performSettlement(int256 settlementAmount, string memory settlementData) external; ``` -#### performSettlement +#### Trade Settlement Phase: `afterTransfer` -Valuation may be provided off-chain via an external oracle service that calculates net present value and uses external market data. -Method serves as callback called from an external oracle providing settlement amount and used settlement data which also get stored. -Settlement amount will be checked according to contract terms resulting in either a reqular settlement or a termination of the trade. +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. ```solidity -function performSettlement(int256 settlementAmount, string memory settlementData) external; +function afterTransfer(uint256 transactionHash, bool success) external; ``` -#### requestTermination -Allows an eligible party to request a mutual termination +#### Trade Termination: `requestTermination` + +Allows an eligible party to request a mutual termination with a termination amount she is willing to pay -```js -function requestTradeTermination(string memory tradeId) external; +```solidity +function requestTradeTermination(string memory tradeId, int256 _terminationPayment) external; ``` -#### confirmTradeTermination +#### Trade Termination: `confirmTradeTermination` -Allows eligible parties to confirm a formerly-requested mutual trade termination. +Allows an eligible party to confirm a previously requested (mutual) trade termination, including termination payment value ```solidity -function confirmTradeTermination(string memory tradeId) external; +function confirmTradeTermination(string memory tradeId, int256 _terminationPayment) external; ``` ### Trade Events -The following events are emitted during an SDC trade livecycle. +The following events are emitted during an SDC Trade life-cycle. #### TradeIncepted @@ -125,101 +142,90 @@ event TradeConfirmed(address confirmer, string tradeId); #### TradeActivated -Emitted when trade is activated +Emitted when a Trade is activated ```solidity event TradeActivated(string tradeId); ``` -#### TradeTerminationRequest +### TradeSettlementRequest -Emitted when termination request is initiated by a counterparty +Emitted when a settlement is requested. May trigger the settlement phase. ```solidity -event TradeTerminationRequest(address cpAddress, string tradeId); +event TradeSettlementRequest(string tradeData, string lastSettlementData); ``` -#### TradeTerminationConfirmed - -Emitted when termination request is confirmed by a counterparty - -```solidity -event TradeTerminationConfirmed(address cpAddress, string tradeId); -``` -#### TradeTerminated +### TradeSettlementPhase -Emitted when trade is terminated +Emitted when the settlement phase is started. ```solidity -event TradeTerminated(string cause); +event TradeSettlementPhase(); ``` -### Process Events - -The following events are emitted during SDC's process livecycle. -#### ProcessAwaitingFunding +#### TradeTerminationRequest -Emitted when funding phase is initiated +Emitted when termination request is initiated by a counterparty ```solidity -event ProcessAwaitingFunding(); +event TradeTerminationRequest(address cpAddress, string tradeId); ``` -#### ProcessFunded +#### TradeTerminationConfirmed -Emitted when funding has completed successfully - method 'initiatePrefunding' +Emitted when termination request is confirmed by a counterparty ```solidity -event ProcessFunded(); +event TradeTerminationConfirmed(address cpAddress, string tradeId); ``` -#### ProcessSettlementRequest +#### TradeTerminated -Emitted when a settlement is initiated - method 'initiateSettlement' +Emitted when trade is terminated ```solidity -event ProcessSettlementRequest(string tradeData, string lastSettlementData); +event TradeTerminated(string cause); ``` -#### ProcessSettled +#### ProcessHalted -Emitted when settlement was processed successfully - method 'performSettlement' +Emitted when trade processing stops. ```solidity -event ProcessSettled(); +event ProcessHalted(); ``` ## Rationale The interface design and reference implementation are based on the following considerations: -- A SDC protocol is supposed to be used by two counterparties and enables them to initiate and process a derivative transaction in a bilateral and digital manner. -- The provided interface specification is supposed to completely reflect the entire trade livecycle. -- The interface specification is generic enough to handle the case that two counterparties process one or even multiple derivative transactions (on a netted base) -- Usually, the valuation of an OTC trade will require advanced valuation methodology. This is why the concept will in most cases rely on external market data and valuation algorithms -- A pull-based valuation based oracle pattern is specified by a simple callback pattern (methods: initiateSettlement, performSettlement) +- An SDC protocol enables interacting parties to initiate and process a financial transaction in a bilateral and deterministic manner. Settlement and Counterparty Risk is managed by the contract. +- The provided interface specification is supposed to completely reflect the entire trade life cycle. +- The interface specification is generic enough to handle the case that parties process one or even multiple financial transactions (on a netted base) +- Usually, the valuation of financial trades (e.g. OTC Derivatives) will require advanced valuation methodology to determine the market value. This is why the concept might rely on an external market data source and hosted valuation algorithms +- A pull-based valuation-based oracle pattern can be implemented by using the provided callback pattern (methods: `initiateSettlement`, `performSettlement`) - The reference implementation `SDC.sol` is based on a state-machine pattern where the states also serve as guards (via modifiers) to check which method is allowed to be called at a particular given process and trade state -- Java based state machine and contract implementations are also available. See the github repo link below. -### State diagram of trade and process states +### State diagram of trade and process states -![image info](../assets/eip-6123/doc/sdc_trade_and_process_states.png) +![image info](../assets/eip-6123/doc/sdc_trade_states.png) -### Sequence diagram of trade initiation and settlement livecycle +### Sequence diagram of trade initiation and settlement life-cycle -![image info](../assets/eip-6123/doc/sdc_livecycle_sequence_diagram.png) +![image info](../assets/eip-6123/doc/sdc_lifecycle_sequence_diagram.png) ## Test Cases -Live-cycle unit tests based on the sample implementation and usage of [EIP-20](./eip-20.md) token is provided. See file [test/SDC.js](../assets/eip-6123/test/SDC.js) +Life-cycle unit tests based on the sample implementation and usage of [ERC-20](./eip-20.md) token is provided. See file [test/SDCTests.js](../assets/eip-6123/test/SDCTests.js) ). ## Reference Implementation -A reference implementation SDC.sol is provided and is based on the [EIP-20](./eip-20.md) token standard. -See folder /assets/contracts, more explanation on the implementation is provided inline. +An abstract contract class SDC.sol as well as a full reference implementation SDCPledgedBalance.sol for an OTC-Derivative is provided and is based on the [ERC-20](./eip-20.md) token standard. +See folder `/assets/contracts`, more explanation on the implementation is provided inline. ### Trade Data Specification (suggestion) @@ -232,5 +238,3 @@ No known security issues up to now. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). - - diff --git a/assets/erc-6123/README.md b/assets/erc-6123/README.md index 2bf456a68bda0..d8ea1baf87489 100644 --- a/assets/erc-6123/README.md +++ b/assets/erc-6123/README.md @@ -1,47 +1,47 @@ # SDC Solidity implementation ## Description -This sdc implementation aims to implement process logic in a very lean way using an integrative solidity implementation and according unit tests + +The reference SDC implementation can be unit tested with Hardhat to understand the trade process logic. + +### Compile and run tests with Hardhat + +We provide the essential steps to compile the contracts and run the provided unit tests. ### Provided Contracts and Tests + - `contracts/ISDC.sol` - Interface contract -- `contracts/SDC.sol` - SDC reference implementation contract -- `contracts/SDCToken.sol` - Mintable token contract for unit tests -- `test/SDC.js` - Unit tests for livecycle of sdc implementation +- `contracts/SDC.sol` - SDC abstract contract for an OTC Derivative +- `contracts/SDCPledgedBalance.sol` - SDC full implementation for an OTC Derivative +- `contracts/IERC20Settlement.sol` - Interface (extending the ERC-20) for settlement tokens used in `SDCPledgedBalance`. +- `contracts/ERC20Settlement.sol` - Mintable settlement token contract implementing `IERC20Settlement` for unit tests +- `test/SDCTests.js` - Unit tests for the life-cycle of the sdc implementation -### Used javascript based testing libraries for solidity -- `ethereum-waffle`: Waffle is a Solidity testing library. It allows you to write tests for your contracts with JavaScript. -- `chai`: Chai is an assertion library and provides functions like expect. -- `ethers`: This is a popular Ethereum client library. It allows you to interface with blockchains that implement the Ethereum API. -- `solidity-coverage`: This library gives you coverage reports on unit tests with the help of Istanbul. +### Compile and run tests with Hardhat -### Compile and run tests with hardhat -We provide the essential steps to compile the contracts and run provided unit tests -Check that you have the latest version of npm and node via `npm -version` (should be better than 8.5.0) and `node -v` (should be better than 16.14.2). - -1. Check out project -2. Go to folder and initialise a new npm project: `npm init -y`. A basic `package.json` file should occur -3. Install Hardhat as local solidity dev environment: `npx hardhat` -4. Select following option: Create an empty hardhat.config.js -5. Install Hardhat as a development dependency: `npm install --save-dev hardhat` -6. Install further testing dependencies: -`npm install --save-dev @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers ethereum-waffle chai ethers solidity-coverage` -7. Install open zeppelin contracts: `npm install @openzeppelin/contracts` -8. add plugins to hardhat.config.ts: +Install dependencies: +```shell +npm i ``` -require("@nomiclabs/hardhat-waffle"); -require('solidity-coverage'); + +Run all tests: +```shell +npm test ``` -9. Adding commands to `package.json`: -``` -"scripts": { - "build": "hardhat compile", - "test:light": "hardhat test", - "test": "hardhat coverage" - }, +Run all tests with coverage (alternatively): +```shell +npm run coverage ``` -9. run `npm run build` -10. run `npm run test` +### Configuration files + +- `package.js` - Javascript package definition. +- `hardhat.config.js` - Hardhat config. +### Used javascript-based testing libraries for solidity + +- `ethereum-waffle`: Waffle is a Solidity testing library. It allows you to write tests for your contracts with JavaScript. +- `chai`: Chai is an assertion library and provides functions like expect. +- `ethers`: This is a popular Ethereum client library. It allows you to interface with blockchains that implement the Ethereum API. +- `solidity-coverage`: This library gives you coverage reports on unit tests with the help of Istanbul. diff --git a/assets/erc-6123/contracts/ERC20Settlement.sol b/assets/erc-6123/contracts/ERC20Settlement.sol new file mode 100644 index 0000000000000..c6fedc9e5fda9 --- /dev/null +++ b/assets/erc-6123/contracts/ERC20Settlement.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity >=0.7.0 <0.9.0; + +import "./ISDC.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import "./IERC20Settlement.sol"; + +contract ERC20Settlement is ERC20, IERC20Settlement{ + +/*------------------------------------------- DESCRIPTION --------------------------------------------------------------------------------------- +* @title Reference (example) Implementation for Settlement Token Interface +* @dev This token performs transfers on-chain. +* Token is tied to one SDC address +* Only SDC can call checkedTransfers +* Settlement Token calls back the referenced SDC by calling "afterTransfer" with a success flag. Depending on this SDC perfoms next state change +*/ + + + modifier onlySDC() { + require(msg.sender == sdcAddress, "Only allowed to be called from SDC Address"); _; + } + + using ERC165Checker for address; + + address sdcAddress; + + constructor() ERC20("SDCToken", "SDCT") { + + } + + function setSDCAddress(address _sdcAddress) public{ + sdcAddress = _sdcAddress; + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + 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); + } + } + + 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 checkedBatchTransfer(address[] memory to, uint256[] memory values, uint256 transactionID ) public onlySDC{ + require (to.length == values.length, "Array Length mismatch"); + uint256 requiredBalance = 0; + for(uint256 i = 0; i < values.length; i++) + requiredBalance += values[i]; + if (balanceOf(msg.sender) < requiredBalance){ + ISDC(sdcAddress).afterTransfer(transactionID, false); + return; + } + else{ + for(uint256 i = 0; i < to.length; i++){ + transfer(to[i],values[i]); + } + ISDC(sdcAddress).afterTransfer(transactionID, true); + } + } + + + function checkedBatchTransferFrom(address[] memory from, address[] memory to, uint256[] memory values, uint256 transactionID ) public onlySDC{ + require (from.length == to.length, "Array Length mismatch"); + require (to.length == values.length, "Array Length mismatch"); + for(uint256 i = 0; i < from.length; i++){ + address fromAddress = from[i]; + uint256 totalRequiredBalance = 0; + for(uint256 j = 0; j < from.length; j++){ + if (from[j] == fromAddress) + totalRequiredBalance += values[j]; + } + if (balanceOf(fromAddress) < totalRequiredBalance){ + ISDC(sdcAddress).afterTransfer(transactionID, false); + break; + } + + } + for(uint256 i = 0; i < to.length; i++){ + transferFrom(from[i],to[i],values[i]); + } + ISDC(sdcAddress).afterTransfer(transactionID, true); + } + +} \ No newline at end of file diff --git a/assets/erc-6123/contracts/IERC20Settlement.sol b/assets/erc-6123/contracts/IERC20Settlement.sol new file mode 100644 index 0000000000000..00878589893f0 --- /dev/null +++ b/assets/erc-6123/contracts/IERC20Settlement.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity >=0.7.0 <0.9.0; + + + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/*------------------------------------------- DESCRIPTION --------------------------------------------------------------------------------------- + * @title ERC6123 - Settlement Token Interface + * @dev Settlement Token Interface enhances the ERC20 Token by introducing so called checked transfer functionality which can be used to directly interact with an SDC. + * Checked transfers can be conducted for single or multiple transactions where SDC will receive a success message whether the transfer was executed successfully or not. + */ + + +interface IERC20Settlement is IERC20 { + + /* + * @dev Performs a single transfer from msg.sender balance and checks whether this transfer can be conducted + * @param to - receiver + * @param value - transfer amount + * @param transactionID + */ + function checkedTransfer(address to, uint256 value, uint256 transactionID) external; + + /* + * @dev Performs a single transfer to a single addresss and checks whether this transfer can be conducted + * @param from - payer + * @param to - receiver + * @param value - transfer amount + * @param transactionID + */ + function checkedTransferFrom(address from, address to, uint256 value, uint256 transactionID) external ; + + + /* + * @dev Performs a multiple transfers from msg.sender balance and checks whether these transfers can be conducted + * @param to - receivers + * @param values - transfer amounts + * @param transactionID + */ + function checkedBatchTransfer(address[] memory to, uint256[] memory values, uint256 transactionID ) external; + + /* + * @dev Performs a multiple transfers between multiple addresses and checks whether these transfers can be conducted + * @param from - payers + * @param to - receivers + * @param value - transfer amounts + * @param transactionID + */ + function checkedBatchTransferFrom(address[] memory from, address[] memory to, uint256[] memory values, uint256 transactionID ) external; + + +} diff --git a/assets/erc-6123/contracts/ISDC.sol b/assets/erc-6123/contracts/ISDC.sol index 45059fa2176a6..1c14923c7a619 100644 --- a/assets/erc-6123/contracts/ISDC.sol +++ b/assets/erc-6123/contracts/ISDC.sol @@ -6,22 +6,21 @@ pragma solidity >=0.7.0 <0.9.0; /** * @title ERC6123 Smart Derivative Contract * @dev Interface specification for a Smart Derivative Contract, which specifies the post-trade live cycle of an OTC financial derivative in a completely deterministic way. - * Counterparty Risk is removed by construction. * - * A Smart Derivative Contract is a deterministic settlement protocol which has economically the same behaviour as a collateralized OTC financial derivative. - * It aims is to remove many inefficiencies in collateralized OTC transactions and remove counterparty credit risk by construction. + * A Smart Derivative Contract (SDC) is a deterministic settlement protocol which aims is to remove many inefficiencies in (collateralized) financial transactions. + * Settlement (Delivery versus payment) and Counterparty Credit Risk are removed by construction. * - * In contrast to a collateralized derivative contract based and collateral flows are netted. As result, the smart derivative contract generates a stream of + * Special Case OTC-Derivatives: In case of a collateralized OTC derivative the SDC nets contract-based and collateral flows . As result, the SDC generates a stream of * reflecting the settlement of a referenced underlying. The settlement cash flows may be daily (which is the standard frequency in traditional markets) * or at higher frequencies. * With each settlement flow the change is the (discounting adjusted) net present value of the underlying contract is exchanged and the value of the contract is reset to zero. * - * To automatically process settlement, counterparties need to provide sufficient prefunded margin amounts and termination fees at the + * To automatically process settlement, parties need to provide sufficient initial funding and termination fees at the * beginning of each settlement cycle. Through a settlement cycle the margin amounts are locked. Simplified, the contract reverts the classical scheme of * 1) underlying valuation, then 2) funding of a margin call to * 1) pre-funding of a margin buffer (a token), then 2) settlement. * - * A SDC automatically terminates the derivatives contract if there is insufficient pre-funding or if the settlement amount exceeds a + * A SDC may automatically terminates the financial contract if there is insufficient pre-funding or if the settlement amount exceeds a * prefunded margin balance. Beyond mutual termination is also intended by the function specification. * * Events and Functionality specify the entire live cycle: TradeInception, TradeConfirmation, TradeTermination, Margin-Account-Mechanics, Valuation and Settlement. @@ -32,7 +31,7 @@ pragma solidity >=0.7.0 <0.9.0; * *
  • * The process runs in cycles. Let i = 0,1,2,... denote the index of the cycle. Within each cycle there are times - * T_{i,0}, T_{i,1}, T_{i,2}, T_{i,3} with T_{i,1} = pre-funding of the Smart Contract, T_{i,2} = request valuation from oracle, T_{i,3} = perform settlement on given valuation, T_{i+1,0} = T_{i,3}. + * T_{i,0}, T_{i,1}, T_{i,2}, T_{i,3} with T_{i,1} = The Activation of the Trade (initial funding provided), T_{i,1} = request valuation from oracle, T_{i,2} = perform settlement on given valuation, T_{i+1,0} = T_{i,3}. *
  • *
  • * Given this time discretization the states are assigned to time points and time intervalls: @@ -43,16 +42,16 @@ pragma solidity >=0.7.0 <0.9.0; *
    Initiation
    *
    T* < t < T_{0}, where T* is time of incept and T_{0} = T_{0,0}
    * - *
    AwaitingFunding
    + *
    InTransfer (Initiation Phase)
    *
    T_{i,0} < t < T_{i,1}
    * - *
    Funding
    + *
    Settled
    *
    t = T_{i,1}
    * - *
    AwaitingSettlement
    + *
    ValuationAndSettlement
    *
    T_{i,1} < t < T_{i,2}
    * - *
    ValuationAndSettlement
    + *
    InTransfer (Settlement Phase)
    *
    T_{i,2} < t < T_{i,3}
    * *
    Settled
    @@ -92,26 +91,21 @@ interface ISDC { event TradeTerminated(string cause); /** - * @dev Emitted when funding phase is initiated + * @dev Emitted when Settlement phase is initiated */ - event ProcessAwaitingFunding(); + event TradeSettlementPhase(); /** - * @dev Emitted when margin balance was updated and sufficient funding is provided + * @dev Emitted when settlement process has been finished */ - event ProcessFunded(); + event TradeSettled(); /** - * @dev Emitted when a valuation and settlement is requested + * @dev Emitted when a settlement gets requested * @param tradeData holding the stored trade data * @param lastSettlementData holding the settlementdata from previous settlement (next settlement will be the increment of next valuation compared to former valuation) */ - event ProcessSettlementRequest(string tradeData, string lastSettlementData); - - /** - * @dev Emitted when a settlement was processed succesfully - */ - event ProcessSettled(); + event TradeSettlementRequest(string tradeData, string lastSettlementData); /** * @dev Emitted when a counterparty proactively requests an early termination of the underlying trade @@ -127,51 +121,64 @@ interface ISDC { */ event TradeTerminationConfirmed(address cpAddress, string tradeId); + /** + * @dev Emitted when trade processing is halted + * @param message of what has happened + */ + event ProcessHalted(string message); + /*------------------------------------------- FUNCTIONALITY ---------------------------------------------------------------------------------------*/ /// Trade Inception /** - * @notice Handles trade inception, stores trade data + * @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 paymentamount which can be positive or negative * @param _initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) - * @param _upfrontPayment provides an initial payment amount upfront */ - function inceptTrade(string memory _tradeData, string memory _initialSettlementData, int256 _upfrontPayment) 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 + * @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 _tradeData a description of the trade in sdc.xml, e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @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 inceptor has in that trade + * @param _paymentAmount is the paymentamount which can be positive or negative * @param _initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) */ - function confirmTrade(string memory _tradeData, string memory _initialSettlementData) external; + function confirmTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external; - /// Settlement Cycle: Prefunding - - /** - * @notice Called from outside to check and secure pre-funding. Terminate the trade if prefunding fails. - * @dev emits a {ProcessFunded} event if prefunding check is successful or a {TradeTerminated} event if prefunding check fails - */ - function initiatePrefunding() external; /// Settlement Cycle: Settlement /** * @notice Called to trigger a (maybe external) valuation of the underlying contract and afterwards the according settlement process - * @dev emits a {ProcessSettlementRequest} + * @dev emits a {TradeSettlementRequest} */ function initiateSettlement() external; /** - * @notice Called from outside to trigger according settlement on chain-balances callback for initiateSettlement() event handler - * @dev emits a {ProcessSettled} if settlement is successful or {TradeTerminated} if settlement fails + * @notice Called to trigger according settlement on chain-balances callback for initiateSettlement() event handler + * @dev perform settlement checks, may initiate transfers and emits {TradeSettlementPhase} * @param settlementAmount the settlement amount. If settlementAmount > 0 then receivingParty receives this amount from other party. If settlementAmount < 0 then other party receives -settlementAmount from receivingParty. * @param settlementData. the tripple (product, previousSettlementData, settlementData) determines the settlementAmount. */ function performSettlement(int256 settlementAmount, string memory settlementData) external; + + /** + * @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 + * @dev may emit a {TradeSettled} event or a {TradeTerminated} event + */ + function afterTransfer(uint256 transactionHash, bool success) external; + + /// Trade termination /** @@ -179,13 +186,12 @@ interface ISDC { * @dev emits a {TradeTerminationRequest} * @param tradeId the trade identifier which is supposed to be terminated */ - function requestTradeTermination(string memory tradeId) external; + function requestTradeTermination(string memory tradeId, int256 _terminationPayment) external; /** - * @notice Called from a counterparty to confirm a termination, which will triggers a final settlement before trade gets inactive + * @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 */ - function confirmTradeTermination(string memory tradeId) external; + function confirmTradeTermination(string memory tradeId, int256 _terminationPayment) external; } - diff --git a/assets/erc-6123/contracts/SDC.sol b/assets/erc-6123/contracts/SDC.sol index 6865c519be6c7..76399f20c2dbf 100644 --- a/assets/erc-6123/contracts/SDC.sol +++ b/assets/erc-6123/contracts/SDC.sol @@ -1,26 +1,24 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity >=0.8.0 <0.9.0; +pragma solidity >=0.7.0 <0.9.0; import "./ISDC.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; +import "./ERC20Settlement.sol"; + /** - * @title Reference Implementation of ERC6123 - Smart Derivative Contract + * @title Reference Implementation of ERC6123 - Abstract Class for OTC Derivatives * @notice This reference implementation is based on a finite state machine with predefined trade and process states (see enums below) * Some comments on the implementation: * - trade and process states are used in modifiers to check which function is able to be called at which state * - trade data are stored in the contract * - trade data matching is done in incept and confirm routine (comparing the hash of the provided data) - * - ERC-20 token is used for three participants: counterparty1 and counterparty2 and sdc - * - when prefunding is done sdc contract will hold agreed amounts and perform settlement on those - * - sdc also keeps track on internal balances for each counterparty - * - during prefunding sdc will transfer required amounts to its own balance - therefore sufficient approval is needed + * - A Settlement Token (based on ERC20) is used for settlement able to process batched transfers * - upon termination all remaining 'locked' amounts will be transferred back to the counterparties */ -contract SDC is ISDC { +abstract contract SDC is ISDC { /* * Trade States */ @@ -42,151 +40,101 @@ contract SDC is ISDC { Confirmed, /* - * Active (Confirmend + Prefunded Termination Fees). Will cycle through process states. + * Valuation Phase */ - Active, + Valuation, /* - * Terminated. + * A Token-based Transfer is in Progress */ - Terminated - } + InTransfer, - /* - * Process States. t < T* (vor incept). The process runs in cycles. Let i = 0,1,2,... denote the index of the cycle. Within each cycle there are times - * T_{i,0}, T_{i,1}, T_{i,2}, T_{i,3} with T_{i,1} = pre-funding of the Smart Contract, T_{i,2} = request valuation from oracle, T_{i,3} = perform settlement on given valuation, T_{i+1,0} = T_{i,3}. - * Given this time discretization the states are assigned to time points and time intervalls: - * Idle: Before incept or after terminate - * Initiation: T* < t < T_{0}, where T* is time of incept and T_{0} = T_{0,0} - * AwaitingFunding: T_{i,0} < t < T_{i,1} - * Funding: t = T_{i,1} - * AwaitingSettlement: T_{i,1} < t < T_{i,2} - * ValuationAndSettlement: T_{i,2} < t < T_{i,3} - * Settled: t = T_{i,3} - */ - enum ProcessState { - /** - * @dev The process has not yet started or is terminated - */ - Idle, - /* - * @dev The process is initiated (incepted, but not yet completed confimation). Next: AwaitingFunding - */ - Initiation, - /* - * @dev Awaiiting preparation for funding the smart contract. Next: Funding - */ - AwaitingFunding, - /* - * @dev Prefunding the smart contract. Next: AwaitingSettlement - */ - Funding, /* - * @dev The smart contract is completely funded and awaits settlement. Next: ValuationAndSettlement + * Settlement is Completed */ - Funded, - /* - * @dev The settlement process is initiated. Next: Settled or InTermination - */ - ValuationAndSettlement, + Settled, + /* - * @dev Termination started. + * Terminated. */ - InTermination - } - - struct MarginRequirement { - int256 buffer; - int256 terminationFee; + Terminated } /* - * Modifiers serve as guards whether at a specific process state a specific function can be called - */ + * Modifiers serve as guards whether at a specific process state a specific function can be called + */ + - modifier onlyCounterparty() { - require(msg.sender == party1 || msg.sender == party2, "You are not a counterparty."); _; - } modifier onlyWhenTradeInactive() { require(tradeState == TradeState.Inactive, "Trade state is not 'Inactive'."); _; } modifier onlyWhenTradeIncepted() { require(tradeState == TradeState.Incepted, "Trade state is not 'Incepted'."); _; } - modifier onlyWhenProcessAwaitingFunding() { - require(processState == ProcessState.AwaitingFunding, "Process state is not 'AwaitingFunding'."); _; + modifier onlyWhenSettled() { + require(tradeState == TradeState.Settled, "Trade state is not 'Settled'."); _; } - modifier onlyWhenProcessFundedAndTradeActive() { - require(processState == ProcessState.Funded && tradeState == TradeState.Active, "Process state is not 'Funded' or Trade is not 'Active'."); _; + modifier onlyWhenValuation() { + require(tradeState == TradeState.Valuation, "Trade state is not 'Valuation'."); _; } - modifier onlyWhenProcessValuationAndSettlement() { - require(processState == ProcessState.ValuationAndSettlement, "Process state is not 'ValuationAndSettlement'."); _; + modifier onlyWhenInTransfer() { + require(tradeState == TradeState.InTransfer, "Trade state is not 'InTransfer'."); _; } - TradeState private tradeState; - ProcessState private processState; - address public party1; - address public party2; - address private immutable receivingPartyAddress; // Determine the receiver: Positive values are consider to be received by receivingPartyAddress. Negative values are received by the other counterparty. + TradeState internal tradeState; - /* - * liquidityToken holds: - * - funding account of party1 - * - funding account of party2 - * - account for SDC (sum - this is split among parties by sdcBalances) - */ - IERC20 private liquidityToken; + modifier onlyCounterparty() { + require(msg.sender == party1 || msg.sender == party2, "You are not a counterparty."); _; + } - string private tradeID; - string private tradeData; - string private lastSettlementData; + address internal party1; + address internal party2; + address internal receivingParty; - mapping(address => MarginRequirement) private marginRequirements; // Storage of M and P per counterparty address - mapping(uint256 => address) private pendingRequests; // Stores open request hashes for several requests: initiation, update and termination + 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; - mapping(address => int256) private sdcBalances; // internal book-keeping: needed to track what part of the gross token balance is held for each party + int256[] internal settlementAmounts; + string[] internal settlementData; - bool private mutuallyTerminated = false; + /* + * SettlementToken holds: + * - balance of party1 + * - balance of party2 + * - balance for SDC + */ + ERC20Settlement internal settlementToken; + constructor( - address counterparty1, - address counterparty2, - address receivingParty, - address tokenAddress, - uint256 initialMarginRequirement, - uint256 initalTerminationFee + address _party1, + address _party2, + address _settlementToken ) { - party1 = counterparty1; - party2 = counterparty2; - receivingPartyAddress = receivingParty; - liquidityToken = IERC20(tokenAddress); + party1 = _party1; + party2 = _party2; + settlementToken = ERC20Settlement(_settlementToken); + settlementToken.setSDCAddress(address(this)); tradeState = TradeState.Inactive; - processState = ProcessState.Idle; - marginRequirements[party1] = MarginRequirement(int256(initialMarginRequirement), int256(initalTerminationFee)); - marginRequirements[party2] = MarginRequirement(int256(initialMarginRequirement), int256(initalTerminationFee)); - sdcBalances[party1] = 0; - sdcBalances[party2] = 0; } - /* - * generates a hash from tradeData and generates a map entry in openRequests - * emits a TradeIncepted - * can be called only when TradeState = Incepted - */ - function inceptTrade(string memory _tradeData, string memory _initialSettlementData, int256 _upfrontPayment) external override onlyCounterparty onlyWhenTradeInactive - { - processState = ProcessState.Initiation; + * generates a hash from tradeData and generates a map entry in openRequests + * emits a TradeIncepted + * can be called only when TradeState = Incepted + */ + function inceptTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external override onlyCounterparty onlyWhenTradeInactive { + require(msg.sender != _withParty, "Calling party cannot be the same as withParty"); + require(_position == 1 || _position == -1, "Position can only be +1 or -1"); tradeState = TradeState.Incepted; // Set TradeState to Incepted - - _upfrontPayment; // To silence warning... TODO: Implement new feature. - - uint256 hash = uint256(keccak256(abi.encode(_tradeData, _initialSettlementData))); - pendingRequests[hash] = msg.sender; - tradeID = Strings.toString(hash); + uint256 transactionHash = uint256(keccak256(abi.encode(msg.sender,_withParty,_tradeData,_position, _paymentAmount,_initialSettlementData))); + pendingRequests[transactionHash] = msg.sender; + receivingParty = _position == 1 ? msg.sender : _withParty; + tradeID = Strings.toString(transactionHash); tradeData = _tradeData; // Set trade data to enable querying already in inception state - lastSettlementData = _initialSettlementData; // Store settlement data to make them available for confirming party - emit TradeIncepted(msg.sender, tradeID, _tradeData); } @@ -196,185 +144,39 @@ contract SDC is ISDC { * emits a TradeConfirmed * can be called only when TradeState = Incepted */ - function confirmTrade(string memory _tradeData, string memory _initialSettlementData) external override onlyCounterparty onlyWhenTradeIncepted - { - address pendingRequestParty = msg.sender == party1 ? party2 : party1; - uint256 tradeIDConf = uint256(keccak256(abi.encode(_tradeData, _initialSettlementData))); - require(pendingRequests[tradeIDConf] == pendingRequestParty, "Confirmation fails due to inconsistent trade data or wrong party address"); - delete pendingRequests[tradeIDConf]; // Delete Pending Request - + function confirmTrade(address _withParty, string memory _tradeData, int _position, int256 _paymentAmount, string memory _initialSettlementData) external override onlyCounterparty onlyWhenTradeIncepted { + address inceptingParty = msg.sender == party1 ? party2 : party1; + uint256 transactionHash = uint256(keccak256(abi.encode(_withParty,msg.sender,_tradeData,-_position, -_paymentAmount,_initialSettlementData))); + require(pendingRequests[transactionHash] == inceptingParty, "Confirmation fails due to inconsistent trade data or wrong party address"); + delete pendingRequests[transactionHash]; // Delete Pending Request tradeState = TradeState.Confirmed; emit TradeConfirmed(msg.sender, tradeID); - - // Pre-Conditions - if(_lockTerminationFees()) { - tradeState = TradeState.Active; - emit TradeActivated(tradeID); - - processState = ProcessState.AwaitingFunding; - emit ProcessAwaitingFunding(); - } - } - - /** - * Check sufficient balances and lock Termination Fees otherwise trade does not get activated - */ - function _lockTerminationFees() internal returns(bool) { - bool isAvailableParty1 = (liquidityToken.balanceOf(party1) >= uint(marginRequirements[party1].terminationFee)) && (liquidityToken.allowance(party1,address(this)) >= uint(marginRequirements[party1].terminationFee)); - bool isAvailableParty2 = (liquidityToken.balanceOf(party2) >= uint(marginRequirements[party2].terminationFee)) && (liquidityToken.allowance(party2,address(this)) >= uint(marginRequirements[party2].terminationFee)); - if (isAvailableParty1 && isAvailableParty2){ - liquidityToken.transferFrom(party1, address(this), uint(marginRequirements[party1].terminationFee)); // transfer termination fee party1 to sdc - liquidityToken.transferFrom(party2, address(this), uint(marginRequirements[party2].terminationFee)); // transfer termination fee party2 to sdc - adjustSDCBalances(marginRequirements[party1].terminationFee, marginRequirements[party2].terminationFee); // Update internal balances - return true; - } - else{ - tradeState == TradeState.Inactive; - processState = ProcessState.Idle; - emit TradeTerminated("Termination Fee could not be locked."); - return false; - } - } - - /* - * Failsafe: Free up accounts upon termination - */ - function _processTermination() internal { - liquidityToken.transfer(party1, uint256(sdcBalances[party1])); - liquidityToken.transfer(party2, uint256(sdcBalances[party2])); - - processState = ProcessState.Idle; - tradeState = TradeState.Inactive; - } - - /* - * Settlement Cycle - */ - - /* - * Send an Lock Request Event only when Process State = Funding - * Puts Process state to Margin Account Check - * can be called only when ProcessState = AwaitingFunding - */ - function initiatePrefunding() external override onlyWhenProcessAwaitingFunding { - processState = ProcessState.Funding; - - uint256 balanceParty1 = liquidityToken.balanceOf(party1); - uint256 balanceParty2 = liquidityToken.balanceOf(party2); - - /* Calculate gap amount for each party, i.e. residual between buffer and termination fee and actual balance */ - // max(M+P - sdcBalance,0) - uint gapAmountParty1 = marginRequirements[party1].buffer + marginRequirements[party1].terminationFee - sdcBalances[party1] > 0 ? uint(marginRequirements[party1].buffer + marginRequirements[party1].terminationFee - sdcBalances[party1]) : 0; - uint gapAmountParty2 = marginRequirements[party2].buffer + marginRequirements[party2].terminationFee - sdcBalances[party2] > 0 ? uint(marginRequirements[party2].buffer + marginRequirements[party2].terminationFee - sdcBalances[party2]) : 0; - - /* Good case: Balances are sufficient and token has enough approval */ - if ( (balanceParty1 >= gapAmountParty1 && liquidityToken.allowance(party1,address(this)) >= gapAmountParty1) && - (balanceParty2 >= gapAmountParty2 && liquidityToken.allowance(party2,address(this)) >= gapAmountParty2) ) { - liquidityToken.transferFrom(party1, address(this), gapAmountParty1); // Transfer of GapAmount to sdc contract - liquidityToken.transferFrom(party2, address(this), gapAmountParty2); // Transfer of GapAmount to sdc contract - processState = ProcessState.Funded; - adjustSDCBalances(int(gapAmountParty1),int(gapAmountParty2)); // Update internal balances - emit ProcessFunded(); - } - /* Party 1 - Bad case: Balances are insufficient or token has not enough approval */ - else if ( (balanceParty1 < gapAmountParty1 || liquidityToken.allowance(party1,address(this)) < gapAmountParty1) && - (balanceParty2 >= gapAmountParty2 && liquidityToken.allowance(party2,address(this)) >= gapAmountParty2) ) { - tradeState = TradeState.Terminated; - processState = ProcessState.InTermination; - - adjustSDCBalances(-marginRequirements[party1].terminationFee,marginRequirements[party1].terminationFee); // Update internal balances - - _processTermination(); // Release all buffers - emit TradeTerminated("Termination caused by party1 due to insufficient prefunding"); - } - /* Party 2 - Bad case: Balances are insufficient or token has not enough approval */ - else if ( (balanceParty1 >= gapAmountParty1 && liquidityToken.allowance(party1,address(this)) >= gapAmountParty1) && - (balanceParty2 < gapAmountParty2 || liquidityToken.allowance(party2,address(this)) < gapAmountParty2) ) { - tradeState = TradeState.Terminated; - processState = ProcessState.InTermination; - - adjustSDCBalances(marginRequirements[party2].terminationFee,-marginRequirements[party2].terminationFee); // Update internal balances - - _processTermination(); // Release all buffers - emit TradeTerminated("Termination caused by party2 due to insufficient prefunding"); - } - /* Both parties fail: Cross Transfer of Termination Fee */ - else { - tradeState = TradeState.Terminated; - processState = ProcessState.InTermination; - // if ( (balanceParty1 < gapAmountParty1 || liquidityToken.allowance(party1,address(this)) < gapAmountParty1) && (balanceParty2 < gapAmountParty2 || liquidityToken.allowance(party2,address(this)) < gapAmountParty2) ) { tradeState = TradeState.Terminated; - adjustSDCBalances(marginRequirements[party2].terminationFee-marginRequirements[party1].terminationFee,marginRequirements[party1].terminationFee-marginRequirements[party2].terminationFee); // Update internal balances: Cross Booking of termination fee - - _processTermination(); // Release all buffers - emit TradeTerminated("Termination caused by both parties due to insufficient prefunding"); - } - } - - /* - * 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 - * can be called only when ProcessState = Funded and TradeState = Active - */ - function initiateSettlement() external override onlyCounterparty onlyWhenProcessFundedAndTradeActive - { - processState = ProcessState.ValuationAndSettlement; - emit ProcessSettlementRequest(tradeData, lastSettlementData); - } - - /* - * Performs a settelement only when processState is ValuationAndSettlement - * Puts process state to "inTransfer" - * 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) onlyWhenProcessValuationAndSettlement external override - { - lastSettlementData = settlementData; - address receivingParty = settlementAmount > 0 ? receivingPartyAddress : other(receivingPartyAddress); - address payingParty = other(receivingParty); - - bool noTermination = abs(settlementAmount) <= marginRequirements[payingParty].buffer; - int256 transferAmount = (noTermination == true) ? abs(settlementAmount) : marginRequirements[payingParty].buffer + marginRequirements[payingParty].terminationFee; // Override with Buffer and Termination Fee: Max Transfer - - if(receivingParty == party1) // Adjust internal Balances, only debit is booked on sdc balance as receiving party obtains transfer amount directly from sdc - adjustSDCBalances(0, -transferAmount); + 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 - adjustSDCBalances(-transferAmount, 0); - - liquidityToken.transfer(receivingParty, uint256(transferAmount)); // SDC contract performs transfer to receiving party - - if (noTermination) { // Regular Settlement - emit ProcessSettled(); - processState = ProcessState.AwaitingFunding; // Set ProcessState to 'AwaitingFunding' - } else { // Termination Event, buffer not sufficient, transfer margin buffer and termination fee and process termination - tradeState = TradeState.Terminated; - processState = ProcessState.InTermination; - _processTermination(); // Transfer all locked amounts - emit TradeTerminated("Termination due to margin buffer exceedance"); - } - - if (mutuallyTerminated) { // Both counterparties agreed on a premature termination - processState = ProcessState.InTermination; - _processTermination(); - } + upfrontPayer = _withParty; + settlementData.push(_initialSettlementData); + uint256 absPaymentAmount = uint256(abs(_paymentAmount)); + processTradeAfterConfirmation(upfrontPayer, absPaymentAmount); } - /* - * End of Cycle - */ /* - * Can be called by a party for mutual termination - * Hash is generated an entry is put into pendingRequests - * TerminationRequest is emitted - * can be called only when ProcessState = Funded and TradeState = Active - */ - function requestTradeTermination(string memory _tradeID) external override onlyCounterparty onlyWhenProcessFundedAndTradeActive - { - require(keccak256(abi.encodePacked(tradeID)) == keccak256(abi.encodePacked(_tradeID)), "Trade ID mismatch"); - uint256 hash = uint256(keccak256(abi.encode(_tradeID, "terminate"))); + * Can be called by a party for mutual termination + * Hash is generated an entry is put into pendingRequests + * TerminationRequest is emitted + * can be called only when ProcessState = Funded and TradeState = Active + */ + function requestTradeTermination(string memory _tradeId, int256 _terminationPayment) external override onlyCounterparty onlyWhenSettled { + require(keccak256(abi.encodePacked(tradeID)) == keccak256(abi.encodePacked(_tradeId)), "Trade ID mismatch"); + uint256 hash = uint256(keccak256(abi.encode(_tradeId, "terminate", _terminationPayment))); pendingRequests[hash] = msg.sender; - emit TradeTerminationRequest(msg.sender, _tradeID); + emit TradeTerminationRequest(msg.sender, _tradeId); } /* @@ -383,67 +185,75 @@ contract SDC is ISDC { * confirming party generates same hash, looks into pendingRequests, if entry is found with correct address, tradeState is put to terminated * can be called only when ProcessState = Funded and TradeState = Active */ - function confirmTradeTermination(string memory tradeId) external override onlyCounterparty onlyWhenProcessFundedAndTradeActive - { + function confirmTradeTermination(string memory _tradeId, int256 _terminationPayment) external override onlyCounterparty onlyWhenSettled { address pendingRequestParty = msg.sender == party1 ? party2 : party1; - uint256 hashConfirm = uint256(keccak256(abi.encode(tradeId, "terminate"))); + uint256 hashConfirm = uint256(keccak256(abi.encode(_tradeId, "terminate", _terminationPayment))); require(pendingRequests[hashConfirm] == pendingRequestParty, "Confirmation of termination failed due to wrong party or missing request"); delete pendingRequests[hashConfirm]; mutuallyTerminated = true; - emit TradeTerminationConfirmed(msg.sender, tradeID); + terminationPayment = _terminationPayment; + emit TradeTerminationConfirmed(msg.sender, _tradeId); + /* Trigger final Settlement */ + tradeState = TradeState.Valuation; + emit TradeSettlementRequest(tradeData, settlementData[settlementData.length - 1]); } - function adjustSDCBalances(int256 adjustmentAmountParty1, int256 adjustmentAmountParty2) internal { - if (adjustmentAmountParty1 < 0) - require(sdcBalances[party1] >= adjustmentAmountParty1, "SDC Balance Adjustment fails for Party1"); - if (adjustmentAmountParty2 < 0) - require(sdcBalances[party2] >= adjustmentAmountParty2, "SDC Balance Adjustment fails for Party2"); - sdcBalances[party1] = sdcBalances[party1] + adjustmentAmountParty1; - sdcBalances[party2] = sdcBalances[party2] + adjustmentAmountParty2; - } + + function processTradeAfterConfirmation(address upfrontPayer, uint256 upfrontPayment) virtual internal; /* * Utilities - */ + */ /** * Absolute value of an integer */ - function abs(int x) private pure returns (int) { + function abs(int x) internal pure returns (int256) { return x >= 0 ? x : -x; } /** - * Other party + * Maximum value of two integers */ - function other(address party) private view returns (address) { - return (party == party1 ? party2 : party1); + 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; } + function getTokenAddress() public view returns(address) { - return address(liquidityToken); + return address(settlementToken); } - function getTradeID() public view returns (string memory) { - return tradeID; + function getTradeState() public view returns (TradeState) { + return tradeState; } - function getTradeData() public view returns (string memory) { - return tradeData; + /** + * Other party + */ + function otherParty(address party) internal view returns (address) { + return (party == party1 ? party2 : party1); } - function getTradeState() public view returns (TradeState) { - return tradeState; + function getTradeID() public view returns (string memory) { + return tradeID; } - function getProcessState() public view returns (ProcessState) { - return processState; + function setTradeId(string memory _tradeID) public { + tradeID= _tradeID; } - function getOwnSdcBalance() public view returns (int256) { - return sdcBalances[msg.sender]; + function getTradeData() public view returns (string memory) { + return tradeData; } - /**END OF FUNCTIONS WHICH ARE ONLY USED FOR TESTING PURPOSES */ + } \ No newline at end of file diff --git a/assets/erc-6123/contracts/SDCPledgedBalance.sol b/assets/erc-6123/contracts/SDCPledgedBalance.sol new file mode 100644 index 0000000000000..cce8f708cd6d4 --- /dev/null +++ b/assets/erc-6123/contracts/SDCPledgedBalance.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity >=0.8.0 <0.9.0; + +import "./SDC.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "./ERC20Settlement.sol"; + + +/** + * @title Reference Implementation of ERC6123 - Smart Derivative Contract + * @notice This reference implementation is based on a finite state machine with predefined trade and process states (see enums below) + * Some comments on the implementation: + * - trade and process states are used in modifiers to check which function is able to be called at which state + * - trade data are stored in the contract + * - trade data matching is done in incept and confirm routine (comparing the hash of the provided data) + * - ERC-20 token is used for three participants: counterparty1 and counterparty2 and sdc + * - when prefunding is done sdc contract will hold agreed amounts and perform settlement on those + * - sdc also keeps track on internal balances for each counterparty + * - during prefunding sdc will transfer required amounts to its own balance - therefore sufficient approval is needed + * - upon termination all remaining 'locked' amounts will be transferred back to the counterparties + *------------------------------------* + * Setup with Pledge Account + * + * Settlement: + * _bookSettlement + * Update internal balances + * Message + * Rebalance: + * Book Party2 -> Party1: X + * Rebalance Check + * Failed + * Book SDC -> Party1: X + * Terminate + *-------------------------------------* +*/ + +contract SDCPledgedBalance is SDC { + + struct MarginRequirement { + uint256 buffer; + uint256 terminationFee; + } + + + mapping(address => MarginRequirement) private marginRequirements; // Storage of M and P per counterparty address + + constructor( + address _party1, + address _party2, + address _settlementToken, + 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 + * can be called only when ProcessState = Rebalanced and TradeState = Active + */ + function initiateSettlement() external override onlyCounterparty onlyWhenSettled { + tradeState = TradeState.Valuation; + emit TradeSettlementRequest(tradeData, settlementData[settlementData.length - 1]); + } + + /* + * Performs a settelement only when processState is ValuationAndSettlement + * Puts process state to "inTransfer" + * 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; + } + + settlementData.push(_settlementData); + settlementAmounts.push(settlementAmount); + + 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); + } + else { /* Bad Case: Process termination by booking from own balance */ + tradeState = TradeState.InTransfer; + _processAfterTransfer(false); + } + } + + 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)))); + else + transferAmount = uint256(abs(max( settlementAmount, -int(marginRequirements[settlementReceiver].buffer)))); + + return (settlementPayer,transferAmount); + } + + function afterTransfer(uint256 /* transactionHash */, bool success) external override onlyWhenInTransfer { + // Note: parameter transactionHash currenty unused + emit TradeSettled(); + _processAfterTransfer(success); + } + + function _processAfterTransfer(bool success) internal{ + if(success){ + emit TradeSettled(); + if (tradeState == TradeState.Terminated || tradeState == 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); + } + } + } +} diff --git a/assets/erc-6123/doc/sdc_livecycle_sequence_diagram.png b/assets/erc-6123/doc/sdc_lifecycle_sequence_diagram.png similarity index 100% rename from assets/erc-6123/doc/sdc_livecycle_sequence_diagram.png rename to assets/erc-6123/doc/sdc_lifecycle_sequence_diagram.png diff --git a/assets/erc-6123/doc/sdc_trade_states.png b/assets/erc-6123/doc/sdc_trade_states.png new file mode 100644 index 0000000000000..53b896b871973 Binary files /dev/null and b/assets/erc-6123/doc/sdc_trade_states.png differ diff --git a/assets/erc-6123/hardhat.config.js b/assets/erc-6123/hardhat.config.js new file mode 100644 index 0000000000000..d04a6ec0dfc1f --- /dev/null +++ b/assets/erc-6123/hardhat.config.js @@ -0,0 +1,6 @@ +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: "0.8.20", +}; +require("@nomiclabs/hardhat-waffle"); +require('solidity-coverage'); diff --git a/assets/erc-6123/package.json b/assets/erc-6123/package.json new file mode 100644 index 0000000000000..24d9974fb70ac --- /dev/null +++ b/assets/erc-6123/package.json @@ -0,0 +1,41 @@ +{ + "name": "@finmath.net/sdc", + "version": "0.3.1", + "description": "Solidity Smart Derivative Contracts", + "author": "Christian Fries, Peter Kohl-Landgraf, Alexandros Korpis", + "license": "ISC", + "homepage": "https://github.com/finmath/finmath-smart-derivative-contract#readme", + "main": "index.js", + "scripts": { + "build": "hardhat compile", + "test": "hardhat test", + "coverage": "hardhat coverage" + }, + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-waffle": "^2.0.6", + "chai": "^4.3.10", + "ethereum-waffle": "^4.0.10", + "ethers": "^5.7.2", + "hardhat": "^2.18.1", + "solidity-coverage": "^0.8.5" + }, + "dependencies": { + "@openzeppelin/contracts": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/finmath/finmath-smart-derivative-contract.git" + }, + "keywords": [ + "Smart Derivative Contract", + "SDC", + "EIP-6123" + ], + "bugs": { + "url": "https://github.com/finmath/finmath-smart-derivative-contract/issues" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/assets/erc-6123/test/SDC.js b/assets/erc-6123/test/SDC.js deleted file mode 100644 index 75049c9c7314b..0000000000000 --- a/assets/erc-6123/test/SDC.js +++ /dev/null @@ -1,128 +0,0 @@ -const { ethers } = require("hardhat"); -const { expect } = require("chai"); -const AbiCoder = ethers.utils.AbiCoder; -const Keccak256 = ethers.utils.keccak256; - -describe("Livecycle Unit-Tests for Smart Derivative Contract", () => { - - // Define objects for TradeState enum, since solidity enums cannot provide their member names... - const TradeState = { - Inactive: 0, - Incepted: 1, - Confirmed: 2, - Active: 3, - Terminated: 4, - }; - - const abiCoder = new AbiCoder(); - const trade_data = "here are the trade specification { - const [_tokenManager, _counterparty1, _counterparty2] = await ethers.getSigners(); - tokenManager = _tokenManager; - counterparty1 = _counterparty1; - counterparty2 = _counterparty2; - const ERC20Factory = await ethers.getContractFactory("SDCToken"); - const SDCFactory = await ethers.getContractFactory("SDC"); - token = await ERC20Factory.deploy(); - await token.deployed(); - sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,counterparty1.address, token.address,marginBufferAmount,terminationFee); - await sdc.deployed(); - console.log("SDC Address: %s", sdc.address); - }); - - it("Initial minting and approvals for SDC", async () => { - await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); - await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); - await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); - await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount); - let allowanceSDCParty1 = await token.connect(counterparty1).allowance(counterparty1.address, sdc.address); - let allowanceSDCParty2 = await token.connect(counterparty2).allowance(counterparty2.address, sdc.address); - await expect(allowanceSDCParty1).equal(terminationFee+marginBufferAmount); - }); - - it("Counterparty1 incepts a trade", async () => { - const incept_call = await sdc.connect(counterparty1).inceptTrade(trade_data, "initialMarketData", 0); - let tradeid = await sdc.connect(counterparty1).getTradeID(); - //console.log("TradeId: %s", tradeid); - await expect(incept_call).to.emit(sdc, "TradeIncepted").withArgs(counterparty1.address, tradeid, trade_data); - let trade_state = await sdc.connect(counterparty1).getTradeState(); - await expect(trade_state).equal(TradeState.Incepted); - }); - - - it("Counterparty2 confirms a trade", async () => { - const confirm_call = await sdc.connect(counterparty2).confirmTrade(trade_data,"initialMarketData"); - //console.log("TradeId: %s", await sdc.callStatic.getTradeState()); - let balanceSDC = await token.connect(counterparty2).balanceOf(sdc.address); - await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); - await expect(balanceSDC).equal(2*terminationFee); - let trade_state = await sdc.connect(counterparty1).getTradeState(); - await expect(trade_state).equal(TradeState.Active); - }); - - it("Processing first prefunding phase", async () => { - const call = await sdc.connect(counterparty2).initiatePrefunding(); - let balanceSDC = await token.connect(counterparty2).balanceOf(sdc.address); - let balanceCP2 = await token.connect(counterparty2).balanceOf(counterparty2.address); - await expect(balanceSDC).equal(2*(terminationFee+marginBufferAmount)); - await expect(balanceCP2).equal(initialLiquidityBalance-(terminationFee+marginBufferAmount)); - await expect(call).to.emit(sdc, "ProcessFunded"); - }); - - it("Initiate and perform first successful settlement in favour to counterparty 1", async () => { - const callInitSettlement = await sdc.connect(counterparty2).initiateSettlement(); - await expect(callInitSettlement).to.emit(sdc, "ProcessSettlementRequest"); - let balanceCP1 = parseInt(await token.connect(counterparty1).balanceOf(counterparty1.address)); - let balanceCP2 = parseInt(await token.connect(counterparty2).balanceOf(counterparty1.address)); - const callPerformSettlement = await sdc.connect(counterparty2).performSettlement(settlementAmount1,"settlementData"); - await expect(callPerformSettlement).to.emit(sdc, "ProcessSettled"); - let balanceSDC_afterSettlement = await token.connect(counterparty2).balanceOf(sdc.address); - let balanceCP1_afterSettlement = await token.connect(counterparty1).balanceOf(counterparty1.address); - let balanceCP2_afterSettlement = await token.connect(counterparty2).balanceOf(counterparty2.address); - await expect(balanceSDC_afterSettlement).equal(2*(terminationFee+marginBufferAmount)-settlementAmount1); // SDC balance less settlement - await expect(balanceCP1_afterSettlement).equal(balanceCP1+settlementAmount1); // settlement in favour to CP1 - await expect(balanceCP2_afterSettlement).equal(balanceCP2); // CP2 balance is not touched as transfer is booked from SDC balance - }); - - it("Process successfully second prefunding phase successful ", async () => { - await token.connect(counterparty2).approve(sdc.address,settlementAmount1); // CP2 increases allowance - const call = await sdc.connect(counterparty1).initiatePrefunding(); //Prefunding: SDC transfers missing gap amount from CP2 - let balanceSDC = await token.connect(counterparty2).balanceOf(sdc.address); - let balanceCP2 = await token.connect(counterparty2).balanceOf(counterparty2.address); - await expect(balanceSDC).equal(2*(terminationFee+marginBufferAmount)); - await expect(balanceCP2).equal(initialLiquidityBalance-(terminationFee+marginBufferAmount)-settlementAmount1); - await expect(call).to.emit(sdc, "ProcessFunded"); - }); - - - it("Second settlement fails due to high transfer amount in favour to counteparty 2 - Trade terminates", async () => { - const callInitSettlement = await sdc.connect(counterparty2).initiateSettlement(); - await expect(callInitSettlement).to.emit(sdc, "ProcessSettlementRequest"); - const callPerformSettlement = await sdc.connect(counterparty2).performSettlement(settlementAmount2,"settlementData"); - await expect(callPerformSettlement).to.emit(sdc, "TradeTerminated"); - - let balanceSDC = parseInt(await token.connect(counterparty2).balanceOf(sdc.address)); - let balanceCP1 = await token.connect(counterparty1).balanceOf(counterparty1.address); - let balanceCP2 = await token.connect(counterparty2).balanceOf(counterparty2.address); - let expectedBalanceCP1 = initialLiquidityBalance + settlementAmount1 - (marginBufferAmount + terminationFee); //CP1 received settlementAmount1 and paid margin buffer and termination fee - let expectedBalanceCP2 = initialLiquidityBalance - settlementAmount1 + (marginBufferAmount + terminationFee); //CP2 paid settlementAmount1 and receives margin buffer and termination fee - await expect(balanceCP1).equal(expectedBalanceCP1); - await expect(balanceCP2).equal(expectedBalanceCP2); - await expect(balanceSDC).equal(0); - }); - - - -}); \ No newline at end of file diff --git a/assets/erc-6123/test/SDCTests.js b/assets/erc-6123/test/SDCTests.js new file mode 100644 index 0000000000000..20fe9ed3e1571 --- /dev/null +++ b/assets/erc-6123/test/SDCTests.js @@ -0,0 +1,118 @@ +const { ethers } = require("hardhat"); +const { expect } = require("chai"); +const AbiCoder = ethers.utils.AbiCoder; +const Keccak256 = ethers.utils.keccak256; + +describe("Livecycle Unit-Tests for SDC Plege Balance", () => { + + // Define objects for TradeState enum, since solidity enums cannot provide their member names... + const TradeState = { + Inactive: 0, + Incepted: 1, + Confirmed: 2, + Valuation: 3, + InTransfer: 4, + Settled: 5, + Terminated: 6 + }; + + const abiCoder = new AbiCoder(); + const trade_data = "here are the trade specification { + const [_tokenManager, _counterparty1, _counterparty2] = await ethers.getSigners(); + tokenManager = _tokenManager; + counterparty1 = _counterparty1; + counterparty2 = _counterparty2; + ERC20Factory = await ethers.getContractFactory("ERC20Settlement"); + SDCFactory = await ethers.getContractFactory("SDCPledgedBalance"); + token = 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 () => { + 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"); + 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); + }); + + 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("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"); + }); + + + it("Successful Settlement", 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+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"); + const confirm_call = await sdc.connect(counterparty2).confirmTrade(counterparty1.address, trade_data, -1, -upfront, "initialMarketData"); + 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"); + await expect(performSettlementCall).to.emit(sdc, "TradeSettlementPhase"); + let trade_state = await sdc.connect(counterparty1).getTradeState(); + await expect(trade_state).equal(TradeState.Settled); + + }); + + + +}); \ No newline at end of file