During my audit of the UFP (unsigned fixed-point number) implementations in the sway-libs , this logic vulnerability were identified. This could lead to incorrect calculations in any smart contracts or scripts relying on this function and cause massive losses.
Vulnerability Details
impl Exponent for UFP128 {
fn exp(exponent: Self) -> Self {
let one = UFP128::from((1, 0));
let p2 = one / UFP128::from((2, 0));
let p3 = one / UFP128::from((6, 0));
let p4 = one / UFP128::from((24, 0));
let p5 = one / UFP128::from((120, 0));
let p6 = one / UFP128::from((720, 0));
let p7 = one / UFP128::from((5040, 0));
// common technique to counter losing sugnifucant numbers in usual approximation
let _res_minus_1 = exponent + exponent * exponent * (p2 + exponent * (p3 + exponent * (p4 + exponent * (p5 + exponent * (p6 + exponent * p7)))));
let res = one;
res
}
}
The function calculates _res_minus_1 correctly .. but then it ignores this calculation and simply returns one. This means the exponential function will always return 1, regardless of the input.
The impact is Loss of funds because of
Incorrect Financial Calculations : using the vulnerable exponential function for compound interest, option pricing, or yield calculations. With this bug, all such calculations would return 1, regardless of the input.
Some token distribution models or staking rewards systems use exponential decay or growth. This bug would make such systems non-functional, potentially: Distributing incorrect amounts of tokens
Recommendation
we should use the calculated result like
impl Exponent for UFP128 {
fn exp(exponent: Self) -> Self {
let one = UFP128::from((1, 0));
let p2 = one / UFP128::from((2, 0));
let p3 = one / UFP128::from((6, 0));
let p4 = one / UFP128::from((24, 0));
let p5 = one / UFP128::from((120, 0));
let p6 = one / UFP128::from((720, 0));
let p7 = one / UFP128::from((5040, 0));
// Common technique to counter losing significant numbers in usual approximation
let res_minus_1 = exponent + exponent * exponent * (p2 +
exponent * (p3 + exponent * (p4 +
exponent * (p5 + exponent * (p6 +
exponent * p7)))));
one + res_minus_1
}
}
Now it's calculates the exponential function using a Taylor series approximation and should return the correct result. the function now adds 1 to the calculated res_minus_1 to get the final result, as the original calculation was for (e^x - 1).
Proof of concept
Add this PoC test in the end of the ufp128.sw
#[test]
fn PoC_exp() {
let one = UFP128::from_uint(1);
let res = UFP128::exp(one);
assert(res == UFP128::from((1, 0)));
let two = UFP128::from_uint(2);
let res = UFP128::exp(two);
assert(res == UFP128::from((1, 0)));
let three = UFP128::from_uint(3);
let res = UFP128::exp(three);
assert(res == UFP128::from((1, 0)));
let four = UFP128::from_uint(4);
let res = UFP128::exp(four);
assert(res == UFP128::from((1, 0)));
let five = UFP128::from_uint(5);
let res = UFP128::exp(five);
assert(res == UFP128::from((1, 0)));
let six = UFP128::from_uint(6);
let res = UFP128::exp(six);
assert(res == UFP128::from((1, 0)));
let seven = UFP128::from_uint(7);
let res = UFP128::exp(seven);
assert(res == UFP128::from((1, 0)));
let eight = UFP128::from_uint(8);
let res = UFP128::exp(eight);
assert(res == UFP128::from((1, 0)));
let nine = UFP128::from_uint(9);
let res = UFP128::exp(nine);
assert(res == UFP128::from((1, 0)));
}