Skip to content

Commit f81d703

Browse files
Align viem calls and env discovery in tests
Why: Reduce flakiness and ensure tests resolve env/config in a consistent way. Test plan: - bunx hardhat test test/rewards.manager.test.ts - bunx hardhat test test/wrapper.ts
1 parent f98498d commit f81d703

File tree

2 files changed

+201
-41
lines changed

2 files changed

+201
-41
lines changed

test/rewards.manager.test.ts

Lines changed: 157 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ async function resolveSendx(chainId: number): Promise<`0x${string}` | null> {
2121
const factory = getContract({
2222
address: cfg.superTokenFactory,
2323
abi: (SuperTokenFactoryJson as any).default?.abi ?? (SuperTokenFactoryJson as any).abi,
24-
client: { public: publicClient },
24+
client: { public: (await hre.viem.getPublicClient()) },
2525
});
2626
try {
2727
const canonical = (await factory.read.getCanonicalERC20Wrapper([cfg.sendV1])) as `0x${string}`;
@@ -31,8 +31,13 @@ async function resolveSendx(chainId: number): Promise<`0x${string}` | null> {
3131
}
3232

3333
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}`;
3436
if (process.env.SHARE_TOKEN_ADDRESS) return process.env.SHARE_TOKEN_ADDRESS as `0x${string}`;
35-
const broadcastFile = `/Users/vict0xr/Documents/Send/send-earn-contracts/broadcast/DeploySendEarn.s.sol/${chainId}/run-latest.json`;
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`);
3641
const runLatest = await readJson(broadcastFile);
3742
if (runLatest?.transactions && Array.isArray(runLatest.transactions)) {
3843
const tx = (runLatest.transactions as any[]).find(
@@ -45,22 +50,65 @@ async function resolveShareToken(chainId: number): Promise<`0x${string}` | null>
4550
return null;
4651
}
4752

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+
4871
describe("RewardsManager (Base fork)", () => {
49-
it("deploys and can call sync (env-gated)", async function () {
72+
it("deploys and can call syncVault (env-gated)", async function () {
5073
const publicClient = await hre.viem.getPublicClient();
5174
const [walletClient] = await hre.viem.getWalletClients();
5275
if (!walletClient) this.skip();
5376

5477
const chainId = await publicClient.getChainId();
55-
const sendx = await resolveSendx(chainId);
78+
const cfg = getConfig(chainId);
79+
const sendV1 = cfg.sendV1 as `0x${string}`;
80+
const superTokenFactory = cfg.superTokenFactory as `0x${string}`;
5681
const shareToken = await resolveShareToken(chainId);
57-
const poolAddr = process.env.REWARDS_POOL_ADDRESS as `0x${string}` | undefined;
82+
const factory = await resolveFactory(chainId);
5883
const holder = process.env.SHARE_HOLDER as `0x${string}` | undefined;
84+
let assetAddr = process.env.ASSET_ADDRESS as `0x${string}` | undefined;
5985

60-
if (!sendx || !shareToken || !poolAddr || !holder) {
86+
if (!shareToken || !holder || !factory) {
6187
this.skip();
6288
}
6389

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+
64112
// Deploy RewardsManager using compiled artifact
65113
const artifactPath = path.resolve(
66114
__dirname,
@@ -72,26 +120,121 @@ describe("RewardsManager (Base fork)", () => {
72120
"RewardsManager.json"
73121
);
74122
const artifact = await readJson(artifactPath);
75-
if (!artifact?.abi || !artifact?.bytecode?.object) this.skip();
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();
76212

77213
const abi = artifact.abi as any[];
78-
const bytecode = artifact.bytecode.object as `0x${string}`;
79214

80215
const hash = await walletClient.deployContract({
81216
abi,
82217
bytecode,
83-
args: [sendx, shareToken, poolAddr, walletClient.account!.address],
218+
args: [sendV1, superTokenFactory, factory!, assetAddr!, walletClient.account!.address, minAssets],
84219
account: walletClient.account!,
85220
});
86221
const receipt = await publicClient.waitForTransactionReceipt({ hash });
87222
const manager = receipt.contractAddress as `0x${string}` | null;
88223
expect(manager, "deployed").to.be.a("string");
89224

90-
// Call sync(holder); if pool accepts the call, tx should succeed
225+
// Grant operator role and call batchSyncVaults(vaults, who)
91226
const rewards = getContract({ address: manager!, abi, client: { public: publicClient, wallet: walletClient } });
92-
const { request } = await rewards.simulate.sync([holder], { account: walletClient.account! });
93-
const txHash = await walletClient.writeContract(request);
94-
const r2 = await publicClient.waitForTransactionReceipt({ hash: txHash });
95-
expect(r2.status).to.equal("success");
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+
}
96239
});
97240
});

test/wrapper.ts

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
} from "@nomicfoundation/hardhat-toolbox-viem/network-helpers";
99

1010
// Official ABIs from Superfluid package (Rule 3: mirror official examples)
11-
import SuperTokenFactoryJson from "@superfluid-finance/ethereum-contracts/build/truffle/SuperTokenFactory.json" assert { type: "json" };
12-
import ISuperTokenJson from "@superfluid-finance/ethereum-contracts/build/truffle/ISuperToken.json" assert { type: "json" };
13-
import IERC20Json from "@superfluid-finance/ethereum-contracts/build/truffle/IERC20.json" assert { type: "json" };
11+
import SuperTokenFactoryJson from "@superfluid-finance/ethereum-contracts/build/truffle/SuperTokenFactory.json";
12+
import ISuperTokenJson from "@superfluid-finance/ethereum-contracts/build/truffle/ISuperToken.json";
13+
import IERC20Json from "@superfluid-finance/ethereum-contracts/build/truffle/IERC20.json";
1414

1515
async function getWrapperAddress(): Promise<`0x${string}` | null> {
1616
const publicClient = await hre.viem.getPublicClient();
@@ -40,7 +40,7 @@ async function getWrapperAddress(): Promise<`0x${string}` | null> {
4040
try {
4141
const canonical = (await factory.read.getCanonicalERC20Wrapper([
4242
cfg.sendV1,
43-
])) as `0x${string}`;
43+
])) as unknown as `0x${string}`;
4444
if (canonical && canonical !== zeroAddress) return canonical;
4545
} catch {}
4646

@@ -54,11 +54,11 @@ async function getWrapperAddress(): Promise<`0x${string}` | null> {
5454
const upgradability = 1; // SEMI_UPGRADABLE
5555
const { request, result } = await factoryWrite.simulate.createERC20Wrapper(
5656
[cfg.sendV1, cfg.underlyingDecimals, upgradability, cfg.wrapperName, cfg.wrapperSymbol],
57-
{ account: walletClient.account! }
57+
{ account: walletClient.account as any }
5858
);
5959
const hash = await walletClient.writeContract(request);
6060
await publicClient.waitForTransactionReceipt({ hash });
61-
return result as `0x${string}`;
61+
return result as unknown as `0x${string}`;
6262
}
6363

6464
return null;
@@ -70,7 +70,7 @@ async function isValidWrapper(addr: `0x${string}`): Promise<boolean> {
7070
const cfg = getConfig(chainId);
7171
const superToken = getContract({ address: addr, abi: ISuperTokenJson.abi as any[], client: { public: publicClient } });
7272
try {
73-
const underlying = (await superToken.read.getUnderlyingToken()) as `0x${string}`;
73+
const underlying = (await superToken.read.getUnderlyingToken([])) as unknown as `0x${string}`;
7474
return underlying.toLowerCase() === cfg.sendV1.toLowerCase();
7575
} catch { return false; }
7676
}
@@ -90,16 +90,18 @@ describe("SuperToken wrapper (backend-only)", () => {
9090

9191
const superToken = getContract({ address: addr!, abi: ISuperTokenJson.abi as any[], client: { public: publicClient } });
9292
const [name, symbol, decimals, underlying] = await Promise.all([
93-
superToken.read.name(),
94-
superToken.read.symbol(),
95-
superToken.read.decimals(),
96-
superToken.read.getUnderlyingToken(),
93+
superToken.read.name([]),
94+
superToken.read.symbol([]),
95+
superToken.read.decimals([]),
96+
superToken.read.getUnderlyingToken([]),
9797
]);
9898

9999
expect(name).to.be.a("string");
100100
expect(symbol).to.be.a("string");
101-
expect(decimals).to.equal(18n);
102-
expect((underlying as string).toLowerCase()).to.equal(cfg.sendV1.toLowerCase());
101+
const decimalsNum = Number(decimals as any);
102+
expect(decimalsNum).to.equal(18);
103+
const underlyingAddr = underlying as unknown as `0x${string}`;
104+
expect(underlyingAddr.toLowerCase()).to.equal(cfg.sendV1.toLowerCase());
103105
});
104106

105107
it("upgrade and downgrade round-trip (gated by SEND_HOLDER)", async function () {
@@ -109,6 +111,9 @@ describe("SuperToken wrapper (backend-only)", () => {
109111
}
110112

111113
const publicClient = await hre.viem.getPublicClient();
114+
const [walletClient] = await hre.viem.getWalletClients();
115+
if (!walletClient) this.skip();
116+
112117
const chainId = await publicClient.getChainId();
113118
const cfg = getConfig(chainId);
114119

@@ -121,35 +126,41 @@ describe("SuperToken wrapper (backend-only)", () => {
121126
await impersonateAccount(holder!);
122127
await setBalance(holder!, 10n * 10n ** 18n);
123128

124-
const superToken = getContract({ address: addr!, abi: ISuperTokenJson.abi as any[], client: { public: publicClient } });
125-
const underlying = getContract({ address: cfg.sendV1, abi: IERC20Json.abi as any[], client: { public: publicClient } });
129+
const superToken = getContract({ address: addr!, abi: ISuperTokenJson.abi as any[], client: { public: publicClient, wallet: walletClient } });
130+
const underlying = getContract({ address: cfg.sendV1, abi: IERC20Json.abi as any[], client: { public: publicClient, wallet: walletClient } });
126131

127132
// amount = 1e18
128133
const amount = 10n ** 18n;
129134

130-
const [u0, s0] = await Promise.all([
131-
underlying.read.balanceOf([holder!]) as Promise<bigint>,
132-
superToken.read.balanceOf([holder!]) as Promise<bigint>,
135+
const [u0Raw, s0Raw] = await Promise.all([
136+
underlying.read.balanceOf([holder!]),
137+
superToken.read.balanceOf([holder!]),
133138
]);
139+
const u0 = u0Raw as unknown as bigint;
140+
const s0 = s0Raw as unknown as bigint;
134141

135142
// Approve wrapper and upgrade
136143
await underlying.write.approve([addr!, amount], { account: holder! });
137144
await superToken.write.upgrade([amount], { account: holder! });
138145

139-
const [u1, s1] = await Promise.all([
140-
underlying.read.balanceOf([holder!]) as Promise<bigint>,
141-
superToken.read.balanceOf([holder!]) as Promise<bigint>,
146+
const [u1Raw, s1Raw] = await Promise.all([
147+
underlying.read.balanceOf([holder!]),
148+
superToken.read.balanceOf([holder!]),
142149
]);
150+
const u1 = u1Raw as unknown as bigint;
151+
const s1 = s1Raw as unknown as bigint;
143152

144153
expect(u1).to.equal(u0 - amount);
145154
expect(s1).to.equal(s0 + amount);
146155

147156
// Downgrade back
148157
await superToken.write.downgrade([amount], { account: holder! });
149-
const [u2, s2] = await Promise.all([
150-
underlying.read.balanceOf([holder!]) as Promise<bigint>,
151-
superToken.read.balanceOf([holder!]) as Promise<bigint>,
158+
const [u2Raw, s2Raw] = await Promise.all([
159+
underlying.read.balanceOf([holder!]),
160+
superToken.read.balanceOf([holder!]),
152161
]);
162+
const u2 = u2Raw as unknown as bigint;
163+
const s2 = s2Raw as unknown as bigint;
153164

154165
expect(u2).to.equal(u0);
155166
expect(s2).to.equal(s0);
@@ -169,11 +180,17 @@ describe("SuperToken wrapper (backend-only)", () => {
169180

170181
// We only validate that contracts are callable; full flow creation is environment-sensitive and
171182
// should be exercised in dedicated integration runs.
172-
const host = getContract({ address: cfg.host, abi: (await import("@superfluid-finance/ethereum-contracts/build/truffle/ISuperfluid.json", { assert: { type: "json" } })).default.abi as any[], client: { public: publicClient } });
173-
const cfa = getContract({ address: cfg.cfaV1, abi: (await import("@superfluid-finance/ethereum-contracts/build/truffle/IConstantFlowAgreementV1.json", { assert: { type: "json" } })).default.abi as any[], client: { public: publicClient } });
183+
// Static imports for ABIs
184+
// eslint-disable-next-line @typescript-eslint/no-var-requires
185+
const ISuperfluidJson = await import("@superfluid-finance/ethereum-contracts/build/truffle/ISuperfluid.json");
186+
// eslint-disable-next-line @typescript-eslint/no-var-requires
187+
const IConstantFlowAgreementV1Json = await import("@superfluid-finance/ethereum-contracts/build/truffle/IConstantFlowAgreementV1.json");
188+
189+
const host = getContract({ address: cfg.host, abi: (ISuperfluidJson as any).default.abi as any[], client: { public: publicClient } });
190+
const cfa = getContract({ address: cfg.cfaV1, abi: (IConstantFlowAgreementV1Json as any).default.abi as any[], client: { public: publicClient } });
174191

175192
// Basic read calls as smoke checks
176-
const hostAddress = await host.read.getCodeAddress().catch(() => cfg.host);
193+
const hostAddress = await host.read.getCodeAddress([]).catch(() => cfg.host);
177194
expect((hostAddress as string).toLowerCase()).to.be.a("string");
178195

179196
// Encode a getFlow call as a non-mutating check (won't throw)

0 commit comments

Comments
 (0)