fix: allow buyers to cancel an escrow (#283)

* fix: allow buyers to cancel an escrow

* fix: refund to seller
This commit is contained in:
Richard Ramos 2019-05-29 11:47:14 -04:00 committed by GitHub
parent 9d032b30e9
commit 6595f92506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 16 deletions

View File

@ -309,31 +309,35 @@ contract Escrow is Pausable, MessageSigned, Fees, Arbitrable {
require(_escrowId < transactions.length, INVALID_ESCROW_ID);
EscrowTransaction storage trx = transactions[_escrowId];
require(trx.expirationTime < block.timestamp, "Transaction has not expired");
require(trx.status == EscrowStatus.FUNDED || trx.status == EscrowStatus.CREATED, "Only transactions in created or funded state can be canceled");
_cancel(_escrowId, trx, false);
address seller = metadataStore.getOfferOwner(trx.offerId);
require(trx.buyer == msg.sender || seller == msg.sender, "Function can only be invoked by the escrow buyer or seller");
if(trx.status == EscrowStatus.FUNDED){
if(msg.sender == seller){
require(trx.expirationTime < block.timestamp, "Can only be canceled after expiration");
}
}
_cancel(_escrowId, trx);
}
/**
* @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
* @param _ignoreExpiration Determines if the require rule for expiration time will apply or not
*/
function _cancel(uint _escrowId, EscrowTransaction storage trx, bool _ignoreExpiration) private {
function _cancel(uint _escrowId, EscrowTransaction storage trx) private {
address payable seller = metadataStore.getOfferOwner(trx.offerId);
if(trx.status == EscrowStatus.FUNDED){
require(msg.sender == metadataStore.getOfferOwner(trx.offerId), "Only seller can cancel transaction");
if(!_ignoreExpiration){
require(trx.expirationTime < block.timestamp, "Can only be canceled after expiration");
}
address token = metadataStore.getAsset(trx.offerId);
if(token == address(0)){
msg.sender.transfer(trx.tokenAmount);
seller.transfer(trx.tokenAmount);
} else {
ERC20Token erc20token = ERC20Token(token);
require(erc20token.transfer(msg.sender, trx.tokenAmount));
require(erc20token.transfer(seller, trx.tokenAmount), "Transfer failed");
}
}
@ -352,7 +356,7 @@ contract Escrow is Pausable, MessageSigned, Fees, Arbitrable {
require(_escrowId < transactions.length, INVALID_ESCROW_ID);
EscrowTransaction storage trx = transactions[_escrowId];
require(trx.status == EscrowStatus.FUNDED, "Cannot withdraw from escrow in a stage different from FUNDED. Open a case");
_cancel(_escrowId, trx, true);
_cancel(_escrowId, trx);
}
/**
@ -437,7 +441,7 @@ contract Escrow is Pausable, MessageSigned, Fees, Arbitrable {
if(_releaseFunds){
_release(_escrowId, trx);
} else {
_cancel(_escrowId, trx, true);
_cancel(_escrowId, trx);
}
}

View File

@ -264,7 +264,7 @@ contract MetadataStore is Ownable {
);
}
function getOfferOwner(uint256 _id) public view returns (address) {
function getOfferOwner(uint256 _id) public view returns (address payable) {
require(_id < offers.length, "Invalid offer id");
return (offers[_id].owner);
}

View File

@ -215,6 +215,20 @@ contract("Escrow", function() {
describe("Canceling an escrow", async () => {
let created;
it("A seller cannot cancel an unexpired funded escrow", async () => {
await SNT.methods.approve(Escrow.options.address, feeAmount).send({from: accounts[0]});
receipt = await Escrow.methods.create_and_fund(accounts[1], ethOfferId, value, expirationTime, 123, FIAT, 140).send({from: accounts[0], value});
created = receipt.events.Created;
escrowId = created.returnValues.escrowId;
try {
receipt = await Escrow.methods.cancel(escrowId).send({from: accounts[0]});
assert.fail('should have reverted before');
} catch (error) {
assert.strictEqual(error.message, "VM Exception while processing transaction: revert Can only be canceled after expiration");
}
});
it("A seller can cancel their ETH escrows", async () => {
await SNT.methods.approve(Escrow.options.address, feeAmount).send({from: accounts[0]});
receipt = await Escrow.methods.create_and_fund(accounts[1], ethOfferId, value, expirationTime, 123, FIAT, 140).send({from: accounts[0], value});
@ -258,6 +272,22 @@ contract("Escrow", function() {
assert.equal(contractBalanceBeforeCreation, contractBalanceAfterCancelation, "Invalid contract balance");
});
it("A buyer can cancel an escrow that hasn't been funded yet", async () => {
receipt = await Escrow.methods.create(accounts[1], ethOfferId, 123, FIAT, 140, [0], "L", "U").send({from: accounts[1]});
receipt = await Escrow.methods.cancel(receipt.events.Created.returnValues.escrowId).send({from: accounts[1]});
let Canceled = receipt.events.Canceled;
assert(!!Canceled, "Canceled() not triggered");
});
it("A buyer can cancel an escrow that has been funded", async () => {
await SNT.methods.approve(Escrow.options.address, feeAmount).send({from: accounts[0]});
receipt = await Escrow.methods.create_and_fund(accounts[1], ethOfferId, value, expirationTime, 123, FIAT, 140).send({from: accounts[0], value});
escrowId = receipt.events.Created.returnValues.escrowId;
receipt = await Escrow.methods.cancel(escrowId).send({from: accounts[1]});
let Canceled = receipt.events.Canceled;
assert(!!Canceled, "Canceled() not triggered");
});
it("An escrow can only be canceled once", async () => {
await SNT.methods.approve(Escrow.options.address, feeAmount).send({from: accounts[0]});
receipt = await Escrow.methods.create_and_fund(accounts[1], ethOfferId, value, expirationTime, 123, FIAT, 140).send({from: accounts[0], value});
@ -277,12 +307,28 @@ contract("Escrow", function() {
escrowId = receipt.events.Created.returnValues.escrowId;
try {
receipt = await Escrow.methods.cancel(escrowId).send({from: accounts[1]}); // Buyer tries to cancel
receipt = await Escrow.methods.cancel(escrowId).send({from: accounts[2]});
assert.fail('should have reverted before');
} catch (error) {
TestUtils.assertJump(error);
}
});
it("A seller cannot cancel an escrow marked as paid", async () => {
await SNT.methods.approve(Escrow.options.address, feeAmount).send({from: accounts[0]});
receipt = await Escrow.methods.create_and_fund(accounts[1], ethOfferId, value, expirationTime, 123, FIAT, 140).send({from: accounts[0], value});
created = receipt.events.Created;
escrowId = created.returnValues.escrowId;
receipt = await Escrow.methods.pay(escrowId).send({from: accounts[1]});
try {
receipt = await Escrow.methods.cancel(escrowId).send({from: accounts[0]});
assert.fail('should have reverted before');
} catch (error) {
assert.strictEqual(error.message, "VM Exception while processing transaction: revert Only transactions in created or funded state can be canceled");
}
});
});