Skip to content

Commit

Permalink
Merge pull request #18 from Abracadabra-money/feature/harvest-magicus…
Browse files Browse the repository at this point in the history
…d0pp

feat(magicusd0pp): Harvest USUAL rewards
  • Loading branch information
0xmDreamy authored Jan 3, 2025
2 parents b269463 + f1ed862 commit 6b26de8
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 54 deletions.
2 changes: 1 addition & 1 deletion DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
| Stargate S\*USDT | [task](https://app.gelato.network/functions/task/0x37ab785e9a1200fb8bac63b431e36da085b177841b24aa0dfab0a4981122da0a:1) |
| GM Strategy Harvester | [task](https://app.gelato.network/functions/task/0x40d7aadde626b52e7df27bcab3f92c42faf3f137d50fc98dffc79d20c9119314:42161) |
| Reward Distributor | [task](https://app.gelato.network/functions/task/0x1b62e611e8e3d87ec8c7ced57230d341802c0ac6c611afb9a9fb4a3c53dc6ac1:42161) |
| MagicUSD0pp Claimer | [task](https://app.gelato.network/functions/task/0x746fd28a88217d74f693dd3ec738ce699a174b01a875bce3c023262020048d3e:1) |
| MagicUSD0pp Handler | [task](https://app.gelato.network/functions/task/0x0e8f9611aaf09ab1b9920064774a44c4e872a161235b0ea7a03ebfb1188b8fe6:1) |
17 changes: 10 additions & 7 deletions scripts/create-magicusd0pp-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import { DEVOPS_SAFE } from "../utils/constants";

const { ethers, w3f } = hre;

const THIRTY_SECONDS = 30 * 1000;

const main = async () => {
const magicUsd0pp = w3f.get("magicusd0pp");

Expand All @@ -21,16 +19,21 @@ const main = async () => {

const { tx } = await automate.prepareBatchExecTask(
{
name: "MagicUSD0pp Off-Chain Distribution Claimer",
name: "MagicUSD0pp Handler",
web3FunctionHash: cid,
trigger: {
type: TriggerType.TIME,
interval: THIRTY_SECONDS,
type: TriggerType.BLOCK,
},
web3FunctionArgs: {
execAddress: "0x75cC0C0DDD2Ccafe6EC415bE686267588011E36A",
usualApiEndpoint: "https://app.usual.money/api/rewards",
distributionAddress: "0x75cC0C0DDD2Ccafe6EC415bE686267588011E36A",
harvesterAddress: "0x80014629Ca75441599A1efd2283E3f71A8EC0AAB",
magicUsd0ppAddress: "0x73075fD1522893D9dC922991542f98F08F2c1C99",
usd0ppAddress: "0x35D8949372D46B7a3D5A56006AE77B215fc69bC0",
usualAddress: "0xC4441c2BE5d8fA8126822B9929CA0b81Ea0DE38E",
usualApiEndpoint: "https://app.usual.money/api",
odosApiEndpoint: "https://api.odos.xyz",
slippageLimitBps: 15,
minimumSwapUsd: 1000,
},
},
{},
Expand Down
80 changes: 80 additions & 0 deletions utils/odos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import ky from "ky";
import * as R from "remeda";
import type { Address, Hex } from "viem";

export type OdosQuoteParameters = {
endpoint: string;
chainId: number;
inputTokens: Array<{ tokenAddress: Address; amount: bigint }>;
outputTokens: Array<{ tokenAddress: Address; proportion: number }>;
userAddr: Address;
slippageLimitPercent?: number;
disableRFQs?: boolean;
compact?: boolean;
simple?: boolean;
};

export async function odosQuote(quoteParameters: OdosQuoteParameters) {
const odosApi = ky.extend({
prefixUrl: quoteParameters.endpoint,
});

const { pathId } = await odosApi
.post("sor/quote/v2", {
json: {
...R.omit(quoteParameters, ["endpoint"]),
inputTokens: quoteParameters.inputTokens.map(
({ tokenAddress, amount }) => ({
tokenAddress,
amount: amount.toString(),
}),
),
},
})
.json<{ pathId: string }>();

const assembledQuote = await odosApi
.post("sor/assemble", {
json: {
pathId,
userAddr: quoteParameters.userAddr,
},
})
.json<{
blockNumber: number;
gasEstimate: number;
gasEstimateValue: number;
inputTokens: Array<{ tokenAddress: Address; amount: `${bigint}` }>;
outputTokens: Array<{ tokenAddress: Address; amount: `${bigint}` }>;
netOutValue: number;
outValues: number[];
transaction: {
gas: number;
gasPrice: number;
value: `${bigint}`;
to: Address;
from: Address;
data: Hex;
nonce: number;
chainId: number;
};
}>();

return {
...assembledQuote,
inputTokens: assembledQuote.inputTokens.map(({ tokenAddress, amount }) => ({
tokenAddress,
amount: BigInt(amount),
})),
outputTokens: assembledQuote.outputTokens.map(
({ tokenAddress, amount }) => ({
tokenAddress,
amount: BigInt(amount),
}),
),
transaction: {
...assembledQuote.transaction,
value: BigInt(assembledQuote.transaction.value),
},
};
}
194 changes: 153 additions & 41 deletions web3-functions/magicusd0pp/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
import {
Web3Function,
type Web3FunctionContext,
Web3FunctionResult,
type Web3FunctionResultCallData,
} from "@gelatonetwork/web3-functions-sdk";
import ky from "ky";
import { type Address, type Hex, encodeFunctionData, parseAbi } from "viem";
import {
type Address,
type Hex,
encodeFunctionData,
erc20Abi,
parseAbi,
parseEther,
} from "viem";
import { odosQuote } from "../../utils/odos";
import { createJsonRpcPublicClient } from "../../utils/viem";

const DISTRIBUTION_ABI = parseAbi([
"function getOffChainDistributionData() external view returns (uint256 timestamp, bytes32 merkleRoot)",
"function getOffChainTokensClaimed(address account) external view returns (uint256)",
"struct QueuedOffChainDistribution { uint256 timestamp; bytes32 merkleRoot; }",
"function getOffChainDistributionQueue() external view returns (QueuedOffChainDistribution[] memory)",
"function claimOffChainDistribution(address account, uint256 amount, bytes32[] calldata proof) external",
"function approveUnchallengedOffChainDistribution() external",
]);

const HARVESTER_ABI = parseAbi([
"function run(address router, bytes memory swapData, uint256 minAmountOut) external",
]);

const USUAL_DISTRIBUTION_CHALLENGE_PERIOD = 604800n; // 1 week
const WAD = 10n ** 18n;

type MagicUsd0ppUserArgs = {
execAddress: Address;
usualApiEndpoint: string;
distributionAddress: Address;
harvesterAddress: Address;
magicUsd0ppAddress: Address;
usd0ppAddress: Address;
usualAddress: Address;
usualApiEndpoint: string;
odosApiEndpoint: string;
slippageLimitBps: number;
minimumSwapUsd: number;
};

type OffChainDistribution = {
Expand All @@ -29,65 +53,153 @@ type OffChainDistribution = {

Web3Function.onRun(
async ({ userArgs, multiChainProvider }: Web3FunctionContext) => {
const { execAddress, usualApiEndpoint, magicUsd0ppAddress } =
userArgs as MagicUsd0ppUserArgs;
const {
distributionAddress,
harvesterAddress,
magicUsd0ppAddress,
usd0ppAddress,
usualAddress,
usualApiEndpoint,
odosApiEndpoint,
slippageLimitBps,
minimumSwapUsd,
} = userArgs as MagicUsd0ppUserArgs;

const client = createJsonRpcPublicClient(multiChainProvider.default());
const usualRewardsApi = ky.extend({
prefixUrl: new URL("rewards", new URL(usualApiEndpoint)),
prefixUrl: `${usualApiEndpoint}/rewards`,
});

const [[, currentMerkleRoot], claimedTokens, distributions] =
await Promise.all([
client.readContract({
abi: DISTRIBUTION_ABI,
address: execAddress,
functionName: "getOffChainDistributionData",
}),
client.readContract({
abi: DISTRIBUTION_ABI,
address: execAddress,
functionName: "getOffChainTokensClaimed",
args: [magicUsd0ppAddress],
}),
usualRewardsApi
.get(magicUsd0ppAddress)
.json<Array<OffChainDistribution>>(),
]);
const [
[, currentMerkleRoot],
offChainDistributionQueue,
claimedTokens,
usualBalance,
distributions,
] = await Promise.all([
client.readContract({
abi: DISTRIBUTION_ABI,
address: distributionAddress,
functionName: "getOffChainDistributionData",
}),
client.readContract({
abi: DISTRIBUTION_ABI,
address: distributionAddress,
functionName: "getOffChainDistributionQueue",
}),
client.readContract({
abi: DISTRIBUTION_ABI,
address: distributionAddress,
functionName: "getOffChainTokensClaimed",
args: [magicUsd0ppAddress],
}),
client.readContract({
abi: erc20Abi,
address: usualAddress,
functionName: "balanceOf",
args: [magicUsd0ppAddress],
}),
usualRewardsApi
.get(magicUsd0ppAddress)
.json<Array<OffChainDistribution>>(),
]);

if (distributions.length === 0) {
return { canExec: false, message: "No distributions" };
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
let pendingDistribution:
| (typeof offChainDistributionQueue)[number]
| undefined = undefined;
for (const distribution of offChainDistributionQueue) {
if (
currentTimestamp >=
distribution.timestamp + USUAL_DISTRIBUTION_CHALLENGE_PERIOD &&
distribution.timestamp > (pendingDistribution?.timestamp ?? 0n)
) {
pendingDistribution = distribution;
}
}

const distributionMerkleRoot =
pendingDistribution?.merkleRoot ?? currentMerkleRoot;
const currentDistribution = distributions.find(
({ merkleRoot }) =>
merkleRoot.toLowerCase() === currentMerkleRoot.toLowerCase(),
merkleRoot.toLowerCase() === distributionMerkleRoot.toLowerCase(),
);

if (currentDistribution === undefined) {
return { canExec: false, message: "No matching distribution" };
}
let newUsualBalance = usualBalance;
const callData: Web3FunctionResultCallData[] = [];

if (claimedTokens === BigInt(currentDistribution.value)) {
return { canExec: false, message: "Already claimed latest distribution" };
}
if (currentDistribution !== undefined) {
const currentDistributionValue = BigInt(currentDistribution.value);

return {
canExec: true,
callData: [
{
to: execAddress,
if (claimedTokens < currentDistributionValue) {
newUsualBalance += currentDistributionValue - claimedTokens;

if (pendingDistribution !== undefined) {
callData.push({
to: distributionAddress,
data: encodeFunctionData({
abi: DISTRIBUTION_ABI,
functionName: "approveUnchallengedOffChainDistribution",
}),
});
}

callData.push({
to: distributionAddress,
data: encodeFunctionData({
abi: DISTRIBUTION_ABI,
functionName: "claimOffChainDistribution",
args: [
magicUsd0ppAddress,
BigInt(currentDistribution.value),
currentDistributionValue,
currentDistribution.merkleProof,
],
}),
},
],
});
}
}

if (newUsualBalance > 0n) {
const quote = await odosQuote({
endpoint: odosApiEndpoint,
chainId: multiChainProvider.default().network.chainId,
inputTokens: [{ tokenAddress: usualAddress, amount: newUsualBalance }],
outputTokens: [{ tokenAddress: usd0ppAddress, proportion: 1 }],
userAddr: harvesterAddress,
slippageLimitPercent: slippageLimitBps / 100,
disableRFQs: true,
});

if (quote.netOutValue < minimumSwapUsd) {
return {
canExec: false,
message: "Insufficient swap output",
};
}

callData.push({
to: harvesterAddress,
data: encodeFunctionData({
abi: HARVESTER_ABI,
functionName: "run",
args: [
quote.transaction.to,
quote.transaction.data,
(quote.outputTokens[0].amount *
parseEther(`${(10_000 - slippageLimitBps) / 10_000}`)) /
WAD,
],
}),
});
}

if (callData.length === 0) {
return { canExec: false, message: "No action needed" };
}

return {
canExec: true,
callData,
};
},
);
10 changes: 8 additions & 2 deletions web3-functions/magicusd0pp/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
"memory": 128,
"timeout": 30,
"userArgs": {
"execAddress": "string",
"distributionAddress": "string",
"harvesterAddress": "string",
"magicUsd0ppAddress": "string",
"usd0ppAddress": "string",
"usualAddress": "string",
"usualApiEndpoint": "string",
"magicUsd0ppAddress": "string"
"odosApiEndpoint": "string",
"slippageLimitBps": "number",
"minimumSwapUsd": "number"
}
}
12 changes: 9 additions & 3 deletions web3-functions/magicusd0pp/userArgs.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"execAddress": "0x75cC0C0DDD2Ccafe6EC415bE686267588011E36A",
"usualApiEndpoint": "https://app.usual.money/api/rewards",
"magicUsd0ppAddress": "0x73075fD1522893D9dC922991542f98F08F2c1C99"
"distributionAddress": "0x75cC0C0DDD2Ccafe6EC415bE686267588011E36A",
"harvesterAddress": "0x80014629Ca75441599A1efd2283E3f71A8EC0AAB",
"magicUsd0ppAddress": "0x73075fD1522893D9dC922991542f98F08F2c1C99",
"usd0ppAddress": "0x35D8949372D46B7a3D5A56006AE77B215fc69bC0",
"usualAddress": "0xC4441c2BE5d8fA8126822B9929CA0b81Ea0DE38E",
"usualApiEndpoint": "https://app.usual.money/api",
"odosApiEndpoint": "https://api.odos.xyz",
"slippageLimitBps": 15,
"minimumSwapUsd": 1000
}

0 comments on commit 6b26de8

Please sign in to comment.