Attackathon _ Fuel Network 33233 - [Smart Contract - Medium] Incorrect Implementation of Unsigned -b
Incorrect Implementation of Unsigned 32-bit Fixed Point Fractional Function
Submitted on Mon Jul 15 2024 16:51:42 GMT-0400 (Atlantic Standard Time) by @Blockian for Attackathon | Fuel Network
Report ID: #33233
Report type: Smart Contract
Report severity: Medium
Target: https://github.com/FuelLabs/sway-libs/tree/0f47d33d6e5da25f782fc117d4be15b7b12d291b
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Incorrect math
Permanent freezing of funds
Temporary freezing of funds for at least 1 hour
Description
Fuel Network bug report
Incorrect Implementation of Unsigned 32-bit Fixed Point Fractional Function
Description
The current implementation of the fractional function in sway-libs
for the unsigned 32-bit Fixed Point and signed 64-bit Fixed Point is flawed. This issue causes the function to revert every time it's called, potentially leading to problems for projects built on the Fuel platform.
Root Cause
The fract
function from the UFP32
implementation is as follows:
pub fn fract(self) -> Self {
Self {
// first move to the left (multiply by the denominator)
// to get rid of integer part, than move to the
// right (divide by the denominator), to ensure
// fixed-point structure
underlying: ((self.underlying << 16) - u32::max() - 1u32) >> 16,
}
}
The issue arises after the left shift operation, where the function subtracts u32::max
and then subtracts 1
. Since left shifting doesn't change the operand's type (i.e., a << 16
keeps a as u32
), (self.underlying << 16)
remains within the bounds of u32
. Specifically, (self.underlying << 16)
is constrained by 4294901760
(0b11111111111111110000000000000000
), with the left shift ensuring the 16 rightmost bits are zero.
Consequently, for any self.underlying
, (self.underlying << 16)
is always less than u32::max
, leading to an underflow and causing a revert.
Impact
Every usage of UFP32.fract
results in a revert, affecting:
UFP32.ceil
- relies on the fract function.IFP64.fract
- internally uses UFP32, causing it to revert.IFP64.ceil
- also reverts since it uses both UFP32 and the fract function.
Any function in a contract that relies on one of those functions will revert, possibly locking user funds.
Proposed fix
Remove the unnecessary subtraction:
pub fn fract(self) -> Self {
Self {
underlying: (self.underlying << 16) >> 16,
}
}
This modification ensures the function operates correctly without causing reverts.
Proof of concept
Proof of Concept
Run the POC's with forc test
contract;
use sway_libs::fixed_point::ufp32::UFP32;
abi Tester {
fn frac_crashing() -> UFP32;
fn new_frac_not_crashing() -> bool;
}
pub fn new_fract(num: UFP32) -> UFP32 {
UFP32::from((num.underlying() << 16) >> 16)
}
impl Tester for Contract {
fn frac_crashing() -> UFP32 {
let num = UFP32::from(u32::max());
num.fract()
}
fn new_frac_not_crashing() -> bool {
let num = UFP32::from(u32::max());
new_fract(num) == UFP32::from(65535u32) // 65535 == 0b1111111111111111, which is the fractional part of u32::max
}
}
#[test]
fn test_sub() {
let caller = abi(Tester, CONTRACT_ID);
assert(caller.new_frac_not_crashing() == true); // the test passes this point!
let _ = caller.frac_crashing(); // crashes here with ArithmeticOverflow at the SUB instruction
}
Last updated
Was this helpful?