Skip to content

Latest commit

 

History

History
208 lines (167 loc) · 7.64 KB

File metadata and controls

208 lines (167 loc) · 7.64 KB

Lone Wintergreen Rattlesnake

High

Loss of Bonded Assets Due to Decimal Precision Issues in Debonding

Summary

A precision loss in the WeightedIndex contract will cause a complete loss of tokens for users when debonding small amounts, as the calculation for tokens with small weights rounds down to zero during the debonding process. This occurs when _percSharesX96 is too small relative to _totalAssets, causing integer division to truncate to zero.

Root Cause

In WeightedIndex.sol, the debonding calculation uses fixed-point arithmetic that can result in rounding to zero for tokens with small weights:

uint256 _debondAmount = (_totalAssets[indexTokens[_i].token] * _percSharesX96) / FixedPoint96.Q96;

When dealing with tokens that have small weights and small debond amounts, the multiplication and subsequent division by Q96 can result in the amount rounding down to zero.

Attack Path

User bonds into a pod that has tokens with varying weights (e.g., 59%, 20%, 1%) User attempts to debond a small amount of pod tokens The debonding calculation for the token with 1% weight:

_percSharesX96 = (_amountAfterFee * FixedPoint96.Q96) / _totalSupply
_debondAmount = (_totalAssets[indexTokens[_i].token] * _percSharesX96) / FixedPoint96.Q96

Results in _debondAmount = 0 due to precision loss User receives zero tokens for the small-weight component of their debonding, effectively losing those tokens

Impact

The users suffer a partial loss of funds proportional to the weight of the affected token(s) in the index. For example, with a 1% weight token, users lose 1% of their expected returns when debonding small amounts. The lost tokens remain trapped in the contract.

PoC

Click to view the full test code
address public dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
    address public usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    uint256 public bondAmt = 1000e18;
    uint16 fee = 2000;
    uint256 public bondAmtAfterFee = bondAmt - (bondAmt * fee) / 10000;
    uint256 public feeAmtOnly1 = (bondAmt * fee) / 10000;
    uint256 public feeAmtOnly2 = (bondAmtAfterFee * fee) / 10000;

    // Test users
    address public alice = address(0x1);
    address public bob = address(0x2);
    address public carol = address(0x3);

    event FlashMint(address indexed executor, address indexed recipient, uint256 amount);

    event AddLiquidity(address indexed user, uint256 idxLPTokens, uint256 pairedLPTokens);

    event RemoveLiquidity(address indexed user, uint256 lpTokens);

    function setUp() public override {
        super.setUp();
        peas = PEAS(0x02f92800F57BCD74066F5709F1Daa1A4302Df875);
        twapUtils = new V3TwapUtilities();
        rewardsWhitelist = new RewardsWhitelist();
        dexAdapter = new UniswapDexAdapter(
            twapUtils,
            0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D, // Uniswap V2 Router
            0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45, // Uniswap SwapRouter02
            false
        );
        IDecentralizedIndex.Config memory _c;
        IDecentralizedIndex.Fees memory _f;
        _f.bond = fee;
        _f.debond = fee;
        address[] memory _t = new address[](3);
        _t[0] = address(peas);
        _t[1] = dai;
        _t[2] = usdc; 
        uint256[] memory _w = new uint256[](3);
        _w[0] = 59;
        _w[1] = 1;
        _w[2] = 20;

        address _pod = _createPod(
            "Test",
            "pTEST",
            _c,
            _f,
            _t,
            _w,
            address(0),
            false,
            abi.encode(
                dai,
                address(peas),
                0x6B175474E89094C44Da98b954EedeAC495271d0F,
                0x7d544DD34ABbE24C8832db27820Ff53C151e949b,
                rewardsWhitelist,
                0x024ff47D552cB222b265D68C7aeB26E586D5229D,
                dexAdapter
            )
        );
        pod = WeightedIndex(payable(_pod));

        flashMintRecipient = new MockFlashMintRecipient();

        // Initial token setup for test users
        deal(address(peas), address(this), bondAmt * 100);
        deal(address(peas), alice, bondAmt * 100);
        deal(address(peas), bob, bondAmt * 100);
        deal(address(peas), carol, bondAmt * 100);
        deal(address(0x6B175474E89094C44Da98b954EedeAC495271d0F), address(this), bondAmt * 100);
        deal(address(usdc), address(this), bondAmt * 100);
        deal(address(0x6B175474E89094C44Da98b954EedeAC495271d0F), bob, bondAmt * 100);
        deal(address(0x6B175474E89094C44Da98b954EedeAC495271d0F), alice, bondAmt * 100);
        deal(address(usdc), bob, bondAmt * 100);
        deal(address(usdc), alice, bondAmt * 100);

        // Approve tokens for all test users
        vm.startPrank(alice);
        peas.approve(address(pod), type(uint256).max);
        IERC20(dai).approve(address(pod), type(uint256).max);
        IERC20(usdc).approve(address(pod), type(uint256).max);
        vm.stopPrank();

        vm.startPrank(bob);
        peas.approve(address(pod), type(uint256).max);
        IERC20(dai).approve(address(pod), type(uint256).max);
        IERC20(usdc).approve(address(pod), type(uint256).max);
        vm.stopPrank();

        vm.startPrank(carol);
        peas.approve(address(pod), type(uint256).max);
        IERC20(dai).approve(address(pod), type(uint256).max);
        vm.stopPrank();
    }
function test_bondWithDecimalLoss() public {
    peas.approve(address(pod), peas.totalSupply());
    IERC20(dai).approve(address(pod), type(uint256).max);
    IERC20(usdc).approve(address(pod), type(uint256).max);
    
    uint256 addressThisUSDCBalanceBeforeBond = IERC20(usdc).balanceOf(address(this));
    pod.bond(address(peas), 1e16, 0);

    vm.startPrank(alice);
    pod.bond(address(peas), 1e16, 0);
    vm.stopPrank();

    vm.startPrank(bob);
    pod.bond(address(peas), 1e16, 0);
    vm.stopPrank();

    uint256 addressThisUSDCBalance = IERC20(usdc).balanceOf(address(this));
    // Shows that the balance decreases after bonding
    assertLt(addressThisUSDCBalance, addressThisUSDCBalanceBeforeBond);

    address[] memory _n1;
    uint8[] memory _n2;
    pod.debond(1e10, _n1, _n2);
    uint256 addressThisUSDCBalanceAfter = IERC20(usdc).balanceOf(address(this));
    
    // Shows that the balance doesnt change after debonding
    assertEq(addressThisUSDCBalanceAfter, addressThisUSDCBalance);
}

test forge test --match-contract WeightedIndexTest --match-test test_bondWithDecimalLoss -vvv --fork-url

[⠊] Compiling...
[⠢] Compiling 1 files with Solc 0.8.28
[⠆] Solc 0.8.28 finished in 2.14s
Compiler run successful!

Ran 1 test for test/WeightedIndex.t.sol:WeightedIndexTest
[PASS] test_bondWithDecimalLoss() (gas: 696242)
Logs:
  ..................................addressThisUSDCBalance 99999999999999999996611
  CA: .........debondAmount 7999999999
  CA: .........debondAmount 135593220
  CA: .........debondAmount 0
  ..................................addressThisUSDCBalance 99999999999999999996611

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.26s (389.01ms CPU time)

Ran 1 test suite in 4.64s (4.26s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Mitigation

Instead of:

if (_debondAmount > 0) {
                _totalAssets[indexTokens[_i].token] -= _debondAmount;
                IERC20(indexTokens[_i].token).safeTransfer(_msgSender(), _debondAmount);
            }

should add a check to ensure that debond amount > 0

require(_debondAmount >, "Amount too small");