mirror of https://github.com/embarklabs/embark.git
750 lines
28 KiB
Solidity
750 lines
28 KiB
Solidity
|
/* solium-disable security/no-block-members */
|
||
|
/* solium-disable security/no-inline-assembly */
|
||
|
|
||
|
pragma solidity >=0.5.0 <0.6.0;
|
||
|
|
||
|
import "../common/Pausable.sol";
|
||
|
import "../common/MessageSigned.sol";
|
||
|
import "../token/ERC20Token.sol";
|
||
|
import "../token/SafeTransfer.sol";
|
||
|
import "./ArbitrationLicense.sol";
|
||
|
import "./License.sol";
|
||
|
import "./UserStore.sol";
|
||
|
import "./OfferStore.sol";
|
||
|
import "./Fees.sol";
|
||
|
import "./Arbitrable.sol";
|
||
|
import "./IEscrow.sol";
|
||
|
import "../proxy/Proxiable.sol";
|
||
|
|
||
|
/**
|
||
|
* @title Escrow
|
||
|
* @dev Escrow contract for selling ETH and ERC20 tokens
|
||
|
*/
|
||
|
contract Escrow is IEscrow, Pausable, MessageSigned, Fees, Arbitrable, Proxiable {
|
||
|
|
||
|
EscrowTransaction[] public transactions;
|
||
|
|
||
|
address public relayer;
|
||
|
OfferStore public offerStore;
|
||
|
UserStore public userStore;
|
||
|
|
||
|
|
||
|
event Created(uint indexed offerId, address indexed seller, address indexed buyer, uint escrowId);
|
||
|
event Funded(uint indexed escrowId, address indexed buyer, uint expirationTime, uint amount);
|
||
|
event Paid(uint indexed escrowId, address indexed seller);
|
||
|
event Released(uint indexed escrowId, address indexed seller, address indexed buyer, address destination, bool isDispute);
|
||
|
event Canceled(uint indexed escrowId, address indexed seller, address indexed buyer, bool isDispute);
|
||
|
event DestinationUpdated(uint indexed escrowId, address indexed buyer, address newAddress);
|
||
|
event Rating(uint indexed offerId, address indexed participant, uint indexed escrowId, uint rating, bool ratingSeller);
|
||
|
|
||
|
/**
|
||
|
* @param _relayer EscrowRelay contract address
|
||
|
* @param _fallbackArbitrator Default arbitrator to use after timeout on solving arbitrations
|
||
|
* @param _arbitratorLicenses License contract instance address for arbitrators
|
||
|
* @param _offerStore OfferStore contract address
|
||
|
* @param _userStore UserStore contract address
|
||
|
* @param _feeDestination Address where the fees are going to be sent
|
||
|
* @param _feeMilliPercent Percentage applied as a fee to each escrow. 1000 == 1%
|
||
|
*/
|
||
|
constructor(
|
||
|
address _relayer,
|
||
|
address _fallbackArbitrator,
|
||
|
address _arbitratorLicenses,
|
||
|
address _offerStore,
|
||
|
address _userStore,
|
||
|
address payable _feeDestination,
|
||
|
uint _feeMilliPercent)
|
||
|
Fees(_feeDestination, _feeMilliPercent)
|
||
|
Arbitrable(_arbitratorLicenses, _fallbackArbitrator)
|
||
|
public {
|
||
|
_initialized = true;
|
||
|
relayer = _relayer;
|
||
|
offerStore = OfferStore(_offerStore);
|
||
|
userStore = UserStore(_userStore);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Initialize contract (used with proxy). Can only be called once
|
||
|
* @param _fallbackArbitrator Default arbitrator to use after timeout on solving arbitrations
|
||
|
* @param _relayer EscrowRelay contract address
|
||
|
* @param _arbitratorLicenses License contract instance address for arbitrators
|
||
|
* @param _offerStore OfferStore contract address
|
||
|
* @param _userStore UserStore contract address
|
||
|
* @param _feeDestination Address where the fees are going to be sent
|
||
|
* @param _feeMilliPercent Percentage applied as a fee to each escrow. 1000 == 1%
|
||
|
*/
|
||
|
function init(
|
||
|
address _fallbackArbitrator,
|
||
|
address _relayer,
|
||
|
address _arbitratorLicenses,
|
||
|
address _offerStore,
|
||
|
address _userStore,
|
||
|
address payable _feeDestination,
|
||
|
uint _feeMilliPercent
|
||
|
) external {
|
||
|
assert(_initialized == false);
|
||
|
|
||
|
_initialized = true;
|
||
|
|
||
|
fallbackArbitrator = _fallbackArbitrator;
|
||
|
arbitratorLicenses = ArbitrationLicense(_arbitratorLicenses);
|
||
|
offerStore = OfferStore(_offerStore);
|
||
|
userStore = UserStore(_userStore);
|
||
|
relayer = _relayer;
|
||
|
feeDestination = _feeDestination;
|
||
|
feeMilliPercent = _feeMilliPercent;
|
||
|
paused = false;
|
||
|
arbitrationTimeout = 5 days;
|
||
|
|
||
|
_setOwner(msg.sender);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Update arbitration timeout. Can only be called by owner
|
||
|
* @param _newTimeout new timeout in seconds
|
||
|
*/
|
||
|
function setArbitrationTimeout(uint _newTimeout) public onlyOwner {
|
||
|
arbitrationTimeout = _newTimeout;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Update proxy implementation. Can only be called by owner
|
||
|
* @param _newCode New contract implementation address
|
||
|
*/
|
||
|
function updateCode(address _newCode) public onlyOwner {
|
||
|
updateCodeAddress(_newCode);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Update relayer contract address. Can only be called by the contract owner
|
||
|
* @param _relayer EscrowRelay contract address
|
||
|
*/
|
||
|
function setRelayer(address _relayer) external onlyOwner {
|
||
|
relayer = _relayer;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Update fallback arbitrator. Can only be called by the contract owner
|
||
|
* @param _fallbackArbitrator New fallback arbitrator
|
||
|
*/
|
||
|
function setFallbackArbitrator(address _fallbackArbitrator) external onlyOwner {
|
||
|
fallbackArbitrator = _fallbackArbitrator;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Update license contract addresses
|
||
|
* @param _arbitratorLicenses License contract instance address for arbitrators
|
||
|
*/
|
||
|
function setArbitratorLicense(address _arbitratorLicenses) external onlyOwner {
|
||
|
arbitratorLicenses = ArbitrationLicense(_arbitratorLicenses);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Update Metadata Stores contract address
|
||
|
* @param _offerStore OfferStore contract address
|
||
|
* @param _userStore UserStore contract address
|
||
|
*/
|
||
|
function setMetadataStore(address _offerStore, address _userStore) external onlyOwner {
|
||
|
offerStore = OfferStore(_offerStore);
|
||
|
userStore = UserStore(_userStore);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Escrow creation logic. Requires contract to be unpaused
|
||
|
* @param _buyer Buyer Address
|
||
|
* @param _destination Address that will receive the crypto
|
||
|
* @param _offerId Offer
|
||
|
* @param _tokenAmount Amount buyer is willing to trade
|
||
|
* @param _fiatAmount Indicates how much FIAT will the user pay for the tokenAmount
|
||
|
* @return Id of the Escrow
|
||
|
*/
|
||
|
function _createTransaction(
|
||
|
address payable _buyer,
|
||
|
address payable _destination,
|
||
|
uint _offerId,
|
||
|
uint _tokenAmount,
|
||
|
uint _fiatAmount
|
||
|
) internal whenNotPaused returns(uint escrowId)
|
||
|
{
|
||
|
address payable seller;
|
||
|
address payable arbitrator;
|
||
|
bool deleted;
|
||
|
address token;
|
||
|
|
||
|
(token, , , , , , seller, arbitrator, deleted) = offerStore.offer(_offerId);
|
||
|
|
||
|
require(!deleted, "Offer is not valid");
|
||
|
require(seller != _buyer, "Seller and Buyer must be different");
|
||
|
require(arbitrator != _buyer && arbitrator != address(0), "Cannot buy offers where buyer is arbitrator");
|
||
|
require(_tokenAmount != 0 && _fiatAmount != 0, "Trade amounts cannot be 0");
|
||
|
|
||
|
escrowId = transactions.length++;
|
||
|
|
||
|
EscrowTransaction storage trx = transactions[escrowId];
|
||
|
|
||
|
trx.offerId = _offerId;
|
||
|
trx.token = token;
|
||
|
trx.buyer = _buyer;
|
||
|
trx.seller = seller;
|
||
|
trx.destination = _destination;
|
||
|
trx.arbitrator = arbitrator;
|
||
|
trx.tokenAmount = _tokenAmount;
|
||
|
trx.fiatAmount = _fiatAmount;
|
||
|
|
||
|
emit Created(
|
||
|
_offerId,
|
||
|
seller,
|
||
|
_buyer,
|
||
|
escrowId
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Create a new escrow
|
||
|
* @param _offerId Offer
|
||
|
* @param _tokenAmount Amount buyer is willing to trade
|
||
|
* @param _fiatAmount Indicates how much FIAT will the user pay for the tokenAmount
|
||
|
* @param _destination Address that will receive the crypto
|
||
|
* @param _contactData Contact Data ContactType:UserId
|
||
|
* @param _location The location on earth
|
||
|
* @param _username The username of the user
|
||
|
* @return Id of the new escrow
|
||
|
* @dev Requires contract to be unpaused.
|
||
|
*/
|
||
|
function createEscrow(
|
||
|
uint _offerId,
|
||
|
uint _tokenAmount,
|
||
|
uint _fiatAmount,
|
||
|
address payable _destination,
|
||
|
string memory _contactData,
|
||
|
string memory _location,
|
||
|
string memory _username
|
||
|
) public returns(uint escrowId) {
|
||
|
userStore.addOrUpdateUser(msg.sender, _contactData, _location, _username);
|
||
|
escrowId = _createTransaction(msg.sender, _destination, _offerId, _tokenAmount, _fiatAmount);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Create a new escrow
|
||
|
* @param _offerId Offer
|
||
|
* @param _tokenAmount Amount buyer is willing to trade
|
||
|
* @param _fiatAmount Indicates how much FIAT will the user pay for the tokenAmount
|
||
|
* @param _destination Address that will receive the crypto
|
||
|
* @param _contactData Contact Data ContactType:UserId
|
||
|
* @param _username The username of the user
|
||
|
* @param _nonce The nonce for the user (from UserStore.user_nonce(address))
|
||
|
* @param _signature buyer's signature
|
||
|
* @return Id of the new escrow
|
||
|
* @dev Requires contract to be unpaused.
|
||
|
* The seller needs to be licensed.
|
||
|
*/
|
||
|
function createEscrow(
|
||
|
uint _offerId,
|
||
|
uint _tokenAmount,
|
||
|
uint _fiatAmount,
|
||
|
address payable _destination,
|
||
|
string memory _contactData,
|
||
|
string memory _location,
|
||
|
string memory _username,
|
||
|
uint _nonce,
|
||
|
bytes memory _signature
|
||
|
) public returns(uint escrowId) {
|
||
|
address payable _buyer = userStore.addOrUpdateUser(_signature, _contactData, _location, _username, _nonce);
|
||
|
escrowId = _createTransaction(_buyer, _destination, _offerId, _tokenAmount, _fiatAmount);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Relay function for creating a transaction
|
||
|
* Can only be called by relayer address
|
||
|
* @param _sender Address marking the transaction as paid
|
||
|
* @param _offerId Offer
|
||
|
* @param _tokenAmount Amount buyer is willing to trade
|
||
|
* @param _fiatAmount Indicates how much FIAT will the user pay for the tokenAmount
|
||
|
* @param _destination Address that will receive the crypto
|
||
|
* @param _contactData Contact Data ContactType:UserId
|
||
|
* @param _location The location on earth
|
||
|
* @param _username The username of the user
|
||
|
* @return Id of the new escrow
|
||
|
* @dev Requires contract to be unpaused.
|
||
|
* The seller needs to be licensed.
|
||
|
*/
|
||
|
function createEscrow_relayed(
|
||
|
address payable _sender,
|
||
|
uint _offerId,
|
||
|
uint _tokenAmount,
|
||
|
uint _fiatAmount,
|
||
|
address payable _destination,
|
||
|
string calldata _contactData,
|
||
|
string calldata _location,
|
||
|
string calldata _username
|
||
|
) external returns(uint escrowId) {
|
||
|
assert(msg.sender == relayer);
|
||
|
|
||
|
userStore.addOrUpdateUser(_sender, _contactData, _location, _username);
|
||
|
escrowId = _createTransaction(_sender, _destination, _offerId, _tokenAmount, _fiatAmount);
|
||
|
}
|
||
|
|
||
|
function updateDestination(uint _escrowId, address payable _destination) external whenNotPaused {
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
|
||
|
EscrowStatus mStatus = trx.status;
|
||
|
require(mStatus == EscrowStatus.FUNDED || mStatus == EscrowStatus.CREATED || mStatus == EscrowStatus.PAID,
|
||
|
"Only transactions in created, funded or paid state can have their destination updated");
|
||
|
|
||
|
require(trx.buyer == msg.sender, "Only the buyer can invoke this function");
|
||
|
|
||
|
trx.destination = _destination;
|
||
|
|
||
|
emit DestinationUpdated(_escrowId, trx.buyer, _destination);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Fund a new escrow
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @dev Requires contract to be unpaused.
|
||
|
* The expiration time must be at least 10min in the future
|
||
|
* For eth transfer, _amount must be equals to msg.value, for token transfer, requires an allowance and transfer valid for _amount
|
||
|
*/
|
||
|
function fund(uint _escrowId) external payable whenNotPaused {
|
||
|
_fund(msg.sender, _escrowId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Escrow funding logic
|
||
|
* @param _from Seller address
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @dev Requires contract to be unpaused.
|
||
|
* The expiration time must be at least 10min in the future
|
||
|
* For eth transfer, _amount must be equals to msg.value, for token transfer, requires an allowance and transfer valid for _amount
|
||
|
*/
|
||
|
function _fund(address _from, uint _escrowId) internal whenNotPaused {
|
||
|
require(transactions[_escrowId].seller == _from, "Only the seller can invoke this function");
|
||
|
require(transactions[_escrowId].status == EscrowStatus.CREATED, "Invalid escrow status");
|
||
|
|
||
|
transactions[_escrowId].expirationTime = block.timestamp + 5 days;
|
||
|
transactions[_escrowId].status = EscrowStatus.FUNDED;
|
||
|
|
||
|
uint tokenAmount = transactions[_escrowId].tokenAmount;
|
||
|
|
||
|
address token = transactions[_escrowId].token;
|
||
|
|
||
|
_payFee(_from, _escrowId, tokenAmount, token);
|
||
|
|
||
|
emit Funded(_escrowId, transactions[_escrowId].buyer, block.timestamp + 5 days, tokenAmount);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Create and fund a new escrow, as a seller, once you get a buyer signature
|
||
|
* @param _offerId Offer
|
||
|
* @param _tokenAmount Amount buyer is willing to trade
|
||
|
* @param _fiatAmount Indicates how much FIAT will the user pay for the tokenAmount
|
||
|
* @param _bContactData Contact Data ContactType:UserId
|
||
|
* @param _bLocation The location on earth
|
||
|
* @param _bUsername The username of the user
|
||
|
* @param _bNonce The nonce for the user (from userStore.user_nonce(address))
|
||
|
* @param _bSignature buyer's signature
|
||
|
* @return Id of the new escrow
|
||
|
* @dev Requires contract to be unpaused.
|
||
|
* Restrictions from escrow creation and funding applies
|
||
|
*/
|
||
|
function createAndFund (
|
||
|
uint _offerId,
|
||
|
uint _tokenAmount,
|
||
|
uint _fiatAmount,
|
||
|
string memory _bContactData,
|
||
|
string memory _bLocation,
|
||
|
string memory _bUsername,
|
||
|
uint _bNonce,
|
||
|
bytes memory _bSignature
|
||
|
) public payable returns(uint escrowId) {
|
||
|
address payable _buyer = userStore.addOrUpdateUser(_bSignature, _bContactData, _bLocation, _bUsername, _bNonce);
|
||
|
escrowId = _createTransaction(_buyer, _buyer, _offerId, _tokenAmount, _fiatAmount);
|
||
|
_fund(msg.sender, escrowId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Buyer marks transaction as paid
|
||
|
* @param _sender Address marking the transaction as paid
|
||
|
* @param _escrowId Id of the escrow
|
||
|
*/
|
||
|
function _pay(address _sender, uint _escrowId) internal {
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
|
||
|
require(trx.status == EscrowStatus.FUNDED, "Transaction is not funded");
|
||
|
require(trx.expirationTime > block.timestamp, "Transaction already expired");
|
||
|
require(trx.buyer == _sender, "Only the buyer can invoke this function");
|
||
|
|
||
|
trx.status = EscrowStatus.PAID;
|
||
|
|
||
|
emit Paid(_escrowId, trx.seller);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Mark transaction as paid
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @dev Can only be executed by the buyer
|
||
|
*/
|
||
|
function pay(uint _escrowId) external {
|
||
|
_pay(msg.sender, _escrowId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Relay function for marking a transaction as paid
|
||
|
* Can only be called by relayer address
|
||
|
* @param _sender Address marking the transaction as paid
|
||
|
* @param _escrowId Id of the escrow
|
||
|
*/
|
||
|
function pay_relayed(address _sender, uint _escrowId) external {
|
||
|
assert(msg.sender == relayer);
|
||
|
_pay(_sender, _escrowId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Obtain message hash to be signed for marking a transaction as paid
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @return message hash
|
||
|
* @dev Once message is signed, pass it as _signature of pay(uint256,bytes)
|
||
|
*/
|
||
|
function paySignHash(uint _escrowId) public view returns(bytes32){
|
||
|
uint256 cid;
|
||
|
assembly {
|
||
|
cid := chainid()
|
||
|
}
|
||
|
return keccak256(
|
||
|
abi.encodePacked(
|
||
|
address(this),
|
||
|
"pay(uint256)",
|
||
|
_escrowId,
|
||
|
cid
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Mark transaction as paid (via signed message)
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _signature Signature of the paySignHash result.
|
||
|
* @dev There's a high probability of buyers not having ether to pay for the transaction.
|
||
|
* This allows anyone to relay the transaction.
|
||
|
* TODO: consider deducting funds later on release to pay the relayer (?)
|
||
|
*/
|
||
|
function pay(uint _escrowId, bytes calldata _signature) external {
|
||
|
address sender = _recoverAddress(_getSignHash(paySignHash(_escrowId)), _signature);
|
||
|
_pay(sender, _escrowId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Release funds to buyer
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _trx EscrowTransaction with data of transaction to be released
|
||
|
* @param _isDispute indicates if the release happened due to a dispute
|
||
|
*/
|
||
|
function _release(uint _escrowId, EscrowTransaction storage _trx, bool _isDispute) internal {
|
||
|
require(_trx.status != EscrowStatus.RELEASED, "Already released");
|
||
|
_trx.status = EscrowStatus.RELEASED;
|
||
|
|
||
|
if(!_isDispute){
|
||
|
offerStore.refundStake(_trx.offerId);
|
||
|
}
|
||
|
|
||
|
address token = _trx.token;
|
||
|
if(token == address(0)){
|
||
|
(bool success, ) = _trx.destination.call.value(_trx.tokenAmount)("");
|
||
|
require(success, "Transfer failed.");
|
||
|
} else {
|
||
|
require(_safeTransfer(ERC20Token(token), _trx.destination, _trx.tokenAmount), "Couldn't transfer funds");
|
||
|
}
|
||
|
|
||
|
_releaseFee(_trx.arbitrator, _trx.tokenAmount, token, _isDispute);
|
||
|
|
||
|
emit Released(_escrowId, _trx.seller, _trx.buyer, _trx.destination, _isDispute);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Release escrow funds to buyer
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @dev Requires contract to be unpaused.
|
||
|
* Can only be executed by the seller
|
||
|
* Transaction must not be expired, or previously canceled or released
|
||
|
*/
|
||
|
function release(uint _escrowId) external {
|
||
|
EscrowStatus mStatus = transactions[_escrowId].status;
|
||
|
require(transactions[_escrowId].seller == msg.sender, "Only the seller can invoke this function");
|
||
|
require(mStatus == EscrowStatus.PAID || mStatus == EscrowStatus.FUNDED, "Invalid transaction status");
|
||
|
require(!_isDisputed(_escrowId), "Can't release a transaction that has an arbitration process");
|
||
|
_release(_escrowId, transactions[_escrowId], false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Cancel an escrow operation
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @notice Requires contract to be unpaused.
|
||
|
* Can only be executed by the seller
|
||
|
* Transaction must be expired, or previously canceled or released
|
||
|
*/
|
||
|
function cancel(uint _escrowId) external whenNotPaused {
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
|
||
|
EscrowStatus mStatus = trx.status;
|
||
|
require(mStatus == EscrowStatus.FUNDED || mStatus == EscrowStatus.CREATED,
|
||
|
"Only transactions in created or funded state can be canceled");
|
||
|
|
||
|
require(trx.buyer == msg.sender || trx.seller == msg.sender, "Only participants can invoke this function");
|
||
|
|
||
|
if(mStatus == EscrowStatus.FUNDED){
|
||
|
if(msg.sender == trx.seller){
|
||
|
require(trx.expirationTime < block.timestamp, "Can only be canceled after expiration");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_cancel(_escrowId, trx, false);
|
||
|
}
|
||
|
|
||
|
// Same as cancel, but relayed by a contract so we get the sender as param
|
||
|
function cancel_relayed(address _sender, uint _escrowId) external {
|
||
|
assert(msg.sender == relayer);
|
||
|
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
EscrowStatus mStatus = trx.status;
|
||
|
require(trx.buyer == _sender, "Only the buyer can invoke this function");
|
||
|
require(mStatus == EscrowStatus.FUNDED || mStatus == EscrowStatus.CREATED,
|
||
|
"Only transactions in created or funded state can be canceled");
|
||
|
|
||
|
_cancel(_escrowId, trx, false);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Cancel transaction and send funds back to seller
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param trx EscrowTransaction with details of transaction to be marked as canceled
|
||
|
*/
|
||
|
function _cancel(uint _escrowId, EscrowTransaction storage trx, bool isDispute) internal {
|
||
|
EscrowStatus origStatus = trx.status;
|
||
|
|
||
|
require(trx.status != EscrowStatus.CANCELED, "Already canceled");
|
||
|
|
||
|
trx.status = EscrowStatus.CANCELED;
|
||
|
|
||
|
if (origStatus == EscrowStatus.FUNDED) {
|
||
|
address token = trx.token;
|
||
|
uint amount = trx.tokenAmount;
|
||
|
if (!isDispute) {
|
||
|
amount += _getValueOffMillipercent(trx.tokenAmount, feeMilliPercent);
|
||
|
}
|
||
|
|
||
|
if (token == address(0)) {
|
||
|
(bool success, ) = trx.seller.call.value(amount)("");
|
||
|
require(success, "Transfer failed.");
|
||
|
} else {
|
||
|
ERC20Token erc20token = ERC20Token(token);
|
||
|
require(_safeTransfer(erc20token, trx.seller, amount), "Transfer failed");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
trx.status = EscrowStatus.CANCELED;
|
||
|
|
||
|
emit Canceled(_escrowId, trx.seller, trx.buyer, isDispute);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* @notice Rates a transaction
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _rate rating of the transaction from 1 to 5
|
||
|
* @dev Can only be executed by the buyer
|
||
|
* Transaction must released
|
||
|
*/
|
||
|
function _rateTransaction(address _sender, uint _escrowId, uint _rate) internal {
|
||
|
require(_rate >= 1, "Rating needs to be at least 1");
|
||
|
require(_rate <= 5, "Rating needs to be at less than or equal to 5");
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
require(trx.status == EscrowStatus.RELEASED || hadDispute(_escrowId), "Transaction not completed yet");
|
||
|
|
||
|
if (trx.buyer == _sender) {
|
||
|
require(trx.sellerRating == 0, "Transaction already rated");
|
||
|
emit Rating(trx.offerId, trx.seller, _escrowId, _rate, true);
|
||
|
trx.sellerRating = _rate;
|
||
|
} else if (trx.seller == _sender) {
|
||
|
require(trx.buyerRating == 0, "Transaction already rated");
|
||
|
emit Rating(trx.offerId, trx.buyer, _escrowId, _rate, false);
|
||
|
trx.buyerRating = _rate;
|
||
|
} else {
|
||
|
revert("Only participants can invoke this function");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Rates a transaction
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _rate rating of the transaction from 1 to 5
|
||
|
* @dev Can only be executed by the buyer
|
||
|
* Transaction must released
|
||
|
*/
|
||
|
function rateTransaction(uint _escrowId, uint _rate) external {
|
||
|
_rateTransaction(msg.sender, _escrowId, _rate);
|
||
|
}
|
||
|
|
||
|
// Same as rateTransaction, but relayed by a contract so we get the sender as param
|
||
|
function rateTransaction_relayed(address _sender, uint _escrowId, uint _rate) external {
|
||
|
assert(msg.sender == relayer);
|
||
|
_rateTransaction(_sender, _escrowId, _rate);
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Returns basic trade informations (buyer address, seller address, token address and token amount)
|
||
|
* @param _escrowId Id of the escrow
|
||
|
*/
|
||
|
function getBasicTradeData(uint _escrowId)
|
||
|
external
|
||
|
view
|
||
|
returns(address payable buyer, address payable seller, address token, uint tokenAmount) {
|
||
|
buyer = transactions[_escrowId].buyer;
|
||
|
seller = transactions[_escrowId].seller;
|
||
|
tokenAmount = transactions[_escrowId].tokenAmount;
|
||
|
token = transactions[_escrowId].token;
|
||
|
|
||
|
return (buyer, seller, token, tokenAmount);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Open case as a buyer or seller for arbitration
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _motive Motive for opening the dispute
|
||
|
*/
|
||
|
function openCase(uint _escrowId, uint8 _motive) external {
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
|
||
|
require(!isDisputed(_escrowId), "Case already exist");
|
||
|
require(trx.buyer == msg.sender || trx.seller == msg.sender, "Only participants can invoke this function");
|
||
|
require(trx.status == EscrowStatus.PAID, "Cases can only be open for paid transactions");
|
||
|
|
||
|
_openDispute(_escrowId, msg.sender, _motive);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Open case via relayer
|
||
|
* @param _sender Address initiating the relayed transaction
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _motive Motive for opening the dispute
|
||
|
*/
|
||
|
function openCase_relayed(address _sender, uint256 _escrowId, uint8 _motive) external {
|
||
|
assert(msg.sender == relayer);
|
||
|
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
|
||
|
require(!isDisputed(_escrowId), "Case already exist");
|
||
|
require(trx.buyer == _sender, "Only the buyer can invoke this function");
|
||
|
require(trx.status == EscrowStatus.PAID, "Cases can only be open for paid transactions");
|
||
|
|
||
|
_openDispute(_escrowId, _sender, _motive);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Open case as a buyer or seller for arbitration via a relay account
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _motive Motive for opening the dispute
|
||
|
* @param _signature Signed message result of openCaseSignHash(uint256)
|
||
|
* @dev Consider opening a dispute in aragon court.
|
||
|
*/
|
||
|
function openCase(uint _escrowId, uint8 _motive, bytes calldata _signature) external {
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
|
||
|
require(!isDisputed(_escrowId), "Case already exist");
|
||
|
require(trx.status == EscrowStatus.PAID, "Cases can only be open for paid transactions");
|
||
|
|
||
|
address senderAddress = _recoverAddress(_getSignHash(openCaseSignHash(_escrowId, _motive)), _signature);
|
||
|
|
||
|
require(trx.buyer == senderAddress || trx.seller == senderAddress, "Only participants can invoke this function");
|
||
|
|
||
|
_openDispute(_escrowId, msg.sender, _motive);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Set arbitration result in favour of the buyer or seller and transfer funds accordingly
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @param _releaseFunds Release funds to buyer or cancel escrow
|
||
|
* @param _arbitrator Arbitrator address
|
||
|
*/
|
||
|
function _solveDispute(uint _escrowId, bool _releaseFunds, address _arbitrator) internal {
|
||
|
EscrowTransaction storage trx = transactions[_escrowId];
|
||
|
|
||
|
require(trx.buyer != _arbitrator && trx.seller != _arbitrator, "Arbitrator cannot be part of transaction");
|
||
|
|
||
|
if (_releaseFunds) {
|
||
|
_release(_escrowId, trx, true);
|
||
|
} else {
|
||
|
_cancel(_escrowId, trx, true);
|
||
|
_releaseFee(trx.arbitrator, trx.tokenAmount, trx.token, true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Get arbitrator
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @return Arbitrator address
|
||
|
*/
|
||
|
function _getArbitrator(uint _escrowId) internal view returns(address) {
|
||
|
return transactions[_escrowId].arbitrator;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @notice Obtain message hash to be signed for opening a case
|
||
|
* @param _escrowId Id of the escrow
|
||
|
* @return message hash
|
||
|
* @dev Once message is signed, pass it as _signature of openCase(uint256,bytes)
|
||
|
*/
|
||
|
function openCaseSignHash(uint _escrowId, uint8 _motive) public view returns(bytes32){
|
||
|
uint256 cid;
|
||
|
assembly {
|
||
|
cid := chainid()
|
||
|
}
|
||
|
return keccak256(
|
||
|
abi.encodePacked(
|
||
|
address(this),
|
||
|
"openCase(uint256,uint8)",
|
||
|
_escrowId,
|
||
|
_motive,
|
||
|
cid
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Support for "approveAndCall". Callable only by the fee token.
|
||
|
* @param _from Who approved.
|
||
|
* @param _amount Amount being approved, need to be equal `getPrice()`.
|
||
|
* @param _token Token being approved, need to be equal `token()`.
|
||
|
* @param _data Abi encoded data with selector of `register(bytes32,address,bytes32,bytes32)`.
|
||
|
*/
|
||
|
function receiveApproval(address _from, uint256 _amount, address _token, bytes memory _data) public {
|
||
|
require(_token == address(msg.sender), "Wrong call");
|
||
|
require(_data.length == 36, "Wrong data length");
|
||
|
|
||
|
bytes4 sig;
|
||
|
uint256 escrowId;
|
||
|
|
||
|
(sig, escrowId) = _abiDecodeFundCall(_data);
|
||
|
|
||
|
if (sig == bytes4(0xca1d209d)){ // fund(uint256)
|
||
|
uint tokenAmount = transactions[escrowId].tokenAmount;
|
||
|
require(_amount == tokenAmount + _getValueOffMillipercent(tokenAmount, feeMilliPercent), "Invalid amount");
|
||
|
_fund(_from, escrowId);
|
||
|
} else {
|
||
|
revert("Wrong method selector");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Decodes abi encoded data with selector for "fund".
|
||
|
* @param _data Abi encoded data.
|
||
|
* @return Decoded registry call.
|
||
|
*/
|
||
|
function _abiDecodeFundCall(bytes memory _data) internal pure returns (bytes4 sig, uint256 escrowId) {
|
||
|
assembly {
|
||
|
sig := mload(add(_data, add(0x20, 0)))
|
||
|
escrowId := mload(add(_data, 36))
|
||
|
}
|
||
|
}
|
||
|
}
|