Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed rounding issue with recovery #170

Merged
merged 1 commit into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions spot-contracts/contracts/_utils/BondHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
116 changes: 90 additions & 26 deletions spot-contracts/test/_utils/BondHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"]);
});
});
});
});
Expand Down
7 changes: 4 additions & 3 deletions spot-contracts/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down