Skip to content

Commit

Permalink
Merge branch 'dev' into popzxc-920-refine-project-structure
Browse files Browse the repository at this point in the history
  • Loading branch information
popzxc committed Sep 18, 2020
2 parents 9a18cad + 06bbfd5 commit bd223a9
Show file tree
Hide file tree
Showing 12 changed files with 2,023 additions and 1 deletion.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
!js/client/dist
!js/explorer/index.html
!js/explorer/dist
!js/fee-seller
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- name: init
run: zksync init

- name: js-tests
run: zksync js-tests

- name: zcli-tests
run: ci-zcli.sh

Expand Down
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export GETH_DOCKER_IMAGE ?= matterlabs/geth:latest
export DEV_TICKER_DOCKER_IMAGE ?= matterlabs/dev-ticker:latest
export KEYBASE_DOCKER_IMAGE ?= matterlabs/keybase-secret:latest
export CI_DOCKER_IMAGE ?= matterlabs/ci
export FEE_SELLER_IMAGE ?=matterlabs/fee-seller:latest

# Getting started

Expand All @@ -29,6 +30,7 @@ yarn:
@cd infrastructure/explorer && yarn
@cd contracts && yarn
@cd core/tests/ts-tests && yarn
@cd js/fee-seller && yarn
@cd infrastructure/zcli && yarn


Expand Down Expand Up @@ -113,6 +115,12 @@ image-keybase:
push-image-keybase: image-keybase
docker push "${KEYBASE_DOCKER_IMAGE}"

image-fee-seller:
@docker build -t "${FEE_SELLER_IMAGE}" -f ./docker/fee-seller/Dockerfile .

push-image-fee-seller: image-fee-seller
docker push "${FEE_SELLER_IMAGE}"

# Rust: main stuff

server:
Expand Down Expand Up @@ -181,6 +189,10 @@ price:
prover-tests:
f cargo test -p prover --release -- --ignored

js-tests:
@cd js/zksync.js && yarn tests
@cd js/fee-seller && yarn tests

# Devops: main

# Promote build
Expand Down
19 changes: 19 additions & 0 deletions docker/fee-seller/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM node:12
WORKDIR /usr/src/fee-seller
COPY js/fee-seller/package.json .
COPY js/fee-seller/yarn.lock .
RUN yarn install --frozen-lockfile

COPY js/fee-seller/ .

# required env
# ENV FEE_ACCOUNT_PRIVATE_KEY
# ENV MAX_LIQUIDATION_FEE_PERCENT
# ENV FEE_ACCUMULATOR_ADDRESS
# ENV ETH_NETWORK
# ENV WEB3_URL
# ENV NOTIFICATION_WEBHOOK_URL
# optional env
# ENV MAX_LIQUIDATION_FEE_SLIPPAGE
# ENV ETH_TRANSFER_THRESHOLD
CMD ["yarn", "start"]
5 changes: 5 additions & 0 deletions etc/env/dev.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,8 @@ TICKER_FAST_PROCESSING_COEFF=10.0

# Amount of threads to use to generate witness for blocks.
WITNESS_GENERATORS=2

# FEE LIQUIDATION CONSTANTS
MAX_LIQUIDATION_FEE_PERCENT=5
FEE_ACCUMULATOR_ADDRESS=0xde03a0B5963f75f1C8485B355fF6D30f3093BDE7
FEE_ACCOUNT_PRIVATE_KEY=unset
196 changes: 196 additions & 0 deletions infrastructure/fee-seller/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* Script that sells accumulated transaction fees for ETH using 1inch exchange and transfer ETH to other account.
*
* Selling is done in steps:
* Step 1 - token is withdrawn to the ETH account
* Step 2 - token is swapped for ETH using 1inch
* Step 3 - ETH is transferred to the FEE_ACCUMULATOR_ADDRESS
*
* Each step happens one after another without waiting for previous to complete
* so this script should be run frequently (e.g. once every 15 min).
*
* Each operation is performed only if transaction fee of this operation is less then MAX_LIQUIDATION_FEE_PERCENT.
*
* See Env parameters for available configuration parameters
*/
import Axios from "axios";
import {BigNumber, ethers} from "ethers";
import * as zksync from "zksync";
import {
approveTokenIfNotApproved,
fmtToken,
fmtTokenWithETHValue,
getExpectedETHSwapResult,
isOperationFeeAcceptable,
sendNotification
} from "./utils";

/** Env parameters. */
const FEE_ACCOUNT_PRIVATE_KEY = process.env.FEE_ACCOUNT_PRIVATE_KEY;
const MAX_LIQUIDATION_FEE_PERCENT = parseInt(process.env.MAX_LIQUIDATION_FEE_PERCENT);
const FEE_ACCUMULATOR_ADDRESS = process.env.FEE_ACCUMULATOR_ADDRESS;
const ETH_NETWORK = process.env.ETH_NETWORK as any;
const WEB3_URL = process.env.WEB3_URL;
const MAX_LIQUIDATION_FEE_SLIPPAGE = parseInt(process.env.MAX_LIQUIDATION_FEE_SLIPPAGE) || 5;
/** Amount of ETH that should be left on the fee account after third step. */
const ETH_TRANSFER_THRESHOLD = process.env.ETH_TRANSFER_THRESHOLD ?
ethers.utils.parseEther(process.env.ETH_TRANSFER_THRESHOLD) : ethers.utils.parseEther("3.0");
/** Mattermost webhook url */
const NOTIFICATION_WEBHOOK_URL = process.env.NOTIFICATION_WEBHOOK_URL;


/** Approve ERC-20 tokens for this address */
const INCH_APPROVE = "0xe4c9194962532feb467dce8b3d42419641c6ed2e";
/** Send exchange tx to this address */
const INCH_EXCHANGE = "0x11111254369792b2Ca5d084aB5eEA397cA8fa48B";

/** Withdraw everything that has to be withdrawn */
async function withdrawTokens(zksWallet: zksync.Wallet) {
const provider = zksWallet.provider;
const accountState = await zksWallet.getAccountState();
for (const token in accountState.committed.balances) {
if (provider.tokenSet.resolveTokenSymbol(token) === "MLTT") {
continue;
}

const tokenBalance = BigNumber.from(accountState.verified.balances[token]);
const tokenCommittedBalance = BigNumber.from(accountState.committed.balances[token]);
if (tokenCommittedBalance.lt(tokenBalance)) {
continue;
}

const withdrawFee = (await provider.getTransactionFee("Withdraw", zksWallet.address(), token)).totalFee;

if (isOperationFeeAcceptable(tokenBalance, withdrawFee, MAX_LIQUIDATION_FEE_PERCENT)) {
const amountAfterWithdraw = tokenBalance.sub(withdrawFee);
console.log(`Withdrawing token, amount after withdraw: ${fmtToken(provider, token, amountAfterWithdraw)}, fee: ${fmtToken(provider, token, withdrawFee)}`);
const transaction = await zksWallet.withdrawFromSyncToEthereum({
ethAddress: zksWallet.address(),
token,
amount: amountAfterWithdraw,
fee: withdrawFee,
});
console.log(`Tx hash: ${transaction.txHash}`);
await transaction.awaitReceipt();


await sendNotification(`Withdrawn ${await fmtTokenWithETHValue(provider, token, amountAfterWithdraw)}, tx hash: ${transaction.txHash}`, NOTIFICATION_WEBHOOK_URL);
}
}
}

/** Swap tokens for ETH */
async function sellTokens(zksWallet: zksync.Wallet) {
const zksProvider = zksWallet.provider;
const tokens = await zksProvider.getTokens();
for (const token in tokens) {
if (zksWallet.provider.tokenSet.resolveTokenSymbol(token) === "MLTT" || zksync.utils.isTokenETH(token)) {
continue;
}

const tokenAmount = await zksWallet.getEthereumBalance(token);
if (tokenAmount.eq(0)) {
continue;
}


const req1inch = "https://api.1inch.exchange/v1.1/swapQuote?" +
`fromTokenSymbol=${zksProvider.tokenSet.resolveTokenSymbol(token)}` +
`&toTokenSymbol=ETH` +
`&amount=${tokenAmount.toString()}` +
`&slippage=${MAX_LIQUIDATION_FEE_SLIPPAGE}` +
"&disableEstimate=true" +
`&fromAddress=${zksWallet.address()}`;
try {
const expectedETHAfterTrade = await getExpectedETHSwapResult(tokens[token].symbol, tokens[token].decimals, tokenAmount);

const apiResponse = await Axios.get(req1inch).then((resp) => resp.data);
const approximateTxFee = BigNumber.from("300000").mul(apiResponse.gasPrice);
const estimatedAmountAfterTrade = apiResponse.toTokenAmount;
console.log(`Estimated swap result tokenAmount: ${fmtToken(zksProvider, token, tokenAmount)} resultAmount: ${fmtToken(zksProvider, "ETH", estimatedAmountAfterTrade)}, tx fee: ${fmtToken(zksProvider, "ETH", approximateTxFee)}, coinGecko: ${fmtToken(zksProvider, "ETH", estimatedAmountAfterTrade)}`);

if (approximateTxFee.gte(estimatedAmountAfterTrade)) {
continue;
}

// Crosscheck 1inch trade result with CoinGecko prices
if (!isOperationFeeAcceptable(expectedETHAfterTrade, expectedETHAfterTrade.sub(estimatedAmountAfterTrade).abs(), MAX_LIQUIDATION_FEE_SLIPPAGE)) {
console.log("1inch price is different then CoinGecko price");
continue
}

if (isOperationFeeAcceptable(estimatedAmountAfterTrade, approximateTxFee, MAX_LIQUIDATION_FEE_PERCENT)) {
await approveTokenIfNotApproved(zksWallet.ethSigner, zksProvider.tokenSet.resolveTokenAddress(token), INCH_APPROVE)
if (apiResponse.to.toLowerCase() != INCH_EXCHANGE.toLowerCase()) {
throw new Error("Incorrect exchange address");
}

console.log("Sending swap tx.");
const ethTransaction = await zksWallet.ethSigner.sendTransaction({
from: apiResponse.from,
to: apiResponse.to,
gasLimit: BigNumber.from(apiResponse.gas),
gasPrice: BigNumber.from(apiResponse.gasPrice),
value: BigNumber.from(apiResponse.value),
data: apiResponse.data,
});
console.log(`Tx hash: ${ethTransaction.hash}`);

await sendNotification(`Swap ${await fmtTokenWithETHValue(zksProvider, token, tokenAmount)}, tx hash: ${ethTransaction.hash}`, NOTIFICATION_WEBHOOK_URL);
}
} catch (err) {
console.log(err)
const response = err.response;
console.log(`API error, status: ${response?.status} status: ${response?.statusText}, data.message: ${response?.data.message}`);
}
}
}

/** Send ETH to the accumulator account account */
async function sendETH(zksWallet: zksync.Wallet) {
const ethWallet = zksWallet.ethSigner;
const ethProvider = ethWallet.provider;
const ethBalance = await ethWallet.getBalance();
if (ethBalance.gt(ETH_TRANSFER_THRESHOLD)) {
const ethTransferFee = BigNumber.from("21000").mul(await ethProvider.getGasPrice());
const ethToSend = ethBalance.sub(ETH_TRANSFER_THRESHOLD);
if (isOperationFeeAcceptable(ethToSend, ethTransferFee, MAX_LIQUIDATION_FEE_PERCENT)) {
console.log(`Sending ${fmtToken(zksWallet.provider, "ETH", ethToSend)} to ${FEE_ACCUMULATOR_ADDRESS}`);
const tx = await ethWallet.sendTransaction({to: FEE_ACCUMULATOR_ADDRESS, value: ethToSend});
console.log(`Tx hash: ${tx.hash}`);

await sendNotification(`Send ${fmtToken(zksWallet.provider, "ETH", ethToSend)}, tx hash: ${tx.hash}`, NOTIFICATION_WEBHOOK_URL);
}
}
}

(async () => {
const ethProvider = new ethers.providers.JsonRpcProvider(WEB3_URL);
const ethWallet = new ethers.Wallet(FEE_ACCOUNT_PRIVATE_KEY).connect(ethProvider);
const zksProvider = await zksync.getDefaultProvider(ETH_NETWORK, "HTTP");
const zksWallet = await zksync.Wallet.fromEthSigner(ethWallet, zksProvider);
try {
if (!await zksWallet.isSigningKeySet()) {
console.log("Changing fee account signing key");
const signingKeyTx = await zksWallet.setSigningKey();
await signingKeyTx.awaitReceipt();
}

console.log("Step 1 - withdrawing tokens");
await withdrawTokens(zksWallet);

// Step 2 sell onchain balance tokens
console.log("Step 2 - selling tokens");
await sellTokens(zksWallet);

// Step 3 - moving Ethereum to the operator account
console.log("Step 2 - sending ETH");
await sendETH(zksWallet);
} catch (e) {
console.error("Failed to proceed with fee liquidation: ", e);
process.exit(1);
} finally {
await zksProvider.disconnect();
process.exit(0);
}
})();
23 changes: 23 additions & 0 deletions infrastructure/fee-seller/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "fee-seller",
"version": "1.0.0",
"main": "index.ts",
"license": "MIT",
"dependencies": {
"ethers": "^5.0.12",
"zksync": "^0.6.5"
},
"devDependencies": {
"@types/chai": "^4.2.12",
"@types/mocha": "^8.0.3",
"@types/node": "^14.6.4",
"chai": "^4.2.0",
"mocha": "^8.1.3",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
},
"scripts": {
"start": "ts-node index.ts",
"tests": "mocha -r ts-node/register tests.ts"
}
}
28 changes: 28 additions & 0 deletions infrastructure/fee-seller/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { types } from "zksync";
import { BigNumber, BigNumberish, utils } from "ethers";
import { expect } from "chai";
import { isOperationFeeAcceptable, numberAsFractionInBIPs } from "./utils";

describe("Withdraw token", () => {
it("numberAsFractionInBIPs", () => {
expect(numberAsFractionInBIPs(5, 100).toNumber()).eq(500);
expect(numberAsFractionInBIPs(2, 1).toNumber()).eq(20000);
expect(() => numberAsFractionInBIPs("0.1", 1)).throw("INVALID_ARGUMENT");
expect(() => numberAsFractionInBIPs(1, "0.1")).throw("INVALID_ARGUMENT");
expect(() => numberAsFractionInBIPs(-1, 1)).throw("Numbers should be non-negative");
expect(() => numberAsFractionInBIPs(-1, -1)).throw("Numbers should be non-negative");
expect(() => numberAsFractionInBIPs(1, -1)).throw("Numbers should be non-negative");
expect(() => numberAsFractionInBIPs(1, 0)).throw("Base fraction can't be 0");
expect(numberAsFractionInBIPs(2, 1).toNumber()).eq(20000);

const maxInt = BigNumber.from(Number.MAX_SAFE_INTEGER.toString());
expect(numberAsFractionInBIPs(maxInt.mul(4), maxInt.mul(2)).toNumber()).eq(20000);
});

it("isWithdrawRequired", () => {
expect(isOperationFeeAcceptable(utils.parseEther("100.0"), utils.parseEther("1.0"), 1)).eq(true);
expect(isOperationFeeAcceptable(utils.parseEther("100.0"), utils.parseEther("1.0"), 2)).eq(true);
expect(isOperationFeeAcceptable(utils.parseEther("200.0"), utils.parseEther("5.0"), 2)).eq(false);
expect(isOperationFeeAcceptable("0", BigNumber.from(100), 2)).eq(false);
});
});
14 changes: 14 additions & 0 deletions infrastructure/fee-seller/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"declaration": true,
"lib": ["es2015"],

"preserveSymlinks": true,
"preserveWatchOutput": true
},
"files": [
"./index.ts"
]
}
Loading

0 comments on commit bd223a9

Please sign in to comment.