Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Escrows #1014

Merged
merged 15 commits into from
Jul 3, 2018
Merged

Escrows #1014

merged 15 commits into from
Jul 3, 2018

Conversation

nventuro
Copy link
Contributor

@nventuro nventuro commented Jun 15, 2018

Fixes #901

🚀 Description

  • Adds an Escrow and reimplements PullPayment using it. The totalPayments variable was removed after being deemed useless
  • Adds a ConditionalEscrow abstract contract, and a RefundEscrow derived contract (similar to RefundVault, but the beneficiary must call withdraw after close is called instead of it being automatic)
  • Removes RefundVault and updates RefundableCrowdsale to use RefundEscrow
  • 📘 I've reviewed the OpenZeppelin Contributor Guidelines
  • ✅ I've added tests where applicable to test my new functionality.
  • 📖 I've made sure that my contracts are well-documented.
  • 🎨 I've run the JS/Solidity linters and fixed any issues (npm run lint:all:fix).

Copy link
Contributor

@frangio frangio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @nventuro! I love this "Escrow" name, it's right on the money.

Here's a first round of feedback.

@@ -3,31 +3,31 @@ pragma solidity ^0.4.24;

import "../../math/SafeMath.sol";
import "./FinalizableCrowdsale.sol";
import "./utils/RefundVault.sol";
import "../../payment/RefundEscrow.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think RefundEscrow is very much crowdsale-specific so it should be with the crowdsale stuff. Potentially in this same file. Thoughts?

Copy link
Contributor Author

@nventuro nventuro Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree: a RefundEscrow is a payment method, a crowdsale typically using it doesn't make it crowdsale-specific. As an example, platforms like Amazon or eBay also offer refunds and act as an escrow themselves, and could extend their payment systems to add support for multiple payers.

@@ -10,34 +9,34 @@ import "../math/SafeMath.sol";
* contract and use asyncSend instead of send or transfer.
*/
contract PullPayment {
using SafeMath for uint256;
Escrow public escrow;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this state variable private.

function asyncSend(address dest, uint256 amount) internal {
payments[dest] = payments[dest].add(amount);
totalPayments = totalPayments.add(amount);
function asyncSend(address _dest, uint256 _amount) internal {
Copy link
Contributor

@frangio frangio Jun 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word transfer is quite overloaded but I feel like this is an asyncTransfer more than an asyncSend, in analogy with send vs transfer because of the lack of return value.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this when updating PullPayment, I agree that transfer is indeed the better term.

/**
* @title RefundEscrow
* @dev Escrow that holds investor funds for a unique benefitiary, and allows for
* either withdrawal by the benefiatiary, or refunds to the investors.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo twice here: "benefitiary" → "beneficiary".

Also, "unique" doesn't sound right... "single" is the right word, but it feels redundant to me anyway.

This description doesn't explain the condition according to which either withdrawal or refunding is allowed. We should also explain that this contract was written as the escrow for a RefundableCrowdsale.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, unique is just plain wrong. I think it could just say 'for a beneficiary' and the meaning would remain clear.

I'm not sure about the RefundableCrowdsale part though: supporting refunds in a payment system doesn't scream 'crowdsale' to me. I actually see the inverse of that being true: the RefundableCrowdsale is nothing more than a crowdsale, the escrow, and glue code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that RefundEscrow is not necessarily crowdsale-specific. I didn't mean to say that. I'm just interested in cross-linking related contracts. Perhaps this information should be automatically generated by our documentation generator though.

payee.transfer(payment);
/**
* @dev Returns the credit owed to an address.
& @param _dest The creditor's address.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&*

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I blame Stroustrup

*/
function deposit(address _payee) payable public {
uint256 amount = msg.value;
require(amount > 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line doesn't add any value (ha!). It will unnecessarily propagate the special case to the users of Escrow, when we can easily treat deposit(0) as a no-op. Let's just remove the line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deposit(0) would indeed do nothing: I thought it better to protect the caller from using the API wrong, though the argument could be made that this would make the contract more expensive for all other users. It's more of a library-wide decision, IMO. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is deposit(0) is not a wrong use of the API, it's just a no-op. It's not a matter of gas efficiency, it's about not forcing users of Escrow to add code to treat 0 specially.

We've discussed this before, though I can't remember where, and the decision was to not revert.

uint256 payment = deposits[_payee];

require(payment != 0);
require(address(this).balance >= payment);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of an assert than a require.

function withdraw(address _payee) public {
uint256 payment = deposits[_payee];

require(payment != 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels unnecessary as well. I would remove it.

/**
* @dev Withdraws the beneficiary's funds.
*/
function withdraw() public {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escrow#withdraw(address) has the same name but it does something different. Let's name this one something different. beneficiaryWithdraw? I don't know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. On a related note, withdraw and refund do the same thing, but withdraw emits no events. Should we prevent the user from calling the base function (i.e. override and revert)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I hadn't noticed that. Hm... I think we should keep withdraw instead of adding a new refund function. In the case of RefundableCrowdsale, the user will use RefundableCrowdsale#claimRefund anyway.

*/
function refund(address _investor) public {
uint256 amount = depositsOf(_investor);
super.withdraw(_investor);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to use super here.

* @dev Base escrow contract, holds funds destinated to a payee until they
* withdraw them.
*/
contract Escrow {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add Deposited and Withdrawn events. (I'm open to other names.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would make the Refunded event a bit redundant (since it will also emit an identical Withdrawn): do we want to remove it?

@nventuro nventuro force-pushed the feat/escrow branch 2 times, most recently from 786ceac to 3a33ffb Compare June 18, 2018 20:02
Copy link
Contributor

@frangio frangio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments. I'd also like to do a last review of correctness and security before merging.

@@ -67,23 +61,23 @@ contract RefundEscrow is ConditionalEscrow, Ownable {
/**
* @dev Withdraws the beneficiary's funds.
*/
function withdraw() public {
function beneficiaryWithdraw() public {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to get a second/third opinion on this name. Alternative: withdrawToBeneficiary.

@@ -6,42 +6,42 @@ require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();

export default function ([payer1, payer2, payee1, payee2]) {
export function shouldBehaveLikeEscrow([payer1, payer2, payee1, payee2]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be the default export and have a name at the same time. We need a library-wide convention for this. Currently all of the behavio(u)rs use a default export so we should stick to that.

Copy link
Contributor Author

@nventuro nventuro Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the other behaviors give a name to this function though. I'd leave it anonymous and default, and implement the library-wide change in a different PR.

Opened #1027

function withdraw(address _refundee) public {
uint256 amount = depositsOf(_refundee);
super.withdraw(_refundee);
emit Refunded(_refundee, amount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove when we add events to Escrow.

receipt.logs.length.should.equal(1);
receipt.logs[0].event.should.equal('Refunded');
receipt.logs[0].args.refundee.should.equal(refundee);
inLogs(receipt.logs, 'Refunded', { refundee });
receipt.logs[0].args.weiAmount.should.be.bignumber.equal(amount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that the helper returns the event, so you can do

const event = expectEvent.inLogs(receipt.logs, 'Refunded', { refundee });
event.args.weiAmount.should.be.bignumber.equal(amount);

In this way you also stop relying on the event being at index 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat, I missed that bit, ty!

@@ -1,4 +1,5 @@
import EVMRevert from '../helpers/EVMRevert';
import { inLogs } from '../helpers/expectEvent';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preferred usage for better readability is:

import expectEvent from '../helpers/expectEvent';

expectEvent.inLogs(receipt.logs);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that made more sense, but preferred to copy the existing code style :/

Copy link
Contributor

@frangio frangio Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@shrugs shrugs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a "light" approval; I don't have the time to do a deep review, but the architecture is 👍and after a scan all of the code looks chill. Would prefer that @frangio follows up with his previous review before we merge :)

@nventuro
Copy link
Contributor Author

After discussing offline with @frangio, we decided to restrict the API somewhat, making the base Escrow inherit from Ownable, forcing all interactions to go through an owner contract (like in the PullPayment and RefundCrowdsale case), to reduce the surface vector for potential attacks.

I'll be updating the contracts and tests soon.

@nventuro
Copy link
Contributor Author

nventuro commented Jul 2, 2018

Escrows are now an implementation detail of the contracts that use them as a payment method. Users of these contracts will never interact with an Escrow directly, making it easier to reason about security concerns. The two contracts that implement this pattern are PullPayment and RefundableCrowdsale.

@frangio
Copy link
Contributor

frangio commented Jul 3, 2018

Thanks @nventuro!

@frangio frangio merged commit 8fd072c into OpenZeppelin:master Jul 3, 2018
@nventuro nventuro deleted the feat/escrow branch July 3, 2018 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PullPayment.asyncSend() flow is unreliable
3 participants