Skip to content

Instantly share code, notes, and snippets.

@hackfisher
Created December 10, 2017 08:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hackfisher/d535440362ee106472586f8858d49f60 to your computer and use it in GitHub Desktop.
Save hackfisher/d535440362ee106472586f8858d49f60 to your computer and use it in GitHub Desktop.
pragma solidity ^0.4.18;
contract Token {
function transfer(address _to, uint _value) public returns (bool success);
function transferFrom(address _from, address _to, uint _value) public returns (bool success);
function approve(address _spender, uint _value) public returns (bool success);
}
contract LocalEthereumEscrows {
// The address of the arbitrator
// In the first version, this is always localethereum staff.
address public arbitrator;
address public owner;
address public relayer;
uint32 public requestCancellationMinimumTime;
uint256 public feesAvailableForWithdraw;
uint8 constant ACTION_SELLER_CANNOT_CANCEL = 0x01; // Called when marking as paid or calling a dispute as the buyer
uint8 constant ACTION_BUYER_CANCEL = 0x02;
uint8 constant ACTION_SELLER_CANCEL = 0x03;
uint8 constant ACTION_SELLER_REQUEST_CANCEL = 0x04;
uint8 constant ACTION_RELEASE = 0x05;
uint8 constant ACTION_DISPUTE = 0x06;
event Created(bytes32 _tradeHash);
event SellerCancelDisabled(bytes32 _tradeHash);
event SellerRequestedCancel(bytes32 _tradeHash);
event CancelledBySeller(bytes32 _tradeHash);
event CancelledByBuyer(bytes32 _tradeHash);
event Released(bytes32 _tradeHash);
event DisputeResolved(bytes32 _tradeHash);
struct Escrow {
// Set so we know the trade has already been created
bool exists;
// The timestamp in which the seller can cancel the trade if the buyer has not yet marked as paid. Set to 0 on marked paid or dispute
// 1 = unlimited cancel time
uint32 sellerCanCancelAfter;
// The total cost of gas spent by relaying parties. This amount will be
// refunded/paid to localethereum.com once the escrow is finished.
uint128 totalGasFeesSpentByRelayer;
}
// Mapping of active trades. Key is a hash of the trade data
mapping (bytes32 => Escrow) public escrows;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
modifier onlyArbitrator() {
require(msg.sender == arbitrator);
_;
}
function getRelayedSender(
bytes16 _tradeID, // The unique ID of the trade, generated by localethereum.com
uint8 _actionByte, // The desired action of the user, matching an ACTION_* constant
uint128 _maximumGasPrice, // The maximum gas price the user is willing to pay
uint8 _v, // Signature value
bytes32 _r, // Signature value
bytes32 _s // Signature value
) view private returns (address) {
bytes32 _hash = keccak256(_tradeID, _actionByte, _maximumGasPrice);
if(tx.gasprice > _maximumGasPrice) return;
return ecrecover(_hash, _v, _r, _s);
}
function LocalEthereumEscrows() public {
/**
* Initialize the contract.
*/
owner = msg.sender;
arbitrator = msg.sender;
relayer = msg.sender;
requestCancellationMinimumTime = 2 hours; // TODO
}
function getEscrowAndHash(
/**
* Hashes the values and returns the matching escrow object and trade hash.
* Returns an empty escrow struct and 0 _tradeHash if not found
*/
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee
) view private returns (Escrow, bytes32) {
bytes32 _tradeHash = keccak256(_tradeID, _seller, _buyer, _value, _fee);
return (escrows[_tradeHash], _tradeHash);
}
function createEscrow(
/**
* Create a new escrow and add it to `escrows`.
* _tradeHash is created by hashing _tradeID, _seller, _buyer, _value and _fee variables. These variables must be supplied on future contract calls.
* v, r and s is the signature data supplied from the api. The sig is keccak256(_tradeHash, _paymentWindowInSeconds, _expiry).
*/
bytes16 _tradeID, // The unique ID of the trade, generated by localethereum.com
address _seller, // The selling party of the trade
address _buyer, // The buying party of the trade
uint256 _value, // The ether amount being held in escrow
uint16 _fee, // The localethereum.com fee in 1/10000ths
uint32 _paymentWindowInSeconds, // The time in seconds from contract creation that the buyer has to mark as paid
uint32 _expiry, // Provided by localethereum.com. This transaction must be created before this time.
uint8 _v, // Signature value
bytes32 _r, // Signature value
bytes32 _s // Signature value
) payable external {
bytes32 _tradeHash = keccak256(_tradeID, _seller, _buyer, _value, _fee);
require(!escrows[_tradeHash].exists); // Require that trade does not already exist
require(ecrecover(keccak256(_tradeHash, _paymentWindowInSeconds, _expiry), _v, _r, _s) == relayer); // Signature must have come from the relayer
require(block.timestamp < _expiry);
require(msg.value == _value && msg.value > 0); // Check sent eth against signed _value and make sure is not 0
uint32 _sellerCanCancelAfter = _paymentWindowInSeconds == 0 ? 1 : uint32(block.timestamp) + _paymentWindowInSeconds;
escrows[_tradeHash] = Escrow(true, _sellerCanCancelAfter, 0);
Created(_tradeHash);
}
uint16 constant GAS_doRelease = 36100;
function doRelease(
/**
* Called by the seller to releases the funds for a successful trade.
* Deletes the trade from the `escrows` mapping.
*/
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee,
uint128 _additionalGas
) private returns (bool) {
var (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
if (!_escrow.exists) return false;
uint128 _gasFees = _escrow.totalGasFeesSpentByRelayer + (msg.sender == relayer ? (GAS_doRelease + _additionalGas) * uint128(tx.gasprice) : 0);
delete escrows[_tradeHash];
Released(_tradeHash);
transferMinusFees(_buyer, _value, _gasFees, _fee);
return true;
}
uint16 constant GAS_doDisableSellerCancel = 12100;
function doDisableSellerCancel(
/**
* Stops the seller from cancelling the trade.
* Can only be called the buyer.
* Used to mark the trade as paid, or if the buyer has a dispute.
*/
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee,
uint128 _additionalGas
) private returns (bool) {
var (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
if (!_escrow.exists) return false;
if(_escrow.sellerCanCancelAfter == 0) return false;
escrows[_tradeHash].sellerCanCancelAfter = 0;
SellerCancelDisabled(_tradeHash);
if (msg.sender == relayer) {
increaseGasSpent(_tradeHash, GAS_doDisableSellerCancel + _additionalGas);
}
return true;
}
uint16 constant GAS_doBuyerCancel = 36100;
function doBuyerCancel(
/**
* Cancels the trade and returns the ether to the seller.
* Can only be called the buyer.
*/
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee,
uint128 _additionalGas
) private returns (bool) {
var (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
if (!_escrow.exists) return false;
uint128 _gasFees = _escrow.totalGasFeesSpentByRelayer + (msg.sender == relayer ? (GAS_doBuyerCancel + _additionalGas) * uint128(tx.gasprice) : 0);
delete escrows[_tradeHash];
CancelledByBuyer(_tradeHash);
transferMinusFees(_seller, _value, _gasFees, 0);
return true;
}
uint16 constant GAS_doSellerCancel = 36100;
function doSellerCancel(
/**
* Cancels the trade and returns the ether to the seller.
* Can only be called the seller.
* Can only be called if the payment window was missed by the buyer
*/
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee,
uint128 _additionalGas
) private returns (bool) {
var (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
if (!_escrow.exists) return false;
if(_escrow.sellerCanCancelAfter <= 1 || _escrow.sellerCanCancelAfter > block.timestamp) return false;
uint128 _gasFees = _escrow.totalGasFeesSpentByRelayer + (msg.sender == relayer ? (GAS_doSellerCancel + _additionalGas) * uint128(tx.gasprice) : 0);
delete escrows[_tradeHash];
CancelledBySeller(_tradeHash);
transferMinusFees(_seller, _value, _gasFees, 0);
return true;
}
uint16 constant GAS_doSellerRequestCancel = 12100;
function doSellerRequestCancel(
/**
* Called by the seller if the buyer is unresponsive
* Can only be called on unlimited payment window trades (sellerCanCancelAfter == 1)
* Sets the payment window to `requestCancellationMinimumTime` from now, in which it can be cancelled.
*/
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee,
uint128 _additionalGas
) private returns (bool) {
// Called on unlimited payment window trades wheret the buyer is not responding
var (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
if (!_escrow.exists) return false;
if(_escrow.sellerCanCancelAfter != 1) return false;
escrows[_tradeHash].sellerCanCancelAfter = uint32(block.timestamp) + requestCancellationMinimumTime;
SellerRequestedCancel(_tradeHash);
if (msg.sender == relayer) {
increaseGasSpent(_tradeHash, GAS_doSellerRequestCancel + _additionalGas);
}
return true;
}
uint16 constant GAS_doResolveDispute = 36100;
function resolveDispute(
/**
* Called by the arbitrator to resolve a dispute
* Requires the signed ACTION_DISPUTE actionByte from either the buyer or the seller
*/
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee,
uint8 _v,
bytes32 _r,
bytes32 _s,
uint8 _buyerPercent
) external onlyArbitrator {
address _signature = ecrecover(keccak256(_tradeID, ACTION_DISPUTE), _v, _r, _s);
require(_signature == _buyer || _signature == _seller);
var (_escrow, _tradeHash) = getEscrowAndHash(_tradeID, _seller, _buyer, _value, _fee);
require(_escrow.exists);
require(_buyerPercent <= 100);
uint256 _totalFees = _escrow.totalGasFeesSpentByRelayer + GAS_doResolveDispute;
require(_value - _totalFees <= _value); // Prevent underflow
feesAvailableForWithdraw += _totalFees; // Add the the pot for localethereum to withdraw
delete escrows[_tradeHash];
DisputeResolved(_tradeHash);
_buyer.transfer((_value - _totalFees) * _buyerPercent / 100);
_seller.transfer((_value - _totalFees) * (100 - _buyerPercent) / 100);
}
function release(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee) external returns (bool){
require(msg.sender == _seller);
return doRelease(_tradeID, _seller, _buyer, _value, _fee, 0);
}
function disableSellerCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee) external returns (bool) {
require(msg.sender == _buyer);
return doDisableSellerCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
}
function buyerCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee) external returns (bool) {
require(msg.sender == _buyer);
return doBuyerCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
}
function sellerCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee) external returns (bool) {
require(msg.sender == _seller);
return doSellerCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
}
function sellerRequestCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee) external returns (bool) {
require(msg.sender == _seller);
return doSellerRequestCancel(_tradeID, _seller, _buyer, _value, _fee, 0);
}
function relaySellerCannotCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee, uint128 _maximumGasPrice, uint8 _v, bytes32 _r, bytes32 _s) external returns (bool) {
return relay(_tradeID, _seller, _buyer, _value, _fee, _maximumGasPrice, _v, _r, _s, ACTION_SELLER_CANNOT_CANCEL, 0);
}
function relayBuyerCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee, uint128 _maximumGasPrice, uint8 _v, bytes32 _r, bytes32 _s) external returns (bool) {
return relay(_tradeID, _seller, _buyer, _value, _fee, _maximumGasPrice, _v, _r, _s, ACTION_BUYER_CANCEL, 0);
}
function relayRelease(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee, uint128 _maximumGasPrice, uint8 _v, bytes32 _r, bytes32 _s) external returns (bool) {
return relay(_tradeID, _seller, _buyer, _value, _fee, _maximumGasPrice, _v, _r, _s, ACTION_RELEASE, 0);
}
function relaySellerCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee, uint128 _maximumGasPrice, uint8 _v, bytes32 _r, bytes32 _s) external returns (bool) {
return relay(_tradeID, _seller, _buyer, _value, _fee, _maximumGasPrice, _v, _r, _s, ACTION_SELLER_CANCEL, 0);
}
function relaySellerRequestCancel(bytes16 _tradeID, address _seller, address _buyer, uint256 _value, uint16 _fee, uint128 _maximumGasPrice, uint8 _v, bytes32 _r, bytes32 _s) external returns (bool) {
return relay(_tradeID, _seller, _buyer, _value, _fee, _maximumGasPrice, _v, _r, _s, ACTION_SELLER_REQUEST_CANCEL, 0);
}
function relay(
bytes16 _tradeID,
address _seller,
address _buyer,
uint256 _value,
uint16 _fee,
uint128 _maximumGasPrice,
uint8 _v,
bytes32 _r,
bytes32 _s,
uint8 _actionByte,
uint128 _additionalGas
) private returns (bool) {
address _relayedSender = getRelayedSender(_tradeID, _actionByte, _maximumGasPrice, _v, _r, _s);
if (_relayedSender == _buyer) {
if (_actionByte == ACTION_SELLER_CANNOT_CANCEL) {
return doDisableSellerCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
} else if (_actionByte == ACTION_BUYER_CANCEL) {
return doBuyerCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
}
} else if (_relayedSender == _seller) {
if (_actionByte == ACTION_RELEASE) {
return doRelease(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
} else if (_actionByte == ACTION_SELLER_CANCEL) {
return doSellerCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
} else if (_actionByte == ACTION_SELLER_REQUEST_CANCEL){
return doSellerRequestCancel(_tradeID, _seller, _buyer, _value, _fee, _additionalGas);
}
} else {
return false;
}
}
uint16 constant GAS_batchRelayBaseCost = 28500;
function batchRelay(
/**
* Call multiple relay methods at once to save on gas.
*/
bytes16[] _tradeID,
address[] _seller,
address[] _buyer,
uint256[] _value,
uint16[] _fee,
uint128[] _maximumGasPrice,
uint8[] _v,
bytes32[] _r,
bytes32[] _s,
uint8[] _actionByte
) public returns (bool[]) {
bool[] memory _results = new bool[](_tradeID.length);
uint128 _additionalGas = uint128(msg.sender == relayer ? GAS_batchRelayBaseCost / _tradeID.length : 0);
for (uint8 i=0; i<_tradeID.length; i++) {
_results[i] = relay(_tradeID[i], _seller[i], _buyer[i], _value[i], _fee[i], _maximumGasPrice[i], _v[i], _r[i], _s[i], _actionByte[i], _additionalGas);
}
return _results;
}
function increaseGasSpent(bytes32 _tradeHash, uint128 _gas) private {
/** Increase `totalGasFeesSpentByRelayer` to be charged later on completion of the trade.
*/
escrows[_tradeHash].totalGasFeesSpentByRelayer += _gas * uint128(tx.gasprice);
}
function transferMinusFees(address _to, uint256 _value, uint128 _totalGasFeesSpentByRelayer, uint16 _fee) private {
uint256 _totalFees = (_value * _fee / 10000) + _totalGasFeesSpentByRelayer;
if(_value - _totalFees > _value) return; // Prevent underflow
feesAvailableForWithdraw += _totalFees; // Add the the pot for localethereum to withdraw
_to.transfer(_value - _totalFees);
}
function withdrawFees(address _to, uint256 _amount) onlyOwner external {
/**
* Withdraw fees collected by the contract. Only the owner can call this.
*/
require(_amount <= feesAvailableForWithdraw); // Also prevents underflow
feesAvailableForWithdraw -= _amount;
_to.transfer(_amount);
}
function setArbitrator(address _newArbitrator) onlyOwner external {
/**
* Set the arbitrator to a new address. Only the owner can call this.
* @param address _newArbitrator
*/
arbitrator = _newArbitrator;
}
function setOwner(address _newOwner) onlyOwner external {
/**
* Change the owner to a new address. Only the owner can call this.
* @param address _newOwner
*/
owner = _newOwner;
}
function setRelayer(address _newRelayer) onlyOwner external {
/**
* Change the relayer to a new address. Only the owner can call this.
* @param address _newRelayer
*/
relayer = _newRelayer;
}
function setRequestCancellationMinimumTime(uint32 _newRequestCancellationMinimumTime) onlyOwner external {
/**
* Change the requestCancellationMinimumTime. Only the owner can call this.
* @param uint32 _newRequestCancellationMinimumTime
*/
requestCancellationMinimumTime = _newRequestCancellationMinimumTime;
}
function transferToken(Token _tokenContract, address _transferTo, uint256 _value) onlyOwner external {
/**
* If ERC20 tokens are sent to this contract, they will be trapped forever.
* This function is way for us to withdraw them so we can get them back to their rightful owner
*/
_tokenContract.transfer(_transferTo, _value);
}
function transferTokenFrom(Token _tokenContract, address _transferTo, address _transferFrom, uint256 _value) onlyOwner external {
/**
* If ERC20 tokens are sent to this contract, they will be trapped forever.
* This function is way for us to withdraw them so we can get them back to their rightful owner
*/
_tokenContract.transferFrom(_transferTo, _transferFrom, _value);
}
function approveToken(Token _tokenContract, address _spender, uint256 _value) onlyOwner external {
/**
* If ERC20 tokens are sent to this contract, they will be trapped forever.
* This function is way for us to withdraw them so we can get them back to their rightful owner
*/
_tokenContract.approve(_spender, _value);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment