diff --git a/spot-contracts/contracts/_utils/BondHelpers.sol b/spot-contracts/contracts/_utils/BondHelpers.sol index 7a2fafc7..1a7245e1 100644 --- a/spot-contracts/contracts/_utils/BondHelpers.sol +++ b/spot-contracts/contracts/_utils/BondHelpers.sol @@ -210,24 +210,35 @@ library BondHelpers { BondTranches memory bt = getTranches(b); uint256[] memory redeemableAmts = new uint256[](bt.tranches.length); - // Calculate how many underlying assets could be redeemed from each tranche balance, + // We Calculate how many underlying assets could be redeemed from each tranche balance, // assuming other tranches are not an issue, and record the smallest amount. - uint256 minUnderlyingOut = type(uint256).max; + // + // Usually one tranche balance is the limiting factor, we first loop through to identify + // it by figuring out the one which has the least `trancheBalance/trancheRatio`. + // + uint256 minBalanceToTrancheRatio = type(uint256).max; uint8 i; for (i = 0; i < bt.tranches.length; i++) { - uint256 d = bt.tranches[i].balanceOf(u).mulDiv(TRANCHE_RATIO_GRANULARITY, bt.trancheRatios[i]); - if (d < minUnderlyingOut) { - minUnderlyingOut = d; + // NOTE: We round the avaiable balance down to the nearest multiple of the + // tranche ratio. This ensures that `minBalanceToTrancheRatio` + // can be represented without loss as a fixedPt number. + uint256 bal = bt.tranches[i].balanceOf(u); + bal = bal - (bal % bt.trancheRatios[i]); + + uint256 d = bal.mulDiv(TRANCHE_RATIO_GRANULARITY, bt.trancheRatios[i]); + if (d < minBalanceToTrancheRatio) { + minBalanceToTrancheRatio = d; } // if one of the balances is zero, we return - if (minUnderlyingOut == 0) { + if (minBalanceToTrancheRatio == 0) { return (bt, redeemableAmts); } } + // Now that we have `minBalanceToTrancheRatio`, we compute the redeemable amounts. for (i = 0; i < bt.tranches.length; i++) { - redeemableAmts[i] = bt.trancheRatios[i].mulDiv(minUnderlyingOut, TRANCHE_RATIO_GRANULARITY); + redeemableAmts[i] = bt.trancheRatios[i].mulDiv(minBalanceToTrancheRatio, TRANCHE_RATIO_GRANULARITY); } return (bt, redeemableAmts); diff --git a/spot-contracts/test/_utils/BondHelpers.ts b/spot-contracts/test/_utils/BondHelpers.ts index f2ed5e27..4191fdf8 100644 --- a/spot-contracts/test/_utils/BondHelpers.ts +++ b/spot-contracts/test/_utils/BondHelpers.ts @@ -427,14 +427,14 @@ describe("BondHelpers", function () { }); describe("#computeRedeemableTrancheAmounts", function () { - let bond: Contract, bondLength: number; - beforeEach(async function () { - bondLength = 86400; - bond = await createBondWithFactory(bondFactory, collateralToken, [200, 300, 500], bondLength); - await depositIntoBond(bond, toFixedPtAmt("1000"), deployer); - }); - describe("when the user has all the tranches in the right proportions", function () { + let bond: Contract, bondLength: number; + beforeEach(async function () { + bondLength = 86400; + bond = await createBondWithFactory(bondFactory, collateralToken, [200, 300, 500], bondLength); + await depositIntoBond(bond, toFixedPtAmt("1000"), deployer); + }); + describe("when the user has the entire supply", function () { it("should calculate the amounts", async function () { const b = await bondHelpers.computeRedeemableTrancheAmounts(bond.address, deployerAddress); @@ -467,66 +467,130 @@ describe("BondHelpers", function () { describe("when the user does not have tranches right proportions", function () { async function checkRedeemableAmts( - bond: Contract, - userAddress: string, + trancheRatios: number[] = [], amounts: string[] = [], redemptionAmts: string[] = [], ) { + const bond = await createBondWithFactory(bondFactory, collateralToken, trancheRatios, 86400); + const amt = amounts + .map((a, i) => toFixedPtAmt(a).mul("1000").div(trancheRatios[i])) + .reduce((m, a) => (m.gt(a) ? m : a), toFixedPtAmt("0")); + await depositIntoBond(bond, amt.add(toFixedPtAmt("1")), deployer); + const tranches = await getTranches(bond); for (const a in amounts) { await tranches[a].transfer(userAddress, toFixedPtAmt(amounts[a])); } const b = await bondHelpers.computeRedeemableTrancheAmounts(bond.address, userAddress); + if (b[1][0].gt("0")) { + await bond.connect(user).redeem(b[1]); + } for (const a in redemptionAmts) { expect(b[1][a]).to.eq(toFixedPtAmt(redemptionAmts[a])); } } - describe("[9, 15, 25]", async function () { + describe("[200,300,500]:[9, 15, 25]", async function () { it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["9", "15", "25"], ["9", "13.5", "22.5"]); + await checkRedeemableAmts([200, 300, 500], ["9", "15", "25"], ["9", "13.5", "22.5"]); }); }); - describe("[10, 15, 250]", async function () { + describe("[200,300,500]:[10, 15, 250]", async function () { it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["10", "15", "250"], ["10", "15", "25"]); + await checkRedeemableAmts([200, 300, 500], ["10", "15", "250"], ["10", "15", "25"]); }); }); - describe("[10, 12, 250]", async function () { + describe("[200,300,500]:[10, 12, 250]", async function () { it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["10", "12", "250"], ["8", "12", "20"]); + await checkRedeemableAmts([200, 300, 500], ["10", "12", "250"], ["8", "12", "20"]); }); }); - describe("[10, 12, 5]", async function () { + describe("[200,300,500]:[10, 12, 5]", async function () { it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["10", "12", "5"], ["2", "3", "5"]); + await checkRedeemableAmts([200, 300, 500], ["10", "12", "5"], ["2", "3", "5"]); }); }); - describe("[10, 12, 0.5]", async function () { + describe("[200,300,500]:[10, 12, 0.5]", async function () { it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["10", "12", "0.5"], ["0.2", "0.3", "0.5"]); + await checkRedeemableAmts([200, 300, 500], ["10", "12", "0.5"], ["0.2", "0.3", "0.5"]); }); }); - describe("[10, 0, 25]", async function () { + describe("[200,300,500]:[10, 0, 25]", async function () { it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["10", "0", "25"], ["0", "0", "0"]); + await checkRedeemableAmts([200, 300, 500], ["10", "0", "25"], ["0", "0", "0"]); }); }); - describe("[0, 15, 25]", async function () { + describe("[200,300,500]:[0, 15, 25]", async function () { it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["0", "15", "25"], ["0", "0", "0"]); + await checkRedeemableAmts([200, 300, 500], ["0", "15", "25"], ["0", "0", "0"]); }); }); - describe("[10, 15, 0]", async function () { - it("should calculate the amounts", async function () { - await checkRedeemableAmts(bond, userAddress, ["10", "15", "0"], ["0", "0", "0"]); + describe("imperfect rounding", function () { + describe("[200,300,500]:[10, 15, 7.461048491123254231]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts( + [200, 300, 500], + ["10", "15", "7.461048491123254230"], + ["2.984419396449301600", "4.476629094673952400", "7.461048491123254000"], + ); + }); + }); + + describe("[200,300,500]:[1000e-18,5001e-18,503e-18]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts( + [200, 300, 500], + ["1000e-18", "5001e-18", "503e-18"], + ["200e-18", "300e-18", "500e-18"], + ); + }); + }); + + describe("[200,300,500]:[1000e-18,5001e-18,506e-18]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts( + [200, 300, 500], + ["1000e-18", "5001e-18", "506e-18"], + ["200e-18", "300e-18", "500e-18"], + ); + }); + }); + + describe("[1,999]:[1000e-18,2001e-18]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts([1, 999], ["1000e-18", "2001e-18"], ["2e-18", "1998e-18"]); + }); + }); + + describe("[1,999]:[5e-18,1]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts([1, 999], ["5e-18", "1"], ["5e-18", "4995e-18"]); + }); + }); + + describe("[499,501]:[1232e-18,1]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts([499, 501], ["1232e-18", "1"], ["998e-18", "1002e-18"]); + }); + }); + + describe("[499,501]:[1,499e-18]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts([499, 501], ["1", "499e-18"], ["0", "0"]); + }); + }); + + describe("[499,501]:[13224e-18]", async function () { + it("should calculate the amounts", async function () { + await checkRedeemableAmts([499, 501], ["1", "1322e-18"], ["998e-18", "1002e-18"]); + }); }); }); }); diff --git a/spot-contracts/test/helpers.ts b/spot-contracts/test/helpers.ts index 49c9f0d2..7b4bf8d9 100644 --- a/spot-contracts/test/helpers.ts +++ b/spot-contracts/test/helpers.ts @@ -10,9 +10,10 @@ const TOKEN_DECIMALS = 18; const PRICE_DECIMALS = 8; const DISCOUNT_DECIMALS = 18; -export const toFixedPtAmt = (a: string): BigNumber => utils.parseUnits(a, TOKEN_DECIMALS); -export const toPriceFixedPtAmt = (a: string): BigNumber => utils.parseUnits(a, PRICE_DECIMALS); -export const toDiscountFixedPtAmt = (a: string): BigNumber => utils.parseUnits(a, DISCOUNT_DECIMALS); +const sciParseFloat = (a: string): BigNumber => (a.includes("e") ? parseFloat(a).toFixed(18) : a); +export const toFixedPtAmt = (a: string): BigNumber => utils.parseUnits(sciParseFloat(a), TOKEN_DECIMALS); +export const toPriceFixedPtAmt = (a: string): BigNumber => utils.parseUnits(sciParseFloat(a), PRICE_DECIMALS); +export const toDiscountFixedPtAmt = (a: string): BigNumber => utils.parseUnits(sciParseFloat(a), DISCOUNT_DECIMALS); const ORACLE_BASE_PRICE = toPriceFixedPtAmt("1");