@gooddollar/goodprotocol
Version:
GoodDollar Protocol
446 lines (372 loc) • 17 kB
text/typescript
import { ethers, network, upgrades } from "hardhat";
import { expect } from "chai";
import {
GenericDistributionHelper,
IGoodDollar,
IERC20,
IStaticOracle,
ISwapRouter,
IUniswapV3Pool
} from "../../types";
import * as networkHelpers from "@nomicfoundation/hardhat-network-helpers";
import dao from "../../releases/deployment.json";
import ProtocolSettings from "../../releases/deploy-settings.json";
const BN = ethers.BigNumber;
const XDC_RPC_URL = "https://rpc.ankr.com/xdc";
const XDC_CHAIN_ID = 50;
/**
* E2E test for GenericDistributionHelper on XDC network using XSWAP pools
*
* To run this test:
* 1. Set up fork environment variable: FORK_CHAIN_ID=50
* 2. Set up XDC RPC URL in hardhat config or use: FORK_URL=https://rpc.ankr.com/xdc/...
* 3. Run: npx hardhat test test/reserve/GenericDistributionHelper.e2e.test.ts --network hardhat
*
* Note: This test requires forking XDC mainnet, so it may take longer to run
*/
describe("GenericDistributionHelper - XDC XSWAP E2E Test", function () {
// Use longer timeout for fork tests
this.timeout(600000);
let distHelper: GenericDistributionHelper;
let goodDollar: IGoodDollar;
let reserveToken: IERC20; // CUSD
let gasToken: IERC20; // WXDC
let staticOracle: IStaticOracle;
let swapRouter: ISwapRouter;
let deployer: any;
let testAccount: any;
// XDC development network addresses
const XDC_ADDRESSES = {
WXDC: "0x951857744785e80e2de051c32ee7b25f9c458c42",
StaticOracle: "0x725244458f011551Dde1104c9728746EEBEA19f9",
UniswapV3Router: "0x3b9edecc4286ba33ea6e27119c2a4db99829839d",
...dao["development-xdc"]
};
after(async function () {
await networkHelpers.reset();
});
before(async function () {
await networkHelpers.reset(XDC_RPC_URL);
[deployer, testAccount] = await ethers.getSigners();
// Impersonate the Avatar account to have permissions
const avatarSigner = await ethers.getImpersonatedSigner(XDC_ADDRESSES.Avatar);
await deployer.sendTransaction({
to: XDC_ADDRESSES.Avatar,
value: ethers.utils.parseEther("10")
});
// Get contract instances
goodDollar = (await ethers.getContractAt("IGoodDollar", XDC_ADDRESSES.GoodDollar)) as IGoodDollar;
reserveToken = (await ethers.getContractAt("IERC20", XDC_ADDRESSES.CUSD)) as IERC20;
gasToken = (await ethers.getContractAt("IERC20", XDC_ADDRESSES.WXDC)) as IERC20;
staticOracle = (await ethers.getContractAt("IStaticOracle", XDC_ADDRESSES.StaticOracle)) as IStaticOracle;
swapRouter = (await ethers.getContractAt("ISwapRouter", XDC_ADDRESSES.UniswapV3Router)) as ISwapRouter;
// Check if GenericDistributionHelper is already deployed
const existingDistHelper = dao["development-xdc"] && (dao["development-xdc"] as any).DistributionHelper;
if (existingDistHelper && existingDistHelper !== ethers.constants.AddressZero) {
distHelper = (await ethers.getContractAt(
"GenericDistributionHelper",
existingDistHelper
)) as GenericDistributionHelper;
console.log("Using existing GenericDistributionHelper at:", existingDistHelper);
} else {
// Deploy new GenericDistributionHelper
console.log("Deploying new GenericDistributionHelper...");
const GenericDistributionHelperFactory = await ethers.getContractFactory("GenericDistributionHelper");
// Get NameService instance
const nameService = await ethers.getContractAt("NameService", XDC_ADDRESSES.NameService);
const feeSettings = {
maxFee: ethers.utils.parseEther("100"),
minBalanceForFees: ethers.utils.parseEther("1"),
percentageToSellForFee: 5, // 5%
maxSlippage: 5 // 5%
};
distHelper = (await upgrades.deployProxy(
GenericDistributionHelperFactory,
[
nameService.address,
staticOracle.address,
gasToken.address,
reserveToken.address,
swapRouter.address,
feeSettings
],
{ kind: "uups" }
)) as GenericDistributionHelper;
await distHelper.deployed();
console.log("Deployed GenericDistributionHelper at:", distHelper.address);
}
});
it("should successfully swap G$ to WXDC via XSWAP pools", async function () {
// // Get initial balances
// const initialWXDCBalance = await gasToken.balanceOf(distHelper.address);
// const initialxdcBalance = await ethers.provider.getBalance(distHelper.address);
// Mint some G$ to the distribution helper for testing
const amountToSwap = ethers.utils.parseEther("1000"); // 1000 G$
// Try to mint G$ to the helper (if we have minter role)
try {
// Impersonate a minter if needed
const minterAddress = XDC_ADDRESSES.Avatar; // Avatar typically has minter role
const minterSigner = await ethers.getImpersonatedSigner(minterAddress);
await deployer.sendTransaction({
to: minterAddress,
value: ethers.utils.parseEther("1")
});
// Try to mint via the minter
const goodDollarWithMinter = goodDollar.connect(minterSigner);
await goodDollarWithMinter.mint(distHelper.address, amountToSwap);
} catch (error) {
console.log("Could not mint G$ directly, trying alterxdc approach:", error.message);
// Alterxdc: transfer from an account that has G$
// For fork tests, we might need to find an account with G$ balance
const accountsWithGD = [
XDC_ADDRESSES.AdminWallet, // AdminWallet
XDC_ADDRESSES.Avatar
];
let transferred = false;
for (const account of accountsWithGD) {
const balance = await goodDollar.balanceOf(account);
if (balance.gte(amountToSwap)) {
const accountSigner = await ethers.getImpersonatedSigner(account);
await deployer.sendTransaction({
to: account,
value: ethers.utils.parseEther("1")
});
await goodDollar.connect(accountSigner).transfer(distHelper.address, amountToSwap);
transferred = true;
break;
}
}
if (!transferred) {
console.log("Skipping swap test - insufficient G$ balance available");
this.skip();
}
}
// Get pools for the swap path
const gdPools = await staticOracle.getAllPoolsForPair(reserveToken.address, goodDollar.address);
const gasPools = await staticOracle.getAllPoolsForPair(reserveToken.address, gasToken.address);
expect(gdPools.length).to.be.gt(0, "No G$/CUSD pools found");
expect(gasPools.length).to.be.gt(0, "No CUSD/WXDC pools found");
console.log("Found pools:", {
gdPools: gdPools.length,
gasPools: gasPools.length
});
// Get pool fees
const gdPool = (await ethers.getContractAt("IUniswapV3Pool", gdPools[0])) as IUniswapV3Pool;
const gasPool = (await ethers.getContractAt("IUniswapV3Pool", gasPools[0])) as IUniswapV3Pool;
const gdFee = await gdPool.fee();
const gasFee = await gasPool.fee();
console.log("Pool fees:", {
gdFee: gdFee.toString(),
gasFee: gasFee.toString()
});
// Calculate expected output using oracle
const amountToSell = amountToSwap.div(20); // 5% for fees (50 G$)
const [quoteAmount] = await staticOracle.quoteAllAvailablePoolsWithTimePeriod(
amountToSell,
goodDollar.address,
reserveToken.address,
60
);
const [quoteGasAmount] = await staticOracle.quoteAllAvailablePoolsWithTimePeriod(
quoteAmount,
reserveToken.address,
gasToken.address,
60
);
console.log("Expected swap amounts:", {
gdIn: ethers.utils.formatEther(amountToSell),
cusdOut: ethers.utils.formatUnits(quoteAmount, 6), // CUSD has 6 decimals
wxdcOut: ethers.utils.formatEther(quoteGasAmount)
});
// Set fee settings to trigger swap
const feeSettings = {
maxFee: ethers.utils.parseEther("100"),
minBalanceForFees: ethers.utils.parseEther("1"),
percentageToSellForFee: 5,
maxSlippage: 5
};
await deployer.sendTransaction({
to: XDC_ADDRESSES.Avatar,
value: ethers.utils.parseEther("1")
});
// Impersonate guardian to set fee settings
const avatarSigner = await ethers.getImpersonatedSigner(XDC_ADDRESSES.Avatar);
await distHelper.connect(avatarSigner).setFeeSettings(feeSettings);
// Ensure distHelper has low xdc balance to trigger swap
const currentxdcBalance = await ethers.provider.getBalance(distHelper.address);
console.log("Current xdc balance:", ethers.utils.formatEther(currentxdcBalance));
console.log("Min balance for fees:", ethers.utils.formatEther(feeSettings.minBalanceForFees));
if (currentxdcBalance.gte(feeSettings.minBalanceForFees)) {
// Send xdc token away to trigger swap
const tempAccount = ethers.Wallet.createRandom().connect(ethers.provider);
await deployer.sendTransaction({
to: tempAccount.address,
value: ethers.utils.parseEther("0.01")
});
// Transfer xdc balance from distHelper
const distHelperSigner = await ethers.getImpersonatedSigner(distHelper.address);
await distHelperSigner.sendTransaction({
to: tempAccount.address,
value: currentxdcBalance.sub(ethers.utils.parseEther("0.05"))
});
}
// Trigger distribution which should perform the swap
const xdcBalanceBefore = await ethers.provider.getBalance(distHelper.address);
const goodDollarBalanceBefore = await goodDollar.balanceOf(distHelper.address);
console.log("Balances before swap:", {
xdc: ethers.utils.formatEther(xdcBalanceBefore),
goodDollar: ethers.utils.formatEther(goodDollarBalanceBefore)
});
// Call onDistribution to trigger swap
await distHelper.onDistribution(0);
// Check balances after swap
const xdcBalanceAfter = await ethers.provider.getBalance(distHelper.address);
const goodDollarBalanceAfter = await goodDollar.balanceOf(distHelper.address);
console.log("Balances after swap:", {
xdc: ethers.utils.formatEther(xdcBalanceAfter),
goodDollar: ethers.utils.formatEther(goodDollarBalanceAfter)
});
// Verify swap occurred
// Either WXDC balance increased or xdc balance increased (after unwrapping)
const xdcIncrease = xdcBalanceAfter.sub(xdcBalanceBefore);
expect(xdcIncrease.gt(0), "Swap should have increased either WXDC or xdc balance").to.be.true;
});
it("should correctly calculate swap amounts using XSWAP pools", async function () {
const amountToSell = ethers.utils.parseEther("100"); // 100 G$
// Test quote from G$ to CUSD
const [quoteCUSD, poolsGDCUSD] = await staticOracle.quoteAllAvailablePoolsWithTimePeriod(
amountToSell,
goodDollar.address,
reserveToken.address,
60
);
expect(poolsGDCUSD.length).to.be.gt(0, "Should find G$/CUSD pools");
expect(quoteCUSD.gt(0)).to.be.true;
console.log("G$ -> CUSD quote:", {
gdIn: ethers.utils.formatEther(amountToSell),
cusdOut: ethers.utils.formatUnits(quoteCUSD, 6)
});
// Test quote from CUSD to WXDC
const [quoteWXDC, poolsCUSDWXDC] = await staticOracle.quoteAllAvailablePoolsWithTimePeriod(
quoteCUSD,
reserveToken.address,
gasToken.address,
60
);
expect(poolsCUSDWXDC.length).to.be.gt(0, "Should find CUSD/WXDC pools");
expect(quoteWXDC.gt(0)).to.be.true;
console.log("CUSD -> WXDC quote:", {
cusdIn: ethers.utils.formatUnits(quoteCUSD, 6),
wxdcOut: ethers.utils.formatEther(quoteWXDC)
});
// Test calcGDToSell function
const [gdToSell, minReceived] = await distHelper.calcGDToSell(amountToSell);
expect(gdToSell.gt(0)).to.be.true;
expect(minReceived.gt(0)).to.be.true;
console.log("calcGDToSell result:", {
gdToSell: ethers.utils.formatEther(gdToSell),
minReceived: ethers.utils.formatEther(minReceived)
});
expect(gdToSell).to.be.equal(amountToSell);
expect(quoteWXDC).to.be.equal(minReceived);
});
it("should revert swap when slippage exceeds maxSlippage", async function () {
// Set maxSlippage to 0% to ensure swap will fail if there's any slippage
// This tests the slippage protection mechanism
const feeSettings = {
maxFee: ethers.utils.parseEther("100"),
minBalanceForFees: ethers.utils.parseEther("1"),
percentageToSellForFee: 5,
maxSlippage: 0 // 0% - no slippage tolerance, swap should fail
};
await deployer.sendTransaction({
to: XDC_ADDRESSES.Avatar,
value: ethers.utils.parseEther("1")
});
const avatarSigner = await ethers.getImpersonatedSigner(XDC_ADDRESSES.Avatar);
await distHelper.connect(avatarSigner).setFeeSettings(feeSettings);
// Mint some G$ to the distribution helper for testing
const amountToSwap = ethers.utils.parseEther("1000"); // 1000 G$
// Try to mint G$ to the helper
try {
const minterAddress = XDC_ADDRESSES.Avatar;
const minterSigner = await ethers.getImpersonatedSigner(minterAddress);
await deployer.sendTransaction({
to: minterAddress,
value: ethers.utils.parseEther("1")
});
const goodDollarWithMinter = goodDollar.connect(minterSigner);
await goodDollarWithMinter.mint(distHelper.address, amountToSwap);
} catch (error) {
// Try to transfer from an account that has G$
const accountsWithGD = [XDC_ADDRESSES.AdminWallet, XDC_ADDRESSES.Avatar];
let transferred = false;
for (const account of accountsWithGD) {
const balance = await goodDollar.balanceOf(account);
if (balance.gte(amountToSwap)) {
const accountSigner = await ethers.getImpersonatedSigner(account);
await deployer.sendTransaction({
to: account,
value: ethers.utils.parseEther("1")
});
await goodDollar.connect(accountSigner).transfer(distHelper.address, amountToSwap);
transferred = true;
break;
}
}
if (!transferred) {
console.log("Skipping slippage test - insufficient G$ balance available");
this.skip();
return;
}
}
// Calculate expected output using oracle
const amountToSell = amountToSwap.div(20); // 5% for fees (50 G$)
const [calculatedGDToSell, calculatedMinReceived] = await distHelper.calcGDToSell(amountToSell);
// With maxSlippage = 0%, amountOutMinimum should equal minReceived exactly
// amountOutMinimum = minReceived * (100 - 0) / 100 = minReceived
const expectedMinAmountOut = calculatedMinReceived; // 100% of minReceived (0% slippage)
// Ensure distHelper has low xdc balance to trigger swap
const currentxdcBalance = await ethers.provider.getBalance(distHelper.address);
if (currentxdcBalance.gte(feeSettings.minBalanceForFees)) {
const tempAccount = ethers.Wallet.createRandom().connect(ethers.provider);
await deployer.sendTransaction({
to: tempAccount.address,
value: ethers.utils.parseEther("0.01")
});
const distHelperSigner = await ethers.getImpersonatedSigner(distHelper.address);
await deployer.sendTransaction({
to: distHelper.address,
value: ethers.utils.parseEther("0.01")
});
await distHelperSigner.sendTransaction({
to: tempAccount.address,
value: currentxdcBalance.sub(ethers.utils.parseEther("0.05"))
});
}
// Get balances before swap attempt
const xdcBalanceBefore = await ethers.provider.getBalance(distHelper.address);
const goodDollarBalanceBefore = await goodDollar.balanceOf(distHelper.address);
// Trigger distribution which should attempt the swap
// With 0% maxSlippage, the swap should fail because real swaps always have some slippage
const tx = await distHelper.onDistribution(0);
const receipt = await tx.wait();
// Check for BuyNativeFailed event
const buyNativeFailedEvents = receipt.events?.filter((e: any) => e.event === "BuyNativeFailed");
const xdcBalanceAfter = await ethers.provider.getBalance(distHelper.address);
const xdcIncrease = xdcBalanceAfter.sub(xdcBalanceBefore);
// With 0% slippage tolerance, the swap should fail
// Verify that BuyNativeFailed event was emitted
const swapFailed = buyNativeFailedEvents && buyNativeFailedEvents.length > 0;
expect(swapFailed, "Swap should have failed with BuyNativeFailed event when maxSlippage is 0%").to.be.true;
// Verify the event details
const failedEvent = buyNativeFailedEvents[0];
const eventAmountOutMinimum = failedEvent.args?.amountOutMinimum || BN.from(0);
console.log("BuyNativeFailed event details:", {
reason: failedEvent.args?.reason,
amountToSell: ethers.utils.formatEther(failedEvent.args?.amountToSell || 0),
amountOutMinimum: ethers.utils.formatEther(eventAmountOutMinimum)
});
console.log("Slippage protection test passed: swap correctly reverted when slippage exceeded maxSlippage");
});
});