From adca7512c4ffdd11a5fb0790e104330c2cde2e95 Mon Sep 17 00:00:00 2001 From: Oskar Thoren Date: Mon, 18 Jan 2021 19:26:15 +0800 Subject: [PATCH] Import Swap contracts --- contracts/ERC20SimpleSwap.sol | 344 ++++++++++++++++++++++++++++++++ contracts/SimpleSwapFactory.sol | 35 ++++ 2 files changed, 379 insertions(+) create mode 100644 contracts/ERC20SimpleSwap.sol create mode 100644 contracts/SimpleSwapFactory.sol diff --git a/contracts/ERC20SimpleSwap.sol b/contracts/ERC20SimpleSwap.sol new file mode 100644 index 0000000..e3272d7 --- /dev/null +++ b/contracts/ERC20SimpleSwap.sol @@ -0,0 +1,344 @@ +pragma solidity =0.6.12; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** +@title Chequebook contract without waivers +@author The Swarm Authors +@notice The chequebook contract allows the issuer of the chequebook to send cheques to an unlimited amount of counterparties. +Furthermore, solvency can be guaranteed via hardDeposits +@dev as an issuer, no cheques should be send if the cumulative worth of a cheques send is above the cumulative worth of all deposits +as a beneficiary, we should always take into account the possibility that a cheque bounces (when no hardDeposits are assigned) +*/ +contract ERC20SimpleSwap { + using SafeMath for uint; + + event ChequeCashed( + address indexed beneficiary, + address indexed recipient, + address indexed caller, + uint totalPayout, + uint cumulativePayout, + uint callerPayout + ); + event ChequeBounced(); + event HardDepositAmountChanged(address indexed beneficiary, uint amount); + event HardDepositDecreasePrepared(address indexed beneficiary, uint decreaseAmount); + event HardDepositTimeoutChanged(address indexed beneficiary, uint timeout); + event Withdraw(uint amount); + + uint public defaultHardDepositTimeout; + /* structure to keep track of the hard deposits (on-chain guarantee of solvency) per beneficiary*/ + struct HardDeposit { + uint amount; /* hard deposit amount allocated */ + uint decreaseAmount; /* decreaseAmount substranced from amount when decrease is requested */ + uint timeout; /* issuer has to wait timeout seconds to decrease hardDeposit, 0 implies applying defaultHardDepositTimeout */ + uint canBeDecreasedAt; /* point in time after which harddeposit can be decreased*/ + } + + struct EIP712Domain { + string name; + string version; + uint256 chainId; + } + + bytes32 public constant EIP712DOMAIN_TYPEHASH = keccak256( + "EIP712Domain(string name,string version,uint256 chainId)" + ); + bytes32 public constant CHEQUE_TYPEHASH = keccak256( + "Cheque(address chequebook,address beneficiary,uint256 cumulativePayout)" + ); + bytes32 public constant CASHOUT_TYPEHASH = keccak256( + "Cashout(address chequebook,address sender,uint256 requestPayout,address recipient,uint256 callerPayout)" + ); + bytes32 public constant CUSTOMDECREASETIMEOUT_TYPEHASH = keccak256( + "CustomDecreaseTimeout(address chequebook,address beneficiary,uint256 decreaseTimeout)" + ); + + // the EIP712 domain this contract uses + function domain() internal pure returns (EIP712Domain memory) { + uint256 chainId; + assembly { + chainId := chainid() + } + return EIP712Domain({ + name: "Chequebook", + version: "1.0", + chainId: chainId + }); + } + + // compute the EIP712 domain separator. this cannot be constant because it depends on chainId + function domainSeparator(EIP712Domain memory eip712Domain) internal pure returns (bytes32) { + return keccak256(abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256(bytes(eip712Domain.name)), + keccak256(bytes(eip712Domain.version)), + eip712Domain.chainId + )); + } + + // recover a signature with the EIP712 signing scheme + function recoverEIP712(bytes32 hash, bytes memory sig) internal pure returns (address) { + bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + domainSeparator(domain()), + hash + )); + return ECDSA.recover(digest, sig); + } + + /* The token against which this chequebook writes cheques */ + ERC20 public token; + /* associates every beneficiary with how much has been paid out to them */ + mapping (address => uint) public paidOut; + /* total amount paid out */ + uint public totalPaidOut; + /* associates every beneficiary with their HardDeposit */ + mapping (address => HardDeposit) public hardDeposits; + /* sum of all hard deposits */ + uint public totalHardDeposit; + /* issuer of the contract, set at construction */ + address public issuer; + /* indicates wether a cheque bounced in the past */ + bool public bounced; + + /** + @notice sets the issuer, defaultHardDepositTimeout and receives an initial deposit + @param _issuer the issuer of cheques from this chequebook (needed as an argument for "Setting up a chequebook as a payment"). + _issuer must be an Externally Owned Account, or it must support calling the function cashCheque + @param _defaultHardDepositTimeout duration in seconds which by default will be used to reduce hardDeposit allocations + */ + constructor(address _issuer, address _token, uint _defaultHardDepositTimeout) public { + issuer = _issuer; + token = ERC20(_token); + defaultHardDepositTimeout = _defaultHardDepositTimeout; + } + + /// @return the balance of the chequebook + function balance() public view returns(uint) { + return token.balanceOf(address(this)); + } + /// @return the part of the balance that is not covered by hard deposits + function liquidBalance() public view returns(uint) { + return balance().sub(totalHardDeposit); + } + + /// @return the part of the balance available for a specific beneficiary + function liquidBalanceFor(address beneficiary) public view returns(uint) { + return liquidBalance().add(hardDeposits[beneficiary].amount); + } + /** + @dev internal function responsible for checking the issuerSignature, updating hardDeposit balances and doing transfers. + Called by cashCheque and cashChequeBeneficary + @param beneficiary the beneficiary to which cheques were assigned. Beneficiary must be an Externally Owned Account + @param recipient receives the differences between cumulativePayment and what was already paid-out to the beneficiary minus callerPayout + @param cumulativePayout cumulative amount of cheques assigned to beneficiary + @param issuerSig if issuer is not the sender, issuer must have given explicit approval on the cumulativePayout to the beneficiary + */ + function _cashChequeInternal( + address beneficiary, + address recipient, + uint cumulativePayout, + uint callerPayout, + bytes memory issuerSig + ) internal { + /* The issuer must have given explicit approval to the cumulativePayout, either by being the caller or by signature*/ + if (msg.sender != issuer) { + require(issuer == recoverEIP712(chequeHash(address(this), beneficiary, cumulativePayout), issuerSig), + "SimpleSwap: invalid issuer signature"); + } + /* the requestPayout is the amount requested for payment processing */ + uint requestPayout = cumulativePayout.sub(paidOut[beneficiary]); + /* calculates acutal payout */ + uint totalPayout = Math.min(requestPayout, liquidBalanceFor(beneficiary)); + /* calculates hard-deposit usage */ + uint hardDepositUsage = Math.min(totalPayout, hardDeposits[beneficiary].amount); + require(totalPayout >= callerPayout, "SimpleSwap: cannot pay caller"); + /* if there are some of the hard deposit used, update hardDeposits*/ + if (hardDepositUsage != 0) { + hardDeposits[beneficiary].amount = hardDeposits[beneficiary].amount.sub(hardDepositUsage); + + totalHardDeposit = totalHardDeposit.sub(hardDepositUsage); + } + /* increase the stored paidOut amount to avoid double payout */ + paidOut[beneficiary] = paidOut[beneficiary].add(totalPayout); + totalPaidOut = totalPaidOut.add(totalPayout); + /* do the actual payments */ + + require(token.transfer(recipient, totalPayout.sub(callerPayout)), "SimpleSwap: SimpleSwap: transfer failed"); + /* do a transfer to the caller if specified*/ + if (callerPayout != 0) { + require(token.transfer(msg.sender, callerPayout), "SimpleSwap: SimpleSwap: transfer failed"); + } + emit ChequeCashed(beneficiary, recipient, msg.sender, totalPayout, cumulativePayout, callerPayout); + /* let the world know that the issuer has over-promised on outstanding cheques */ + if (requestPayout != totalPayout) { + bounced = true; + emit ChequeBounced(); + } + } + /** + @notice cash a cheque of the beneficiary by a non-beneficiary and reward the sender for doing so with callerPayout + @dev a beneficiary must be able to generate signatures (be an Externally Owned Account) to make use of this feature + @param beneficiary the beneficiary to which cheques were assigned. Beneficiary must be an Externally Owned Account + @param recipient receives the differences between cumulativePayment and what was already paid-out to the beneficiary minus callerPayout + @param cumulativePayout cumulative amount of cheques assigned to beneficiary + @param beneficiarySig beneficiary must have given explicit approval for cashing out the cumulativePayout by the sender and sending the callerPayout + @param issuerSig if issuer is not the sender, issuer must have given explicit approval on the cumulativePayout to the beneficiary + @param callerPayout when beneficiary does not have ether yet, he can incentivize other people to cash cheques with help of callerPayout + @param issuerSig if issuer is not the sender, issuer must have given explicit approval on the cumulativePayout to the beneficiary + */ + function cashCheque( + address beneficiary, + address recipient, + uint cumulativePayout, + bytes memory beneficiarySig, + uint256 callerPayout, + bytes memory issuerSig + ) public { + require( + beneficiary == recoverEIP712( + cashOutHash( + address(this), + msg.sender, + cumulativePayout, + recipient, + callerPayout + ), beneficiarySig + ), "SimpleSwap: invalid beneficiary signature"); + _cashChequeInternal(beneficiary, recipient, cumulativePayout, callerPayout, issuerSig); + } + + /** + @notice cash a cheque as beneficiary + @param recipient receives the differences between cumulativePayment and what was already paid-out to the beneficiary minus callerPayout + @param cumulativePayout amount requested to pay out + @param issuerSig issuer must have given explicit approval on the cumulativePayout to the beneficiary + */ + function cashChequeBeneficiary(address recipient, uint cumulativePayout, bytes memory issuerSig) public { + _cashChequeInternal(msg.sender, recipient, cumulativePayout, 0, issuerSig); + } + + /** + @notice prepare to decrease the hard deposit + @dev decreasing hardDeposits must be done in two steps to allow beneficiaries to cash any uncashed cheques (and make use of the assgined hard-deposits) + @param beneficiary beneficiary whose hard deposit should be decreased + @param decreaseAmount amount that the deposit is supposed to be decreased by + */ + function prepareDecreaseHardDeposit(address beneficiary, uint decreaseAmount) public { + require(msg.sender == issuer, "SimpleSwap: not issuer"); + HardDeposit storage hardDeposit = hardDeposits[beneficiary]; + /* cannot decrease it by more than the deposit */ + require(decreaseAmount <= hardDeposit.amount, "SimpleSwap: hard deposit not sufficient"); + // if hardDeposit.timeout was never set, apply defaultHardDepositTimeout + uint timeout = hardDeposit.timeout == 0 ? defaultHardDepositTimeout : hardDeposit.timeout; + hardDeposit.canBeDecreasedAt = now + timeout; + hardDeposit.decreaseAmount = decreaseAmount; + emit HardDepositDecreasePrepared(beneficiary, decreaseAmount); + } + + /** + @notice decrease the hard deposit after waiting the necesary amount of time since prepareDecreaseHardDeposit was called + @param beneficiary beneficiary whose hard deposit should be decreased + */ + function decreaseHardDeposit(address beneficiary) public { + HardDeposit storage hardDeposit = hardDeposits[beneficiary]; + require(now >= hardDeposit.canBeDecreasedAt && hardDeposit.canBeDecreasedAt != 0, "SimpleSwap: deposit not yet timed out"); + /* this throws if decreaseAmount > amount */ + //TODO: if there is a cash-out in between prepareDecreaseHardDeposit and decreaseHardDeposit, decreaseHardDeposit will throw and reducing hard-deposits is impossible. + hardDeposit.amount = hardDeposit.amount.sub(hardDeposit.decreaseAmount); + /* reset the canBeDecreasedAt to avoid a double decrease */ + hardDeposit.canBeDecreasedAt = 0; + /* keep totalDeposit in sync */ + totalHardDeposit = totalHardDeposit.sub(hardDeposit.decreaseAmount); + emit HardDepositAmountChanged(beneficiary, hardDeposit.amount); + } + + /** + @notice increase the hard deposit + @param beneficiary beneficiary whose hard deposit should be decreased + @param amount the new hard deposit + */ + function increaseHardDeposit(address beneficiary, uint amount) public { + require(msg.sender == issuer, "SimpleSwap: not issuer"); + /* ensure hard deposits don't exceed the global balance */ + require(totalHardDeposit.add(amount) <= balance(), "SimpleSwap: hard deposit cannot be more than balance"); + + HardDeposit storage hardDeposit = hardDeposits[beneficiary]; + hardDeposit.amount = hardDeposit.amount.add(amount); + // we don't explicitely set hardDepositTimout, as zero means using defaultHardDepositTimeout + totalHardDeposit = totalHardDeposit.add(amount); + /* disable any pending decrease */ + hardDeposit.canBeDecreasedAt = 0; + emit HardDepositAmountChanged(beneficiary, hardDeposit.amount); + } + + /** + @notice allows for setting a custom hardDepositDecreaseTimeout per beneficiary + @dev this is required when solvency must be guaranteed for a period longer than the defaultHardDepositDecreaseTimeout + @param beneficiary beneficiary whose hard deposit decreaseTimeout must be changed + @param hardDepositTimeout new hardDeposit.timeout for beneficiary + @param beneficiarySig beneficiary must give explicit approval by giving his signature on the new decreaseTimeout + */ + function setCustomHardDepositTimeout( + address beneficiary, + uint hardDepositTimeout, + bytes memory beneficiarySig + ) public { + require(msg.sender == issuer, "SimpleSwap: not issuer"); + require( + beneficiary == recoverEIP712(customDecreaseTimeoutHash(address(this), beneficiary, hardDepositTimeout), beneficiarySig), + "SimpleSwap: invalid beneficiary signature" + ); + hardDeposits[beneficiary].timeout = hardDepositTimeout; + emit HardDepositTimeoutChanged(beneficiary, hardDepositTimeout); + } + + /// @notice withdraw ether + /// @param amount amount to withdraw + // solhint-disable-next-line no-simple-event-func-name + function withdraw(uint amount) public { + /* only issuer can do this */ + require(msg.sender == issuer, "SimpleSwap: not issuer"); + /* ensure we don't take anything from the hard deposit */ + require(amount <= liquidBalance(), "SimpleSwap: liquidBalance not sufficient"); + require(token.transfer(issuer, amount), "SimpleSwap: SimpleSwap: transfer failed"); + } + + function chequeHash(address chequebook, address beneficiary, uint cumulativePayout) + internal pure returns (bytes32) { + return keccak256(abi.encode( + CHEQUE_TYPEHASH, + chequebook, + beneficiary, + cumulativePayout + )); + } + + function cashOutHash(address chequebook, address sender, uint requestPayout, address recipient, uint callerPayout) + internal pure returns (bytes32) { + return keccak256(abi.encode( + CASHOUT_TYPEHASH, + chequebook, + sender, + requestPayout, + recipient, + callerPayout + )); + } + + function customDecreaseTimeoutHash(address chequebook, address beneficiary, uint decreaseTimeout) + internal pure returns (bytes32) { + return keccak256(abi.encode( + CUSTOMDECREASETIMEOUT_TYPEHASH, + chequebook, + beneficiary, + decreaseTimeout + )); + } +} + + diff --git a/contracts/SimpleSwapFactory.sol b/contracts/SimpleSwapFactory.sol new file mode 100644 index 0000000..8ae6f09 --- /dev/null +++ b/contracts/SimpleSwapFactory.sol @@ -0,0 +1,35 @@ +pragma solidity =0.6.12; +import "./ERC20SimpleSwap.sol"; + +/** +@title Factory contract for SimpleSwap +@author The Swarm Authors +@notice This contract deploys SimpleSwap contracts +*/ +contract SimpleSwapFactory { + + /* event fired on every new SimpleSwap deployment */ + event SimpleSwapDeployed(address contractAddress); + + /* mapping to keep track of which contracts were deployed by this factory */ + mapping (address => bool) public deployedContracts; + + /* address of the ERC20-token, to be used by the to-be-deployed chequebooks */ + address public ERC20Address; + + constructor(address _ERC20Address) public { + ERC20Address = _ERC20Address; + } + /** + @notice deployes a new SimpleSwap contract + @param issuer the issuer of cheques for the new chequebook + @param defaultHardDepositTimeoutDuration duration in seconds which by default will be used to reduce hardDeposit allocations + */ + function deploySimpleSwap(address issuer, uint defaultHardDepositTimeoutDuration) + public returns (address) { + address contractAddress = address(new ERC20SimpleSwap(issuer, ERC20Address, defaultHardDepositTimeoutDuration)); + deployedContracts[contractAddress] = true; + emit SimpleSwapDeployed(contractAddress); + return contractAddress; + } +} \ No newline at end of file