Submitted on Sat Jul 13 2024 22:57:56 GMT-0400 (Atlantic Standard Time) by @Blockian for Attackathon | Fuel Network
Report ID: #33195
Report type: Smart Contract
Report severity: High
Target: https://github.com/FuelLabs/sway-libs/tree/0f47d33d6e5da25f782fc117d4be15b7b12d291b
Impacts:
- Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
- Incorrect math
The current implementation of the subtraction function in the sway-libs
for signed integers is incorrect.
This can lead to erroneous calculations, potentially causing critical vulnerabilities in projects built on the Fuel platform.
The way the signed numbers work in sway is by taking the indent of the unsigned counterparts and anything above it is positive while anything below is negative, so for example:
The u8 range is 0-255
and the indent is 128
so 128 becomes 0 and 5 == 133
, -5 == 123
and so on.
So to generalize, for every x
the I8 underlying value will be x + 128
.
Let's generalize even more, for every x
the signed underlying value will be x + indent
Lets look at how a naive sub function would look:
The mechanism for handling signed integers in Sway involves using an offset (indent) based on the unsigned counterparts. For example, the range for u8
is 0-255
, with an indent of 128
. Thus, 128
is mapped to 0
, 133
to 5
, and 123
to -5
, and so forth.
In general terms, for any signed integer x
, the underlying value is calculated as x + indent
.
A naive subtraction function might be written as:
sub(a, b) => a - b => (a + indent) - (b + indent) => a - b
This approach loses the indent offset. Therefore, the correct general function should be:
sub(a, b) => a.underlying - b.underlyting + indent
However, due to overflow and underflow issues, the operations need to be ordered correctly. This is what the Sway function attempts, but it still falls short. Ultimately, every calculation should follow the form a.underlying - b.underlying + indent
, albeit with different order of operations.
Examining the Sway implementation (using I8
as an example, applicable to all signed integers):
impl core::ops::Subtract for I8 { // should be of the form a.underlying - b.underlyting + 128
/// Subtract a I8 from a I8. Panics of overflow.
fn subtract(self, other: Self) -> Self {
let mut res = Self::new();
if self.underlying >= Self::indent()
&& other.underlying >= Self::indent()
{
if self.underlying > other.underlying {
res = Self::from_uint(self.underlying - other.underlying + Self::indent());
} else {
res = Self::from_uint(self.underlying - (other.underlying - Self::indent()));
}
} else if self.underlying >= Self::indent()
&& other.underlying < Self::indent()
{
res = Self::from_uint(self.underlying - Self::indent() + other.underlying); // wrong here
} else if self.underlying < Self::indent()
&& other.underlying >= Self::indent()
{
res = Self::from_uint(self.underlying - (other.underlying - Self::indent()));
} else if self.underlying < Self::indent()
&& other.underlying < Self::indent()
{
if self.underlying < other.underlying {
res = Self::from_uint(other.underlying - self.underlying + Self::indent()); // wrong here this returns b - a
} else {
res = Self::from_uint(self.underlying + other.underlying - Self::indent()); // wrong here
}
}
res
}
}
- When self.
underlying >= indent && other.underlying < indent
, the calculation isa.underlying + b.underlying - 128
. - When
self.underlying < indent && other.underlying < indent
, ifself.underlying < other.underlying
, the calculation isb.underlying - a.underlying - 128
. - When
self.underlying < indent && other.underlying < indent
andself.underlying >= other.underlying
, the calculation isa.underlying + b.underlying - 128
.
This issue affects every implementation of signed integers in the Fuel ecosystem. Any project utilizing these implementations will encounter incorrect calculations.
In the crypto space, even minor errors, like off by one, can lead to substantial financial losses, underscoring the critical nature of this bug.
Correct the erroneous branches in the subtraction functions to ensure they follow the format a.underlying - b.underlying + indent
.
Two proofs of concept are provided. The first is a simple demonstration to illustrate the issue, while the second is a more comprehensive example covering all possible branches for all signed integers.
Run the POC's with forc test
Simple Test:
contract;
use sway_libs::signed_integers::i8::I8;
abi Tester {
fn sub_negative_nums() -> bool;
}
impl Tester for Contract {
fn sub_negative_nums() -> bool {
let a = I8::neg_from(5u8);
let b = I8::neg_from(5u8);
return (a - b) == I8::zero(); // should be true since -5 - (-5) == 0
}
}
#[test]
fn test_sub() {
let caller = abi(Tester, CONTRACT_ID);
// assert(caller.sub_negative_nums() == true); // this is what we expect, but the test fails here
assert(caller.sub_negative_nums() == false);
}
Long Test:
contract;
use std::u128::U128;
use sway_libs::signed_integers::i8::I8;
use sway_libs::signed_integers::i16::I16;
use sway_libs::signed_integers::i32::I32;
use sway_libs::signed_integers::i64::I64;
use sway_libs::signed_integers::i128::I128;
use sway_libs::signed_integers::i256::I256;
abi Tester {
fn sub_poc_i8_flow_1() -> bool;
fn sub_poc_i8_flow_2() -> bool;
fn sub_poc_i8_flow_3() -> bool;
fn sub_poc_i16_flow_1() -> bool;
fn sub_poc_i16_flow_2() -> bool;
fn sub_poc_i16_flow_3() -> bool;
fn sub_poc_i32_flow_1() -> bool;
fn sub_poc_i32_flow_2() -> bool;
fn sub_poc_i32_flow_3() -> bool;
fn sub_poc_i64_flow_1() -> bool;
fn sub_poc_i64_flow_2() -> bool;
fn sub_poc_i64_flow_3() -> bool;
fn sub_poc_i128_flow_1() -> bool;
fn sub_poc_i128_flow_2() -> bool;
fn sub_poc_i128_flow_3() -> bool;
fn sub_poc_i256_flow_1() -> bool;
fn sub_poc_i256_flow_2() -> bool;
fn sub_poc_i256_flow_3() -> bool;
}
impl Tester for Contract {
fn sub_poc_i8_flow_1() -> bool { // a >= indent, b < indent
let a = I8::from(5u8);
let b = I8::neg_from(5u8);
return (a - b) == I8::from(10u8); // should be true since 5 - (-5) = 10
}
fn sub_poc_i8_flow_2() -> bool { // a < b < indent
let a = I8::neg_from(6u8);
let b = I8::neg_from(5u8);
return (a - b) == I8::neg_from(1u8); // should be true since -6 - (-5) == -1
}
fn sub_poc_i8_flow_3() -> bool { // a < indent, b < indent
let a = I8::neg_from(5u8);
let b = I8::neg_from(5u8);
return (a - b) == I8::zero(); // should be true since -5 - (-5) == 0
}
fn sub_poc_i16_flow_1() -> bool { // a >= indent, b < indent
let a = I16::from(5u16);
let b = I16::neg_from(5u16);
return (a - b) == I16::from(10u16); // should be true since 5 - (-5) = 10
}
fn sub_poc_i16_flow_2() -> bool { // a < b < indent
let a = I16::neg_from(6u16);
let b = I16::neg_from(5u16);
return (a - b) == I16::neg_from(1u16); // should be true since -6 - (-5) == -1
}
fn sub_poc_i16_flow_3() -> bool { // a < indent, b < indent
let a = I16::neg_from(5u16);
let b = I16::neg_from(5u16);
return (a - b) == I16::zero(); // should be true since -5 - (-5) == 0
}
fn sub_poc_i32_flow_1() -> bool { // a >= indent, b < indent
let a = I32::from(5u32);
let b = I32::neg_from(5u32);
return (a - b) == I32::from(10u32); // should be true since 5 - (-5) == 10
}
fn sub_poc_i32_flow_2() -> bool { // a < b < indent
let a = I32::neg_from(6u32);
let b = I32::neg_from(5u32);
return (a - b) == I32::neg_from(1u32); // should be true since -6 - (-5) == -1
}
fn sub_poc_i32_flow_3() -> bool { // a < indent, b < indent
let a = I32::neg_from(5u32);
let b = I32::neg_from(5u32);
return (a - b) == I32::zero(); // should be true since -5 - (-5) == 0
}
fn sub_poc_i64_flow_1() -> bool { // a >= indent, b < indent
let a = I64::from(5u64);
let b = I64::neg_from(5u64);
return (a - b) == I64::from(10u64); // should be true since 5 - (-5) == 10
}
fn sub_poc_i64_flow_2() -> bool { // a < b < indent
let a = I64::neg_from(6u64);
let b = I64::neg_from(5u64);
return (a - b) == I64::neg_from(1u64); // should be true since -6 - (-5) == -1
}
fn sub_poc_i64_flow_3() -> bool { // a < indent, b < indent
let a = I64::neg_from(5u64);
let b = I64::neg_from(5u64);
return (a - b) == I64::zero(); // should be true since -5 - (-5) == 0
}
fn sub_poc_i128_flow_1() -> bool { // a >= indent, b < indent
let a = I128::from(U128::from(5u8));
let b = I128::neg_from(U128::from(5u8));
return (a - b) == I128::from(U128::from(10u8)); // should be true since 5 - (-5) == 10
}
fn sub_poc_i128_flow_2() -> bool { // a < b < indent
let a = I128::neg_from(U128::from(6u8));
let b = I128::neg_from(U128::from(5u8));
return (a - b) == I128::neg_from(U128::from(1u8)); // should be true since -6 - (-5) == -1
}
fn sub_poc_i128_flow_3() -> bool { // a < indent, b < indent
let a = I128::neg_from(U128::from(5u8));
let b = I128::neg_from(U128::from(5u8));
return (a - b) == I128::zero(); // should be true since -5 - (-5) == 0
}
fn sub_poc_i256_flow_1() -> bool { // a >= indent, b < indent
let a = I256::from(0x5u256);
let b = I256::neg_from(0x5u256);
return (a - b) == I256::from(0x10u256); // should be true since 5 - (-5) == 10
}
fn sub_poc_i256_flow_2() -> bool { // a < b < indent
let a = I256::neg_from(0x6u256);
let b = I256::neg_from(0x5u256);
return (a - b) == I256::neg_from(0x1u256); // should be true since -6 - (-5) == -1
}
fn sub_poc_i256_flow_3() -> bool { // a < indent, b < indent
let a = I256::neg_from(0x5u256);
let b = I256::neg_from(0x5u256);
return (a - b) == I256::zero(); // should be true since -5 - (-5) == 0
}
}
#[test]
fn test_sub() {
let caller = abi(Tester, CONTRACT_ID);
assert(caller.sub_poc_i8_flow_1() == false);
assert(caller.sub_poc_i8_flow_2() == false);
assert(caller.sub_poc_i8_flow_3() == false);
assert(caller.sub_poc_i16_flow_1() == false);
assert(caller.sub_poc_i16_flow_2() == false);
assert(caller.sub_poc_i16_flow_3() == false);
assert(caller.sub_poc_i32_flow_1() == false);
assert(caller.sub_poc_i32_flow_2() == false);
assert(caller.sub_poc_i32_flow_3() == false);
assert(caller.sub_poc_i64_flow_1() == false);
assert(caller.sub_poc_i64_flow_2() == false);
assert(caller.sub_poc_i64_flow_3() == false);
assert(caller.sub_poc_i128_flow_1() == false);
assert(caller.sub_poc_i128_flow_2() == false);
assert(caller.sub_poc_i128_flow_3() == false);
assert(caller.sub_poc_i256_flow_1() == false);
assert(caller.sub_poc_i256_flow_2() == false);
assert(caller.sub_poc_i256_flow_3() == false);
}