Skip to content

Commit

Permalink
Operational automation (#39)
Browse files Browse the repository at this point in the history
* Added minSellPrice and maxBuyPrice to setPrices hardhat task
setPrice Hardhat task can now be done off Curve pool

* Added setPrices Action

* Fix smoke test
  • Loading branch information
naddison36 authored Oct 24, 2024
1 parent 2292ce0 commit e1c04d9
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 91 deletions.
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

0 comments on commit e1c04d9

Please sign in to comment.