Skip to content

Commit

Permalink
update fee taker logic to new spec
Browse files Browse the repository at this point in the history
  • Loading branch information
ZumZoom committed Dec 24, 2024
1 parent 1034bf4 commit 22a18f7
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 59 deletions.
37 changes: 24 additions & 13 deletions contracts/extensions/AmountGetterWithFee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import { AmountGetterBase } from "./AmountGetterBase.sol";
/// @title Price getter contract that adds fee calculation
contract AmountGetterWithFee is AmountGetterBase {
/// @dev Allows fees in range [1e-5, 0.65535]
uint256 internal constant _FEE_BASE = 1e5;
uint256 internal constant _DISCOUNT_BASE = 100;
uint256 internal constant _BASE_1E5 = 1e5;
uint256 internal constant _BASE_1E2 = 100;

error InvalidIntegratorFee();
error InvalidIntegratorShare();
error InvalidResolverFee();
error InvalidWhitelistDiscountNumerator();

/**
* @dev Calculates makingAmount with fee.
Expand All @@ -26,11 +31,11 @@ contract AmountGetterWithFee is AmountGetterBase {
bytes calldata extraData
) internal view virtual override returns (uint256) {
unchecked {
(, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl);
(, uint256 integratorFee, , uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl);
return Math.mulDiv(
super._getMakingAmount(order, extension, orderHash, taker, takingAmount, remainingMakingAmount, tail),
_FEE_BASE,
_FEE_BASE + integratorFee + resolverFee
_BASE_1E5,
_BASE_1E5 + integratorFee + resolverFee
);
}
}
Expand All @@ -48,11 +53,11 @@ contract AmountGetterWithFee is AmountGetterBase {
bytes calldata extraData
) internal view virtual override returns (uint256) {
unchecked {
(, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl);
(, uint256 integratorFee, , uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedGetterImpl);
return Math.mulDiv(
super._getTakingAmount(order, extension, orderHash, taker, makingAmount, remainingMakingAmount, tail),
_FEE_BASE + integratorFee + resolverFee,
_FEE_BASE,
_BASE_1E5 + integratorFee + resolverFee,
_BASE_1E5,
Math.Rounding.Ceil
);
}
Expand All @@ -61,6 +66,7 @@ contract AmountGetterWithFee is AmountGetterBase {
/**
* @dev `extraData` consists of:
* 2 bytes — integrator fee percentage (in 1e5)
* 1 byte - integrator share percentage (in 1e2)
* 2 bytes — resolver fee percentage (in 1e5)
* 1 byte - whitelist discount numerator (in 1e2)
* bytes — whitelist structure determined by `_isWhitelisted` implementation
Expand All @@ -71,14 +77,19 @@ contract AmountGetterWithFee is AmountGetterBase {
bytes calldata extraData,
address taker,
function (bytes calldata, address) internal view returns (bool, bytes calldata) _isWhitelisted
) internal view returns (bool isWhitelisted, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) {
) internal view returns (bool isWhitelisted, uint256 integratorFee, uint256 integratorShare, uint256 resolverFee, bytes calldata tail) {
unchecked {
integratorFee = uint256(uint16(bytes2(extraData)));
resolverFee = uint256(uint16(bytes2(extraData[2:])));
uint256 whitelistDiscountNumerator = uint256(uint8(bytes1(extraData[4:])));
(isWhitelisted, tail) = _isWhitelisted(extraData[5:], taker);
if (integratorFee > _BASE_1E5) revert InvalidIntegratorFee();
integratorShare = uint256(uint8(bytes1(extraData[2:])));
if (integratorShare > _BASE_1E2) revert InvalidIntegratorShare();
resolverFee = uint256(uint16(bytes2(extraData[3:])));
if (resolverFee > _BASE_1E5) revert InvalidResolverFee();
uint256 whitelistDiscountNumerator = uint256(uint8(bytes1(extraData[5:])));
if (whitelistDiscountNumerator > _BASE_1E2) revert InvalidWhitelistDiscountNumerator();
(isWhitelisted, tail) = _isWhitelisted(extraData[6:], taker);
if (isWhitelisted) {
resolverFee = resolverFee * whitelistDiscountNumerator / _DISCOUNT_BASE;
resolverFee = resolverFee * whitelistDiscountNumerator / _BASE_1E2;
}
}
}
Expand Down
43 changes: 29 additions & 14 deletions contracts/extensions/FeeTaker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable {
* @dev Takes the fee in taking tokens and transfers the rest to the maker.
* `extraData` consists of:
* 1 byte - flags
* 20 bytes — fee recipient
* 20 bytes — integrator fee recipient
* 20 bytes - protocol fee recipient
* 20 bytes — receiver of taking tokens (optional, if not set, maker is used)
* 2 bytes — integrator fee percentage (in 1e5)
* 1 bytes - integrator rev share percentage (in 1e2)
* 2 bytes — resolver fee percentage (in 1e5)
* bytes — whitelist structure determined by `_isWhitelistedPostInteractionImpl` implementation
* bytes — custom data to call extra postInteraction (optional)
Expand All @@ -121,34 +123,47 @@ contract FeeTaker is IPostInteraction, AmountGetterWithFee, Ownable {
) internal virtual {
unchecked {
bool customReceiver = extraData[0] & _CUSTOM_RECEIVER_FLAG == _CUSTOM_RECEIVER_FLAG;
address feeRecipient = address(bytes20(extraData[1:21]));
extraData = extraData[21:];
address integratorFeeRecipient = address(bytes20(extraData[1:21]));
address protocolFeeRecipient = address(bytes20(extraData[21:41]));
extraData = extraData[41:];

address receiver = order.maker.get();
if (customReceiver) {
receiver = address(bytes20(extraData));
extraData = extraData[20:];
}
(bool isWhitelisted, uint256 integratorFee, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedPostInteractionImpl);
(bool isWhitelisted, uint256 integratorFee, uint256 integratorShare, uint256 resolverFee, bytes calldata tail) = _parseFeeData(extraData, taker, _isWhitelistedPostInteractionImpl);
if (!isWhitelisted && _ACCESS_TOKEN.balanceOf(taker) == 0) revert OnlyWhitelistOrAccessToken();

uint256 denominator = _FEE_BASE + integratorFee + resolverFee;
// fee is calculated as a sum of separate fees to limit rounding errors
uint256 fee = Math.mulDiv(takingAmount, integratorFee, denominator) + Math.mulDiv(takingAmount, resolverFee, denominator);
uint256 integratorFeeAmount;
uint256 protocolFeeAmount;

{
uint256 denominator = _BASE_1E5 + integratorFee + resolverFee;
uint256 integratorFeeTotal = Math.mulDiv(takingAmount, integratorFee, denominator);
integratorFeeAmount = Math.mulDiv(integratorFeeTotal, integratorShare, _BASE_1E2);
protocolFeeAmount = Math.mulDiv(takingAmount, resolverFee, denominator) + integratorFeeTotal - integratorFeeAmount;
}

if (order.receiver.get() == address(this)) {
if (order.takerAsset.get() == address(_WETH) && order.makerTraits.unwrapWeth()) {
if (fee > 0) {
_sendEth(feeRecipient, fee);
if (integratorFeeAmount > 0) {
_sendEth(integratorFeeRecipient, integratorFeeAmount);
}
_sendEth(receiver, takingAmount - fee);
if (protocolFeeAmount > 0) {
_sendEth(protocolFeeRecipient, protocolFeeAmount);
}
_sendEth(receiver, takingAmount - integratorFeeAmount - protocolFeeAmount);
} else {
if (fee > 0) {
IERC20(order.takerAsset.get()).safeTransfer(feeRecipient, fee);
if (integratorFeeAmount > 0) {
IERC20(order.takerAsset.get()).safeTransfer(integratorFeeRecipient, integratorFeeAmount);
}
if (protocolFeeAmount > 0) {
IERC20(order.takerAsset.get()).safeTransfer(protocolFeeRecipient, protocolFeeAmount);
}
IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee);
IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - integratorFeeAmount - protocolFeeAmount);
}
} else if (fee > 0) {
} else if (integratorFeeAmount + protocolFeeAmount > 0) {
revert InconsistentFee();
}

Expand Down
67 changes: 44 additions & 23 deletions test/FeeTaker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const { buildOrder, buildTakerTraits, signOrder, buildMakerTraits, buildFeeTaker
const { ether } = require('./helpers/utils');

describe('FeeTaker', function () {
let addr, addr1, addr2, addr3;
let addr, addr1, addr2, addr3, addr4;
before(async function () {
[addr, addr1, addr2, addr3] = await ethers.getSigners();
[addr, addr1, addr2, addr3, addr4] = await ethers.getSigners();
});

async function deployContractsAndInit () {
Expand Down Expand Up @@ -100,7 +100,8 @@ describe('FeeTaker', function () {
const takingAmount = ether('0.3');
const integratorFee = BigInt(1e4);
const resolverFee = BigInt(1e3);
const feeRecipient = addr2.address;
const integratorFeeRecipient = addr2.address;
const protocolFeeRecipient = addr3.address;
const whitelist = '0x0a' + addr.address.slice(-20).repeat(10);

const order = buildOrder(
Expand All @@ -114,7 +115,8 @@ describe('FeeTaker', function () {
},
buildFeeTakerExtensions({
feeTaker: await feeTaker.getAddress(),
feeRecipient,
integratorFeeRecipient,
protocolFeeRecipient,
integratorFee,
resolverFee,
whitelist,
Expand All @@ -129,9 +131,11 @@ describe('FeeTaker', function () {
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`);

const feeCalculated = takingAmount * (integratorFee + resolverFee / 2n) / BigInt(1e5);
const integratorFeeCalculated = takingAmount * (integratorFee / 2n) / BigInt(1e5);
const protocolFeeCalculated = takingAmount * (integratorFee / 2n + resolverFee / 2n) / BigInt(1e5);
const totalFeeCalculated = protocolFeeCalculated + integratorFeeCalculated;
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient], [-takingAmount - feeCalculated, takingAmount, feeCalculated]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, integratorFeeRecipient, protocolFeeRecipient], [-takingAmount - totalFeeCalculated, takingAmount, integratorFeeCalculated, protocolFeeCalculated]);
});

it('should charge fee when out of whitelist', async function () {
Expand All @@ -141,7 +145,8 @@ describe('FeeTaker', function () {
const takingAmount = ether('0.3');
const integratorFee = BigInt(1e4);
const resolverFee = BigInt(1e3);
const feeRecipient = addr2.address;
const integratorFeeRecipient = addr2.address;
const protocolFeeRecipient = addr3.address;
const whitelist = '0x0a' + addr2.address.slice(-20).repeat(10);

const order = buildOrder(
Expand All @@ -155,7 +160,8 @@ describe('FeeTaker', function () {
},
buildFeeTakerExtensions({
feeTaker: await feeTaker.getAddress(),
feeRecipient,
integratorFeeRecipient,
protocolFeeRecipient,
integratorFee,
resolverFee,
whitelist,
Expand All @@ -170,11 +176,13 @@ describe('FeeTaker', function () {
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`);

const feeCalculated = takingAmount * (integratorFee + resolverFee) / BigInt(1e5);
const integratorFeeCalculated = takingAmount * (integratorFee / 2n) / BigInt(1e5);
const protocolFeeCalculated = takingAmount * (integratorFee / 2n + resolverFee) / BigInt(1e5);
const totalFeeCalculated = protocolFeeCalculated + integratorFeeCalculated;
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalances(weth,
[addr, addr1, feeRecipient],
[-takingAmount - feeCalculated, takingAmount, feeCalculated],
[addr, addr1, integratorFeeRecipient, protocolFeeRecipient],
[-takingAmount - totalFeeCalculated, takingAmount, integratorFeeCalculated, protocolFeeCalculated],
);
});

Expand All @@ -184,9 +192,9 @@ describe('FeeTaker', function () {
const makingAmount = ether('300');
const takingAmount = ether('0.3');
const integratorFee = BigInt(1e4);
const feeCalculated = takingAmount * integratorFee / BigInt(1e5);
const feeRecipient = addr2.address;
const makerReceiver = addr3.address;
const integratorFeeRecipient = addr2.address;
const protocolFeeRecipient = addr3.address;
const makerReceiver = addr4.address;

const order = buildOrder(
{
Expand All @@ -199,7 +207,8 @@ describe('FeeTaker', function () {
},
buildFeeTakerExtensions({
feeTaker: await feeTaker.getAddress(),
feeRecipient,
integratorFeeRecipient,
protocolFeeRecipient,
makerReceiver,
integratorFee,
}),
Expand All @@ -211,8 +220,16 @@ describe('FeeTaker', function () {
});
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`);

const integratorFeeCalculated = takingAmount * integratorFee / 2n / BigInt(1e5);
const protocolFeeCalculated = takingAmount * integratorFee / 2n / BigInt(1e5);
const totalFeeCalculated = integratorFeeCalculated + protocolFeeCalculated;

await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, feeRecipient, makerReceiver], [-takingAmount - feeCalculated, 0, feeCalculated, takingAmount]);
await expect(fillTx).to.changeTokenBalances(
weth,
[addr, addr1, integratorFeeRecipient, protocolFeeRecipient, makerReceiver],
[-takingAmount - totalFeeCalculated, 0, integratorFeeCalculated, protocolFeeCalculated, takingAmount]);
});

it('should charge fee in eth', async function () {
Expand All @@ -222,7 +239,8 @@ describe('FeeTaker', function () {
const takingAmount = ether('0.3');
const integratorFee = BigInt(1e4);
const feeCalculated = takingAmount * integratorFee / BigInt(1e5);
const feeRecipient = addr2.address;
const integratorFeeRecipient = addr2.address;
const protocolFeeRecipient = addr3.address;

const order = buildOrder(
{
Expand All @@ -236,7 +254,8 @@ describe('FeeTaker', function () {
},
buildFeeTakerExtensions({
feeTaker: await feeTaker.getAddress(),
feeRecipient,
integratorFeeRecipient,
protocolFeeRecipient,
integratorFee,
}),
);
Expand All @@ -249,7 +268,7 @@ describe('FeeTaker', function () {
console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalance(weth, addr, -takingAmount - feeCalculated);
await expect(fillTx).to.changeEtherBalances([addr1, feeRecipient], [takingAmount, feeCalculated]);
await expect(fillTx).to.changeEtherBalances([addr1, integratorFeeRecipient, protocolFeeRecipient], [takingAmount, feeCalculated / 2n, feeCalculated / 2n]);
});

it('should charge fee in eth and send the rest to the maker receiver', async function () {
Expand All @@ -259,8 +278,9 @@ describe('FeeTaker', function () {
const takingAmount = ether('0.3');
const integratorFee = BigInt(1e4);
const feeCalculated = takingAmount * integratorFee / BigInt(1e5);
const feeRecipient = addr2.address;
const makerReceiver = addr3.address;
const integratorFeeRecipient = addr2.address;
const protocolFeeRecipient = addr3.address;
const makerReceiver = addr4.address;

const order = buildOrder(
{
Expand All @@ -274,7 +294,8 @@ describe('FeeTaker', function () {
},
buildFeeTakerExtensions({
feeTaker: await feeTaker.getAddress(),
feeRecipient,
integratorFeeRecipient,
protocolFeeRecipient,
makerReceiver,
integratorFee,
}),
Expand All @@ -288,6 +309,6 @@ describe('FeeTaker', function () {
console.log(`GasUsed: ${(await (await fillTx).wait()).gasUsed.toString()}`);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalance(weth, addr, -takingAmount - feeCalculated);
await expect(fillTx).to.changeEtherBalances([addr1, feeRecipient, makerReceiver], [0, feeCalculated, takingAmount]);
await expect(fillTx).to.changeEtherBalances([addr1, integratorFeeRecipient, protocolFeeRecipient, makerReceiver], [0, feeCalculated / 2n, feeCalculated / 2n, takingAmount]);
});
});
Loading

0 comments on commit 22a18f7

Please sign in to comment.