code-423n4 / 2024-02-althea-liquid-infrastructure-findings

3 stars 1 forks source link

Reward tokens with less than 18 decimals will not be distributed due to precision loss. #753

Closed c4-bot-10 closed 9 months ago

c4-bot-10 commented 9 months ago

Lines of code

https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L275

Vulnerability details

Vulnerability details

Tokens inteded to be used as reward tokens (i.e. distributableERC20s), mentioned by the sponsor, such as USDC and USDT have only 6 decimals and LiquidInfrastructureERC20 has 18 decimals. As a result, the calculation for entitlement in LiquidInfrastructureERC20.sol#L275 will result in 0 due to precision loss and such tokens will never be properly distributed.

// File: althea-l1-pocs/src/LiquidInfrastructureERC20.sol::_beginDistribution
269:         // Calculate the entitlement per token held
270:         uint256 supply = this.totalSupply();
271:         for (uint i = 0; i < distributableERC20s.length; i++) {
272:             uint256 balance = IERC20(distributableERC20s[i]).balanceOf(
273:                 address(this)
274:             );
275: @>          uint256 entitlement = balance / supply; // will result in zero
276:             erc20EntitlementPerUnit.push(entitlement);
277:         }

Imapact

Tokens with less than 18 decimals will never be distributed due to precision loss.

Proof of Concept

The code bellow illustrates how tokens with less than than 18 decimals, such as USDC, will never be distributed due to precision loss:

01: //SPDX-License-Identifier: Apache-2.0
02: pragma solidity 0.8.12;
03: 
04: import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
05: import "../src/LiquidInfrastructureNFT.sol";
06: import "../src/LiquidInfrastructureERC20.sol";
07: import "forge-std/Test.sol";
08: import "forge-std/console2.sol";
09: 
10: contract DT is ERC20 {
11:     address private owner;
12: 
13:     constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {
14:         owner = msg.sender;
15:     }
16: 
17:     function mint(address to, uint256 amount) public {
18:         require(msg.sender == owner, "not allowed");
19:         _mint(to, amount);
20:     }
21: }
22: 
23: contract USDC is ERC20 {
24:     address private owner;
25: 
26:     constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {
27:         owner = msg.sender;
28:     }
29: 
30:     function mint(address to, uint256 amount) public {
31:         require(msg.sender == owner, "not allowed");
32:         _mint(to, amount);
33:     }
34: 
35:     function decimals() public view override returns (uint8) {
36:         return 6;
37:     }
38: }
39: 
40: 
41: contract LiquidInfrastructureERC20Test is Test {
42:     address owner = 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496;
43:     address alice = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
44:     address bob   = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
45:     address eve  = 0x1Df3d35F66b4C30686a1604E3af08779A3914b51;
46:     uint256 MIN_DISTRIBUTION_PERIOD = 2;
47: 
48: 
49:     function test_low_precision_token() public {
50:         uint256 totalUSDCLocked = 100_000 * 1e6; // 100000 USDC
51:         uint256 totalDAILocked = 100_000 * 1e18; // 100000 DAI
52:         uint256 initialTokensAmount = 1e18; // 1 LiquidInfrastructureERC20 token
53:         uint256 nextDistributionBlock = block.number + MIN_DISTRIBUTION_PERIOD;
54: 
55:         ////// Begin Setup ////////////////////////////////////////////
56:         address[] memory distributable_tokens = new address[](2);
57:         address[] memory managed_nfts = new address[](1);
58:         address[] memory approved_holders = new address[](1);
59: 
60:         USDC usdc = new USDC("usd coin", "USDC");
61:         DT dai = new DT("Dai Stablecoin string", "DAI");
62:         distributable_tokens[0] = address(usdc);
63:         distributable_tokens[1] = address(dai);
64: 
65:         LiquidInfrastructureNFT nft = new LiquidInfrastructureNFT("A");
66:         managed_nfts[0] = address(nft);
67:         approved_holders[0] = alice;
68: 
69:         LiquidInfrastructureERC20 erc20 = new LiquidInfrastructureERC20(
70:             "liquid infrastructure token",
71:             "LIT",
72:             managed_nfts, 
73:             approved_holders, 
74:             MIN_DISTRIBUTION_PERIOD,
75:             distributable_tokens
76:         );
77:         erc20.mint(alice, initialTokensAmount);
78:         usdc.mint(address(erc20), totalUSDCLocked);
79:         dai.mint(address(erc20), totalDAILocked);
80:         ////// End Setup //////////////////////////////////////////////
81: 
82:         assertEq(usdc.balanceOf(alice), 0); 
83:         assertEq(dai.balanceOf(alice), 0); 
84: 
85:         vm.roll(nextDistributionBlock);
86:         erc20.distributeToAllHolders();
87: 
88:         // Low precision reward tokens are never distributed
89:         assertFalse(usdc.balanceOf(alice) == totalUSDCLocked); 
90:         assertEq(usdc.balanceOf(address(erc20)), totalUSDCLocked); 
91:         assertEq(usdc.balanceOf(alice), 0); 
92: 
93:         assertEq(dai.balanceOf(alice), totalDAILocked); 
94:         assertEq(dai.balanceOf(address(erc20)), 0); 
95:     }
96: }

Tools Used

Foundry

Recommended Mitigation Steps

Consider dividing balance and supply by their respective decimals during entitlement calculation on LiquidInfrastructureERC20.sol#L275 and properly converting the entitlement calculation on LiquidInfrastructureERC20.sol#L222-L223

Assessed type

Decimal

c4-pre-sort commented 9 months ago

0xRobocop marked the issue as duplicate of #757

c4-judge commented 8 months ago

0xA5DF marked the issue as unsatisfactory: Out of scope

c4-judge commented 8 months ago

0xA5DF marked the issue as satisfactory

c4-judge commented 8 months ago

0xA5DF marked the issue as unsatisfactory: Out of scope

c4-judge commented 8 months ago

0xA5DF marked the issue as unsatisfactory: Out of scope