Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Exchange Protocol Fees #2097

Merged
merged 21 commits into from
Aug 30, 2019
Merged

Exchange Protocol Fees #2097

merged 21 commits into from
Aug 30, 2019

Conversation

jalextowle
Copy link
Contributor

@jalextowle jalextowle commented Aug 24, 2019

Description

image

This PR adds protocol fees to the Exchange contract, implementing part of ZEIP-42. Several things had to happen for this to occur.

Refundable

A new contract was added to @0x:contracts-utils called Refundable. Refundable is a contract with no dependencies that introduces two new modifiers and a state variable called shouldNotRefund. Both modifiers are designed to respect the refundFinalBalance state variable in that they will only refund if shouldNotRefund is false.

  • disableRefundUntilEnd: This modifier will set shouldNotRefund to true, call the function that it modifies, and then refund all of the ether in the contract to the sender. This will disable all nested functions that use disableRefundUntilEnd and refundFinalBalance, and this modifier will itself be disabled if shouldNotRefund is true.
  • refundFinalBalance: This modifier will call the function that it is modifying and then refund all of the ether in the contract to the sender. This will be disabled if shouldNotRefund is true.

Payable Functions

Almost every function in the exchange had to be made payable. Some notable exceptions include

  • cancelOrder
  • batchCancelOrders
  • cancelOrdersUpTo

Since these functions are now payable, they all were also modified by refundFinalBalance or disableRefundUntilEnd. The functions that were given refundFinalBalance have no risk of calling a function that also uses refundFinalBalance and experiencing strange behavior. Similarly, functions whose behavior would be affected by refunds in nested calls are modified by disableRefundUntilEnd.

Protocol Fees

Protocol fees were added to the internal functions _fillOrder and _matchOrders, so every function that calls these functions or has the potential to call them will potentially pay protocol fees. fillOrder protocol fees are half of the price of matchOrders protocol fees, and these fees will be paid on a per-fill basis. For example, a call to batchFillOrders with k orders that are filled will pay k * protocolFeePerFill, where protocolFeePerFill = exchange.protocolFeeMultiplier * tx.gasprice.

New state that was added to the exchange (like protocolFeeMultiplier from the above example), including:

  • protocolFeeMultiplier: This is the number that will be multiplied to tx.gasprice to calculate the protocol fee of a single fill.
  • protocolFeeCollector: This is the contract that will receive protocolFee payments.

Both of these state variables are protected by the onlyOwner modifier, and will thus not be mutable by normal addresses.

Glorious Solidity Tests

One of my favorite parts of writing this PR was discovering that writing tests in Solidity is actually surprisingly nice. The tests that I wrote are in the form:

function testSomething(SomethingContract testContract, ...)
    external
{
    testContract.something(...); // Call something with args

    verifySomething(...); // Make assertions about the call to something
}

The assertions that are made are generally made by leveraging contract inheritance's ability to override important internal functions. These overridden functions are made to change state in ways that indicate internal behavior.

Both Protocol Fees and Refundable received good Solidity test treatment that runs quickly and makes strong assertions about their behavior.

Testing instructions

yarn build:contracts && yarn test:contracts && yarn lint:contracts (or ycall for those with my .bash_profile sourced :))

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • Prefix PR title with [WIP] if necessary.
  • Add tests to cover changes as needed.
  • Update documentation as needed.
  • Add new entries to the relevant CHANGELOG.jsons.

@jalextowle jalextowle force-pushed the feature/3.0/exchange/protocol-fees branch 5 times, most recently from 3b0365d to 3481e1d Compare August 24, 2019 08:06
contracts/exchange/contracts/src/MixinExchangeCore.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinExchangeCore.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinMatchOrders.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinMatchOrders.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinStakingManager.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinStakingManager.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinStakingManager.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinStakingManager.sol Outdated Show resolved Hide resolved
contracts/utils/contracts/src/Refundable.sol Show resolved Hide resolved
contracts/utils/contracts/src/Refundable.sol Outdated Show resolved Hide resolved
@abandeali1 abandeali1 self-assigned this Aug 26, 2019
Copy link
Contributor

@dorothy-zbornak dorothy-zbornak left a comment

Choose a reason for hiding this comment

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

Still 🤔 about stuff.

@@ -75,6 +77,17 @@ describe('Reference Functions', () => {
);
expect(() => addFillResults(a, b)).to.throw(expectedError.message);
});

it('reverts if computing `protocolFeePaid` overflows', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

🙏

@@ -73,7 +73,8 @@ export function getPartialAmountFloor(numerator: BigNumber, denominator: BigNumb
* Calculates partial value given a numerator and denominator rounded down.
*/
export function getPartialAmountCeil(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber {
return safeDiv(safeAdd(safeMul(numerator, target), safeSub(denominator, new BigNumber(1))), denominator);
const sub = safeSub(denominator, new BigNumber(1)); // This is computed first to simulate Solidity's order of operations
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this the discrepancy?

Copy link
Contributor Author

@jalextowle jalextowle Aug 27, 2019

Choose a reason for hiding this comment

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

Yep! I guess when you chain something like:

uint256 someValue = someValue1
    .safeAdd(someValue2)
    .safeSub(someValue3.safeAdd(someValue4))

the someValue3.safeAdd(someValue4) expression is the first thing to get executed. I guess this isn't a huge surprise since this does follow PEMDAS, but it's still pretty weird. We had a case that was somewhat similar to this, which is why the safeSub needs be evaluated first.

@@ -78,6 +81,9 @@ library LibFillResults {
order.takerFee
);

// Compute the protocol fee for a single fill.
fillResults.protocolFeePaid = tx.gasprice.safeMul(protocolFeeMultiplier);
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it would be better to just pass tx.gasprice into this function to keep it pure if we want to go down the route of converting reference functions to ethjsvm. It also creates parity with the reference functions as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It ended up being more efficient to just entirely exclude the calculation from this function, since it could end up being inaccurate if a protocolFeeCollector is unregistered. I'd be interested to hear your thoughts on this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Still true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, I took your suggestion when I changed it back, as a bonus.

@@ -162,6 +170,11 @@ library LibFillResults {
rightOrder.takerFee
);

// Compute the protocol fees
uint256 protocolFee = tx.gasprice.safeMul(protocolFeeMultiplier);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as in calculateFillResults (ethjsvm).

contracts/exchange/contracts/src/MixinExchangeCore.sol Outdated Show resolved Hide resolved
contracts/exchange/test/wrapper.ts Show resolved Hide resolved
contracts/exchange-libs/test/lib_fill_results.ts Outdated Show resolved Hide resolved
contracts/test-utils/src/constants.ts Outdated Show resolved Hide resolved
contracts/exchange-libs/test/lib_fill_results.ts Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinExchangeCore.sol Outdated Show resolved Hide resolved
@jalextowle jalextowle force-pushed the feature/3.0/exchange/protocol-fees branch 9 times, most recently from f76b518 to ce53c71 Compare August 28, 2019 21:51
@jalextowle jalextowle marked this pull request as ready for review August 28, 2019 23:35
contracts/exchange/contracts/src/MixinExchangeCore.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinMatchOrders.sol Outdated Show resolved Hide resolved
valuePaid = 0;

// Pay the right order's protocol fee.
if (address(this).balance >= protocolFee) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: we can save some gas by setting address(this).balance to a stack variable and then checking balance - valuePaid >= protocolFee here. The ADDRESS opcode costs 200 gas if I recall.

Copy link
Contributor Author

@jalextowle jalextowle Aug 29, 2019

Choose a reason for hiding this comment

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

Turns out that ADDRESS opcode only costs 2 gas as of now. With this in mind, the use of ADDRESS will actually be cheaper here. It's specified as Gbase in the Ethereum Yellow Paper and in the Jello Paper (and Gbase = 2 gas in both). Is this going to change soon?

contracts/exchange/contracts/src/MixinProtocolFees.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinProtocolFees.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinProtocolFees.sol Outdated Show resolved Hide resolved
contracts/utils/contracts/src/Refundable.sol Outdated Show resolved Hide resolved
contracts/exchange/contracts/src/MixinWrapperFunctions.sol Outdated Show resolved Hide resolved
@jalextowle jalextowle force-pushed the feature/3.0/exchange/protocol-fees branch from cdf5080 to 911c007 Compare August 29, 2019 07:42
contracts/exchange-libs/test/lib_fill_results.ts Outdated Show resolved Hide resolved
emit Fill(
order.makerAddress,
order.feeRecipientAddress,
order.makerAssetData,
order.takerAssetData,
order.makerFeeAssetData,
order.takerFeeAssetData,
orderHash,
Copy link
Contributor

Choose a reason for hiding this comment

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

Reordering worked?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep! Amir figured it out. The heuristic is that you use all of the fields from order together and do the same with fillResults. It also seems that maintaining the order of parameters in the event reduces stack usage.

contracts/exchange/test/protocol_fees_manager.ts Outdated Show resolved Hide resolved
contracts/exchange/test/internal.ts Outdated Show resolved Hide resolved
contracts/exchange/test/protocol_fees.ts Outdated Show resolved Hide resolved
contracts/exchange/test/protocol_fees.ts Outdated Show resolved Hide resolved
@@ -78,6 +81,9 @@ library LibFillResults {
order.takerFee
);

// Compute the protocol fee for a single fill.
fillResults.protocolFeePaid = tx.gasprice.safeMul(protocolFeeMultiplier);
Copy link
Contributor

Choose a reason for hiding this comment

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

Still true?

Copy link
Contributor

@dorothy-zbornak dorothy-zbornak left a comment

Choose a reason for hiding this comment

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

Really awesome stuff here, and obviously a lot of work. Just a few nits and concerns.

@jalextowle jalextowle force-pushed the feature/3.0/exchange/protocol-fees branch 9 times, most recently from dc049df to 84e88a8 Compare August 29, 2019 22:18
@jalextowle jalextowle force-pushed the feature/3.0/exchange/protocol-fees branch from 84e88a8 to dba0d84 Compare August 29, 2019 22:22
@coveralls
Copy link

coveralls commented Aug 29, 2019

Coverage Status

Coverage remained the same at 75.804% when pulling 2c1393f on feature/3.0/exchange/protocol-fees into a9857fa on 3.0.

Copy link
Contributor

@dorothy-zbornak dorothy-zbornak left a comment

Choose a reason for hiding this comment

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

🔥

import { artifacts } from '../src';

describe('Contract Size Checks', () => {
const MAX_CODE_SIZE = 24576;
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 👍 👍

contracts/exchange/test/internal.ts Outdated Show resolved Hide resolved
@jalextowle jalextowle force-pushed the feature/3.0/exchange/protocol-fees branch from 7640183 to c50c997 Compare August 30, 2019 05:47
@jalextowle jalextowle force-pushed the feature/3.0/exchange/protocol-fees branch from c50c997 to 2c1393f Compare August 30, 2019 06:51
@jalextowle jalextowle merged commit 63f051a into 3.0 Aug 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants