diff --git a/package-lock.json b/package-lock.json index cd18bc29..b49122e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14864,4 +14864,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 68fb53af..326523d8 100644 --- a/package.json +++ b/package.json @@ -27,4 +27,4 @@ "prepare": "husky install", "gen-types": "./gen-types.sh" } -} +} \ No newline at end of file diff --git a/src/SupplySchedule.sol b/src/SupplySchedule.sol index be43bf5f..00884971 100644 --- a/src/SupplySchedule.sol +++ b/src/SupplySchedule.sol @@ -42,7 +42,6 @@ contract SupplySchedule is GlobalAccessControlManaged, DSTest { function initialize(address _gac) public initializer { __GlobalAccessControlManaged_init(_gac); - _setEpochRates(); } /// ======================= @@ -170,75 +169,4 @@ contract SupplySchedule is GlobalAccessControlManaged, DSTest { epochRate[_epoch] = _rate; emit EpochSupplyRateSet(_epoch, _rate); } - - /// ======================== - /// ===== Test actions ===== - /// ======================== - - // @dev Set rates for the initial epochs - function _setEpochRates() internal { - epochRate[0] = 593962000000000000000000 / epochLength; - epochRate[1] = 591445000000000000000000 / epochLength; - epochRate[2] = 585021000000000000000000 / epochLength; - epochRate[3] = 574138000000000000000000 / epochLength; - epochRate[4] = 558275000000000000000000 / epochLength; - epochRate[5] = 536986000000000000000000 / epochLength; - } - - function getMintableDebug(uint256 lastMintTimestamp) external { - require( - globalStartTimestamp > 0, - "SupplySchedule: minting not started" - ); - require( - lastMintTimestamp > globalStartTimestamp, - "SupplySchedule: attempting to mint before start block" - ); - require( - block.timestamp > lastMintTimestamp, - "SupplySchedule: already minted up to current block" - ); - - uint256 mintable = 0; - - emit log_named_uint("mintable", mintable); - emit log_named_uint("block.timestamp", block.timestamp); - emit log_named_uint("lastMintTimestamp", lastMintTimestamp); - emit log_named_uint("globalStartTimestamp", globalStartTimestamp); - emit log_named_uint("epochLength", epochLength); - - uint256 startingEpoch = (lastMintTimestamp - globalStartTimestamp) / - epochLength; - emit log_named_uint("startingEpoch", startingEpoch); - - uint256 endingEpoch = (block.timestamp - globalStartTimestamp) / - epochLength; - emit log_named_uint("endingEpoch", endingEpoch); - - for (uint256 i = startingEpoch; i <= endingEpoch; i++) { - uint256 rate = epochRate[i]; - - uint256 epochStartTime = globalStartTimestamp + i * epochLength; - uint256 epochEndTime = globalStartTimestamp + (i + 1) * epochLength; - - emit log_named_uint("epoch iteration", i); - emit log_named_uint("epochStartTime", epochStartTime); - emit log_named_uint("epochEndTime", epochEndTime); - - uint256 time = MathUpgradeable.min(block.timestamp, epochEndTime) - - MathUpgradeable.max(lastMintTimestamp, epochStartTime); - - emit log_named_uint("time to mint over", time); - - mintable += rate * time; - - emit log_named_uint("mintable from this iteration", rate * time); - emit log_named_uint( - "total mintable after this iteration", - mintable - ); - } - - // return mintable; - } } diff --git a/src/test/AtomicLaunchTest.t.sol b/src/test/AtomicLaunchTest.t.sol new file mode 100644 index 00000000..ff4b7427 --- /dev/null +++ b/src/test/AtomicLaunchTest.t.sol @@ -0,0 +1,775 @@ +pragma solidity 0.8.12; + +import {BaseFixture} from "./BaseFixture.sol"; +import {KnightingRound} from "../KnightingRound.sol"; +import {KnightingRoundWithEth} from "../KnightingRoundWithEth.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {Funding} from "../Funding.sol"; +import {SupplySchedule} from "../SupplySchedule.sol"; +import "../interfaces/erc20/IERC20.sol"; + +// General interfaces for special non-ERC20 tokens' mint function +interface ISpecialMinter { + function mint(address account, uint256 amount) external; +} + +interface IUSDCMasterMinter { + function incrementMinterAllowance(uint256 _allowanceIncrement) external; +} + +// Curve Pool Factory +interface ICurvePoolFactory { + function deploy_pool( + string memory _name, + string memory _symbol, + address[2] memory _coins, + uint256 A, + uint256 gamma, + uint256 mid_fee, + uint256 out_fee, + uint256 allowed_extra_profit, + uint256 fee_gamma, + uint256 adjustment_step, + uint256 admin_fee, + uint256 ma_half_time, + uint256 initial_price + ) external returns (address); +} + +interface ICurvePool { + function token() external view returns (address); + + function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) + external; + + function balances(uint256 arg0) external view returns (uint256); + + function exchange( + int128 i, + int128 j, + uint256 _dx, + uint256 _min_dy + ) external; + + function get_dy( + int128 i, + int128 j, + uint256 _dx + ) external returns (uint256); +} + +contract AtomicLaunchTest is BaseFixture { + using FixedPointMathLib for uint256; + + event Sale( + address indexed buyer, + uint8 indexed daoId, + uint256 amountIn, + uint256 amountOut + ); + + uint256 constant MAX_UINT256 = type(uint256).max; + uint256 constant MAX_BPS = 10000; + uint256 constant epochLength = 21 days; + + // Asset minting addresses (Just for testing) + address constant renBTC_owner = 0xe4b679400F0f267212D5D812B95f58C83243EE71; + address constant ust_owner = 0x3ee18B2214AFF97000D974cf647E7C347E8fa585; + address constant usdcMasterMinter = + 0xE982615d461DD5cD06575BbeA87624fda4e3de17; + address constant usdcMasterMinter_owner = + 0x79E0946e1C186E745f1352d7C21AB04700C99F71; + address constant usdc_owner = 0x5B6122C109B78C6755486966148C1D70a50A47D7; + address constant badger_treasury = + 0xD0A7A8B98957b9CD3cFB9c0425AbE44551158e9e; + + // Involved addresses + address constant curvePoolFactory = + 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99; + + // NOTE: wBTC and wETH are handled on fixture + KnightingRound knightingRound_cvx = new KnightingRound(); + KnightingRound knightingRound_renBTC = new KnightingRound(); + KnightingRound knightingRound_ibBTC = new KnightingRound(); + KnightingRound knightingRound_frax = new KnightingRound(); + KnightingRound knightingRound_ust = new KnightingRound(); + KnightingRound knightingRound_usdc = new KnightingRound(); + KnightingRound knightingRound_badger = new KnightingRound(); + KnightingRound knightingRound_bveCVX = new KnightingRound(); + KnightingRound[] roundsArray; + + // Re-deploying Funding contracts for the sake of maintianing actions atomically + Funding fundingWbtc_launch = new Funding(); + Funding fundingCvx_launch = new Funding(); + Funding fundingBadger_launch = new Funding(); + + // Re-deploying SupplySchedule for the sake of maintaining actions atomically + SupplySchedule schedule_launch = new SupplySchedule(); + + // Using a temporary mock address for the DiscountManager until developed + address immutable discountManager = getAddress("discountManager"); + + address btc_user = address(1); + address stable_user = address(2); + address influence_user = address(3); + address eth_user = address(4); + + function setUp() public override { + BaseFixture.setUp(); + + // There will be no tokenIn limit (Round can only finish based on time) + vm.startPrank(techOps); + knightingRound.setTokenInLimit(MAX_UINT256); // wBTC + knightingRoundWithEth.setTokenInLimit(MAX_UINT256); + vm.stopPrank(); + + // Setup rounds with somehow realistic prices + vm.startPrank(governance); + knightingRound.setTokenOutPerTokenIn(1500e18); //1500 xCTDL per wBTC + knightingRoundWithEth.setTokenOutPerTokenIn(115e18); // 115 xCTDL per ETH + + roundsArray.push(knightingRound); + + knightingRound_renBTC.initialize( + address(gac), + address(xCitadel), + address(renBTC), + knightingRoundParams.start, + knightingRoundParams.duration, + 1500e18, // 1500 xCTDL per renBTC + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_renBTC); + + knightingRound_ibBTC.initialize( + address(gac), + address(xCitadel), + address(ibBTC), + knightingRoundParams.start, + knightingRoundParams.duration, + 1500e18, // 1500 xCTDL per ibBTC + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_ibBTC); + + knightingRound_frax.initialize( + address(gac), + address(xCitadel), + address(frax), + knightingRoundParams.start, + knightingRoundParams.duration, + 47619047610000000, // 0.0476 xCTDL per frax + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_frax); + + knightingRound_ust.initialize( + address(gac), + address(xCitadel), + address(ust), + knightingRoundParams.start, + knightingRoundParams.duration, + 47619047610000000, // 0.0476 xCTDL per ust + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_ust); + + knightingRound_usdc.initialize( + address(gac), + address(xCitadel), + address(usdc), + knightingRoundParams.start, + knightingRoundParams.duration, + 47619047610000000, // 0.0476 xCTDL per usdc + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_usdc); + + knightingRound_cvx.initialize( + address(gac), + address(xCitadel), + address(cvx), + knightingRoundParams.start, + knightingRoundParams.duration, + 1e18, // 1 xCTDL per CVX + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_cvx); + + knightingRound_bveCVX.initialize( + address(gac), + address(xCitadel), + address(bveCVX), + knightingRoundParams.start, + knightingRoundParams.duration, + 1e18, // 1 xCTDL per bveCVX + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_bveCVX); + + knightingRound_badger.initialize( + address(gac), + address(xCitadel), + address(badger), + knightingRoundParams.start, + knightingRoundParams.duration, + 333333333333333333, // 0.333 xCTDL per badger + address(governance), + address(guestList), + MAX_UINT256 + ); + roundsArray.push(knightingRound_badger); + + schedule_launch.initialize(address(gac)); + + vm.stopPrank(); + + // Mint assets for users + + // Mint BTC based assets + erc20utils.forceMintTo(btc_user, wbtc_address, 1000e8); + erc20utils.forceMintTo(btc_user, ibBTC_address, 1000e18); + vm.prank(renBTC_owner); // RenBTC Owner + ISpecialMinter(renBTC_address).mint(btc_user, 1000e8); + + // Mint Stablecoin assets + erc20utils.forceMintTo(stable_user, frax_address, 100000e18); + vm.prank(ust_owner); // UST Owner + ISpecialMinter(ust_address).mint(stable_user, 100000e6); + vm.prank(usdcMasterMinter_owner); + IUSDCMasterMinter(usdcMasterMinter).incrementMinterAllowance(100000e6); + vm.prank(usdc_owner); // USDC Owner + ISpecialMinter(usdc_address).mint(stable_user, 100000e6); + + // Mint influence assets + erc20utils.forceMintTo(influence_user, cvx_address, 10000e18); + vm.startPrank(badger_treasury); + bveCVX.transfer(influence_user, bveCVX.balanceOf(badger_treasury)); + badger.transfer(influence_user, badger.balanceOf(badger_treasury)); + require(bveCVX.balanceOf(influence_user) > 0, "No bveCVX tranferred"); + require(badger.balanceOf(influence_user) > 0, "No BADGER tranferred"); + vm.stopPrank(); + + // Deal ETH + vm.deal(eth_user, 100 ether); + } + + function testAtomicLaunch() public { + _simulateeKnightingRound(); + + // BEGINNING OF ATOMIC LAUNCH + + vm.startPrank(governance); + + // Get all the Citadel bought from all KRs + uint256 totalCitadelBought; + uint256[] memory citdatelBoughtPerRound = new uint256[]( + roundsArray.length + ); + for (uint256 i; i < roundsArray.length; i++) { + totalCitadelBought += roundsArray[i].totalTokenOutBought(); + citdatelBoughtPerRound[i] = roundsArray[i].totalTokenOutBought(); + } + uint256 citadelBoughtEthRound = knightingRoundWithEth + .totalTokenOutBought(); + totalCitadelBought += citadelBoughtEthRound; + + // Mint the required TotalSupply of CTDL + uint256 initialSupply = (totalCitadelBought * 1666666666666666667) / + 1e18; // Amount bought = 60% of initial supply, therefore total citadel ~= 1.67 amount bought. + + citadel.mint(governance, initialSupply); + assertEq(citadel.balanceOf(governance), initialSupply); + + // Distribute bought amounts of xCTDL to each round + citadel.approve(address(xCitadel), totalCitadelBought); + + for (uint256 i; i < roundsArray.length; i++) { + xCitadel.depositFor( + address(roundsArray[i]), + citdatelBoughtPerRound[i] + ); + assertEq( + xCitadel.balanceOf(address(roundsArray[i])), + citdatelBoughtPerRound[i] + ); + } + xCitadel.depositFor( + address(knightingRoundWithEth), + citadelBoughtEthRound + ); + assertEq( + xCitadel.balanceOf(address(knightingRoundWithEth)), + citadelBoughtEthRound + ); + assertEq(xCitadel.balanceOf(governance), 0); + + uint256 remainingSupply = initialSupply - totalCitadelBought - 1e18; // one coin for seeding xCitadel + uint256 toLiquidity = (remainingSupply * 4e17) / 1e18; // 15% of total, or 40% of remaining 40% + uint256 toTreasury = (remainingSupply * 6e17) / 1e18; // 25% of total, or 60% of remaining 40% + // 3 wei tolerance for rounding errors (In practice it is 1 wei different) + require( + initialSupply - + (totalCitadelBought + toTreasury + toLiquidity + 1e18) < + 3, + "Bad distribution calc" + ); + + // Seed xCTDL + citadel.approve(address(xCitadel), 1e18); + xCitadel.deposit(1e18); + assertEq(xCitadel.balanceOf(governance), 1e18); + + // Transfer 25% of total CTDL and acquired sale assets to Treasury + citadel.transfer(treasuryVault, toTreasury); + assertEq(citadel.balanceOf(treasuryVault), toTreasury); + + for (uint256 i; i < roundsArray.length; i++) { + IERC20 tokenIn = IERC20(address(roundsArray[i].tokenIn())); + uint256 govTokenInBalance = tokenIn.balanceOf(governance); + tokenIn.transfer(treasuryVault, govTokenInBalance); + assertEq(tokenIn.balanceOf(governance), 0); + assertEq(tokenIn.balanceOf(treasuryVault), govTokenInBalance); + } + uint256 govWethBalance = weth.balanceOf(governance); + weth.transfer(treasuryVault, govWethBalance); + assertEq(weth.balanceOf(governance), 0); + assertEq(weth.balanceOf(treasuryVault), govWethBalance); + + // Use 15% of total CTDL to deploy and seed liquidity pool on Curve + address[2] memory coins; + coins[0] = address(citadel); + coins[1] = address(wbtc); + + // NOTE: Parameters acquired from test deployment: + // https://etherscan.io/tx/0x20a9182e7644e216d7a26785223fb2947a3ba70998eac4da98a63ec4652b1821 + address poolAddress = ICurvePoolFactory(curvePoolFactory).deploy_pool( + "CTDL/wBTC", + "CTDL", + coins, + 400000, + 145000000000000, + 26000000, + 45000000, + 2000000000000, + 230000000000000, + 146000000000000, + 5000000000, + 600, + 1428571428570000000000 // ~$30k/$21 = 1428.57142857 (Current external rate for WBTC/CTDL) + ); + + ICurvePool pool = ICurvePool(poolAddress); + + // Calculate wBTC amount as: $21/~$30k = 0.0007 (Divide by 1e10 to normalize to wBTC decimals) + uint256 wbtcToLiquidity = ((toLiquidity * 7e14) / 1e18) / 1e10; + emit log_named_uint("CTDL Liquidity", toLiquidity); + emit log_named_uint("wBTC Liquidity", wbtcToLiquidity); + erc20utils.forceMintTo(governance, wbtc_address, wbtcToLiquidity); + + citadel.approve(poolAddress, toLiquidity); + wbtc.approve(poolAddress, wbtcToLiquidity); + + uint256[2] memory amounts; + amounts[0] = toLiquidity; + amounts[1] = wbtcToLiquidity; + pool.add_liquidity(amounts, 0); + + assertEq(pool.balances(0), toLiquidity); + assertEq(pool.balances(1), wbtcToLiquidity); + + // Check tat CTDL amounts add up + require( + citadel.balanceOf(governance) < 3, + "Not all CTDL was distributed" + ); // 3 wei tolerance for rounding errors + + // Remove ADMIN Minting role + gac.revokeRole(CITADEL_MINTER_ROLE, governance); // Remove admin mint, only CitadelMinter rules can mint now + + // Finilize KRs + for (uint256 i; i < roundsArray.length; i++) { + roundsArray[i].finalize(); + require(roundsArray[i].finalized(), "KR not finalized"); + } + knightingRoundWithEth.finalize(); + require(knightingRoundWithEth.finalized(), "ETH KR not finalized"); + + // Launch initial Funding contracts (This may happen through a Factory contract) + // Reference: https://thecitadeldao.medium.com/citadel-funding-mechanics-4851147e31f3 + fundingWbtc_launch.initialize( + address(gac), + address(citadel), + address(wbtc), + address(xCitadel), + treasuryVault, + address(medianOracleWbtc), + 100e8 + ); + fundingCvx_launch.initialize( + address(gac), + address(citadel), + address(cvx), + address(xCitadel), + treasuryVault, + address(medianOracleCvx), + 100000e18 + ); + fundingBadger_launch.initialize( + address(gac), + address(citadel), + address(badger), + address(xCitadel), + treasuryVault, + address(medianOracleBadger), + 100000e18 + ); + // Set Discount limits (Arbitrary values for testing) + fundingWbtc_launch.setDiscountLimits(1000, 2000); // Min: 10% and max: 20% + fundingCvx_launch.setDiscountLimits(1000, 2000); // Min: 10% and max: 20% + fundingBadger_launch.setDiscountLimits(1000, 2000); // Min: 10% and max: 20% + // Set Discount managers + fundingWbtc_launch.setDiscountManager(discountManager); + fundingCvx_launch.setDiscountManager(discountManager); + fundingBadger_launch.setDiscountManager(discountManager); + // Set Pricing bounds (Arbitrary values for testing) + fundingWbtc_launch.setCitadelPerAssetBounds(1300e18, 2000e18); + fundingCvx_launch.setCitadelPerAssetBounds(8e17, 2e18); + fundingBadger_launch.setCitadelPerAssetBounds(8e17, 2e18); + // Set Discounts + gac.grantRole(POLICY_OPERATIONS_ROLE, governance); // Grant Role temporarily (Can/should be done upon GAC Setup) + fundingWbtc_launch.setDiscount(1050); // 10.5% + fundingCvx_launch.setDiscount(1050); // 10.5% + fundingBadger_launch.setDiscount(1050); // 10.5% + // Unpause funding contracts if paused + if (fundingWbtc_launch.paused()) { + fundingWbtc_launch.unpause(); + } + if (fundingCvx_launch.paused()) { + fundingCvx_launch.unpause(); + } + if (fundingBadger_launch.paused()) { + fundingBadger_launch.unpause(); + } + // Confirm funding params + confirmFundingParams( + fundingWbtc_launch, + 1050, + 1000, + 2000, + discountManager, + 100e8 + ); + confirmFundingParams( + fundingCvx_launch, + 1050, + 1000, + 2000, + discountManager, + 100000e18 + ); + confirmFundingParams( + fundingBadger_launch, + 1050, + 1000, + 2000, + discountManager, + 100000e18 + ); + + // Setup SupplySchedule + + // Set first few epoch rates + schedule_launch.setEpochRate( + 0, + 593962000000000000000000 / schedule_launch.epochLength() + ); + schedule_launch.setEpochRate( + 1, + 591445000000000000000000 / schedule_launch.epochLength() + ); + schedule_launch.setEpochRate( + 2, + 585021000000000000000000 / schedule_launch.epochLength() + ); + schedule_launch.setEpochRate( + 3, + 574138000000000000000000 / schedule_launch.epochLength() + ); + schedule_launch.setEpochRate( + 4, + 558275000000000000000000 / schedule_launch.epochLength() + ); + schedule_launch.setEpochRate( + 5, + 536986000000000000000000 / schedule_launch.epochLength() + ); + // Set minting start + schedule_launch.setMintingStart(block.timestamp); + + vm.stopPrank(); + + // END OF ATOMIC LAUNCH + + // Simulation of post launch user actions + _simulatePostLaunchActions(pool); + } + + function _simulateeKnightingRound() public { + // Move to knighting round start + vm.warp(knightingRound.saleStart()); + + knightingRoundBuy(knightingRound, wbtc, btc_user); + knightingRoundBuy(knightingRound_ibBTC, ibBTC, btc_user); + knightingRoundBuy(knightingRound_renBTC, renBTC, btc_user); + knightingRoundBuy(knightingRound_frax, frax, stable_user); + knightingRoundBuy(knightingRound_ust, ust, stable_user); + knightingRoundBuy(knightingRound_usdc, usdc, stable_user); + knightingRoundBuy(knightingRound_cvx, cvx, influence_user); + knightingRoundBuy(knightingRound_bveCVX, bveCVX, influence_user); + knightingRoundBuy(knightingRound_badger, badger, influence_user); + knightingRoundBuy_ETH(knightingRoundWithEth, eth_user); + + vm.stopPrank(); + + // Knighting round concludes... + vm.warp(knightingRoundParams.start + knightingRoundParams.duration); + } + + function knightingRoundBuy( + KnightingRound round, + IERC20 tokenIn, + address user + ) internal { + bytes32[] memory emptyProof = new bytes32[](1); + vm.startPrank(user); + + uint256 amountIn = tokenIn.balanceOf(user); + + tokenIn.approve(address(round), amountIn); + + uint256 tokenOutAmountExpected = (amountIn * + round.tokenOutPerTokenIn()) / round.tokenInNormalizationValue(); + + vm.expectEmit(true, true, false, true); + emit Sale(user, 0, amountIn, tokenOutAmountExpected); + uint256 tokenOutAmount = round.buy(amountIn, 0, emptyProof); + + assertEq(round.totalTokenIn(), amountIn); // totalTokenIn should be equal to deposit + assertEq(tokenOutAmount, tokenOutAmountExpected); // transferred amount should be equal to expected + assertEq(round.totalTokenOutBought(), tokenOutAmount); + assertEq(round.daoVotedFor(user), 0); // daoVotedFor should be set + assertEq(round.daoCommitments(0), tokenOutAmount); // daoCommitments should be tokenOutAmount + + require(tokenIn.balanceOf(user) == 0, "Token in not deposited"); + require( + tokenIn.balanceOf(round.saleRecipient()) == amountIn, + "Token in not received" + ); + + vm.stopPrank(); + } + + function knightingRoundBuy_ETH(KnightingRoundWithEth round, address user) + internal + { + bytes32[] memory emptyProof = new bytes32[](1); + vm.startPrank(user); + + uint256 amountIn = user.balance; + + weth.approve(address(round), amountIn); + + uint256 tokenOutAmountExpected = (amountIn * + round.tokenOutPerTokenIn()) / round.tokenInNormalizationValue(); + + vm.expectEmit(true, true, false, true); + emit Sale(user, 0, amountIn, tokenOutAmountExpected); + uint256 tokenOutAmount = round.buyEth{value: amountIn}(0, emptyProof); + + assertEq(round.totalTokenIn(), amountIn); // totalTokenIn should be equal to deposit + assertEq(tokenOutAmount, tokenOutAmountExpected); // transferred amount should be equal to expected + assertEq(round.totalTokenOutBought(), tokenOutAmount); + assertEq(round.daoVotedFor(user), 0); // daoVotedFor should be set + assertEq(round.daoCommitments(0), tokenOutAmount); // daoCommitments should be tokenOutAmount + + require(user.balance == 0, "ETH in not deposited"); + require( + weth.balanceOf(round.saleRecipient()) == amountIn, + "wETH in not received" + ); + + vm.stopPrank(); + } + + function confirmFundingParams( + Funding _funding, + uint256 _discount, + uint256 _minDiscount, + uint256 _maxDiscount, + address _discountManager, + uint256 _assetCap + ) internal { + ( + uint256 discount, + uint256 minDiscount, + uint256 maxDiscount, + address discountManager, + , + uint256 assetCap + ) = _funding.funding(); + require(_discount == discount, "Wrong discount set"); + require(_minDiscount == minDiscount, "Wrong minDiscount set"); + require(_maxDiscount == maxDiscount, "Wrong maxDiscount set"); + require( + _discountManager == discountManager, + "Wrong discountManager set" + ); + require(_assetCap == assetCap, "Wrong assetCap set"); + } + + function _simulatePostLaunchActions(ICurvePool pool) internal { + // TODO: Add user flows to be tested post launch with all test users. + // These may include but not be limited to: + // - Withdrawing xCTDL/vesting + // - Swapping through Curve pool + // - Providing more liquidity + // - Collecting rewards + + // withdraw xCTDL as the knighting Rounds are finalized + knightingRoundClaim(knightingRound, btc_user); + knightingRoundClaim(knightingRound_ibBTC, btc_user); + knightingRoundClaim(knightingRound_renBTC, btc_user); + knightingRoundClaim(knightingRound_frax, stable_user); + knightingRoundClaim(knightingRound_ust, stable_user); + knightingRoundClaim(knightingRound_usdc, stable_user); + knightingRoundClaim(knightingRound_cvx, influence_user); + knightingRoundClaim(knightingRound_bveCVX, influence_user); + knightingRoundClaim(knightingRound_badger, influence_user); + knightingRoundClaim(knightingRoundWithEth, eth_user); + + // user wants to stake citadel + vm.prank(address(citadelMinter)); + citadel.mint(btc_user, 100e18); + + vm.startPrank(btc_user); + // approve staking amount + citadel.approve(address(xCitadel), 10e18); + // deposit + xCitadel.deposit(10e18); + uint256 userxCitadelBalance = xCitadel.balanceOf(btc_user); + uint256 expectedClaimableBalance = (xCitadel.balance() * + userxCitadelBalance) / xCitadel.totalSupply(); + xCitadel.withdrawAll(); + vm.warp(block.timestamp + xCitadelVester.INITIAL_VESTING_DURATION()); + + uint256 userCitadelBefore = citadel.balanceOf(btc_user); + xCitadelVester.claim(btc_user, expectedClaimableBalance); + uint256 userCitadelAfter = citadel.balanceOf(btc_user); + + assertEq( + userCitadelAfter - userCitadelBefore, + expectedClaimableBalance + ); + + uint256 citadelPoolBalanceBefore = pool.balances(0); + uint256 wbtcPoolBalanceBefore = pool.balances(1); + + // provide liquidity to pool + + uint256[2] memory amounts; + amounts[0] = 10e18; + amounts[1] = 0; + citadel.approve(address(pool), 10e18); + pool.add_liquidity(amounts, 0); + + uint256 citadelPoolBalanceAfter = pool.balances(0); + uint256 wbtcPoolBalanceAfter = pool.balances(1); + + assertEq(citadelPoolBalanceAfter - citadelPoolBalanceBefore, 10e18); + assertEq(wbtcPoolBalanceAfter, wbtcPoolBalanceBefore); + + // swap tokens + uint256 wbtcUserBalanceBefore = wbtc.balanceOf(btc_user); + uint256 userCitadelBalanceBefore = citadel.balanceOf(btc_user); + citadelPoolBalanceBefore = pool.balances(0); + wbtcPoolBalanceBefore = pool.balances(1); + citadel.approve(address(pool), 20e18); + + // uint min_dy = pool.get_dy(0, 1, 20e18); + pool.exchange(0, 1, 20e18, 0); + citadelPoolBalanceAfter = pool.balances(0); + wbtcPoolBalanceAfter = pool.balances(1); + uint256 userCitadelBalanceAfter = citadel.balanceOf(btc_user); + + uint256 wbtcUserBalanceAfter = wbtc.balanceOf(btc_user); + emit log_named_uint("citadelPoolBalanceAfter", citadelPoolBalanceAfter); + emit log_named_uint( + "citadelPoolBalanceBefore", + citadelPoolBalanceBefore + ); + emit log_named_uint( + "userCitadelBalanceBefore", + userCitadelBalanceBefore + ); + emit log_named_uint("userCitadelBalanceAfter", userCitadelBalanceAfter); + emit log_named_uint("wbtcUserBalanceAfter", wbtcUserBalanceAfter); + emit log_named_uint("wbtcUserBalanceBefore", wbtcUserBalanceBefore); + emit log_named_uint("wbtcPoolBalanceBefore", wbtcPoolBalanceBefore); + emit log_named_uint("wbtcPoolBalanceAfter", wbtcPoolBalanceAfter); + + assertEq( + wbtcUserBalanceAfter - wbtcUserBalanceBefore, + wbtcPoolBalanceBefore - wbtcPoolBalanceAfter + ); + + // approve staking amount + citadel.approve(address(xCitadel), 10e18); + // deposit + xCitadel.deposit(10e18); + uint256 xCitadelUserBalanceBefore = xCitadel.balanceOf(btc_user); + uint256 lockedAmount = xCitadelUserBalanceBefore; + xCitadel.approve(address(xCitadelLocker), xCitadelUserBalanceBefore); + + xCitadelLocker.lock(btc_user, xCitadelUserBalanceBefore, 0); // lock xCitadel + uint256 xCitadelUserBalanceAfter = xCitadel.balanceOf(btc_user); + + // try to withdraw before the lock duration ends + vm.expectRevert("no exp locks"); + xCitadelLocker.withdrawExpiredLocksTo(btc_user); // withdraw + + vm.warp(block.timestamp + 148 days); // lock period = 147 days + 1 day(rewards_duration cause 1st time lock) + xCitadelUserBalanceBefore = xCitadel.balanceOf(btc_user); + xCitadelLocker.withdrawExpiredLocksTo(btc_user); // withdraw + xCitadelUserBalanceAfter = xCitadel.balanceOf(btc_user); + uint256 xCitadelUnlocked = xCitadelUserBalanceAfter - + xCitadelUserBalanceBefore; + + // user gets unlocked amount + assertEq(xCitadelUnlocked, lockedAmount); + } + + function knightingRoundClaim(KnightingRound round, address user) internal { + uint256 userBalanceBefore = xCitadel.balanceOf(user); + vm.prank(user); + uint256 tokenOutAmount = round.claim(); + + uint256 userBalanceAfter = xCitadel.balanceOf(user); + // check if user has received xCitadel + assertEq(tokenOutAmount, userBalanceAfter - userBalanceBefore); + } +} diff --git a/src/test/BaseFixture.sol b/src/test/BaseFixture.sol index a33599f3..c0bce650 100644 --- a/src/test/BaseFixture.sol +++ b/src/test/BaseFixture.sol @@ -86,10 +86,27 @@ contract BaseFixture is DSTest, Utils, stdCheats { address constant wbtc_address = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; address constant weth_address = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant cvx_address = 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B; + address constant renBTC_address = + 0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D; + address constant ibBTC_address = 0xc4E15973E6fF2A35cC804c2CF9D2a1b817a8b40F; + address constant frax_address = 0x853d955aCEf822Db058eb8505911ED77F175b99e; + address constant ust_address = 0xa693B19d2931d498c5B318dF961919BB4aee87a5; // UST(Wormhole) + address constant usdc_address = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address constant badger_address = + 0x3472A5A71965499acd81997a54BBA8D852C6E53d; + address constant bveCVX_address = + 0xfd05D3C7fe2924020620A8bE4961bBaA747e6305; IERC20 wbtc = IERC20(wbtc_address); - IERC20 cvx = IERC20(cvx_address); IERC20 weth = IERC20(weth_address); + IERC20 cvx = IERC20(cvx_address); + IERC20 renBTC = IERC20(renBTC_address); + IERC20 ibBTC = IERC20(ibBTC_address); + IERC20 frax = IERC20(frax_address); + IERC20 ust = IERC20(ust_address); + IERC20 usdc = IERC20(usdc_address); + IERC20 badger = IERC20(badger_address); + IERC20 bveCVX = IERC20(bveCVX_address); GlobalAccessControl gac = new GlobalAccessControl(); @@ -115,6 +132,10 @@ contract BaseFixture is DSTest, Utils, stdCheats { IMedianOracle( deployCode(medianOracleArtifact, abi.encode(1 days, 0, 1)) ); + IMedianOracle medianOracleBadger = + IMedianOracle( + deployCode(medianOracleArtifact, abi.encode(1 days, 0, 1)) + ); Funding fundingWbtc = new Funding(); Funding fundingCvx = new Funding(); @@ -261,6 +282,7 @@ contract BaseFixture is DSTest, Utils, stdCheats { // Oracle medianOracleWbtc.addProvider(keeper); medianOracleCvx.addProvider(keeper); + medianOracleBadger.addProvider(keeper); // Funding fundingWbtc.initialize( @@ -282,8 +304,34 @@ contract BaseFixture is DSTest, Utils, stdCheats { 100000e18 ); - // Grant roles + // Set test epoch rates vm.startPrank(governance); + schedule.setEpochRate( + 0, + 593962000000000000000000 / schedule.epochLength() + ); + schedule.setEpochRate( + 1, + 591445000000000000000000 / schedule.epochLength() + ); + schedule.setEpochRate( + 2, + 585021000000000000000000 / schedule.epochLength() + ); + schedule.setEpochRate( + 3, + 574138000000000000000000 / schedule.epochLength() + ); + schedule.setEpochRate( + 4, + 558275000000000000000000 / schedule.epochLength() + ); + schedule.setEpochRate( + 5, + 536986000000000000000000 / schedule.epochLength() + ); + + // Grant roles gac.grantRole(CONTRACT_GOVERNANCE_ROLE, governance); gac.grantRole(TREASURY_GOVERNANCE_ROLE, treasuryVault);