// SPDX-License-Identifier: CC0-1.0 pragma solidity >=0.8.0 <0.9.0; import "./ISDC.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/Strings.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 */ contract SDC is ISDC { /* * Trade States */ enum TradeState { /* * State before the trade is incepted. */ Inactive, /* * Incepted: Trade data submitted by one party. Market data for initial valuation is set. */ Incepted, /* * Confirmed: Trade data accepted by other party. */ Confirmed, /* * Active (Confirmend + Prefunded Termination Fees). Will cycle through process states. */ Active, /* * Terminated. */ Terminated } /* * 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 */ Funded, /* * @dev The settlement process is initiated. Next: Settled or InTermination */ ValuationAndSettlement, /* * @dev Termination started. */ InTermination } struct MarginRequirement { int256 buffer; int256 terminationFee; } /* * 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 onlyWhenProcessFundedAndTradeActive() { require(processState == ProcessState.Funded && tradeState == TradeState.Active, "Process state is not 'Funded' or Trade is not 'Active'."); _; } modifier onlyWhenProcessValuationAndSettlement() { require(processState == ProcessState.ValuationAndSettlement, "Process state is not 'ValuationAndSettlement'."); _; } 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. /* * liquidityToken holds: * - funding account of party1 * - funding account of party2 * - account for SDC (sum - this is split among parties by sdcBalances) */ IERC20 private liquidityToken; string private tradeID; string private tradeData; string private lastSettlementData; 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 mapping(address => int256) private sdcBalances; // internal book-keeping: needed to track what part of the gross token balance is held for each party bool private mutuallyTerminated = false; constructor( address counterparty1, address counterparty2, address receivingParty, address tokenAddress, uint256 initialMarginRequirement, uint256 initalTerminationFee ) { party1 = counterparty1; party2 = counterparty2; receivingPartyAddress = receivingParty; liquidityToken = IERC20(tokenAddress); 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) external override onlyCounterparty onlyWhenTradeInactive { processState = ProcessState.Initiation; tradeState = TradeState.Incepted; // Set TradeState to Incepted uint256 hash = uint256(keccak256(abi.encode(_tradeData, _initialSettlementData))); pendingRequests[hash] = msg.sender; tradeID = Strings.toString(hash); 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); } /* * generates a hash from tradeData and checks whether an open request can be found by the opposite party * if so, data are stored and open request is deleted * 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 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); 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(); } } /* * 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"))); pendingRequests[hash] = msg.sender; emit TradeTerminationRequest(msg.sender, _tradeID); } /* * Same pattern as for initiation * 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 { address pendingRequestParty = msg.sender == party1 ? party2 : party1; uint256 hashConfirm = uint256(keccak256(abi.encode(tradeId, "terminate"))); 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); } 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; } /* * Utilities */ /** * Absolute value of an integer */ function abs(int x) private pure returns (int) { return x >= 0 ? x : -x; } /** * Other party */ function other(address party) private view returns (address) { return (party == party1 ? party2 : party1); } function getTokenAddress() public view returns(address) { return address(liquidityToken); } function getTradeID() public view returns (string memory) { return tradeID; } function getTradeData() public view returns (string memory) { return tradeData; } function getTradeState() public view returns (TradeState) { return tradeState; } function getProcessState() public view returns (ProcessState) { return processState; } function getOwnSdcBalance() public view returns (int256) { return sdcBalances[msg.sender]; } /**END OF FUNCTIONS WHICH ARE ONLY USED FOR TESTING PURPOSES */ }