447 lines
20 KiB
Solidity
447 lines
20 KiB
Solidity
// 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 */
|
|
} |