When a holder of position tokens sends an odd number of tokens to another address, then upon attempting to redeem, the latest address receives fewer collateral tokens than it is entitled to.
describe("proofOfConcept", async () => {
let s: SetupOutput;
const depositAmount = 10_000_000;
const expectedWithdrawalAmount = 9_970_000;
beforeEach(async () => {
s = await loadFixture(setup);
});
it("Should return the expected withdrawal amount when there is only one position token holder", async () => {
await s.aaveDIVAWrapper
.connect(s.owner)
.registerCollateralToken(collateralToken);
await s.aaveDIVAWrapper
.connect(s.impersonatedSigner)
.createContingentPool({
...s.createContingentPoolParams,
collateralAmount: depositAmount,
shortRecipient: s.impersonatedSigner.address
});
const poolId = await getPoolIdFromAaveDIVAWrapperEvent(s.aaveDIVAWrapper);
const poolParams = await s.diva.getPoolParameters(poolId);
const nextBlockTimestamp = Number(poolParams.expiryTime) + 1;
await mine(nextBlockTimestamp);
await s.diva
.connect(s.dataProvider)
.setFinalReferenceValue(poolId, parseUnits("10"), false);
const longTokenContract = await ethers.getContractAt(
"IERC20",
poolParams.longToken
);
const shortTokenContract = await ethers.getContractAt(
"IERC20",
poolParams.shortToken
);
const longTokenBalance = await longTokenContract.balanceOf(
s.impersonatedSigner.address
);
const longRecipientBalanceBeforeRedeem =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner.address
);
await longTokenContract
.connect(s.impersonatedSigner)
.approve(s.aaveDIVAWrapper.target, longTokenBalance);
await s.aaveDIVAWrapper
.connect(s.impersonatedSigner)
.redeemPositionToken(
poolParams.longToken,
longTokenBalance,
s.impersonatedSigner.address
);
const longRecipientBalanceAfterRedeem =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner.address
);
const longDelta = longRecipientBalanceAfterRedeem - longRecipientBalanceBeforeRedeem
const acc1ShortTokenBalance = await shortTokenContract.balanceOf(
s.impersonatedSigner.address
);
const acc1BalanceBefore =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner
);
await shortTokenContract
.connect(s.impersonatedSigner)
.approve(s.aaveDIVAWrapper.target, acc1ShortTokenBalance);
await s.aaveDIVAWrapper
.connect(s.impersonatedSigner)
.redeemPositionToken(
poolParams.shortToken,
acc1ShortTokenBalance,
s.impersonatedSigner.address
);
const acc1BalanceAfter =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner
);
const acc1Delta = acc1BalanceAfter - acc1BalanceBefore
expect(longDelta + acc1Delta).eq(expectedWithdrawalAmount)
});
it("Should return the expected withdrawal amount when there are multiple position token holders", async () => {
await s.aaveDIVAWrapper
.connect(s.owner)
.registerCollateralToken(collateralToken);
await s.aaveDIVAWrapper
.connect(s.impersonatedSigner)
.createContingentPool({
...s.createContingentPoolParams,
collateralAmount: depositAmount,
shortRecipient: s.impersonatedSigner.address
});
const poolId = await getPoolIdFromAaveDIVAWrapperEvent(s.aaveDIVAWrapper);
const poolParams = await s.diva.getPoolParameters(poolId);
const nextBlockTimestamp = Number(poolParams.expiryTime) + 1;
await mine(nextBlockTimestamp);
await s.diva
.connect(s.dataProvider)
.setFinalReferenceValue(poolId, parseUnits("10"), false);
const longTokenContract = await ethers.getContractAt(
"IERC20",
poolParams.longToken
);
const shortTokenContract = await ethers.getContractAt(
"IERC20",
poolParams.shortToken
);
const longTokenBalance = await longTokenContract.balanceOf(
s.impersonatedSigner.address
);
const longRecipientBalanceBeforeRedeem =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner.address
);
await longTokenContract
.connect(s.impersonatedSigner)
.approve(s.aaveDIVAWrapper.target, longTokenBalance);
await s.aaveDIVAWrapper
.connect(s.impersonatedSigner)
.redeemPositionToken(
poolParams.longToken,
longTokenBalance,
s.impersonatedSigner.address
);
const longRecipientBalanceAfterRedeem =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner.address
);
const longDelta = longRecipientBalanceAfterRedeem - longRecipientBalanceBeforeRedeem
await shortTokenContract
.connect(s.impersonatedSigner)
.transfer(s.acc2, 2497000)
const acc1ShortTokenBalance = await shortTokenContract.balanceOf(
s.impersonatedSigner.address
);
const acc1BalanceBefore =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner
);
await shortTokenContract
.connect(s.impersonatedSigner)
.approve(s.aaveDIVAWrapper.target, acc1ShortTokenBalance);
await s.aaveDIVAWrapper
.connect(s.impersonatedSigner)
.redeemPositionToken(
poolParams.shortToken,
acc1ShortTokenBalance,
s.impersonatedSigner.address
);
const acc1BalanceAfter =
await s.collateralTokenContract.balanceOf(
s.impersonatedSigner
);
const acc1Delta = acc1BalanceAfter - acc1BalanceBefore
const acc2BalanceBefore =
await s.collateralTokenContract.balanceOf(
s.acc2
);
const acc2ShortTokenBalance = await shortTokenContract.balanceOf(
s.acc2.address
);
await shortTokenContract
.connect(s.acc2)
.approve(s.aaveDIVAWrapper.target, acc2ShortTokenBalance);
await s.aaveDIVAWrapper
.connect(s.acc2)
.redeemPositionToken(
poolParams.shortToken,
acc2ShortTokenBalance,
s.acc2.address
);
const acc2BalanceAfter =
await s.collateralTokenContract.balanceOf(
s.acc2
);
const acc2Delta = acc2BalanceAfter - acc2BalanceBefore
expect(longDelta + acc1Delta + acc2Delta).eq(expectedWithdrawalAmount)
});
});
The issue arises due to rounding errors when calculating redemption amounts, specifically in the redeemPositionToken function of the DIVA Protocol.
Users who receive transferred position tokens may receive slightly less collateral tokens than they are entitled to when redeeming. While the amounts are small, this could accumulate to material losses if exploited at scale.
Unfortunately, I was unable to find a suitable solution within the given time frame :(