31597 - [SC - High] Loss of precision while calculating claimable f...
Submitted on May 21st 2024 at 16:09:53 UTC by @savi0ur for Boost | Alchemix
Report ID: #31597
Report type: Smart Contract
Report severity: High
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Bug Description
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L38
/// @notice Multiplier for the slope of the decay
uint256 public constant MULTIPLIER = 2;
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L44
int256 internal constant iMULTIPLIER = 2;
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1157-L1160
function _calculatePoint(LockedBalance memory _locked, uint256 _time) internal pure returns (Point memory point) {
if (_locked.end > _time && _locked.amount > 0) {
point.slope = _locked.maxLockEnabled ? int256(0) : (int256(_locked.amount) * iMULTIPLIER) / iMAXTIME;
point.bias = _locked.maxLockEnabled
? ((int256(_locked.amount) * iMULTIPLIER) / iMAXTIME) * (int256(_locked.end - _time))
: (point.slope * (int256(_locked.end - _time)));
}
}
As we can see, iMULTIPLIER
and MULTIPLIER
is set to 2, which is not sufficient to preserve the precision.
point.slope
and point.bias
are calculated as follow:
point.slope = _locked.maxLockEnabled ? int256(0) : (int256(_locked.amount) * iMULTIPLIER) / iMAXTIME;
point.bias = _locked.maxLockEnabled
? ((int256(_locked.amount) * iMULTIPLIER) / iMAXTIME) * (int256(_locked.end - _time))
: (point.slope * (int256(_locked.end - _time)));
When (int256(_locked.amount) * iMULTIPLIER) < iMAXTIME
and ((int256(_locked.amount) * iMULTIPLIER) < iMAXTIME
, it results in zero. So for the locked amounts which is small enough when multiplied by iMULTIPLIER=2
end up being lower than iMAXTIME
, will not be counted in bias and slope calculation. At that point in time, there slope
and bias
will be zero.
For example,
_locked.amount = (iMAXTIME / 2) - 1 = (365 days / 2) - 1 = 15767999
_locked.maxLockEnabled = false
point.slope = (int256(_locked.amount) * iMULTIPLIER) / iMAXTIME;
= (15767999 * 2) / 365 days
= 0
point.bias = (point.slope * (int256(_locked.end - _time)))
= (0 * (int256(_locked.end - _time)))
= 0
As we can, both slope
and bias
become zero.
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/FluxToken.sol#L224
function getClaimableFlux(uint256 _amount, address _nft) public view returns (uint256 claimableFlux) {
uint256 bpt = calculateBPT(_amount);
uint256 veMul = IVotingEscrow(veALCX).MULTIPLIER();
uint256 veMax = IVotingEscrow(veALCX).MAXTIME();
uint256 fluxPerVe = IVotingEscrow(veALCX).fluxPerVeALCX();
uint256 fluxMul = IVotingEscrow(veALCX).fluxMultiplier();
// Amount of flux earned in 1 yr from _amount assuming it was deposited for maxtime
claimableFlux = (((bpt * veMul) / veMax) * veMax * (fluxPerVe + BPS)) / BPS / fluxMul; //@audit
// Claimable flux for alchemechNFT is different than patronNFT
if (_nft == alchemechNFT) {
claimableFlux = (claimableFlux * alchemechMultiplier) / BPS;
}
}
Similarly, in getClaimableFlux
function, claimableFlux
is calculated as
claimableFlux = (((bpt * veMul) / veMax) * veMax * (fluxPerVe + BPS)) / BPS / fluxMul;
Here also, veMul = 2
as its fetched from VotingEscrow's
multiplier.
Impact
Precision loss while calculating claimable flux and point's bias
and slope
.
Recommendation
Increase MULTIPLIER
and iMULTIPLIER
precision from 2
to 1e18
, for higher precision.
References
https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol
https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol
Proof Of Concept
Steps to Run using Foundry:
Paste following foundry code in
src/test/FluxToken.t.sol
Run using
FOUNDRY_PROFILE=default forge test --fork-url $FORK_URL --fork-block-number 17133822 --match-contract FluxTokenTest --match-test testPrecisionLoss -vv
function testPrecisionLoss() external {
uint256 tokenId = 4;
address ownerOfPatronNFT = IAlEthNFT(patronNFT).ownerOf(tokenId);
// address ownerOfAlchemechNFT = IAlchemechNFT(alchemechNFT).ownerOf(tokenId);
assertEq(flux.balanceOf(ownerOfPatronNFT), 0, "owner should have no flux");
// assertEq(flux.balanceOf(ownerOfAlchemechNFT), 0, "owner should have no flux");
uint256 tokenData1 = IAlEthNFT(patronNFT).tokenData(tokenId);
uint256 patronTotal = flux.getClaimableFlux(tokenData1, patronNFT);
// console.log("patronTotal:", patronTotal);
uint256 _bpt = flux.calculateBPT(tokenData1);
uint256 veMul = 200000;//IVotingEscrow(veALCX).MULTIPLIER();
uint256 veMax = IVotingEscrow(veALCX).MAXTIME();
uint256 fluxPerVe = IVotingEscrow(veALCX).fluxPerVeALCX();
uint256 fluxMul = IVotingEscrow(veALCX).fluxMultiplier();
// Amount of flux earned in 1 yr from _amount assuming it was deposited for maxtime
uint claimableFlux = (((_bpt * veMul) / veMax) * veMax * (fluxPerVe + BPS)) / BPS / fluxMul;
console.log("patronTotal :", patronTotal);
console.log("claimableFlux:", claimableFlux);
}
Console Output:
> FOUNDRY_PROFILE=default forge test --fork-url $FORK_URL --fork-block-number 17133822 --match-contract FluxTokenTest --match-test testPrecisionLoss -vv
Ran 1 test for src/test/FluxToken.t.sol:FluxTokenTest
[PASS] testPrecisionLoss() (gas: 47036)
Logs:
patronTotal : 7499756087469342000
claimableFlux: 749975608747561793994000
As we can see, by increasing MULTIPLIER, precision has increased. We have used 200000
as MULTIPLIER
here to show precision issue as it was set 2
initially.
Last updated
Was this helpful?