364 lines
16 KiB
Solidity
364 lines
16 KiB
Solidity
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
|
|
));
|
|
}
|
|
}
|