Skip to content

Commit

Permalink
feat(magicusd0pp): Harvest USUAL rewards
Browse files Browse the repository at this point in the history
  • Loading branch information
0xmDreamy committed Dec 28, 2024
1 parent b269463 commit 2688134
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 47 deletions.
11 changes: 8 additions & 3 deletions scripts/create-magicusd0pp-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,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,
},
web3FunctionArgs: {
execAddress: "0x75cC0C0DDD2Ccafe6EC415bE686267588011E36A",
usualApiEndpoint: "https://app.usual.money/api/rewards",
distributionAddress: "0x75cC0C0DDD2Ccafe6EC415bE686267588011E36A",
harvesterAddress: "0x0F9af7168CC8819ce3066867509a7F9170fb108B",
magicUsd0ppAddress: "0x73075fD1522893D9dC922991542f98F08F2c1C99",
usd0ppAddress: "0x35D8949372D46B7a3D5A56006AE77B215fc69bC0",
usualAddress: "0xC4441c2BE5d8fA8126822B9929CA0b81Ea0DE38E",
usualApiEndpoint: "https://app.usual.money/api",
odosApiEndpoint: "https://api.odos.xyz",
slippageLimitPercent: 0.1,
},
},
{},
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),
},
};
}
182 changes: 143 additions & 39 deletions web3-functions/magicusd0pp/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
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

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

type OffChainDistribution = {
Expand All @@ -29,65 +51,147 @@ type OffChainDistribution = {

Web3Function.onRun(
async ({ userArgs, multiChainProvider }: Web3FunctionContext) => {
const { execAddress, usualApiEndpoint, magicUsd0ppAddress } =
userArgs as MagicUsd0ppUserArgs;
const {
distributionAddress,
harvesterAddress,
magicUsd0ppAddress,
usd0ppAddress,
usualAddress,
usualApiEndpoint,
odosApiEndpoint,
slippageLimitPercent,
} = 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,
disableRFQs: true,
});
callData.push({
to: harvesterAddress,
data: encodeFunctionData({
abi: HARVESTER_ABI,
functionName: "run",
args: [
quote.transaction.to,
quote.transaction.data,
(quote.outputTokens[0].amount * parseEther("1")) /
parseEther(`${(100 - slippageLimitPercent) / 100}`),
],
}),
});
}

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

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

0 comments on commit 2688134

Please sign in to comment.