DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: medium
Invalid

Downcast from uint256 to uint8 in the `OwnerFacet::createBridge` function may possibly lead to accounting issues and impossibility of removing bridges in the future

Summary

Downcast from uint256 to uint8 of the vault parameter in the OwnerFacet::createBridge function may lead to the wrong assignment of the vault parameter (if it is more than 255) to the vault element in the bridge struct. This downcasted element vault of the bridge struct is used in all core functions of the BridgeRouterFacet contract to select a vault, based on which accounting state the execution of any of these functions will be performed: deposit, depositEth, withdraw, unstakeEth, withdrawTapp. The same vault element of the bridge struct is also used in the removeBridge function of the OwnerFacet contract to remove the bridge from the existing vault, but due to the wrong assignment of the downcasted value of the vault element in the bridge struct the bridge actually will not be deleted, even though the function will be executed.

Vulnerability Details

Firstly, we should take a look at the bridge struct (DataTypes contract, lines 168-173):

struct Bridge {
// @dev SLOT 1: 16 + 8 + 8 = 32 (224 unused)
uint16 withdrawalFee;
uint8 unstakeFee;
uint8 vault; //@audit can only accept values that are lower than or equal to 255
}

Then, at the mapping vaultBridges (AppStorage contract, line 24):

mapping(uint256 vault => address[]) vaultBridges; //@audit number of vaults can exceed uint8 borders

Then, at the function createBridge, where we can see an unsafe downcast from uint256 to uint8 of the parameter vault (OwnerFacet contract, lines 280-281):

s.vaultBridges[vault].push(bridge); //@audit bridge is actually being added to the passed uint256 `vault` parameter
s.bridge[bridge].vault = uint8(vault); //@audit downcast if >255: downcasted value gets assigned to the struct

Then, we shall examine the core functions of BridgeRouterFacet contract – all of the core functions follow the same type of execution: 1) if rethBridge or stethBridge is used – then we are using Vault.Carbon, whose uint index is 1; 2) if we are using another bridge – then the vault is chosen based on the extracted value of the vault element from the bridge struct:

uint256 vault; //@audit `vault` value is in uint256, not uint8
if (bridge == rethBridge || bridge == stethBridge) {
vault = Vault.CARBON;
} else {
vault = s.bridge[bridge].vault; //@audit if other bridge - then extract `vault` from the `bridge` struct
}

The situation in our scope is the second one: if amount of vaults operating in the protocol exceeds the borders of uint8 max value (i.e. >255) and a bridge was created for any of these vaults, then the bridge struct will be incorrectly organized because of the downcast. For example: bridge created for the vault n.256 – the vault element in the bridge struct is set to 0, for n.257 – to 1, for n.258 – to 2 and so on (I realize that this maybe an unrealistic case – still documentation says that there is a possibility of adding new vaults without any given info about limitations, so with the growth of the protocol this condition might be reached. Also in the already stated mapping vaultBridges the key vault is set to uint256, implicating that the amount of vaults can be more than uint8, also when used in functions vault parameter is never being used as uint8).
As a result, all functions in the BridgeRouterFacet executed through a non-standart bridge will be executed on the wrongly vault based on the extracted value of the downcasted vault element from the bridge struct, resulting in a serious accounting issues.
Also, another major issue is connected with the deleteBridge function of the OwnerFacet contract, because the selection of the vault, from which the bridge should be deleted, is based on the extraction of the vault element from the bridge struct (line 288):

uint256 vault = s.bridge[bridge].vault;

As a result, even though the function will be executed, actually the bridge will not be deleted for the required vault, since the vault element in the bridge struct was wrongly assigned.

Impact

  1. The accounting of the existing vaults during the execution of any asset-managing functions in the BridgeRouterFacet contract that were meant to operate with vaults >255 is going to be changed, resulting in serious accounting issues for the protocol;

  2. There will be no possibility to perform accounting operations on any of the vaults that is more than 255, since the vault parameter in the bridge struct will never point on them, even though the bridge itself will be connected;

  3. With vaults, which can be divided by 256 (i.e. 256, 512, 786 etc...), it will be impossible to execute any operations via the BridgeRouterFacet because of the onlyValidBridge modifier, since the s.bridge[bridge].vault value will be 0, which will cause functions to revert;

  4. There will be no possibility to delete bridges for newly created vaults with index >255.

Tools Used

Manual Review, Foundry

Proof of concept

Actors:

Admin: – creates a bridge for a vault, whose tracking number is more than 255 (for the clearer explanation I will take the number 257); After some time Admin decides to remove that bridge.
User: - via the newly created bridge decides to use any of the BridgeRouterFacet function, e.g. deposit:

Overview

  1. The new bridge has been added by the admin to a vault n.257; As a result, the bridge for a vault n.257 will be created (we can check through the output of the getBridges function), but in a bridge struct itself value of the vault element will be set to 1. In order to illustrate this I have added this test to Owner.t.sol contract (using forge test --match-contract Owner --match-test testCreateBridge_MoreThanUint8 -vvvv):

function testCreateBridge_MoreThanUint8() public {
address randomBridge = 0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b; //@audit just random address for example;
vm.prank(owner);
diamond.createBridge(randomBridge, 257, 1000, 0);
diamond.getBridges(257);
}

Here are the results:

Running 1 test for test/Owner.t.sol:OwnerTest
[PASS] testCreateBridge_MoreThanUint8() (gas: 98764)
Traces:
[98764] OwnerTest::testCreateBridge_MoreThanUint8()
├─ [0] VM::prank(owner: [0x71C05a4eA5E9d5b1Ac87Bf962a043f5265d4Bdc8])
│ └─ ← ()
├─ [78995] Diamond::createBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, 257, 1000, 0)
│ ├─ [73999] OwnerFacet::createBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, 257, 1000, 0) [delegatecall]
│ │ ├─ emit CreateBridge(bridge: 0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, bridgeStruct: (1000, 0, 1))
│ │ └─ ← ()
│ └─ ← ()
├─ [6797] Diamond::getBridges(257) [staticcall]
│ ├─ [1807] BridgeRouterFacet::getBridges(257) [delegatecall]
│ │ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
│ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 111.48ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

As we can see in the CreateBridge emitted event the bridge struct for the provided bridge address has been organized that way - withdrawalFee: 1000 (10%), unstakeFee: 0, vault: 1 (not 257), even though the bridge was actually connected to the required vault n.257, as we can see via the returned value of the getBridges function.
2. So, when the user calls the deposit function via the newly created bridge, it will follow the "vault = s.bridge[bridge].vault scenario", the chosen vault to perform the rest of the deposit function will be the first, the Vault.CARBON vault, not the vault n.257 as it should have been;
3. After that, all accounting operations will be performed in the CARBON vault

vault.addZeth(zethAmount);
maybeUpdateYield(vault, zethAmount);

resulting in wrong accounting.
4. Moreover, if the bridge was connected to the vault, which number can be divided by 256 (e.g. 256, 512, 768) – not a single operation via that bridge can be executed, since it will not pass the onlyValidBridge modifier (AppStorage contract, lines 123-126)

modifier onlyValidBridge(address bridge) {
if (s.bridge[bridge].vault == 0) revert Errors.InvalidBridge();
_;
}

because the value of the vault parameter will be downcasted to 0 during the execution of the createBridge function and this 0 value will be assigned to the vault element in the bridge struct.
In order to illustrate this issue, we can just change the 257 value to 256 in our test:

Running 1 test for test/Owner.t.sol:OwnerTest
[PASS] testCreateBridge_MoreThanUint8() (gas: 98764)
Traces:
[98764] OwnerTest::testCreateBridge_MoreThanUint8()
├─ [0] VM::prank(owner: [0x71C05a4eA5E9d5b1Ac87Bf962a043f5265d4Bdc8])
│ └─ ← ()
├─ [78995] Diamond::createBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, 256, 1000, 0)
│ ├─ [73999] OwnerFacet::createBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, 256, 1000, 0) [delegatecall]
│ │ ├─ emit CreateBridge(bridge: 0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, bridgeStruct: (1000, 0, 0))
│ │ └─ ← ()
│ └─ ← ()
├─ [6797] Diamond::getBridges(256) [staticcall]
│ ├─ [1807] BridgeRouterFacet::getBridges(256) [delegatecall]
│ │ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
│ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 108.55ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

As we can see, the vault element in the bridge struct is 0. So, being called in any of the functions, that contain onlyValidBridge modifier, the function will revert.

This issue can be applied to all other core functions of the BridgeRouterFacet contract, because all of them contain the stated pattern:

vault = s.bridge[bridge].vault;

(deposit function was just taken as an example). Depending on the calling function, this issue also makes the private maybeUpdateYield (if deposit or depositEth is called) function to wrongly update yield and for the _ethConversion (if withdraw, unstakeEth or withdrawTapp is called) it makes it return wrong values.
Finally, the previously stated pattern is also used in the deleteBridge function of the OwnerFacet contract, which means that the admin will not be able to delete a bridge for any vaults, which index will be more than 255. In order to illustrate this issue, I have added this test to the Owner.t.sol contract:

function testDeleteBridge_MoreThanUint8() public {
address randomBridge = 0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b;
vm.startPrank(owner);
diamond.createBridge(randomBridge, 257, 1000, 0);
diamond.getBridges(257);
diamond.deleteBridge(randomBridge);
diamond.getBridges(257);
}

Here are the results:

Running 1 test for test/Owner.t.sol:OwnerTest
[PASS] testDeleteBridge_MoreThanUint8() (gas: 96543)
Traces:
[96543] OwnerTest::testDeleteBridge_MoreThanUint8()
├─ [0] VM::startPrank(owner: [0x71C05a4eA5E9d5b1Ac87Bf962a043f5265d4Bdc8])
│ └─ ← ()
├─ [78995] Diamond::createBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, 257, 1000, 0)
│ ├─ [73999] OwnerFacet::createBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, 257, 1000, 0) [delegatecall]
│ │ ├─ emit CreateBridge(bridge: 0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b, bridgeStruct: (1000, 0, 1))
│ │ └─ ← ()
│ └─ ← ()
├─ [6797] Diamond::getBridges(257) [staticcall]
│ ├─ [1807] BridgeRouterFacet::getBridges(257) [delegatecall]
│ │ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
│ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
├─ [10411] Diamond::deleteBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b)
│ ├─ [8426] OwnerFacet::deleteBridge(0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b) [delegatecall]
│ │ ├─ emit DeleteBridge(bridge: 0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b)
│ │ └─ ← ()
│ └─ ← ()
├─ [2297] Diamond::getBridges(257) [staticcall]
│ ├─ [1807] BridgeRouterFacet::getBridges(257) [delegatecall]
│ │ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
│ └─ ← [0x08B965FAD6B5e606a2cE7f83EEfc9924cb4Fa42b]
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 114.15ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

As we can see, the returned value of the getBridges function have not changed after the execution of the deleteBridge function, even though the function has been executed.

Recommendations

Reorganize the bridge struct by changing the uint8 parameter to uint256 or any uint up to 232 (256 - 8 (unstakeFee) - 16(withdrawalFee)) if there is still need to occupy only 1slot for the bridge struct (this way it will be more than enough vaults) and remove the unsafe typecast in the createBridge function of the OwnerFacet contract.

Updates

Lead Judging Commences

0xnevi Lead Judge
almost 2 years ago
0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.