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

[SC-1081] Fee extension for limit orders #306

Merged
merged 5 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions contracts/extensions/FeeTaker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

import { Address, AddressLib } from "@1inch/solidity-utils/contracts/libraries/AddressLib.sol";
import { SafeERC20 } from "@1inch/solidity-utils/contracts/libraries/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { IOrderMixin } from "../interfaces/IOrderMixin.sol";
import { IPostInteraction } from "../interfaces/IPostInteraction.sol";

contract FeeTaker is IPostInteraction {
using AddressLib for Address;
using SafeERC20 for IERC20;

uint256 internal constant _FEE_BASE = 1e7;

/**
* @notice See {IPostInteraction-postInteraction}.
* @dev Takes the fee in taking tokens and transfers the rest to the maker.
* `extraData` consists of:
* 3 bytes — fee percentage (in 1e7)
* 20 bytes — fee recipient
* 20 bytes — receiver of taking tokens (optional, if not set, maker is used)
*/
function postInteraction(
IOrderMixin.Order calldata order,
bytes calldata /* extension */,
bytes32 /* orderHash */,
address /* taker */,
uint256 /* makingAmount */,
uint256 takingAmount,
uint256 /* remainingMakingAmount */,
bytes calldata extraData
) external {
uint256 fee = takingAmount * uint256(uint24(bytes3(extraData))) / _FEE_BASE;
address feeRecipient = address(bytes20(extraData[3:23]));

address receiver = order.maker.get();
if (extraData.length > 23) {
receiver = address(bytes20(extraData[23:43]));
}

if (fee > 0) {
IERC20(order.takerAsset.get()).safeTransfer(feeRecipient, fee);
}

unchecked {
IERC20(order.takerAsset.get()).safeTransfer(receiver, takingAmount - fee);
}
}
}
32 changes: 32 additions & 0 deletions deploy/deploy-FeeTaker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const hre = require('hardhat');
const { getChainId } = hre;
const { deployAndGetContract } = require('@1inch/solidity-utils');

module.exports = async ({ deployments, getNamedAccounts }) => {
const networkName = hre.network.name;
console.log(`running ${networkName} deploy script`);
const chainId = await getChainId();
console.log('network id ', chainId);
if (
networkName in hre.config.networks[networkName] &&
chainId !== hre.config.networks[networkName].chainId.toString()
) {
console.log(`network chain id: ${hre.config.networks[networkName].chainId}, your chain id ${chainId}`);
console.log('skipping wrong chain id deployment');
return;
}

const { deployer } = await getNamedAccounts();

const constructorArgs = [];
const contractName = 'FeeTaker';

await deployAndGetContract({
contractName,
constructorArgs,
deployments,
deployer,
});
};

module.exports.skip = async () => true;
167 changes: 167 additions & 0 deletions test/FeeTaker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
const hre = require('hardhat');
const { ethers } = hre;
const { expect } = require('@1inch/solidity-utils');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { deploySwapTokens } = require('./helpers/fixtures');
const { buildOrder, buildTakerTraits, signOrder } = require('./helpers/orderUtils');
const { ether, trim0x } = require('./helpers/utils');

describe('FeeTaker', function () {
let addr, addr1, addr2, addr3;
byshape marked this conversation as resolved.
Show resolved Hide resolved
before(async function () {
[addr, addr1, addr2, addr3] = await ethers.getSigners();
});

async function deployContractsAndInit () {
const { dai, weth, inch, swap, chainId } = await deploySwapTokens();

await dai.mint(addr, ether('1000000'));
await weth.deposit({ value: ether('100') });
await inch.mint(addr, ether('1000000'));
await dai.mint(addr1, ether('1000000'));
await weth.connect(addr1).deposit({ value: ether('100') });
await inch.mint(addr1, ether('1000000'));

await dai.approve(swap, ether('1000000'));
await weth.approve(swap, ether('1000000'));
await inch.approve(swap, ether('1000000'));
await dai.connect(addr1).approve(swap, ether('1000000'));
await weth.connect(addr1).approve(swap, ether('1000000'));
await inch.connect(addr1).approve(swap, ether('1000000'));

const FeeTaker = await ethers.getContractFactory('FeeTaker');
const feeTaker = await FeeTaker.deploy();

return { dai, weth, inch, swap, chainId, feeTaker };
};

it('should send all tokens to the maker with 0 fee', async function () {
const { dai, weth, swap, chainId, feeTaker } = await loadFixture(deployContractsAndInit);

const makingAmount = ether('300');
const takingAmount = ether('0.3');
const fee = 0;
const feeRecipient = addr2.address;

const order = buildOrder(
{
maker: addr1.address,
receiver: await feeTaker.getAddress(),
makerAsset: await dai.getAddress(),
takerAsset: await weth.getAddress(),
makingAmount,
takingAmount,
},
{
postInteraction: await feeTaker.getAddress() + trim0x(ethers.solidityPacked(['uint24', 'address'], [fee, feeRecipient])),
},
);

const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1));
const takerTraits = buildTakerTraits({
extension: order.extension,
});
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2], [-takingAmount, takingAmount, 0]);
});

it('should send all tokens to the maker receiver with 0 fee', async function () {
const { dai, weth, swap, chainId, feeTaker } = await loadFixture(deployContractsAndInit);

const makingAmount = ether('300');
const takingAmount = ether('0.3');
const fee = 0;
const feeRecipient = addr2.address;
const makerReceiver = addr3.address;

const order = buildOrder(
{
maker: addr1.address,
receiver: await feeTaker.getAddress(),
makerAsset: await dai.getAddress(),
takerAsset: await weth.getAddress(),
makingAmount,
takingAmount,
},
{
postInteraction: await feeTaker.getAddress() +
trim0x(ethers.solidityPacked(['uint24', 'address', 'address'], [fee, feeRecipient, makerReceiver])),
},
);

const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1));
const takerTraits = buildTakerTraits({
extension: order.extension,
});
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2, addr3], [-takingAmount, 0, 0, takingAmount]);
});

it('should charge fee', async function () {
const { dai, weth, swap, chainId, feeTaker } = await loadFixture(deployContractsAndInit);

const makingAmount = ether('300');
const takingAmount = ether('0.3');
const fee = BigInt(1e6);
const feeCalculated = takingAmount * fee / BigInt(1e7);
const feeRecipient = addr2.address;

const order = buildOrder(
{
maker: addr1.address,
receiver: await feeTaker.getAddress(),
makerAsset: await dai.getAddress(),
takerAsset: await weth.getAddress(),
makingAmount,
takingAmount,
},
{
postInteraction: await feeTaker.getAddress() + trim0x(ethers.solidityPacked(['uint24', 'address'], [fee, feeRecipient])),
},
);

const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1));
const takerTraits = buildTakerTraits({
extension: order.extension,
});
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2], [-takingAmount, takingAmount - feeCalculated, feeCalculated]);
});

it('should charge fee and send the rest to the maker receiver', async function () {
const { dai, weth, swap, chainId, feeTaker } = await loadFixture(deployContractsAndInit);

const makingAmount = ether('300');
const takingAmount = ether('0.3');
const fee = BigInt(1e6);
const feeCalculated = takingAmount * fee / BigInt(1e7);
const feeRecipient = addr2.address;
const makerReceiver = addr3.address;

const order = buildOrder(
{
maker: addr1.address,
receiver: await feeTaker.getAddress(),
makerAsset: await dai.getAddress(),
takerAsset: await weth.getAddress(),
makingAmount,
takingAmount,
},
{
postInteraction: await feeTaker.getAddress() +
trim0x(ethers.solidityPacked(['uint24', 'address', 'address'], [fee, feeRecipient, makerReceiver])),
},
);

const { r, yParityAndS: vs } = ethers.Signature.from(await signOrder(order, chainId, await swap.getAddress(), addr1));
const takerTraits = buildTakerTraits({
extension: order.extension,
});
const fillTx = swap.fillOrderArgs(order, r, vs, makingAmount, takerTraits.traits, takerTraits.args);
await expect(fillTx).to.changeTokenBalances(dai, [addr, addr1], [makingAmount, -makingAmount]);
await expect(fillTx).to.changeTokenBalances(weth, [addr, addr1, addr2, addr3], [-takingAmount, 0, feeCalculated, takingAmount - feeCalculated]);
});
});
Loading