|
| 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 | +describe("RewardsAggregator (CFA flows)", function () { |
| 13 | + it("creates/updates CFA flow on deposit/withdraw (no env)", async function () { |
| 14 | + const publicClient = await hre.viem.getPublicClient(); |
| 15 | + const [walletClient] = await hre.viem.getWalletClients(); |
| 16 | + if (!walletClient) this.skip(); |
| 17 | + |
| 18 | + const chainId = await publicClient.getChainId(); |
| 19 | + const cfg = getConfig(chainId); |
| 20 | + |
| 21 | + // Load artifacts |
| 22 | + const artifactsRoot = path.resolve(__dirname, "..", "artifacts", "contracts"); |
| 23 | + const aggArtifact = await readJson(path.resolve(artifactsRoot, "rewards", "RewardsAggregator.sol", "RewardsAggregator.json")); |
| 24 | + const mockErc20Artifact = await readJson(path.resolve(artifactsRoot, "mocks", "MockERC20.sol", "MockERC20.json")); |
| 25 | + const mockVaultArtifact = await readJson(path.resolve(artifactsRoot, "mocks", "MockERC4626Vault.sol", "MockERC4626Vault.json")); |
| 26 | + const mockFactoryArtifact = await readJson(path.resolve(artifactsRoot, "mocks", "MockSendEarnFactory.sol", "MockSendEarnFactory.json")); |
| 27 | + if (!aggArtifact?.abi || !aggArtifact?.bytecode || !mockErc20Artifact?.abi || !mockVaultArtifact?.abi || !mockFactoryArtifact?.abi) this.skip(); |
| 28 | + |
| 29 | + // 1) Underlying asset (6 decimals) and SuperToken wrapper |
| 30 | + const erc20Abi = mockErc20Artifact.abi as any[]; |
| 31 | + const erc20Bytecode = (mockErc20Artifact.bytecode?.object ?? mockErc20Artifact.bytecode) as `0x${string}`; |
| 32 | + const hashUSDC = await walletClient.deployContract({ abi: erc20Abi, bytecode: erc20Bytecode, args: ["USDC", "USDC", 6], account: walletClient.account! }); |
| 33 | + const receiptUSDC = await publicClient.waitForTransactionReceipt({ hash: hashUSDC }); |
| 34 | + const usdc = receiptUSDC.contractAddress as `0x${string}` | null; |
| 35 | + if (!usdc) this.skip(); |
| 36 | + |
| 37 | + // Create wrapper via SuperTokenFactory |
| 38 | + // eslint-disable-next-line @typescript-eslint/no-var-requires |
| 39 | + const SuperTokenFactoryJson = await import("@superfluid-finance/ethereum-contracts/build/truffle/SuperTokenFactory.json"); |
| 40 | + const factorySF = getContract({ address: cfg.superTokenFactory, abi: (SuperTokenFactoryJson as any).default.abi as any[], client: { public: publicClient, wallet: walletClient } }); |
| 41 | + const { request: createReq, result: sendxRes } = await factorySF.simulate.createERC20Wrapper([usdc, 6, 1, cfg.wrapperName, cfg.wrapperSymbol], { account: walletClient.account! }); |
| 42 | + const txCreate = await walletClient.writeContract(createReq); |
| 43 | + await publicClient.waitForTransactionReceipt({ hash: txCreate }); |
| 44 | + const sendx = sendxRes as unknown as `0x${string}`; |
| 45 | + |
| 46 | + // 2) Deploy mock SendEarnFactory and a valid vault |
| 47 | + const mockFactoryAbi = mockFactoryArtifact.abi as any[]; |
| 48 | + const mockFactoryBytecode = (mockFactoryArtifact.bytecode?.object ?? mockFactoryArtifact.bytecode) as `0x${string}`; |
| 49 | + const hashF = await walletClient.deployContract({ abi: mockFactoryAbi, bytecode: mockFactoryBytecode, args: [], account: walletClient.account! }); |
| 50 | + const receiptF = await publicClient.waitForTransactionReceipt({ hash: hashF }); |
| 51 | + const factoryAddr = receiptF.contractAddress as `0x${string}`; |
| 52 | + const factoryC = getContract({ address: factoryAddr, abi: mockFactoryAbi, client: { public: publicClient, wallet: walletClient } }); |
| 53 | + |
| 54 | + const mockVaultAbi = mockVaultArtifact.abi as any[]; |
| 55 | + const mockVaultBytecode = (mockVaultArtifact.bytecode?.object ?? mockVaultArtifact.bytecode) as `0x${string}`; |
| 56 | + const hashV = await walletClient.deployContract({ abi: mockVaultAbi, bytecode: mockVaultBytecode, args: [usdc, "vUSDC", "vUSDC", 1, 1], account: walletClient.account! }); |
| 57 | + const receiptV = await publicClient.waitForTransactionReceipt({ hash: hashV }); |
| 58 | + const vault = receiptV.contractAddress as `0x${string}`; |
| 59 | + |
| 60 | + // Mark vault as SendEarn-approved |
| 61 | + const { request: setSE } = await factoryC.simulate.setIsSendEarn([vault, true], { account: walletClient.account! }); |
| 62 | + await walletClient.writeContract(setSE); |
| 63 | + |
| 64 | + // 3) Deploy RewardsAggregator |
| 65 | + const aggAbi = aggArtifact.abi as any[]; |
| 66 | + const aggBytecode = (aggArtifact.bytecode?.object ?? aggArtifact.bytecode) as `0x${string}`; |
| 67 | + const hashAgg = await walletClient.deployContract({ abi: aggAbi, bytecode: aggBytecode, args: [sendx, factoryAddr, usdc, walletClient.account!.address], account: walletClient.account! }); |
| 68 | + const receiptAgg = await publicClient.waitForTransactionReceipt({ hash: hashAgg }); |
| 69 | + const aggregator = receiptAgg.contractAddress as `0x${string}` | null; |
| 70 | + if (!aggregator) this.skip(); |
| 71 | + |
| 72 | + const agg = getContract({ address: aggregator!, abi: aggAbi, client: { public: publicClient, wallet: walletClient } }); |
| 73 | + |
| 74 | + // 4) Configure high per-second rate (secondsPerYear=1) and pre-fund aggregator with SENDx |
| 75 | + // eslint-disable-next-line @typescript-eslint/no-var-requires |
| 76 | + const ISuperTokenJson = await import("@superfluid-finance/ethereum-contracts/build/truffle/ISuperToken.json"); |
| 77 | + // eslint-disable-next-line @typescript-eslint/no-var-requires |
| 78 | + const IERC20Json = await import("@superfluid-finance/ethereum-contracts/build/truffle/IERC20.json"); |
| 79 | + |
| 80 | + await agg.write.setSecondsPerYear([1n], { account: walletClient.account! }); |
| 81 | + |
| 82 | + const underlying = getContract({ address: usdc!, abi: (IERC20Json as any).default.abi as any[], client: { public: publicClient, wallet: walletClient } }); |
| 83 | + // Mint underlying to wallet for upgrade funding and deposit |
| 84 | + const minterAbi = [{ type: "function", name: "mint", stateMutability: "nonpayable", inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], outputs: [] }] as const; |
| 85 | + const minter = getContract({ address: usdc!, abi: minterAbi as any, client: { public: publicClient, wallet: walletClient } }); |
| 86 | + |
| 87 | + const fundUnderlying = 10_000_000_000n; // 10,000 USDC (6 decimals) |
| 88 | + await minter.write.mint([walletClient.account!.address, fundUnderlying]); |
| 89 | + |
| 90 | + const superToken = getContract({ address: sendx, abi: (ISuperTokenJson as any).default.abi as any[], client: { public: publicClient, wallet: walletClient } }); |
| 91 | + await underlying.write.approve([sendx, fundUnderlying], { account: walletClient.account! }); |
| 92 | + await superToken.write.upgrade([fundUnderlying], { account: walletClient.account! }); |
| 93 | + |
| 94 | + // Transfer SENDx to aggregator so it can open flows |
| 95 | + const fundSendx = 5_000_000_000n; // 5,000 USDC worth in SENDx |
| 96 | + await superToken.write.transfer([aggregator!, fundSendx], { account: walletClient.account! }); |
| 97 | + |
| 98 | + // 5) Deposit underlying via aggregator: approve + depositAssets |
| 99 | + const depositAmount = 1_000_000n; // 1 USDC |
| 100 | + await underlying.write.approve([aggregator!, depositAmount], { account: walletClient.account! }); |
| 101 | + |
| 102 | + const { request: depReq } = await agg.simulate.depositAssets([vault, depositAmount], { account: walletClient.account! }); |
| 103 | + const depHash = await walletClient.writeContract(depReq); |
| 104 | + await publicClient.waitForTransactionReceipt({ hash: depHash }); |
| 105 | + |
| 106 | + // 6) Verify CFA flow exists and equals expected per-second rate |
| 107 | + // flowRate = floor(depositAmount * 0.03) since secondsPerYear=1 and exchangeRate=1 |
| 108 | + const expectedRate = (depositAmount * 3n) / 100n; |
| 109 | + |
| 110 | + // Read CFA getFlow |
| 111 | + // eslint-disable-next-line @typescript-eslint/no-var-requires |
| 112 | + const IConstantFlowAgreementV1Json = await import("@superfluid-finance/ethereum-contracts/build/truffle/IConstantFlowAgreementV1.json"); |
| 113 | + const cfa = getContract({ address: cfg.cfaV1, abi: (IConstantFlowAgreementV1Json as any).default.abi as any[], client: { public: publicClient } }); |
| 114 | + const flowInfo = await cfa.read.getFlow([sendx, aggregator!, walletClient.account!.address]) as any[]; |
| 115 | + const onchainRate = BigInt(flowInfo[3] ?? flowInfo[1] ?? 0); // ABI variants: result index differs across builds |
| 116 | + expect(onchainRate).to.eq(expectedRate); |
| 117 | + |
| 118 | + // 7) Withdraw everything; expect flow to be deleted (rate -> 0) |
| 119 | + const { request: wReq } = await agg.simulate.withdrawAssets([vault, depositAmount, walletClient.account!.address], { account: walletClient.account! }); |
| 120 | + const wHash = await walletClient.writeContract(wReq); |
| 121 | + await publicClient.waitForTransactionReceipt({ hash: wHash }); |
| 122 | + |
| 123 | + const flowAfter = await cfa.read.getFlow([sendx, aggregator!, walletClient.account!.address]) as any[]; |
| 124 | + const rateAfter = BigInt(flowAfter[3] ?? flowAfter[1] ?? 0); |
| 125 | + expect(rateAfter).to.eq(0n); |
| 126 | + |
| 127 | + // 8) Vault gating: invalid vault should revert |
| 128 | + const invalidVault = `0x${"b".repeat(40)}` as `0x${string}`; |
| 129 | + let reverted = false; |
| 130 | + try { |
| 131 | + await agg.simulate.depositAssets([invalidVault, 1n], { account: walletClient.account! }); |
| 132 | + } catch { reverted = true; } |
| 133 | + expect(reverted).to.eq(true); |
| 134 | + }); |
| 135 | +}); |
0 commit comments