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

Operational automation #39

Merged
merged 3 commits into from
Oct 24, 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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ lcov.info*
# Defender Actions
dist

build/deployments-fork*.json
build/deployments-fork*.json

# Reports. eg stats.html
*.html
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ npx hardhat setActionVars --id 563d8d0c-17dc-46d3-8955-e4824864869f
npx hardhat setActionVars --id c010fb76-ea63-409d-9981-69322d27993a
npx hardhat setActionVars --id 127171fd-7b85-497e-8335-fd7907c08386
npx hardhat setActionVars --id 84b5f134-8351-4402-8f6a-fb4376034bc4
npx hardhat setActionVars --id ffcfc580-7b0a-42ed-a4f2-3f0a3add9779

# The Defender autotask client uses generic env var names so we'll set them first from the values in the .env file
export API_KEY=
Expand All @@ -250,6 +251,11 @@ npx defender-autotask update-code 563d8d0c-17dc-46d3-8955-e4824864869f ./dist/au
npx defender-autotask update-code c010fb76-ea63-409d-9981-69322d27993a ./dist/autoRequestLidoWithdraw
npx defender-autotask update-code 127171fd-7b85-497e-8335-fd7907c08386 ./dist/autoClaimLidoWithdraw
npx defender-autotask update-code 84b5f134-8351-4402-8f6a-fb4376034bc4 ./dist/collectLidoFees
npx defender-autotask update-code ffcfc580-7b0a-42ed-a4f2-3f0a3add9779 ./dist/setPrices
```

`rollup` and `defender-autotask` can be installed globally to avoid the `npx` prefix.

The Defender Actions need to be under 5MB in size. The [rollup-plugin-visualizer](https://www.npmjs.com/package/rollup-plugin-visualizer) can be used to visualize the size of an Action's dependencies.
A `stats.html` file is generated in the`src/js/actions` folder that can be opened in a browser to see the size of the Action's dependencies.
This will be for the last Action in the rollup config `src/js/actions/rollup.config.cjs`.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"ethers": "6.13.2",
"graphql": "^16.9.0",
"hardhat": "^2.22.9",
"rollup": "^4.9.1"
"rollup": "^4.9.1",
"rollup-plugin-visualizer": "^5.12.0"
}
}
14 changes: 13 additions & 1 deletion src/js/actions/rollup.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ const resolve = require("@rollup/plugin-node-resolve");
const commonjs = require("@rollup/plugin-commonjs");
const json = require("@rollup/plugin-json");
const builtins = require("builtin-modules");
const { visualizer } = require("rollup-plugin-visualizer");

const commonConfig = {
plugins: [
resolve({ preferBuiltins: true }),
commonjs(),
json({ compact: true }),
// Generates a stats.html file in the actions folder.
// This is a visual of the Action dependencies for the last Action in the rollup config.
visualizer(),
],
// Do not bundle these packages.
// ethers is required to be bundled even though it an Autotask package.
// ethers is required to be bundled as we need v6 and not v5 that is packaged with Defender Actions.
external: [
...builtins,
"axios",
Expand Down Expand Up @@ -68,4 +72,12 @@ module.exports = [
},
...commonConfig,
},
{
input: "setPrices.js",
output: {
file: "dist/setPrices/index.js",
format: "cjs",
},
...commonConfig,
},
];
42 changes: 42 additions & 0 deletions src/js/actions/setPrices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const { Defender } = require("@openzeppelin/defender-sdk");
const { ethers } = require("ethers");

const { setPrices } = require("../tasks/lidoPrices");
const { mainnet } = require("../utils/addresses");
const lidoARMAbi = require("../../abis/LidoARM.json");

// Entrypoint for the Defender Action
const handler = async (event) => {
// Initialize defender relayer provider and signer
const client = new Defender(event);
const provider = client.relaySigner.getProvider({ ethersVersion: "v6" });
const signer = await client.relaySigner.getSigner(provider, {
speed: "fastest",
ethersVersion: "v6",
});

console.log(
`DEBUG env var in handler before being set: "${process.env.DEBUG}"`
);

// References to contracts
const arm = new ethers.Contract(mainnet.lidoARM, lidoARMAbi, signer);

try {
await setPrices({
signer,
arm,
curve: true,
amount: 50,
tolerance: 0.2,
maxBuyPrice: 0.9997,
minSellPrice: 0.9999,
fee: 1,
blockTag: "latest",
});
} catch (error) {
console.error(error);
}
};

module.exports = { handler };
78 changes: 2 additions & 76 deletions src/js/tasks/lido.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const {
logUniswapSpotPrices,
} = require("./markets");
const { getBlock } = require("../utils/block");
const { abs } = require("../utils/maths");
const { get1InchPrices } = require("../utils/1Inch");
const { getSigner } = require("../utils/signers");
const { logTxDetails } = require("../utils/txLogger");
const {
Expand All @@ -20,78 +18,6 @@ const { resolveAddress, resolveAsset } = require("../utils/assets");

const log = require("../utils/logger")("task:lido");

const setPrices = async (options) => {
const { signer, arm, fee, tolerance, buyPrice, midPrice, sellPrice, inch } =
options;

// get current ARM stETH/WETH prices
const currentTradeRate0 = parseUnits("1", 72) / (await arm.traderate0());
const currentTradeRate1 = await arm.traderate1();
log(`current sell price : ${formatUnits(currentTradeRate0, 36)}`);
log(`current buy price : ${formatUnits(currentTradeRate1, 36)}`);

let targetSellPrice;
let targetBuyPrice;
if (!buyPrice && !sellPrice && (midPrice || inch)) {
// get latest 1inch prices if no midPrice is provided
const referencePrices = midPrice
? {
midPrice: parseUnits(midPrice.toString(), 18),
}
: await get1InchPrices(options.amount);
log(`mid price : ${formatUnits(referencePrices.midPrice)}`);

const FeeScale = BigInt(1e6);
const feeRate = FeeScale - BigInt(fee * 100);
log(`fee : ${formatUnits(BigInt(fee * 1000000), 6)} bps`);
log(`fee rate : ${formatUnits(feeRate, 6)} bps`);

targetSellPrice =
(referencePrices.midPrice * BigInt(1e18) * FeeScale) / feeRate;
targetBuyPrice =
(referencePrices.midPrice * BigInt(1e18) * feeRate) / FeeScale;
} else if (buyPrice && sellPrice) {
targetSellPrice = parseUnits(sellPrice.toString(), 18) * BigInt(1e18);
targetBuyPrice = parseUnits(buyPrice.toString(), 18) * BigInt(1e18);
} else {
throw new Error(
`Either both buy and sell prices should be provided or midPrice`
);
}

log(`target sell price : ${formatUnits(targetSellPrice, 36)}`);
log(`target buy price : ${formatUnits(targetBuyPrice, 36)}`);

const diffBuyPrice = abs(targetBuyPrice - currentTradeRate1);
log(`buy price diff : ${formatUnits(diffBuyPrice, 36)}`);

// tolerance option is in basis points
const toleranceScaled = parseUnits(tolerance.toString(), 36 - 4);
log(`tolerance : ${formatUnits(toleranceScaled, 36)}`);

// decide if rates need to be updated
if (diffBuyPrice > toleranceScaled) {
// Note the prices of setPrices is from the AMM perspective and not the Trader
// hence the buy and sell prices are swapped
console.log(`About to update ARM prices`);
console.log(`sell: ${formatUnits(targetSellPrice, 36)}`);
console.log(`buy : ${formatUnits(targetBuyPrice, 36)}`);

const tx = await arm
.connect(signer)
.setPrices(targetBuyPrice, targetSellPrice);

await logTxDetails(tx, "setPrices", options.confirm);
} else {
console.log(
`No price update as price diff of ${formatUnits(
diffBuyPrice,
36
)} < tolerance ${formatUnits(toleranceScaled, 36)}`
);
}
};

async function setZapper() {
const signer = await getSigner();

Expand Down Expand Up @@ -138,7 +64,8 @@ const submitLido = async ({ amount }) => {

const snapLido = async ({ amount, block, curve, oneInch, uniswap, gas }) => {
const blockTag = await getBlock(block);
const commonOptions = { amount, blockTag, pair: "stETH/ETH", gas };
const signer = await getSigner();
const commonOptions = { amount, blockTag, pair: "stETH/ETH", gas, signer };

const armAddress = await parseAddress("LIDO_ARM");
const lidoARM = await ethers.getContractAt("LidoARM", armAddress);
Expand Down Expand Up @@ -349,6 +276,5 @@ module.exports = {
submitLido,
swapLido,
snapLido,
setPrices,
setZapper,
};
152 changes: 152 additions & 0 deletions src/js/tasks/lidoPrices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
const { formatUnits, parseUnits } = require("ethers");

const addresses = require("../utils/addresses");

const { abs } = require("../utils/maths");
const { get1InchPrices } = require("../utils/1Inch");
const { logTxDetails } = require("../utils/txLogger");
const { getCurvePrices } = require("../utils/curve");

const log = require("../utils/logger")("task:lido");

const setPrices = async (options) => {
const {
signer,
arm,
fee,
tolerance,
buyPrice,
midPrice,
sellPrice,
minSellPrice,
maxBuyPrice,
curve,
inch,
} = options;

// get current ARM stETH/WETH prices
const currentSellPrice = parseUnits("1", 72) / (await arm.traderate0());
const currentBuyPrice = await arm.traderate1();
log(`current sell price : ${formatUnits(currentSellPrice, 36)}`);
log(`current buy price : ${formatUnits(currentBuyPrice, 36)}`);

let targetSellPrice;
let targetBuyPrice;
if (!buyPrice && !sellPrice && (midPrice || curve || inch)) {
// get latest 1inch prices if no midPrice is provided
const referencePrices = midPrice
? {
midPrice: parseUnits(midPrice.toString(), 18),
}
: inch
? await get1InchPrices(options.amount)
: await getCurvePrices({
...options,
poolAddress: addresses.mainnet.CurveStEthPool,
});
log(`mid price : ${formatUnits(referencePrices.midPrice)}`);

const FeeScale = BigInt(1e6);
const feeRate = FeeScale - BigInt(fee * 100);
log(`fee : ${formatUnits(BigInt(fee * 1000000), 6)} bps`);
log(`fee rate : ${formatUnits(feeRate, 6)} bps`);

targetSellPrice =
(referencePrices.midPrice * BigInt(1e18) * FeeScale) / feeRate;
targetBuyPrice =
(referencePrices.midPrice * BigInt(1e18) * feeRate) / FeeScale;

const minSellPriceBN = parseUnits(minSellPrice.toString(), 36);
const maxBuyPriceBN = parseUnits(maxBuyPrice.toString(), 36);
if (targetSellPrice < minSellPriceBN) {
log(
`target sell price ${formatUnits(
targetSellPrice,
36
)} is below min sell price ${minSellPrice} so will use min`
);
targetSellPrice = minSellPriceBN;
}
if (targetBuyPrice > maxBuyPriceBN) {
log(
`target buy price ${formatUnits(
targetBuyPrice,
36
)} is above max buy price ${maxBuyPrice} so will use max`
);
targetBuyPrice = maxBuyPriceBN;
}

const crossPrice = await arm.crossPrice();
if (targetSellPrice < crossPrice) {
log(
`target sell price ${formatUnits(
targetSellPrice,
36
)} is below cross price ${formatUnits(
crossPrice,
36
)} so will use cross price`
);
targetSellPrice = crossPrice;
}
if (targetBuyPrice >= crossPrice) {
log(
`target buy price ${formatUnits(
targetBuyPrice,
36
)} is above cross price ${formatUnits(
crossPrice,
36
)} so will use cross price`
);
targetBuyPrice = crossPrice - 1n;
}
} else if (buyPrice && sellPrice) {
targetSellPrice = parseUnits(sellPrice.toString(), 18) * BigInt(1e18);
targetBuyPrice = parseUnits(buyPrice.toString(), 18) * BigInt(1e18);
} else {
throw new Error(
`Either both buy and sell prices should be provided or midPrice`
);
}

log(`target sell price : ${formatUnits(targetSellPrice, 36)}`);
log(`target buy price : ${formatUnits(targetBuyPrice, 36)}`);

const diffSellPrice = abs(targetSellPrice - currentSellPrice);
log(`sell price diff : ${formatUnits(diffSellPrice, 36)}`);
const diffBuyPrice = abs(targetBuyPrice - currentBuyPrice);
log(`buy price diff : ${formatUnits(diffBuyPrice, 36)}`);

// tolerance option is in basis points
const toleranceScaled = parseUnits(tolerance.toString(), 36 - 4);
log(`tolerance : ${formatUnits(toleranceScaled, 36)}`);

// decide if rates need to be updated
if (diffSellPrice > toleranceScaled || diffBuyPrice > toleranceScaled) {
console.log(`About to update ARM prices`);
console.log(`sell: ${formatUnits(targetSellPrice, 36)}`);
console.log(`buy : ${formatUnits(targetBuyPrice, 36)}`);

const tx = await arm
.connect(signer)
.setPrices(targetBuyPrice, targetSellPrice);

await logTxDetails(tx, "setPrices", options.confirm);
} else {
console.log(
`No price update as price diff of buy ${formatUnits(
diffBuyPrice,
32
)} and sell ${formatUnits(diffSellPrice, 32)} < tolerance ${formatUnits(
toleranceScaled,
32
)} basis points`
);
}
};

module.exports = {
setPrices,
};
2 changes: 1 addition & 1 deletion src/js/tasks/markets.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ const logUniswapSpotPrices = async (options, ammPrices) => {
20
)} ${pair}, diff ${formatUnits(sellRateDiff, 14)} bps to ARM${sellGasCosts}`
);
console.log(`spread : ${formatUnits(uniswap.spread, 14)} bps`);
console.log(`spread : ${formatUnits(uniswap.spread, 14)} bps`);

return uniswap;
};
Expand Down
Loading
Loading