diff --git a/README.md b/README.md index 2b31b25..3af5983 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,4 @@ Alternatively you can use the parameter `local` instead of `foucoco`. This expec ``` brew install binaryen ``` + diff --git a/nabla/config.json b/nabla/config.json index cfcf041..32854cd 100644 --- a/nabla/config.json +++ b/nabla/config.json @@ -1,5 +1,9 @@ { "contracts": { + "TestableERC20Wrapper": { + "repository": "pendulumWrappers", + "path": "testable-erc20-wrapper/TestableERC20Wrapper.sol" + }, "Router": { "repository": "nablaContracts", "path": "contracts/src/core/Router.sol" @@ -50,11 +54,14 @@ "git": "https://github.com/0xamberhq/contracts.git", "branch": "feature/backstop-pool-coverage-ratio", "init": "yarn", - "importpaths": ["contracts/@chain/pendulum", "node_modules"] + "importpaths": [ + "contracts/@chain/pendulum", + "node_modules" + ] }, "pendulumWrappers": { "git": "https://github.com/pendulum-chain/pendulum-ink-wrapper", - "branch": "32-support-latest-solang-version" + "branch": "master" } }, "networks": { @@ -89,9 +96,9 @@ "buildFolder": "../target", "limits": { "gas": { - "refTime": "100000000000", - "proofSize": "10000000" + "refTime": "10000000000000000", + "proofSize": "10000000000000000" }, "storageDeposit": "10000000000000" } -} +} \ No newline at end of file diff --git a/nabla/test/TestableERC20Tests.ts b/nabla/test/TestableERC20Tests.ts new file mode 100644 index 0000000..e8ea8fa --- /dev/null +++ b/nabla/test/TestableERC20Tests.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import { TestContract, TestSuiteEnvironment } from "../../src/index"; +import { assertApproxEqAbs, assertApproxEqRel, assertEq, assertGt, assertLt, e } from "../../src/index"; + +const MAX_UINT256 = 2n ** 256n - 1n; + +const BOB = "6k6gXPB9idebCxqSJuqpjPaqfYLQbdLHhvsANH8Dg8GQN3tT"; + +export default async function (environment: TestSuiteEnvironment) { + const { + address, + unit, + milliUnit, + microUnit, + getContractByAddress, + vm, + tester, + constructors: { + newTestableERC20Wrapper, + }, + } = environment; + + let router: TestContract; + let backstop: TestContract; + let swapPool1: TestContract; + let swapPool2: TestContract; + + const assetNative = await newTestableERC20Wrapper("TestNative", "TEST1", 12, [0], [0], [], []); + const token1 = await newTestableERC20Wrapper("TestNonNative", "TEST2", 12, [1], [1], [], []); + + const MINT_AMOUNT = unit(10000); + const BURN_AMOUNT = unit(10); + + return { + async setUp() { + + }, + async testMintsNative() { + let totalSupplyBef = await assetNative.totalSupply(); + await assetNative.mint(BOB, MINT_AMOUNT); + let totalSupplyAft = await assetNative.totalSupply(); + + assertEq(totalSupplyAft - totalSupplyBef, MINT_AMOUNT); + }, + + async testMintsTokensPallet() { + let totalSupplyBef = await token1.totalSupply(); + let balanceBobBef = await token1.balanceOf(BOB); + + await token1.mint(BOB, MINT_AMOUNT); + + let totalSupplyAft = await token1.totalSupply(); + let balanceBob = await token1.balanceOf(BOB); + + assertEq(totalSupplyAft - totalSupplyBef, MINT_AMOUNT); + assertEq(balanceBob - balanceBobBef, MINT_AMOUNT); + }, + + async testBurnsNative() { + let totalSupplyBef = await assetNative.totalSupply(); + await assetNative.burn(BOB, BURN_AMOUNT); + let totalSupplyAft = await assetNative.totalSupply(); + + assertEq(totalSupplyBef - totalSupplyAft, BURN_AMOUNT); + }, + + async testBurnsToken() { + let totalSupplyBef = await token1.totalSupply(); + await token1.burn(BOB, BURN_AMOUNT); + let totalSupplyAft = await token1.totalSupply(); + + assertEq(totalSupplyBef - totalSupplyAft, BURN_AMOUNT); + }, + }; +} \ No newline at end of file diff --git a/nabla/test/backstop.ts b/nabla/test/backstop.ts index c0af99b..4ac554c 100644 --- a/nabla/test/backstop.ts +++ b/nabla/test/backstop.ts @@ -21,6 +21,7 @@ export default async function (environment: TestSuiteEnvironment) { newRouter, newTestableBackstopPool, newMockERC20, + newTestableERC20Wrapper, newMockOracle, newNablaCurve, newTestableSwapPool, @@ -41,9 +42,10 @@ export default async function (environment: TestSuiteEnvironment) { let swapPool1: TestContract; let swapPool2: TestContract; - const asset1 = await newMockERC20("Test Token 1", "TEST1"); - const asset2 = await newMockERC20("Test Token 2", "TEST2"); - const usd = await newMockERC20("Test Backstop Token", "USD"); + const usd = await newTestableERC20Wrapper("Test Backstop Token", "USD", 18, [1], [1], [], []); + const asset1 = await newTestableERC20Wrapper("Test Token 1", "TEST1", 18, [1], [2], [], []); + const asset2 = await newTestableERC20Wrapper("Test Token 2", "TEST2", 18, [1], [3], [], []); + const oracleUsd = await newMockOracle(address(usd), unit(1)); const oracle1 = await newMockOracle(address(asset1), unit(5)); @@ -118,17 +120,35 @@ export default async function (environment: TestSuiteEnvironment) { await backstop.addSwapPool(address(swapPool1), 0); await backstop.addSwapPool(address(swapPool2), 0); + //we ensure that only the MINT_AMOUNT is on the required accounts by + //burning pre-existing balances. + + //This is required since the assets are on the standalone testing + //chain and we cannot ensure in the test alone that the balances + //of these tokens is indeed 0 (a test could have run earlier) + await asset1.burn(tester, await asset1.balanceOf(tester)); await asset1.mint(tester, MINT_AMOUNT); + + await asset2.burn(tester, await asset2.balanceOf(tester)); await asset2.mint(tester, MINT_AMOUNT); + + await usd.burn(tester, await usd.balanceOf(tester)); await usd.mint(tester, MINT_AMOUNT); vm.startPrank(BOB); await asset1.approve(address(swapPool1), MAX_UINT256); await asset2.approve(address(swapPool2), MAX_UINT256); await usd.approve(address(backstop), MAX_UINT256); + + await asset1.burn(BOB, await asset1.balanceOf(BOB)); await asset1.mint(BOB, MINT_AMOUNT); + + await asset2.burn(BOB, await asset2.balanceOf(BOB)); await asset2.mint(BOB, MINT_AMOUNT); + + await usd.burn(BOB, await usd.balanceOf(BOB)); await usd.mint(BOB, MINT_AMOUNT); + await swapPool1.deposit(MINT_AMOUNT); await swapPool2.deposit(MINT_AMOUNT); await backstop.deposit(MINT_AMOUNT); @@ -210,6 +230,7 @@ export default async function (environment: TestSuiteEnvironment) { }, async testImmediateBackstopWithdrawal() { + const [lpTokens, fee] = await backstop.deposit(unit(20)); const [reservesBefore, liabilitiesBefore] = await backstop.coverage(); const [simulatedPayout] = await backstop.simulateWithdrawal((lpTokens * 3n) / 4n); diff --git a/nabla/test/swapPool.ts b/nabla/test/swapPool.ts index 96764e4..65d6bcf 100644 --- a/nabla/test/swapPool.ts +++ b/nabla/test/swapPool.ts @@ -15,7 +15,7 @@ export default async function (environment: TestSuiteEnvironment) { getContractByAddress, vm, tester, - constructors: { newMockERC20, newNablaCurve, newTestableSwapPool }, + constructors: { newMockERC20, newNablaCurve, newTestableSwapPool, newTestableERC20Wrapper }, } = environment; function assertApproxEq(a: bigint, b: bigint, errorMessage: string): void { @@ -46,17 +46,29 @@ export default async function (environment: TestSuiteEnvironment) { }; const nablaCurve = await newNablaCurve(0, e(0.01, 18)); - const asset = await newMockERC20("Test Token", "TEST"); + + const asset = await newTestableERC20Wrapper("Test Token", "TEST", 18, [1], [1], [], []); const pool = await newTestableSwapPool(address(asset), address(nablaCurve), 0, 0, 0, "Test LP Token", "LP"); return { async setUp() { await asset.approve(address(pool), MAX_UINT256); + + await asset.burn(tester, await asset.balanceOf(tester)); await asset.mint(tester, MINT_AMOUNT); // Important to deposit something before tests start, as first deposit // does not invoke usual _sharesToMint() logic, due to total supply being 0 + + //we ensure that only the MINT_AMOUNT is on the required accounts by + //burning pre-existing balances. + + //This is required since the assets are on the standalone testing + //chain and we cannot ensure in the test alone that the balances + //of these tokens is indeed 0 (a test could have run earlier) + await asset.burn(CHARLIE, await asset.balanceOf(CHARLIE)); await asset.mint(CHARLIE, unit(1)); + vm.startPrank(CHARLIE); await asset.approve(address(pool), unit(1)); await pool.deposit(unit(1)); diff --git a/nabla/test/swaps.ts b/nabla/test/swaps.ts index 8d45b1a..e226658 100644 --- a/nabla/test/swaps.ts +++ b/nabla/test/swaps.ts @@ -24,9 +24,11 @@ export default async function (environment: TestSuiteEnvironment) { getContractByAddress, vm, tester, - constructors: { newRouter, newMockERC20, newSwapPool, newMockOracle, newNablaCurve }, + constructors: { newRouter, newMockERC20, newTestableERC20Wrapper, newSwapPool, newMockOracle, newNablaCurve }, } = environment; + const deadlineTime = Math.floor((Date.now() / 1000) + 10); + function assertApproxEq(a: bigint, b: bigint, errorMessage: string): void { if (a !== 0n && b !== 0n) { assertApproxEqRel(a, b, 5n * 10n ** 15n, errorMessage); @@ -72,7 +74,7 @@ export default async function (environment: TestSuiteEnvironment) { WITHDRAW_AMOUNT / 2n - LOW_SLIPPAGE, path, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); const balanceAfter = await asset2.balanceOf(tester); @@ -94,8 +96,9 @@ export default async function (environment: TestSuiteEnvironment) { const router = await newRouter(); const treasury = FERDIE; - const asset1 = await newMockERC20("Test Token 1", "TEST1"); - const asset2 = await newMockERC20("Test Token 2", "TEST2"); + const asset1 = await newTestableERC20Wrapper("Test Token 1", "TEST1", 18, [1], [2], [], []); + const asset2 = await newTestableERC20Wrapper("Test Token 2", "TEST2", 18, [1], [3], [], []); + const oracle1 = await newMockOracle(address(asset1), unit(1)); const oracle2 = await newMockOracle(address(asset2), unit(2)); @@ -120,12 +123,33 @@ export default async function (environment: TestSuiteEnvironment) { return { async setUp() { + + //we ensure that only the MINT_AMOUNT is on the required accounts by + //burning pre-existing balances. + + //This is required since the assets are on the standalone testing + //chain and we cannot ensure in the test alone that the balances + //of these tokens is indeed 0 (a test could have run earlier) + await asset1.burn(tester, await asset1.balanceOf(tester)); + await asset2.burn(tester, await asset2.balanceOf(tester)); + + await asset1.burn(treasury, await asset1.balanceOf(treasury)); + await asset2.burn(treasury, await asset2.balanceOf(treasury)); + + await asset1.burn(address(swapPool1), await asset1.balanceOf(address(swapPool1))); + await asset1.burn(address(swapPool2), await asset1.balanceOf(address(swapPool2))); + + await asset2.burn(address(swapPool1), await asset2.balanceOf(address(swapPool1))); + await asset2.burn(address(swapPool2), await asset2.balanceOf(address(swapPool2))); + + await asset1.approve(address(router), MAX_UINT256); await asset2.approve(address(router), MAX_UINT256); await asset1.approve(address(swapPool1), MAX_UINT256); await asset2.approve(address(swapPool2), MAX_UINT256); + //set up await router.setPriceOracle(address(asset1), address(oracle1)); await router.setPriceOracle(address(asset2), address(oracle2)); @@ -134,6 +158,7 @@ export default async function (environment: TestSuiteEnvironment) { await asset1.mint(tester, MINT_AMOUNT); await asset2.mint(tester, MINT_AMOUNT); + }, async testSwap() { @@ -166,12 +191,15 @@ export default async function (environment: TestSuiteEnvironment) { ((WITHDRAW_AMOUNT / 2n) * 98n) / 100n - LOW_SLIPPAGE, path, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); + + const balanceAfter = await asset2.balanceOf(tester); const treasuryAfter = await asset2.balanceOf(treasury); + const swapFees = quoteWithoutFee - quoteWithFee; const [, liabilitiesAfter] = await swapPool2.coverage(); @@ -228,7 +256,7 @@ export default async function (environment: TestSuiteEnvironment) { unit(10) - HIGH_SLIPPAGE, path1, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); const path2 = makePath(asset2, asset1); @@ -237,7 +265,7 @@ export default async function (environment: TestSuiteEnvironment) { unit(20) - HIGH_SLIPPAGE, path2, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); const [finalReserves, finalLiabilities] = await swapPool1.coverage(); @@ -288,37 +316,43 @@ export default async function (environment: TestSuiteEnvironment) { unit(10) - HIGH_SLIPPAGE, path1, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); const initialBalance1 = await asset1.balanceOf(tester); const initialBalance2 = await asset2.balanceOf(tester); + // Swap forward const [initialReserves, initialLiabilities] = await swapPool1.coverage(); const [initialReserves2, initialLiabilities2] = await swapPool2.coverage(); + const path2 = makePath(asset1, asset2); const [, forwardSwapOutput] = await router.swapExactTokensForTokens( unit(20), unit(10) - HIGH_SLIPPAGE, path2, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); + const path3 = makePath(asset2, asset1); + await router.swapExactTokensForTokens( forwardSwapOutput, unit(20) - HIGH_SLIPPAGE, path3, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); const [finalReserves, finalLiabilities] = await swapPool1.coverage(); + const [finalReserves2, finalLiabilities2] = await swapPool2.coverage(); + assertEq(finalLiabilities, initialLiabilities, "Liabilities must eventually be equal to initial ones"); assertEq(finalLiabilities2, initialLiabilities2, "Liabilities must eventually be equal to initial ones"); assertGt(finalReserves, (initialReserves * 99999n) / 100000n, "Reserves 1 must eventually be >= initially"); @@ -364,12 +398,14 @@ export default async function (environment: TestSuiteEnvironment) { assertTrue(await router.paused()); vm.expectRevert("Pausable: paused"); + + await router.swapExactTokensForTokens( WITHDRAW_AMOUNT, WITHDRAW_AMOUNT / 2n - LOW_SLIPPAGE, path, tester, - Math.floor(Date.now() / 1000) + deadlineTime ); await router.unpause();