pragma solidity ^0.7.0; 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"; import "hardhat/console.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 returns (bytes32) { console.log("domainSeparator name", eip712Domain.name); console.log("domainSeparator version", eip712Domain.version); console.log("domainSeparator chainId", eip712Domain.chainId); 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 returns (address) { console.log("recoverEIP712 hash"); console.logBytes32(hash); bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", domainSeparator(domain()), hash )); console.log("recoverEIP712 digest"); console.logBytes32(digest); console.log("recoverEIP712 sig"); console.logBytes(sig); console.log("ECDSA recover", ECDSA.recover(digest, sig)); // TODO redo and print 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 { //var hash = chequeHash(address(this), beneficiary, cumulativePayout); console.log("_cashChequeInternal"); console.log("address this", address(this)); console.log("beneficiary", beneficiary); console.log("cumulativePayout", cumulativePayout); // XXX don't work //console.log("issuerSig", issuerSig); //console.log("hash", chequeHash(address(this), beneficiary, cumulativePayout)); 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 = block.timestamp + 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(block.timestamp >= 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 )); } }