Skip to content

Commit add9262

Browse files
chore: propagate refactor from refactor_rename_rewardsaggregator_-_sendearnrewards_bubble_erc4626_hooks_factory-gated_join_flow (886f744)
Why: Apply the same non-artifacts changes from top-of-stack to this branch: - Rename RewardsAggregator -> SendEarnRewards - Bubble ERC4626 hooks; factory-gated vault routing - Ignition module rename; minimal tests/mocks; docs labels Test plan: - npx hardhat compile - npx hardhat test --grep "SendEarnRewards"
1 parent 497858c commit add9262

File tree

2 files changed

+442
-0
lines changed

2 files changed

+442
-0
lines changed

test/rewards.manager.test.ts

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { expect } from "chai";
2+
import hre from "hardhat";
3+
import { getConfig } from "../config/superfluid";
4+
import { getContract } from "viem";
5+
import fs from "fs/promises";
6+
import path from "node:path";
7+
8+
async function readJson(file: string): Promise<any | null> {
9+
try { return JSON.parse(await fs.readFile(file, "utf8")); } catch { return null; }
10+
}
11+
12+
async function resolveSendx(chainId: number): Promise<`0x${string}` | null> {
13+
const publicClient = await hre.viem.getPublicClient();
14+
const cfg = getConfig(chainId);
15+
// deployments cache first
16+
const wrapperFile = path.resolve(__dirname, "..", "deployments", `wrapper.${chainId}.json`);
17+
const existing = await readJson(wrapperFile);
18+
if (existing?.address && existing.address !== "") return existing.address as `0x${string}`;
19+
// canonical lookup
20+
const SuperTokenFactoryJson = await import("@superfluid-finance/ethereum-contracts/build/truffle/SuperTokenFactory.json");
21+
const factory = getContract({
22+
address: cfg.superTokenFactory,
23+
abi: (SuperTokenFactoryJson as any).default?.abi ?? (SuperTokenFactoryJson as any).abi,
24+
client: { public: (await hre.viem.getPublicClient()) },
25+
});
26+
try {
27+
const canonical = (await factory.read.getCanonicalERC20Wrapper([cfg.sendV1])) as `0x${string}`;
28+
if (canonical && canonical !== "0x0000000000000000000000000000000000000000") return canonical;
29+
} catch {}
30+
return null;
31+
}
32+
33+
async function resolveShareToken(chainId: number): Promise<`0x${string}` | null> {
34+
// Prefer VAULT_ADDRESS (new name), fallback to SHARE_TOKEN_ADDRESS for backward compatibility
35+
if (process.env.VAULT_ADDRESS) return process.env.VAULT_ADDRESS as `0x${string}`;
36+
if (process.env.SHARE_TOKEN_ADDRESS) return process.env.SHARE_TOKEN_ADDRESS as `0x${string}`;
37+
const broadcastRoot =
38+
process.env.SEND_EARN_BROADCAST_DIR ??
39+
path.resolve(__dirname, "..", "..", "send-earn-contracts", "broadcast");
40+
const broadcastFile = path.resolve(broadcastRoot, `DeploySendEarn.s.sol/${chainId}/run-latest.json`);
41+
const runLatest = await readJson(broadcastFile);
42+
if (runLatest?.transactions && Array.isArray(runLatest.transactions)) {
43+
const tx = (runLatest.transactions as any[]).find(
44+
(t) => (t.contractName === "SendEarn" || t.contractName === "ERC4626" || t.contractName === "SendEarnFactory#SendEarn") &&
45+
(t.transactionType === "CREATE" || t.transactionType === "CREATE2") &&
46+
typeof t.contractAddress === "string" && t.contractAddress.startsWith("0x")
47+
);
48+
if (tx?.contractAddress) return tx.contractAddress as `0x${string}`;
49+
}
50+
return null;
51+
}
52+
53+
async function resolveFactory(chainId: number): Promise<`0x${string}` | null> {
54+
if (process.env.SEND_EARN_FACTORY) return process.env.SEND_EARN_FACTORY as `0x${string}`;
55+
const broadcastRoot =
56+
process.env.SEND_EARN_BROADCAST_DIR ??
57+
path.resolve(__dirname, "..", "..", "send-earn-contracts", "broadcast");
58+
const broadcastFile = path.resolve(broadcastRoot, `DeploySendEarn.s.sol/${chainId}/run-latest.json`);
59+
const runLatest = await readJson(broadcastFile);
60+
if (runLatest?.transactions && Array.isArray(runLatest.transactions)) {
61+
const tx = (runLatest.transactions as any[]).find(
62+
(t) => (t.contractName === "SendEarnFactory" || t.contractName === "SendEarnFactory#SendEarnFactory") &&
63+
(t.transactionType === "CREATE" || t.transactionType === "CREATE2") &&
64+
typeof t.contractAddress === "string" && t.contractAddress.startsWith("0x")
65+
);
66+
if (tx?.contractAddress) return tx.contractAddress as `0x${string}`;
67+
}
68+
return null;
69+
}
70+
71+
describe("RewardsManager (Base fork)", () => {
72+
it("deploys and can call syncVault (env-gated)", async function () {
73+
const publicClient = await hre.viem.getPublicClient();
74+
const [walletClient] = await hre.viem.getWalletClients();
75+
if (!walletClient) this.skip();
76+
77+
const chainId = await publicClient.getChainId();
78+
const cfg = getConfig(chainId);
79+
const sendV1 = cfg.sendV1 as `0x${string}`;
80+
const superTokenFactory = cfg.superTokenFactory as `0x${string}`;
81+
const shareToken = await resolveShareToken(chainId);
82+
const factory = await resolveFactory(chainId);
83+
const holder = process.env.SHARE_HOLDER as `0x${string}` | undefined;
84+
let assetAddr = process.env.ASSET_ADDRESS as `0x${string}` | undefined;
85+
86+
if (!shareToken || !holder || !factory) {
87+
this.skip();
88+
}
89+
90+
// If asset address is not provided, try to resolve from the vault via IERC4626.asset()
91+
if (!assetAddr) {
92+
const ierc4626Abi = [
93+
{ type: "function", name: "asset", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "address" }] },
94+
] as const;
95+
const vault = getContract({ address: shareToken!, abi: ierc4626Abi as any, client: { public: publicClient } });
96+
try {
97+
const a = (await vault.read.asset([])) as unknown as `0x${string}`;
98+
assetAddr = a;
99+
} catch {
100+
this.skip();
101+
}
102+
}
103+
104+
// Derive decimals for minAssets calculation (5 tokens in underlying units)
105+
const erc20MetaAbi = [
106+
{ type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "uint8" }] },
107+
] as const;
108+
const assetMeta = getContract({ address: assetAddr!, abi: erc20MetaAbi as any, client: { public: publicClient } });
109+
const dec = Number(await assetMeta.read.decimals([]));
110+
const minAssets = 5n * (10n ** BigInt(dec));
111+
112+
// Deploy RewardsManager using compiled artifact
113+
const artifactPath = path.resolve(
114+
__dirname,
115+
"..",
116+
"artifacts",
117+
"contracts",
118+
"rewards",
119+
"RewardsManager.sol",
120+
"RewardsManager.json"
121+
);
122+
const artifact = await readJson(artifactPath);
123+
const bytecode = (artifact?.bytecode?.object ?? artifact?.bytecode) as `0x${string}` | undefined;
124+
if (!artifact?.abi || !bytecode) this.skip();
125+
126+
const abi = artifact.abi as any[];
127+
128+
const hash = await walletClient.deployContract({
129+
abi,
130+
bytecode,
131+
args: [sendV1, superTokenFactory, factory!, assetAddr!, walletClient.account!.address, minAssets],
132+
account: walletClient.account!,
133+
});
134+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
135+
const manager = receipt.contractAddress as `0x${string}` | null;
136+
expect(manager, "deployed").to.be.a("string");
137+
138+
// Call syncVault(shareToken, holder) using operator path; grant role first
139+
const rewards = getContract({ address: manager!, abi, client: { public: publicClient, wallet: walletClient } });
140+
try {
141+
const opRole = (await rewards.read.SYNC_OPERATOR_ROLE([])) as `0x${string}`;
142+
const { request: grantOp } = await rewards.simulate.grantRole([opRole, walletClient.account!.address], { account: walletClient.account as any });
143+
await walletClient.writeContract(grantOp);
144+
145+
const { request } = await rewards.simulate.syncVault([shareToken!, holder!], { account: walletClient.account as any });
146+
const txHash = await walletClient.writeContract(request);
147+
const r2 = await publicClient.waitForTransactionReceipt({ hash: txHash });
148+
expect(r2.status).to.equal("success");
149+
} catch {
150+
this.skip();
151+
}
152+
});
153+
154+
it("deploys and can call batchSyncVaults for multiple vaults (env-gated)", async function () {
155+
const publicClient = await hre.viem.getPublicClient();
156+
const [walletClient] = await hre.viem.getWalletClients();
157+
if (!walletClient) this.skip();
158+
159+
const chainId = await publicClient.getChainId();
160+
const cfg = getConfig(chainId);
161+
const sendV1 = cfg.sendV1 as `0x${string}`;
162+
const superTokenFactory = cfg.superTokenFactory as `0x${string}`;
163+
const holdersCsv = process.env.VAULT_HOLDERS || process.env.SHARE_HOLDER;
164+
const vaultsCsv = process.env.VAULT_ADDRESSES || process.env.SHARE_TOKEN_ADDRESSES;
165+
166+
if (!vaultsCsv || !holdersCsv) this.skip();
167+
168+
const vaults = vaultsCsv.split(",").map((s) => s.trim()).filter(Boolean) as `0x${string}`[];
169+
const holders = holdersCsv.split(",").map((s) => s.trim()).filter(Boolean) as `0x${string}`[];
170+
const who = holders[0] as `0x${string}`; // use first holder for aggregation target
171+
172+
const factory = await resolveFactory(chainId);
173+
174+
// Derive asset from first vault if not provided
175+
let assetAddr = process.env.ASSET_ADDRESS as `0x${string}` | undefined;
176+
if (!assetAddr) {
177+
const ierc4626Abi = [
178+
{ type: "function", name: "asset", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "address" }] },
179+
] as const;
180+
const vault = getContract({ address: vaults[0]!, abi: ierc4626Abi as any, client: { public: publicClient } });
181+
try {
182+
const a = (await vault.read.asset([])) as unknown as `0x${string}`;
183+
assetAddr = a;
184+
} catch {
185+
this.skip();
186+
}
187+
}
188+
189+
if (!factory) this.skip();
190+
191+
// Derive decimals for minAssets calculation (5 tokens in underlying units)
192+
const erc20MetaAbi = [
193+
{ type: "function", name: "decimals", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "uint8" }] },
194+
] as const;
195+
const assetMeta = getContract({ address: assetAddr!, abi: erc20MetaAbi as any, client: { public: publicClient } });
196+
const dec = Number(await assetMeta.read.decimals([]));
197+
const minAssets = 5n * (10n ** BigInt(dec));
198+
199+
// Deploy RewardsManager
200+
const artifactPath = path.resolve(
201+
__dirname,
202+
"..",
203+
"artifacts",
204+
"contracts",
205+
"rewards",
206+
"RewardsManager.sol",
207+
"RewardsManager.json"
208+
);
209+
const artifact = await readJson(artifactPath);
210+
const bytecode = (artifact?.bytecode?.object ?? artifact?.bytecode) as `0x${string}` | undefined;
211+
if (!artifact?.abi || !bytecode) this.skip();
212+
213+
const abi = artifact.abi as any[];
214+
215+
const hash = await walletClient.deployContract({
216+
abi,
217+
bytecode,
218+
args: [sendV1, superTokenFactory, factory!, assetAddr!, walletClient.account!.address, minAssets],
219+
account: walletClient.account!,
220+
});
221+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
222+
const manager = receipt.contractAddress as `0x${string}` | null;
223+
expect(manager, "deployed").to.be.a("string");
224+
225+
// Grant operator role and call batchSyncVaults(vaults, who)
226+
const rewards = getContract({ address: manager!, abi, client: { public: publicClient, wallet: walletClient } });
227+
try {
228+
const opRole = (await rewards.read.SYNC_OPERATOR_ROLE([])) as `0x${string}`;
229+
const { request: grantOp } = await rewards.simulate.grantRole([opRole, walletClient.account!.address], { account: walletClient.account as any });
230+
await walletClient.writeContract(grantOp);
231+
232+
const { request } = await rewards.simulate.batchSyncVaults([vaults, who], { account: walletClient.account as any });
233+
const txHash = await walletClient.writeContract(request);
234+
const r2 = await publicClient.waitForTransactionReceipt({ hash: txHash });
235+
expect(r2.status).to.equal("success");
236+
} catch {
237+
this.skip();
238+
}
239+
});
240+
});

0 commit comments

Comments
 (0)