From 792138d115ab873d418fd95c6dfe05e1ce71ae6c Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Sun, 28 Jan 2024 08:55:36 +0800 Subject: [PATCH 1/9] add flashswap v3 --- contracts/fund/PrimaryMarketV5.sol | 52 ++++++ contracts/interfaces/IPrimaryMarketV5.sol | 10 +- contracts/swap/FlashSwapRouterV3.sol | 198 ++++++++++++++++++++++ 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 contracts/swap/FlashSwapRouterV3.sol diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 0d5e2bd9..8e7817c7 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -122,6 +122,30 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, } } + /// @notice Calculate the amount of underlying tokens to create at least the given QUEEN amount. + /// This only works with non-empty fund for simplicity. + /// @param minOutQ Minimum received QUEEN amount + /// @return underlying Underlying amount that should be used for creation + function getCreationForQ(uint256 minOutQ) external view override returns (uint256 underlying) { + // Assume: + // minOutQ * fundUnderlying = a * fundEquivalentTotalQ - b + // where a and b are integers and 0 <= b < fundEquivalentTotalQ + // Then + // underlying = a + // getCreation(underlying) + // = floor(a * fundEquivalentTotalQ / fundUnderlying) + // >= floor((a * fundEquivalentTotalQ - b) / fundUnderlying) + // = minOutQ + // getCreation(underlying - 1) + // = floor((a * fundEquivalentTotalQ - fundEquivalentTotalQ) / fundUnderlying) + // < (a * fundEquivalentTotalQ - b) / fundUnderlying + // = minOutQ + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + require(fundEquivalentTotalQ > 0, "Cannot calculate creation for empty fund"); + return minOutQ.mul(fundUnderlying).add(fundEquivalentTotalQ - 1).div(fundEquivalentTotalQ); + } + function _getRedemption(uint256 inQ) private view returns (uint256 underlying) { uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); @@ -148,6 +172,19 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, outB = outR.mul(_weightB); } + /// @notice Calculate the amount of QUEEN that can be split into at least the given amount of + /// BISHOP and ROOK. + /// @param minOutR Received ROOK amount + /// @return inQ QUEEN amount that should be split + /// @return outB Received BISHOP amount + function getSplitForR( + uint256 minOutR + ) external view override returns (uint256 inQ, uint256 outB) { + uint256 splitRatio = IFundV3(fund).splitRatio(); + outB = minOutR.mul(_weightB); + inQ = minOutR.mul(1e18).add(splitRatio.sub(1)).div(splitRatio); + } + /// @notice Calculate the result of a merge. /// @param inB Spent BISHOP amount /// @return inR Spent ROOK amount @@ -167,6 +204,21 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, } } + /// @notice Calculate the result of a merge using ROOK. + /// @param inR Spent ROOK amount + /// @return inB Spent BISHOP amount + /// @return outQ Received QUEEN amount + /// @return feeQ QUEEN amount charged as merge fee + function getMergeForR( + uint256 inR + ) public view override returns (uint256 inB, uint256 outQ, uint256 feeQ) { + inB = inR.mul(_weightB); + uint256 splitRatio = IFundV5(fund).splitRatio(); + uint256 outQBeforeFee = inR.divideDecimal(splitRatio); + feeQ = outQBeforeFee.multiplyDecimal(mergeFeeRate); + outQ = outQBeforeFee.sub(feeQ); + } + /// @notice Return index of the first queued redemption that cannot be claimed now. /// Users can use this function to determine which indices can be passed to /// `claimRedemptions()`. diff --git a/contracts/interfaces/IPrimaryMarketV5.sol b/contracts/interfaces/IPrimaryMarketV5.sol index 20c5f136..df2e1d6e 100644 --- a/contracts/interfaces/IPrimaryMarketV5.sol +++ b/contracts/interfaces/IPrimaryMarketV5.sol @@ -6,12 +6,20 @@ interface IPrimaryMarketV5 { function getCreation(uint256 underlying) external view returns (uint256 outQ); + function getCreationForQ(uint256 minOutQ) external view returns (uint256 underlying); + function getRedemption(uint256 inQ) external view returns (uint256 underlying, uint256 fee); - function getSplit(uint256 inQ) external view returns (uint256 outB, uint256 outQ); + function getSplit(uint256 inQ) external view returns (uint256 outB, uint256 outR); + + function getSplitForR(uint256 minOutR) external view returns (uint256 inQ, uint256 outB); function getMerge(uint256 inB) external view returns (uint256 inR, uint256 outQ, uint256 feeQ); + function getMergeForR( + uint256 inR + ) external view returns (uint256 inB, uint256 outQ, uint256 feeQ); + function canBeRemovedFromFund() external view returns (bool); function create( diff --git a/contracts/swap/FlashSwapRouterV3.sol b/contracts/swap/FlashSwapRouterV3.sol new file mode 100644 index 00000000..e47308a8 --- /dev/null +++ b/contracts/swap/FlashSwapRouterV3.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.6.10 <0.8.0; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../interfaces/ITranchessSwapCallee.sol"; +import "../interfaces/IFundV5.sol"; +import "../interfaces/IPrimaryMarketV5.sol"; +import "../interfaces/ISwapRouter.sol"; +import "../interfaces/ITrancheIndexV2.sol"; + +/// @title Tranchess Flash Swap Router +/// @notice Router for stateless execution of flash swaps against Tranchess stable swaps +contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + event SwapRook( + address indexed recipient, + uint256 baseIn, + uint256 quoteIn, + uint256 baseOut, + uint256 quoteOut + ); + + ISwapRouter public immutable tranchessRouter; + + constructor(address tranchessRouter_) public { + tranchessRouter = ISwapRouter(tranchessRouter_); + } + + /// @dev Only meant for an off-chain client to call with eth_call. + function getBuyR( + IFundV5 fund, + address queenSwapOrPrimaryMarketRouter, + address tokenQuote, + uint256 outR + ) external returns (uint256 quoteDelta, uint256 rookDelta) { + (uint256 inQ, uint256 outB) = IPrimaryMarketV5(fund.primaryMarket()).getSplitForR(outR); + uint256 totalQuoteAmount = IStableSwapCoreInternalRevertExpected( + queenSwapOrPrimaryMarketRouter + ).getQuoteIn(inQ); + // Calculate the amount of quote asset for selling BISHOP + IStableSwap tranchessPair = tranchessRouter.getSwap(fund.tokenB(), tokenQuote); + uint256 quoteAmount = tranchessPair.getQuoteOut(outB); + // Calculate the user's portion of the payment to Tranchess swap + quoteDelta = totalQuoteAmount.sub(quoteAmount); + // Calculate creation of borrowed underlying for QUEEN + uint256 outQ = IStableSwapCoreInternalRevertExpected(queenSwapOrPrimaryMarketRouter) + .getBaseOut(totalQuoteAmount); + // Calculate the split result of QUEEN into BISHOP and ROOK + (, rookDelta) = IPrimaryMarketV5(fund.primaryMarket()).getSplit(outQ); + } + + /// @dev Only meant for an off-chain client to call with eth_call. + function getSellR( + IFundV5 fund, + address queenSwapOrPrimaryMarketRouter, + address tokenQuote, + uint256 inR + ) external returns (uint256 quoteDelta, uint256 rookDelta) { + rookDelta = inR; + // Calculate merge result of BISHOP and ROOK into QUEEN + (uint256 inB, uint256 outQ, ) = IPrimaryMarketV5(fund.primaryMarket()).getMergeForR(inR); + uint256 quoteAmount = IStableSwap(tranchessRouter.getSwap(fund.tokenB(), tokenQuote)) + .getQuoteIn(inB); + // Calculate the redemption from QUEEN to underlying + uint256 totalQuoteAmount = IStableSwapCoreInternalRevertExpected( + queenSwapOrPrimaryMarketRouter + ).getQuoteOut(outQ); + // Calculate the rest of quote asset to user + quoteDelta = totalQuoteAmount.sub(quoteAmount); + } + + function buyR( + IFundV5 fund, + address queenSwapOrPrimaryMarketRouter, + uint256 maxQuote, + address recipient, + address tokenQuote, + uint256 version, + uint256 outR + ) external { + (uint256 inQ, uint256 outB) = IPrimaryMarketV5(fund.primaryMarket()).getSplitForR(outR); + // Calculate the exact amount of quote asset to pay + uint256 totalQuoteAmount = IStableSwapCoreInternalRevertExpected( + queenSwapOrPrimaryMarketRouter + ).getQuoteIn(inQ); + // Arrange the stable swap path + IStableSwap tranchessPair = tranchessRouter.getSwap(fund.tokenB(), tokenQuote); + // Calculate the amount of quote asset for selling BISHOP + uint256 quoteAmount = tranchessPair.getQuoteOut(outB); + // Send the user's portion of the payment to Tranchess swap + uint256 resultAmount = totalQuoteAmount.sub(quoteAmount); + require(resultAmount <= maxQuote, "Excessive input"); + IERC20(tokenQuote).safeTransferFrom(msg.sender, address(this), resultAmount); + bytes memory data = abi.encode( + fund, + queenSwapOrPrimaryMarketRouter, + totalQuoteAmount, + recipient, + version + ); + tranchessPair.sell(version, quoteAmount, address(this), data); + emit SwapRook(recipient, 0, resultAmount, outR, 0); + } + + function sellR( + IFundV5 fund, + address queenSwapOrPrimaryMarketRouter, + uint256 minQuote, + address recipient, + address tokenQuote, + uint256 version, + uint256 inR + ) external { + // Calculate merge result of BISHOP and ROOK into QUEEN + (uint256 inB, , ) = IPrimaryMarketV5(fund.primaryMarket()).getMergeForR(inR); + // Send the user's ROOK to this router + fund.trancheTransferFrom(TRANCHE_R, msg.sender, address(this), inR, version); + bytes memory data = abi.encode( + fund, + queenSwapOrPrimaryMarketRouter, + minQuote, + recipient, + version + ); + tranchessRouter.getSwap(fund.tokenB(), tokenQuote).buy(version, inB, address(this), data); + } + + function tranchessSwapCallback( + uint256 baseOut, + uint256 quoteOut, + bytes calldata data + ) external override { + ( + IFundV5 fund, + address queenSwapOrPrimaryMarketRouter, + uint256 expectQuoteAmount, + address recipient, + uint256 version + ) = abi.decode(data, (IFundV5, address, uint256, address, uint256)); + address tokenQuote = IStableSwap(msg.sender).quoteAddress(); + require( + msg.sender == address(tranchessRouter.getSwap(tokenQuote, fund.tokenB())), + "Tranchess Pair check failed" + ); + if (baseOut > 0) { + uint256 resultAmount; + { + require(quoteOut == 0, "Unidirectional check failed"); + uint256 quoteAmount = IStableSwap(msg.sender).getQuoteIn(baseOut); + // Merge BISHOP and ROOK into QUEEN + uint256 outQ = IPrimaryMarketV5(fund.primaryMarket()).merge( + queenSwapOrPrimaryMarketRouter, + baseOut, + version + ); + // Redeem or swap QUEEN for underlying + uint256 totalQuoteAmount = IStableSwapCoreInternalRevertExpected( + queenSwapOrPrimaryMarketRouter + ).sell(version, 0, address(this), ""); + // Send back quote asset to tranchess swap + IERC20(tokenQuote).safeTransfer(msg.sender, quoteAmount); + // Send the rest of quote asset to user + resultAmount = totalQuoteAmount.sub(quoteAmount); + require(resultAmount >= expectQuoteAmount, "Insufficient output"); + IERC20(tokenQuote).safeTransfer(recipient, resultAmount); + } + uint256 weightB = fund.weightB(); + emit SwapRook(recipient, baseOut.div(weightB), 0, 0, resultAmount); + } else { + address tokenUnderlying = fund.tokenUnderlying(); + // Create or swap borrowed underlying for QUEEN + uint256 outQ = IStableSwapCoreInternalRevertExpected(queenSwapOrPrimaryMarketRouter) + .getBaseOut(expectQuoteAmount); + IERC20(tokenUnderlying).safeTransfer(queenSwapOrPrimaryMarketRouter, expectQuoteAmount); + outQ = IStableSwapCoreInternalRevertExpected(queenSwapOrPrimaryMarketRouter).buy( + version, + outQ, + address(this), + "" + ); + // Split QUEEN into BISHOP and ROOK + (uint256 outB, uint256 outR) = IPrimaryMarketV5(fund.primaryMarket()).split( + address(this), + outQ, + version + ); + // Send back BISHOP to tranchess swap + fund.trancheTransfer(TRANCHE_B, msg.sender, outB, version); + // Send ROOK to user + fund.trancheTransfer(TRANCHE_R, recipient, outR, version); + } + } +} From b5791a2b0ca96cb44817f18d61101ec08b4d65df Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Sun, 28 Jan 2024 09:24:23 +0800 Subject: [PATCH 2/9] add flashswap v3 test --- test/flashSwapRouterV3.ts | 335 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 test/flashSwapRouterV3.ts diff --git a/test/flashSwapRouterV3.ts b/test/flashSwapRouterV3.ts new file mode 100644 index 00000000..7de0c121 --- /dev/null +++ b/test/flashSwapRouterV3.ts @@ -0,0 +1,335 @@ +import { expect } from "chai"; +import { BigNumber, Contract, Wallet } from "ethers"; +import type { Fixture, MockProvider } from "ethereum-waffle"; +import { waffle, ethers } from "hardhat"; +const { loadFixture } = waffle; +const { parseEther } = ethers.utils; +import { deployMockForName } from "./mock"; +import { + TRANCHE_Q, + TRANCHE_B, + TRANCHE_R, + DAY, + WEEK, + SETTLEMENT_TIME, + FixtureWalletMap, + advanceBlockAtTime, +} from "./utils"; + +const UNIT = parseEther("1"); +const WEIGHT_B = 9; +const REDEMPTION_FEE_BPS = 100; +const MERGE_FEE_BPS = 75; +const AMPL = 80; +const FEE_RATE = parseEther("0.03"); +const ADMIN_FEE_RATE = parseEther("0.4"); + +const INIT_PRICE = parseEther("1.1"); +const INIT_SPLIT_RATIO = parseEther("0.11"); +const USER_STETH = parseEther("1000"); +const LP_B = parseEther("10000"); +const LP_STETH = parseEther("11000"); + +describe("FlashSwapRouterV3", function () { + this.timeout(60000); + + interface FixtureData { + readonly wallets: FixtureWalletMap; + readonly wsteth: Contract; + readonly fund: Contract; + readonly stableSwap: Contract; + readonly primaryMarketRouter: Contract; + readonly flashSwapRouter: Contract; + } + + let currentFixture: Fixture; + let fixtureData: FixtureData; + + let user1: Wallet; + let user2: Wallet; + let owner: Wallet; + let addr1: string; + let addr2: string; + let wsteth: Contract; + let fund: Contract; + let stableSwap: Contract; + let primaryMarketRouter: Contract; + let flashSwapRouter: Contract; + + async function deployFixture(_wallets: Wallet[], provider: MockProvider): Promise { + const [user1, user2, owner, feeCollector] = provider.getWallets(); + + const startTimestamp = (await ethers.provider.getBlock("latest")).timestamp; + const lastDay = Math.ceil(startTimestamp / WEEK) * WEEK + WEEK + SETTLEMENT_TIME; + const startDay = lastDay + DAY; + await advanceBlockAtTime(lastDay + DAY / 2); + + const MockStETH = await ethers.getContractFactory("MockToken"); + const steth = await MockStETH.connect(owner).deploy("stETH", "stETH", 18); + await steth.mint(user1.address, USER_STETH); + await steth.mint(user2.address, USER_STETH); + + const MockWstETH = await ethers.getContractFactory("MockWstETH"); + const wsteth = await MockWstETH.connect(owner).deploy(steth.address); + await wsteth.update(UNIT); + await steth.connect(user1).approve(wsteth.address, USER_STETH); + await steth.connect(user2).approve(wsteth.address, USER_STETH); + await wsteth.connect(user1).wrap(USER_STETH); + await wsteth.connect(user2).wrap(USER_STETH); + + const twapOracle = await deployMockForName(owner, "ITwapOracleV2"); + await twapOracle.mock.getTwap.withArgs(lastDay).returns(INIT_PRICE); + await twapOracle.mock.getLatest.returns(INIT_PRICE); + const aprOracle = await deployMockForName(owner, "IAprOracle"); + await aprOracle.mock.capture.returns(0); + + const fundAddress = ethers.utils.getContractAddress({ + from: owner.address, + nonce: (await owner.getTransactionCount("pending")) + 3, + }); + const primaryMarketAddress = ethers.utils.getContractAddress({ + from: owner.address, + nonce: (await owner.getTransactionCount("pending")) + 4, + }); + const Share = (await ethers.getContractFactory("ShareV2")).connect(owner); + const shareQ = await Share.deploy("wstETH Queen", "QUEEN", fundAddress, TRANCHE_Q); + const shareB = await Share.deploy("wstETH Bishop", "BISHOP", fundAddress, TRANCHE_B); + const shareR = await Share.deploy("wstETH Rook", "ROOK", fundAddress, TRANCHE_R); + const Fund = await ethers.getContractFactory("FundV5"); + const fund = await Fund.connect(owner).deploy([ + WEIGHT_B, + 365 * DAY, + wsteth.address, + 18, + shareQ.address, + shareB.address, + shareR.address, + primaryMarketAddress, + ethers.constants.AddressZero, + twapOracle.address, + aprOracle.address, + feeCollector.address, + ]); + const PrimaryMarket = await ethers.getContractFactory("PrimaryMarketV5"); + const primaryMarket = await PrimaryMarket.connect(owner).deploy( + fund.address, + parseEther("0.0001").mul(REDEMPTION_FEE_BPS), + parseEther("0.0001").mul(MERGE_FEE_BPS), + BigNumber.from(1).shl(256).sub(1), + true + ); + const PrimaryMarketRouter = await ethers.getContractFactory("PrimaryMarketRouter"); + const primaryMarketRouter = await PrimaryMarketRouter.connect(owner).deploy( + primaryMarket.address + ); + await fund.initialize(INIT_SPLIT_RATIO, parseEther("1"), parseEther("1"), 0); + + const chessSchedule = await deployMockForName(owner, "ChessSchedule"); + await chessSchedule.mock.getRate.returns(UNIT); + const chessController = await deployMockForName(owner, "ChessControllerV5"); + await chessController.mock.getFundRelativeWeight.returns(UNIT); + const votingEscrow = await deployMockForName(owner, "IVotingEscrow"); + await votingEscrow.mock.balanceOf.returns(0); + await votingEscrow.mock.totalSupply.returns(1); + const swapBonus = await deployMockForName(owner, "SwapBonus"); + await swapBonus.mock.bonusToken.returns(ethers.constants.AddressZero); + await swapBonus.mock.getBonus.returns(0); + + const lpTokenAddress = ethers.utils.getContractAddress({ + from: owner.address, + nonce: (await owner.getTransactionCount("pending")) + 1, + }); + const StableSwap = await ethers.getContractFactory("WstETHBishopStableSwap"); + const stableSwap = await StableSwap.connect(owner).deploy( + lpTokenAddress, + fund.address, + wsteth.address, + 18, + AMPL, + feeCollector.address, + FEE_RATE, + ADMIN_FEE_RATE + ); + const LiquidityGauge = await ethers.getContractFactory("LiquidityGauge"); + await LiquidityGauge.connect(owner).deploy( + "LP Token", + "LP", + stableSwap.address, + chessSchedule.address, + chessController.address, + fund.address, + votingEscrow.address, + swapBonus.address, + 0 + ); + + const wstETH = await deployMockForName(owner, "IWstETH"); + await wstETH.mock.stETH.returns(ethers.constants.AddressZero); + const SwapRouter = await ethers.getContractFactory("SwapRouter"); + const swapRouter = await SwapRouter.connect(owner).deploy(wstETH.address); + await swapRouter.addSwap(shareB.address, wsteth.address, stableSwap.address); + const FlashSwapRouter = await ethers.getContractFactory("FlashSwapRouterV3"); + const flashSwapRouter = await FlashSwapRouter.connect(owner).deploy(swapRouter.address); + + await wsteth.connect(user1).approve(primaryMarketRouter.address, USER_STETH); + await wsteth.connect(user2).approve(primaryMarketRouter.address, USER_STETH); + await wsteth.connect(user1).approve(flashSwapRouter.address, USER_STETH); + await wsteth.connect(user2).approve(flashSwapRouter.address, USER_STETH); + + // Add initial liquidity + const initQ = LP_B.mul(UNIT).div(INIT_SPLIT_RATIO); + const initWstETH = initQ; + await steth.mint(owner.address, initWstETH); + await steth.approve(wsteth.address, initWstETH); + await wsteth.wrap(initWstETH); + await wsteth.approve(primaryMarketRouter.address, initWstETH); + await primaryMarketRouter.create(owner.address, initWstETH, 0, 0); + await primaryMarket.split(owner.address, initQ, 0); + await fund.trancheApprove(TRANCHE_B, swapRouter.address, LP_B, 0); + await steth.mint(owner.address, LP_STETH); + await steth.approve(wsteth.address, LP_STETH); + await wsteth.wrap(LP_STETH); + await wsteth.approve(swapRouter.address, LP_STETH); + await swapRouter.addLiquidity( + await fund.tokenShare(TRANCHE_B), + wsteth.address, + LP_B, + LP_STETH, + 0, + 0, + startDay + ); + await wsteth.update(INIT_PRICE); + + return { + wallets: { user1, user2, owner }, + wsteth, + fund: fund.connect(user1), + stableSwap: stableSwap.connect(user1), + primaryMarketRouter: primaryMarketRouter.connect(user1), + flashSwapRouter: flashSwapRouter.connect(user1), + }; + } + + before(function () { + currentFixture = deployFixture; + }); + + beforeEach(async function () { + fixtureData = await loadFixture(currentFixture); + user1 = fixtureData.wallets.user1; + user2 = fixtureData.wallets.user2; + owner = fixtureData.wallets.owner; + addr1 = user1.address; + addr2 = user2.address; + wsteth = fixtureData.wsteth; + fund = fixtureData.fund; + stableSwap = fixtureData.stableSwap; + primaryMarketRouter = fixtureData.primaryMarketRouter; + flashSwapRouter = fixtureData.flashSwapRouter; + }); + + describe("buyR", function () { + const outR = parseEther("1.1"); + const inQ = outR.mul(UNIT).div(INIT_SPLIT_RATIO); + const outB = outR.mul(WEIGHT_B); + const totalQuoteAmount = inQ.mul(1); + let inWstETH: BigNumber; + + beforeEach(async function () { + const quoteAmount = await stableSwap.getQuoteOut(outB); + inWstETH = totalQuoteAmount.sub(quoteAmount); + }); + + it("Should transfer quote and ROOK tokens", async function () { + await expect( + flashSwapRouter.buyR( + fund.address, + primaryMarketRouter.address, + USER_STETH, + addr2, + wsteth.address, + 0, + outR + ) + ) + .to.emit(flashSwapRouter, "SwapRook") + .withArgs(addr2, 0, inWstETH.add(1), outR, 0); + expect(await fund.trancheBalanceOf(TRANCHE_R, addr2)).to.equal(outR); + const spentWstETH = USER_STETH.sub(await wsteth.balanceOf(addr1)); + expect(spentWstETH).to.be.closeTo(inWstETH, inWstETH.div(10000)); + }); + + it("Should check maximum input", async function () { + await expect( + flashSwapRouter.buyR( + fund.address, + primaryMarketRouter.address, + inWstETH.div(2), + addr2, + wsteth.address, + 0, + outR + ) + ).to.be.revertedWith("Excessive input"); + }); + }); + + describe("sellR", function () { + const inR = parseEther("1.1"); + const inB = inR.mul(WEIGHT_B); + const swappedQ = inR + .mul(UNIT) + .div(INIT_SPLIT_RATIO) + .mul(10000 - MERGE_FEE_BPS) + .div(10000) + .add(1); + const totalQuoteAmount = swappedQ + .mul(10000 - REDEMPTION_FEE_BPS) + .div(10000) + .add(1); + let outWstETH: BigNumber; + + beforeEach(async function () { + const quoteAmount = await stableSwap.getQuoteIn(inB); + outWstETH = totalQuoteAmount.sub(quoteAmount); + }); + + it("Should transfer quote and ROOK tokens", async function () { + await fund.connect(owner).trancheTransfer(TRANCHE_R, addr1, inR, 0); + await fund.trancheApprove(TRANCHE_R, flashSwapRouter.address, inR, 0); + await expect( + flashSwapRouter.sellR( + fund.address, + primaryMarketRouter.address, + 0, + addr2, + wsteth.address, + 0, + inR + ) + ) + .to.emit(flashSwapRouter, "SwapRook") + .withArgs(addr2, inR, 0, 0, outWstETH.sub(1)); + expect(await fund.trancheBalanceOf(TRANCHE_R, addr1)).to.equal(0); + const diffWstETH = (await wsteth.balanceOf(addr2)).sub(USER_STETH); + expect(diffWstETH).to.be.closeTo(outWstETH, outWstETH.div(10000)); + }); + + it("Should check minimum output", async function () { + await fund.connect(owner).trancheTransfer(TRANCHE_R, addr1, inR, 0); + await fund.trancheApprove(TRANCHE_R, flashSwapRouter.address, inR, 0); + await expect( + flashSwapRouter.sellR( + fund.address, + primaryMarketRouter.address, + outWstETH.mul(2), + addr2, + wsteth.address, + 0, + inR + ) + ).to.be.revertedWith("Insufficient output"); + }); + }); +}); From 23adaed280b8d75f11f36c5462090e4f13cd3bff Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 31 Jan 2024 08:43:09 +0800 Subject: [PATCH 3/9] fix flashswap sell logic; add back swapcore interface --- contracts/fund/PrimaryMarketV5.sol | 33 ++++++++++++++ contracts/fund/WstETHPrimaryMarketRouter.sol | 48 +++++++++++++++++++- contracts/interfaces/IPrimaryMarketV5.sol | 2 + contracts/swap/FlashSwapRouterV3.sol | 5 +- test/flashSwapRouterV3.ts | 4 +- 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 8e7817c7..2b39e80b 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -163,6 +163,39 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, underlying = _getRedemption(inQ - feeQ); } + /// @notice Calculate the amount of QUEEN that can be redeemed for at least the given amount + /// of underlying tokens. + /// @dev The return value may not be the minimum solution due to rounding errors. + /// @param minUnderlying Minimum received underlying amount + /// @return inQ QUEEN amount that should be redeemed + function getRedemptionForUnderlying( + uint256 minUnderlying + ) external view override returns (uint256 inQ) { + // Assume: + // minUnderlying * fundEquivalentTotalQ = a * fundUnderlying - b + // a * 1e18 = c * (1e18 - redemptionFeeRate) + d + // where + // a, b, c, d are integers + // 0 <= b < fundUnderlying + // 0 <= d < 1e18 - redemeptionFeeRate + // Then + // inQAfterFee = a + // inQ = c + // getRedemption(inQ).underlying + // = floor((c - floor(c * redemptionFeeRate / 1e18)) * fundUnderlying / fundEquivalentTotalQ) + // = floor(ceil(c * (1e18 - redemptionFeeRate) / 1e18) * fundUnderlying / fundEquivalentTotalQ) + // = floor(((c * (1e18 - redemptionFeeRate) + d) / 1e18) * fundUnderlying / fundEquivalentTotalQ) + // = floor(a * fundUnderlying / fundEquivalentTotalQ) + // => floor((a * fundUnderlying - b) / fundEquivalentTotalQ) + // = minUnderlying + uint256 fundUnderlying = IFundV3(fund).getTotalUnderlying(); + uint256 fundEquivalentTotalQ = IFundV3(fund).getEquivalentTotalQ(); + uint256 inQAfterFee = minUnderlying.mul(fundEquivalentTotalQ).add(fundUnderlying - 1).div( + fundUnderlying + ); + return inQAfterFee.divideDecimal(1e18 - redemptionFeeRate); + } + /// @notice Calculate the result of a split. /// @param inQ QUEEN amount to be split /// @return outB Received BISHOP amount diff --git a/contracts/fund/WstETHPrimaryMarketRouter.sol b/contracts/fund/WstETHPrimaryMarketRouter.sol index 96e4cd0a..a3b71c24 100644 --- a/contracts/fund/WstETHPrimaryMarketRouter.sol +++ b/contracts/fund/WstETHPrimaryMarketRouter.sol @@ -9,8 +9,9 @@ import "../interfaces/IPrimaryMarketV5.sol"; import "../interfaces/IFundV3.sol"; import "../interfaces/IWstETH.sol"; import "../interfaces/ITrancheIndexV2.sol"; +import "../interfaces/IStableSwap.sol"; -contract WstETHPrimaryMarketRouter is ITrancheIndexV2 { +contract WstETHPrimaryMarketRouter is IStableSwapCore, ITrancheIndexV2 { using SafeERC20 for IERC20; IPrimaryMarketV5 public immutable primaryMarket; @@ -28,6 +29,51 @@ contract WstETHPrimaryMarketRouter is ITrancheIndexV2 { _tokenB = fund_.tokenB(); } + /// @dev Get redemption with StableSwap getQuoteOut interface. + function getQuoteOut(uint256 baseIn) external view override returns (uint256 quoteOut) { + (quoteOut, ) = primaryMarket.getRedemption(baseIn); + } + + /// @dev Get creation for QUEEN with StableSwap getQuoteIn interface. + function getQuoteIn(uint256 baseOut) external view override returns (uint256 quoteIn) { + quoteIn = primaryMarket.getCreationForQ(baseOut); + } + + /// @dev Get creation with StableSwap getBaseOut interface. + function getBaseOut(uint256 quoteIn) external view override returns (uint256 baseOut) { + baseOut = primaryMarket.getCreation(quoteIn); + } + + /// @dev Get redemption for underlying with StableSwap getBaseIn interface. + function getBaseIn(uint256 quoteOut) external view override returns (uint256 baseIn) { + baseIn = primaryMarket.getRedemptionForUnderlying(quoteOut); + } + + /// @dev Create QUEEN with StableSwap buy interface. + /// Underlying should have already been sent to this contract + function buy( + uint256 version, + uint256 baseOut, + address recipient, + bytes calldata + ) external override returns (uint256 realBaseOut) { + uint256 routerQuoteBalance = IERC20(_wstETH).balanceOf(address(this)); + IERC20(_wstETH).safeTransfer(address(primaryMarket), routerQuoteBalance); + realBaseOut = primaryMarket.create(recipient, baseOut, version); + } + + /// @dev Redeem QUEEN with StableSwap sell interface. + /// QUEEN should have already been sent to this contract + function sell( + uint256 version, + uint256 quoteOut, + address recipient, + bytes calldata + ) external override returns (uint256 realQuoteOut) { + uint256 routerBaseBalance = fund.trancheBalanceOf(TRANCHE_Q, address(this)); + realQuoteOut = primaryMarket.redeem(recipient, routerBaseBalance, quoteOut, version); + } + function create( address recipient, bool needWrap, diff --git a/contracts/interfaces/IPrimaryMarketV5.sol b/contracts/interfaces/IPrimaryMarketV5.sol index df2e1d6e..f69d4a4b 100644 --- a/contracts/interfaces/IPrimaryMarketV5.sol +++ b/contracts/interfaces/IPrimaryMarketV5.sol @@ -10,6 +10,8 @@ interface IPrimaryMarketV5 { function getRedemption(uint256 inQ) external view returns (uint256 underlying, uint256 fee); + function getRedemptionForUnderlying(uint256 minUnderlying) external view returns (uint256 inQ); + function getSplit(uint256 inQ) external view returns (uint256 outB, uint256 outR); function getSplitForR(uint256 minOutR) external view returns (uint256 inQ, uint256 outB); diff --git a/contracts/swap/FlashSwapRouterV3.sol b/contracts/swap/FlashSwapRouterV3.sol index e47308a8..e6d85392 100644 --- a/contracts/swap/FlashSwapRouterV3.sol +++ b/contracts/swap/FlashSwapRouterV3.sol @@ -159,9 +159,12 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { version ); // Redeem or swap QUEEN for underlying + uint256 underlyingAmount = IStableSwapCoreInternalRevertExpected( + queenSwapOrPrimaryMarketRouter + ).getQuoteOut(outQ); uint256 totalQuoteAmount = IStableSwapCoreInternalRevertExpected( queenSwapOrPrimaryMarketRouter - ).sell(version, 0, address(this), ""); + ).sell(version, underlyingAmount, address(this), ""); // Send back quote asset to tranchess swap IERC20(tokenQuote).safeTransfer(msg.sender, quoteAmount); // Send the rest of quote asset to user diff --git a/test/flashSwapRouterV3.ts b/test/flashSwapRouterV3.ts index 7de0c121..f6361789 100644 --- a/test/flashSwapRouterV3.ts +++ b/test/flashSwapRouterV3.ts @@ -118,7 +118,7 @@ describe("FlashSwapRouterV3", function () { BigNumber.from(1).shl(256).sub(1), true ); - const PrimaryMarketRouter = await ethers.getContractFactory("PrimaryMarketRouter"); + const PrimaryMarketRouter = await ethers.getContractFactory("WstETHPrimaryMarketRouter"); const primaryMarketRouter = await PrimaryMarketRouter.connect(owner).deploy( primaryMarket.address ); @@ -183,7 +183,7 @@ describe("FlashSwapRouterV3", function () { await steth.approve(wsteth.address, initWstETH); await wsteth.wrap(initWstETH); await wsteth.approve(primaryMarketRouter.address, initWstETH); - await primaryMarketRouter.create(owner.address, initWstETH, 0, 0); + await primaryMarketRouter.create(owner.address, false, initWstETH, 0, 0); await primaryMarket.split(owner.address, initQ, 0); await fund.trancheApprove(TRANCHE_B, swapRouter.address, LP_B, 0); await steth.mint(owner.address, LP_STETH); From 8d227ec6de85f03f9727fc7154327d062c156794 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 31 Jan 2024 09:42:43 +0800 Subject: [PATCH 4/9] add unwrapped mode --- contracts/swap/FlashSwapRouterV3.sol | 17 ++++++++++++++-- test/flashSwapRouterV3.ts | 30 +++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/contracts/swap/FlashSwapRouterV3.sol b/contracts/swap/FlashSwapRouterV3.sol index e6d85392..be3ba2f8 100644 --- a/contracts/swap/FlashSwapRouterV3.sol +++ b/contracts/swap/FlashSwapRouterV3.sol @@ -10,6 +10,7 @@ import "../interfaces/IFundV5.sol"; import "../interfaces/IPrimaryMarketV5.sol"; import "../interfaces/ISwapRouter.sol"; import "../interfaces/ITrancheIndexV2.sol"; +import "../interfaces/IWstETH.sol"; /// @title Tranchess Flash Swap Router /// @notice Router for stateless execution of flash swaps against Tranchess stable swaps @@ -34,6 +35,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { /// @dev Only meant for an off-chain client to call with eth_call. function getBuyR( IFundV5 fund, + bool needWrap, address queenSwapOrPrimaryMarketRouter, address tokenQuote, uint256 outR @@ -76,6 +78,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { function buyR( IFundV5 fund, + bool needWrap, address queenSwapOrPrimaryMarketRouter, uint256 maxQuote, address recipient, @@ -94,8 +97,18 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { uint256 quoteAmount = tranchessPair.getQuoteOut(outB); // Send the user's portion of the payment to Tranchess swap uint256 resultAmount = totalQuoteAmount.sub(quoteAmount); - require(resultAmount <= maxQuote, "Excessive input"); - IERC20(tokenQuote).safeTransferFrom(msg.sender, address(this), resultAmount); + if (needWrap) { + address stETH = IWstETH(tokenQuote).stETH(); + uint256 unwrappedAmount = IWstETH(tokenQuote).getStETHByWstETH(resultAmount).add(1); + require(unwrappedAmount <= maxQuote, "Excessive input"); + IERC20(stETH).safeTransferFrom(msg.sender, address(this), unwrappedAmount); + IERC20(stETH).approve(tokenQuote, unwrappedAmount); + resultAmount = IWstETH(tokenQuote).wrap(unwrappedAmount); + totalQuoteAmount = quoteAmount.add(resultAmount); + } else { + require(resultAmount <= maxQuote, "Excessive input"); + IERC20(tokenQuote).safeTransferFrom(msg.sender, address(this), resultAmount); + } bytes memory data = abi.encode( fund, queenSwapOrPrimaryMarketRouter, diff --git a/test/flashSwapRouterV3.ts b/test/flashSwapRouterV3.ts index f6361789..9869c8b1 100644 --- a/test/flashSwapRouterV3.ts +++ b/test/flashSwapRouterV3.ts @@ -35,6 +35,7 @@ describe("FlashSwapRouterV3", function () { interface FixtureData { readonly wallets: FixtureWalletMap; + readonly steth: Contract; readonly wsteth: Contract; readonly fund: Contract; readonly stableSwap: Contract; @@ -50,6 +51,7 @@ describe("FlashSwapRouterV3", function () { let owner: Wallet; let addr1: string; let addr2: string; + let steth: Contract; let wsteth: Contract; let fund: Contract; let stableSwap: Contract; @@ -66,7 +68,7 @@ describe("FlashSwapRouterV3", function () { const MockStETH = await ethers.getContractFactory("MockToken"); const steth = await MockStETH.connect(owner).deploy("stETH", "stETH", 18); - await steth.mint(user1.address, USER_STETH); + await steth.mint(user1.address, USER_STETH.mul(2)); await steth.mint(user2.address, USER_STETH); const MockWstETH = await ethers.getContractFactory("MockWstETH"); @@ -175,6 +177,7 @@ describe("FlashSwapRouterV3", function () { await wsteth.connect(user2).approve(primaryMarketRouter.address, USER_STETH); await wsteth.connect(user1).approve(flashSwapRouter.address, USER_STETH); await wsteth.connect(user2).approve(flashSwapRouter.address, USER_STETH); + await steth.connect(user1).approve(flashSwapRouter.address, USER_STETH); // Add initial liquidity const initQ = LP_B.mul(UNIT).div(INIT_SPLIT_RATIO); @@ -203,6 +206,7 @@ describe("FlashSwapRouterV3", function () { return { wallets: { user1, user2, owner }, + steth, wsteth, fund: fund.connect(user1), stableSwap: stableSwap.connect(user1), @@ -222,6 +226,7 @@ describe("FlashSwapRouterV3", function () { owner = fixtureData.wallets.owner; addr1 = user1.address; addr2 = user2.address; + steth = fixtureData.steth; wsteth = fixtureData.wsteth; fund = fixtureData.fund; stableSwap = fixtureData.stableSwap; @@ -245,6 +250,7 @@ describe("FlashSwapRouterV3", function () { await expect( flashSwapRouter.buyR( fund.address, + false, primaryMarketRouter.address, USER_STETH, addr2, @@ -260,10 +266,32 @@ describe("FlashSwapRouterV3", function () { expect(spentWstETH).to.be.closeTo(inWstETH, inWstETH.div(10000)); }); + it("Should transfer unwrapped quote and ROOK tokens", async function () { + const inStETH = inWstETH.mul(INIT_PRICE).div(UNIT); + await expect( + flashSwapRouter.buyR( + fund.address, + true, + primaryMarketRouter.address, + USER_STETH, + addr2, + wsteth.address, + 0, + outR + ) + ) + .to.emit(flashSwapRouter, "SwapRook") + .withArgs(addr2, 0, inWstETH.add(1), outR, 0); + expect(await fund.trancheBalanceOf(TRANCHE_R, addr2)).to.equal(outR); + const spentStETH = USER_STETH.sub(await steth.balanceOf(addr1)); + expect(spentStETH).to.be.closeTo(inStETH, inStETH.div(10000)); + }); + it("Should check maximum input", async function () { await expect( flashSwapRouter.buyR( fund.address, + false, primaryMarketRouter.address, inWstETH.div(2), addr2, From 536e1eee5e6b54b95abae4a7a4c3f64c24c87f92 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 31 Jan 2024 10:42:52 +0800 Subject: [PATCH 5/9] add sell unwrap mode --- contracts/swap/FlashSwapRouterV3.sol | 16 ++++++++++++---- test/flashSwapRouterV3.ts | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/contracts/swap/FlashSwapRouterV3.sol b/contracts/swap/FlashSwapRouterV3.sol index be3ba2f8..8f469766 100644 --- a/contracts/swap/FlashSwapRouterV3.sol +++ b/contracts/swap/FlashSwapRouterV3.sol @@ -122,6 +122,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { function sellR( IFundV5 fund, + bool needUnwrap, address queenSwapOrPrimaryMarketRouter, uint256 minQuote, address recipient, @@ -141,6 +142,16 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { version ); tranchessRouter.getSwap(fund.tokenB(), tokenQuote).buy(version, inB, address(this), data); + // Send the rest of quote asset to user + uint256 resultAmount = IERC20(tokenQuote).balanceOf(address(this)); + if (needUnwrap) { + uint256 unwrappedAmount = IWstETH(tokenQuote).unwrap(resultAmount); + require(unwrappedAmount >= minQuote, "Insufficient output"); + IERC20(IWstETH(tokenQuote).stETH()).safeTransfer(recipient, unwrappedAmount); + } else { + require(resultAmount >= minQuote, "Insufficient output"); + IERC20(tokenQuote).safeTransfer(recipient, resultAmount); + } } function tranchessSwapCallback( @@ -180,10 +191,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { ).sell(version, underlyingAmount, address(this), ""); // Send back quote asset to tranchess swap IERC20(tokenQuote).safeTransfer(msg.sender, quoteAmount); - // Send the rest of quote asset to user - resultAmount = totalQuoteAmount.sub(quoteAmount); - require(resultAmount >= expectQuoteAmount, "Insufficient output"); - IERC20(tokenQuote).safeTransfer(recipient, resultAmount); + resultAmount = IERC20(tokenQuote).balanceOf(address(this)); } uint256 weightB = fund.weightB(); emit SwapRook(recipient, baseOut.div(weightB), 0, 0, resultAmount); diff --git a/test/flashSwapRouterV3.ts b/test/flashSwapRouterV3.ts index 9869c8b1..3f8c2709 100644 --- a/test/flashSwapRouterV3.ts +++ b/test/flashSwapRouterV3.ts @@ -329,6 +329,7 @@ describe("FlashSwapRouterV3", function () { await expect( flashSwapRouter.sellR( fund.address, + false, primaryMarketRouter.address, 0, addr2, @@ -344,12 +345,35 @@ describe("FlashSwapRouterV3", function () { expect(diffWstETH).to.be.closeTo(outWstETH, outWstETH.div(10000)); }); + it("Should transfer unwrapped quote and ROOK tokens", async function () { + const outStETH = outWstETH.mul(INIT_PRICE).div(UNIT); + await fund.connect(owner).trancheTransfer(TRANCHE_R, addr1, inR, 0); + await fund.trancheApprove(TRANCHE_R, flashSwapRouter.address, inR, 0); + await expect( + flashSwapRouter.sellR( + fund.address, + true, + primaryMarketRouter.address, + 0, + addr2, + wsteth.address, + 0, + inR + ) + ) + .to.emit(flashSwapRouter, "SwapRook") + .withArgs(addr2, inR, 0, 0, outWstETH.sub(1)); + expect(await fund.trancheBalanceOf(TRANCHE_R, addr1)).to.equal(0); + expect(await steth.balanceOf(addr2)).to.be.closeTo(outStETH, outStETH.div(10000)); + }); + it("Should check minimum output", async function () { await fund.connect(owner).trancheTransfer(TRANCHE_R, addr1, inR, 0); await fund.trancheApprove(TRANCHE_R, flashSwapRouter.address, inR, 0); await expect( flashSwapRouter.sellR( fund.address, + false, primaryMarketRouter.address, outWstETH.mul(2), addr2, From e36ed3cca79bbf1299a200605d4acce54bc8192a Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Wed, 31 Jan 2024 11:13:45 +0800 Subject: [PATCH 6/9] add wrap mode for getters --- contracts/swap/FlashSwapRouterV3.sol | 52 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/contracts/swap/FlashSwapRouterV3.sol b/contracts/swap/FlashSwapRouterV3.sol index 8f469766..2919539c 100644 --- a/contracts/swap/FlashSwapRouterV3.sol +++ b/contracts/swap/FlashSwapRouterV3.sol @@ -49,6 +49,9 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { uint256 quoteAmount = tranchessPair.getQuoteOut(outB); // Calculate the user's portion of the payment to Tranchess swap quoteDelta = totalQuoteAmount.sub(quoteAmount); + if (needWrap) { + quoteDelta = IWstETH(tokenQuote).getStETHByWstETH(quoteDelta).add(1); + } // Calculate creation of borrowed underlying for QUEEN uint256 outQ = IStableSwapCoreInternalRevertExpected(queenSwapOrPrimaryMarketRouter) .getBaseOut(totalQuoteAmount); @@ -59,6 +62,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { /// @dev Only meant for an off-chain client to call with eth_call. function getSellR( IFundV5 fund, + bool needUnwrap, address queenSwapOrPrimaryMarketRouter, address tokenQuote, uint256 inR @@ -74,6 +78,9 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { ).getQuoteOut(outQ); // Calculate the rest of quote asset to user quoteDelta = totalQuoteAmount.sub(quoteAmount); + if (needUnwrap) { + quoteDelta = IWstETH(tokenQuote).getStETHByWstETH(quoteDelta); + } } function buyR( @@ -152,6 +159,8 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { require(resultAmount >= minQuote, "Insufficient output"); IERC20(tokenQuote).safeTransfer(recipient, resultAmount); } + uint256 weightB = fund.weightB(); + emit SwapRook(recipient, inB.div(weightB), 0, 0, resultAmount); } function tranchessSwapCallback( @@ -172,29 +181,26 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { "Tranchess Pair check failed" ); if (baseOut > 0) { - uint256 resultAmount; - { - require(quoteOut == 0, "Unidirectional check failed"); - uint256 quoteAmount = IStableSwap(msg.sender).getQuoteIn(baseOut); - // Merge BISHOP and ROOK into QUEEN - uint256 outQ = IPrimaryMarketV5(fund.primaryMarket()).merge( - queenSwapOrPrimaryMarketRouter, - baseOut, - version - ); - // Redeem or swap QUEEN for underlying - uint256 underlyingAmount = IStableSwapCoreInternalRevertExpected( - queenSwapOrPrimaryMarketRouter - ).getQuoteOut(outQ); - uint256 totalQuoteAmount = IStableSwapCoreInternalRevertExpected( - queenSwapOrPrimaryMarketRouter - ).sell(version, underlyingAmount, address(this), ""); - // Send back quote asset to tranchess swap - IERC20(tokenQuote).safeTransfer(msg.sender, quoteAmount); - resultAmount = IERC20(tokenQuote).balanceOf(address(this)); - } - uint256 weightB = fund.weightB(); - emit SwapRook(recipient, baseOut.div(weightB), 0, 0, resultAmount); + require(quoteOut == 0, "Unidirectional check failed"); + uint256 quoteAmount = IStableSwap(msg.sender).getQuoteIn(baseOut); + // Merge BISHOP and ROOK into QUEEN + uint256 outQ = IPrimaryMarketV5(fund.primaryMarket()).merge( + queenSwapOrPrimaryMarketRouter, + baseOut, + version + ); + // Redeem or swap QUEEN for underlying + uint256 underlyingAmount = IStableSwapCoreInternalRevertExpected( + queenSwapOrPrimaryMarketRouter + ).getQuoteOut(outQ); + IStableSwapCoreInternalRevertExpected(queenSwapOrPrimaryMarketRouter).sell( + version, + underlyingAmount, + address(this), + "" + ); + // Send back quote asset to tranchess swap + IERC20(tokenQuote).safeTransfer(msg.sender, quoteAmount); } else { address tokenUnderlying = fund.tokenUnderlying(); // Create or swap borrowed underlying for QUEEN From efdc65033807c81cc8aab8db57cd8651faa2ae33 Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Thu, 1 Feb 2024 07:39:12 +0800 Subject: [PATCH 7/9] add error term to wrapping swap --- contracts/swap/WstETHWrappingSwap.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/swap/WstETHWrappingSwap.sol b/contracts/swap/WstETHWrappingSwap.sol index b29dd670..3a72862f 100644 --- a/contracts/swap/WstETHWrappingSwap.sol +++ b/contracts/swap/WstETHWrappingSwap.sol @@ -3,12 +3,14 @@ pragma solidity >=0.6.10 <0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; import "../interfaces/IStableSwap.sol"; import "../interfaces/IWstETH.sol"; import "../utils/SafeDecimalMath.sol"; contract WstETHWrappingSwap is IStableSwap { using SafeERC20 for IERC20; + using SafeMath for uint256; using SafeDecimalMath for uint256; address public immutable wstETH; // Base @@ -24,7 +26,7 @@ contract WstETHWrappingSwap is IStableSwap { } function getQuoteIn(uint256 baseOut) external view override returns (uint256 quoteIn) { - quoteIn = IWstETH(wstETH).getStETHByWstETH(baseOut); + quoteIn = IWstETH(wstETH).getStETHByWstETH(baseOut).add(1); } function getBaseOut(uint256 quoteIn) external view override returns (uint256 baseOut) { @@ -32,7 +34,7 @@ contract WstETHWrappingSwap is IStableSwap { } function getBaseIn(uint256 quoteOut) external view override returns (uint256 baseIn) { - baseIn = IWstETH(wstETH).getWstETHByStETH(quoteOut); + baseIn = IWstETH(wstETH).getWstETHByStETH(quoteOut).add(1); } function buy( From 3800f0dd18a44c5cc2fcc889f7a40f0f1a6c769c Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Thu, 1 Feb 2024 11:44:46 +0800 Subject: [PATCH 8/9] revert when fund frozen for getMergeByR --- contracts/fund/PrimaryMarketV5.sol | 3 ++- contracts/interfaces/IPrimaryMarketV5.sol | 2 +- contracts/swap/FlashSwapRouterV3.sol | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/fund/PrimaryMarketV5.sol b/contracts/fund/PrimaryMarketV5.sol index 2b39e80b..71f3eaf5 100644 --- a/contracts/fund/PrimaryMarketV5.sol +++ b/contracts/fund/PrimaryMarketV5.sol @@ -242,9 +242,10 @@ contract PrimaryMarketV5 is IPrimaryMarketV5, ReentrancyGuard, ITrancheIndexV2, /// @return inB Spent BISHOP amount /// @return outQ Received QUEEN amount /// @return feeQ QUEEN amount charged as merge fee - function getMergeForR( + function getMergeByR( uint256 inR ) public view override returns (uint256 inB, uint256 outQ, uint256 feeQ) { + require(!IFundV5(fund).frozen(), "Fund frozen"); inB = inR.mul(_weightB); uint256 splitRatio = IFundV5(fund).splitRatio(); uint256 outQBeforeFee = inR.divideDecimal(splitRatio); diff --git a/contracts/interfaces/IPrimaryMarketV5.sol b/contracts/interfaces/IPrimaryMarketV5.sol index f69d4a4b..0def3e8d 100644 --- a/contracts/interfaces/IPrimaryMarketV5.sol +++ b/contracts/interfaces/IPrimaryMarketV5.sol @@ -18,7 +18,7 @@ interface IPrimaryMarketV5 { function getMerge(uint256 inB) external view returns (uint256 inR, uint256 outQ, uint256 feeQ); - function getMergeForR( + function getMergeByR( uint256 inR ) external view returns (uint256 inB, uint256 outQ, uint256 feeQ); diff --git a/contracts/swap/FlashSwapRouterV3.sol b/contracts/swap/FlashSwapRouterV3.sol index 2919539c..3bc4147e 100644 --- a/contracts/swap/FlashSwapRouterV3.sol +++ b/contracts/swap/FlashSwapRouterV3.sol @@ -69,7 +69,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { ) external returns (uint256 quoteDelta, uint256 rookDelta) { rookDelta = inR; // Calculate merge result of BISHOP and ROOK into QUEEN - (uint256 inB, uint256 outQ, ) = IPrimaryMarketV5(fund.primaryMarket()).getMergeForR(inR); + (uint256 inB, uint256 outQ, ) = IPrimaryMarketV5(fund.primaryMarket()).getMergeByR(inR); uint256 quoteAmount = IStableSwap(tranchessRouter.getSwap(fund.tokenB(), tokenQuote)) .getQuoteIn(inB); // Calculate the redemption from QUEEN to underlying @@ -138,7 +138,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { uint256 inR ) external { // Calculate merge result of BISHOP and ROOK into QUEEN - (uint256 inB, , ) = IPrimaryMarketV5(fund.primaryMarket()).getMergeForR(inR); + (uint256 inB, , ) = IPrimaryMarketV5(fund.primaryMarket()).getMergeByR(inR); // Send the user's ROOK to this router fund.trancheTransferFrom(TRANCHE_R, msg.sender, address(this), inR, version); bytes memory data = abi.encode( From 6802af68d494c338a1fbda895b9f1131140eb2ac Mon Sep 17 00:00:00 2001 From: Bill Clippy Date: Fri, 2 Feb 2024 01:52:16 +0800 Subject: [PATCH 9/9] log swapRook event with inR --- contracts/swap/FlashSwapRouterV3.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/swap/FlashSwapRouterV3.sol b/contracts/swap/FlashSwapRouterV3.sol index 3bc4147e..58b816e4 100644 --- a/contracts/swap/FlashSwapRouterV3.sol +++ b/contracts/swap/FlashSwapRouterV3.sol @@ -159,8 +159,7 @@ contract FlashSwapRouterV3 is ITranchessSwapCallee, ITrancheIndexV2, Ownable { require(resultAmount >= minQuote, "Insufficient output"); IERC20(tokenQuote).safeTransfer(recipient, resultAmount); } - uint256 weightB = fund.weightB(); - emit SwapRook(recipient, inB.div(weightB), 0, 0, resultAmount); + emit SwapRook(recipient, inR, 0, 0, resultAmount); } function tranchessSwapCallback(