// RWATransferAdapter tests. const { expect } = require("chai"); const { ethers } = require("hardhat"); const ONE6 = 10n ** 6n; const ONE18 = 10n ** 18n; async function deployStack(opts = {}) { const [deployer, foundation, alice, liquidator] = await ethers.getSigners(); const ERC20 = await ethers.getContractFactory("MockERC20Lending"); const buidl = await ERC20.deploy("BUIDL.e", "BUIDL.e", 18); await buidl.waitForDeployment(); const usdce = await ERC20.deploy("USDC.e", "USDC.e", 6); await usdce.waitForDeployment(); // Real oracle infra. const Agg = await ethers.getContractFactory("MockAggregator"); const aggBuidl = await Agg.deploy(1_00000000); // $1.00 await aggBuidl.waitForDeployment(); const aggUsdc = await Agg.deploy(1_00000000); await aggUsdc.waitForDeployment(); const Oracle = await ethers.getContractFactory("AereLendingOracle"); const oracle = await Oracle.deploy(); await oracle.waitForDeployment(); await oracle.configureFeed(await buidl.getAddress(), 0, await aggBuidl.getAddress(), ethers.ZeroHash, 86400, opts.devBps ?? 5000, 8); await oracle.configureFeed(await usdce.getAddress(), 0, await aggUsdc.getAddress(), ethers.ZeroHash, 86400, opts.devBps ?? 5000, 8); const Adapter = await ethers.getContractFactory("RWATransferAdapter"); const adapter = await Adapter.deploy(await usdce.getAddress(), await oracle.getAddress(), foundation.address, 6); await adapter.waitForDeployment(); return { deployer, foundation, alice, liquidator, buidl, usdce, oracle, adapter, aggBuidl }; } describe("RWATransferAdapter", function () { it("Foundation registers asset; rejects double-register + bad haircut", async function () { const env = await deployStack(); await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18 ); await expect(env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 100n, 86400, 18 )).to.be.revertedWithCustomError(env.adapter, "AlreadyRegistered"); // Bad haircut > 50%. await expect(env.adapter.connect(env.foundation).registerAsset( await env.usdce.getAddress(), 6000, 100n, 86400, 6 )).to.be.revertedWithCustomError(env.adapter, "BadHaircut"); }); it("non-Foundation cannot register", async function () { const env = await deployStack(); await expect(env.adapter.connect(env.alice).registerAsset( await env.buidl.getAddress(), 100, 100n, 86400, 18 )).to.be.revertedWithCustomError(env.adapter, "NotFoundation"); }); it("redeem: BUIDL.e → USDC.e at oracle price minus haircut", async function () { const env = await deployStack(); // Register BUIDL with 1% haircut, $100k daily cap. await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18 ); // Foundation seeds USDC.e treasury (would normally come from off-chain sale of swept RWA). await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6); await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6); // Liquidator has 100 BUIDL.e at $1 → $100 → 99 USDC.e after 1% haircut. await env.buidl.mint(env.liquidator.address, 100n * ONE18); await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18); const before = await env.usdce.balanceOf(env.liquidator.address); await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 100n * ONE18); const after = await env.usdce.balanceOf(env.liquidator.address); expect(after - before).to.equal(99n * ONE6); // Accumulated balance for sweep grows by 100 BUIDL.e. const s = await env.adapter.states(await env.buidl.getAddress()); expect(s.totalAccumulated).to.equal(100n * ONE18); }); it("rate limit enforced per period", async function () { const env = await deployStack(); await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 50n * ONE6, 86400, 18 // $50/day cap ); await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6); await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6); await env.buidl.mint(env.liquidator.address, 100n * ONE18); await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18); // 50 BUIDL.e ≈ $49.5 after haircut → under cap, ok. await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 50n * ONE18); // Next 5 BUIDL ≈ $4.95 → 49.5 + 4.95 = 54.45 > 50 → rate limit. await expect(env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 5n * ONE18)) .to.be.revertedWithCustomError(env.adapter, "RateLimitExceeded"); }); it("rate limit resets after period", async function () { const env = await deployStack(); await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 50n * ONE6, 60, 18 // $50 / 60s cap ); await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6); await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6); await env.buidl.mint(env.liquidator.address, 100n * ONE18); await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18); await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 50n * ONE18); // Advance time past period. await ethers.provider.send("evm_increaseTime", [120]); await ethers.provider.send("evm_mine", []); await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 50n * ONE18); }); it("paused asset rejects redeem; can resume", async function () { const env = await deployStack(); await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18 ); await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6); await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6); await env.buidl.mint(env.liquidator.address, 100n * ONE18); await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18); await env.adapter.connect(env.foundation).setAssetPaused(await env.buidl.getAddress(), true); await expect(env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 10n * ONE18)) .to.be.revertedWithCustomError(env.adapter, "AssetIsPaused"); await env.adapter.connect(env.foundation).setAssetPaused(await env.buidl.getAddress(), false); await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 10n * ONE18); }); it("sweepRWA: Foundation can pull accumulated RWA to off-chain wallet", async function () { const env = await deployStack(); await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18 ); await env.usdce.mint(env.foundation.address, 1_000_000n * ONE6); await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 1_000_000n * ONE6); await env.buidl.mint(env.liquidator.address, 100n * ONE18); await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 100n * ONE18); await env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 100n * ONE18); const [, , , , securitizeAllowlisted] = await ethers.getSigners(); await env.adapter.connect(env.foundation).sweepRWA(await env.buidl.getAddress(), 100n * ONE18, securitizeAllowlisted.address); expect(await env.buidl.balanceOf(securitizeAllowlisted.address)).to.equal(100n * ONE18); const s = await env.adapter.states(await env.buidl.getAddress()); expect(s.totalAccumulated).to.equal(0n); }); it("sweepRWA: cannot pull more than accumulated", async function () { const env = await deployStack(); await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18 ); await env.usdce.mint(env.foundation.address, 100_000n * ONE6); await env.usdce.connect(env.foundation).transfer(await env.adapter.getAddress(), 100_000n * ONE6); const [, , , , securitizeAllowlisted] = await ethers.getSigners(); await expect(env.adapter.connect(env.foundation).sweepRWA(await env.buidl.getAddress(), 1n, securitizeAllowlisted.address)) .to.be.revertedWithCustomError(env.adapter, "InsufficientSweepBalance"); }); it("Foundation cannot touch USDC.e treasury directly (no withdraw fn)", async function () { const env = await deployStack(); expect(env.adapter.interface.getFunction("withdraw")).to.equal(null); // sweepRWA on USDC.e itself reverts because it's not registered as an asset const [, , , , recipient] = await ethers.getSigners(); await expect(env.adapter.connect(env.foundation).sweepRWA(await env.usdce.getAddress(), 1n, recipient.address)) .to.be.revertedWithCustomError(env.adapter, "UnknownAsset"); }); it("insufficient payout liquidity reverts cleanly", async function () { const env = await deployStack(); await env.adapter.connect(env.foundation).registerAsset( await env.buidl.getAddress(), 100, 100_000n * ONE6, 86400, 18 ); // No USDC.e seeded. await env.buidl.mint(env.liquidator.address, 10n * ONE18); await env.buidl.connect(env.liquidator).approve(await env.adapter.getAddress(), 10n * ONE18); await expect(env.adapter.connect(env.liquidator).redeem(await env.buidl.getAddress(), 10n * ONE18)) .to.be.revertedWithCustomError(env.adapter, "InsufficientPayoutLiquidity"); }); });