Platform: Code4rena
Start Date: 30/05/2023
Pot Size: $300,500 USDC
Total HM: 79
Participants: 101
Period: about 1 month
Judge: Trust
Total Solo HM: 36
Id: 242
League: ETH
Rank: 5/101
Findings: 5
Award: $13,507.62
π Selected for report: 3
π Solo Findings: 3
π Selected for report: shealtielanz
Also found by: 0xStalin, 0xnev, Breeje, RED-LOTUS-REACH, kutugu, xuwinnie
333.3031 USDC - $333.30
Loss of funds caused by MEV bots sandwiching the transaction because the price can be easly manipulated.
RootBridgeAgent:anyExecute()
when swapping all the deposited gas on the _gasSwapIn()
it is performed a swap, the problem is that the swap uses the instantaneous price from slot0 instead of the TWAP price. The slot0 price is calculated from the ratios of the assets of the pool. This ratio can however be easily manipulated and cause the swap to receive less funds than what it could've received.Manual Review & This Article Explaining in depth the Uniswap v3 TWAP Oracle
Use TWAP price instead of slot0 price. Here is an example implementation of TWAP.
Uniswap
#0 - c4-judge
2023-07-10T11:22:04Z
trust1995 marked the issue as duplicate of #823
#1 - c4-judge
2023-07-10T11:22:09Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T16:57:26Z
trust1995 changed the severity to 3 (High Risk)
π Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
47.6891 USDC - $47.69
function _normalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { return _decimals == 18 ? _amount : _amount * (10 ** _decimals) / 1 ether; }
_amount
is scaled up by the tokensDecimals!
_amount * (10 ** _decimals) / 1 ether
[(10**8) * (10**8)] / (10**18)
===> (10**16) / (10**18)
===> 10**-2
_amount
is scaled up by the tokensDecimals!
_amount * (10 ** _decimals) / 1 ether
[(10**30) * (10**30)] / (10**18)
===> (10**60) / (10**18)
===> 10**42
10**42
Manual Audit
function _normalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { - return _decimals == 18 ? _amount : _amount * (10 ** _decimals) / 1 ether; + return _decimals == 18 ? _amount : _amount * 1 ether / (10 ** _decimals); }
_amount
is scaled up by the tokensDecimals!
The formula: _amount * 1 ether / (10 ** _decimals)
Substituting values: [(10**8) * (10**18)] / (10**8)
===> (10**26) / (10**8)
===> 10**18
result: 10**18
_amount
is scaled up by the tokensDecimals!
The formula: _amount * 1 ether / (10 ** _decimals)
Substituting values: [(10**30) * (10**18)] / (10**30)
===> (10**48) / (10**30)
===> 10**18
result: 10**18
Math
#0 - c4-judge
2023-07-09T15:19:09Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-09T15:19:16Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-28T11:16:52Z
trust1995 marked the issue as partial-50
π Selected for report: peakbolt
Also found by: 0xStalin, 0xTheC0der, BPZ, LokiThe5th, RED-LOTUS-REACH, adeolu, bin2chen, jasonxiale, kodyvim, kutugu, ltyu, ubermensch
47.6891 USDC - $47.69
https://github.com/code-423n4/2023-05-maia/blob/main/src/ulysses-omnichain/BranchPort.sol#L383-L390
function _denormalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { return _decimals == 18 ? _amount : _amount * 1 ether / (10 ** _decimals); }
_amount
is scaled up by 10**18
because is the amount that is deposited inside the system, and all the deposits were normalized to 18 decimals
The formula: _amount * 1 ether / (10 ** _decimals)
Substituting values: [(10**18) * (10**18)] / (10**8)
===> (10**36) / (10**8)
===> 10**28
result: 10**28
_amount
is scaled up by 10**18
because is the amount that is deposited inside the system, and all the deposits were normalized to 18 decimals
The formula: _amount * 1 ether / (10 ** _decimals)
Substituting values: [(10**18) * (10**18)] / (10**30)
===> (10**36) / (10**30)
===> 10**6
result: 10**6
Manual Audit
function _denormalizeDecimals(uint256 _amount, uint8 _decimals) internal pure returns (uint256) { - return _decimals == 18 ? _amount : _amount * 1 ether / (10 ** _decimals); + return _decimals == 18 ? _amount : _amount * (10 ** _decimals) / 1 ether; }
_amount
is scaled up by 10**18
because is the amount that is deposited inside the system, and all the deposits were normalized to 18 decimals
The formula: _amount * (10 ** _decimals) / 1 ether
Substituting values: [(10**18) * (10**8)] / (10**18)
===> (10**26) / (10**18)
===> 10**8
result: 10**8
_amount
is scaled up by 10**18
because is the amount that is deposited inside the system, and all the deposits were normalized to 18 decimals
The formula: _amount * (10 ** _decimals) / 1 ether
Substituting values: [(10**18) * (10**30)] / (10**18)
===> (10**48) / (10**18)
===> 10**30
result: 10**30
Decimal
#0 - c4-judge
2023-07-09T15:47:10Z
trust1995 marked the issue as duplicate of #758
#1 - c4-judge
2023-07-09T15:47:14Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-28T11:16:58Z
trust1995 marked the issue as partial-50
π Selected for report: 0xStalin
5707.2337 USDC - $5,707.23
nonce
is located can lead to set as executed the incorrect nonce, which will cause unexpected behavior and potentially a DoS when attempting to execute a nonce
that incorrectly was marked as already executed.IRootBridgeAgent
contract* | Flag | Deposit Info | Token Info | DATA | Gas Info | * | 1 byte | 4-25 bytes | 3 + (105 or 128) * n bytes | --- | 32 bytes | * |_______________________________|____________________________|____________________________________|__________|_____________| * | callOutSignedMultiple = 0x6 | 20b + 1b(n) + 4b(nonce) | 32b + 32b + 32b + 32b + 3b | --- | 16b + 16b |
The actual encoding of the data happens on the BranchBridgeAgent
contract, on these lines
Based on the data structure we can decode and determine on what offset is located what data
data[0]
=> flagdata[1:21]
=> an addressdata[21]
=> hTokens.lengthdata[22:26]
=> The 4 bytes of the nonceSo, when flag is 0x06
, the nonce is located at the offset data[22:26]
, that indicates that the current offset that is been accessed is wrong (data[PARAMS_START_SIGNED:25]
=== data[21:]
)
Manual Audit
Make sure to read the nonce
from the correct offset, based on the data structure as explain in the IRootBridgeAgent
contract
For flag 0x06
, read the offset as follows, either of the two options are correct:
nonce
is located at: data[22:26]
nonce = uint32(bytes4(data[PARAMS_START_SIGNED + PARAMS_START : 26])); nonce = uint32(bytes4(data[22:26]));
en/de-code
#0 - c4-judge
2023-07-10T09:40:17Z
trust1995 marked the issue as duplicate of #169
#1 - c4-judge
2023-07-10T09:40:23Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-11T17:29:06Z
trust1995 changed the severity to 3 (High Risk)
#3 - stalinMacias
2023-07-26T20:37:31Z
Hey @trust1995 , while this issue and #169 are about accessing an incorrect offset of the encoded parameters, the root cause of the issue on this one is reading the offset from [21:25] and in the other issue the offset is currently being read from the offset [22:27]
0x06
, apart from reading an incorrect offset is reading more bytes, the offset is being read at [22:27], which is actually reading 5 bytes, and the depositNonce
is only 4 bytes, that is why the correct offset should be [22:26]I think that the two issues should not be a duplicate of each other and they should be assessed individually.
The fact that the issue was found in one of the two contracts doesn't imply that the other issue would be caught, that's why I feel like the two findings should be handled separately.
#4 - c4-judge
2023-07-27T08:17:54Z
trust1995 marked the issue as not a duplicate
#5 - c4-judge
2023-07-27T08:18:21Z
trust1995 marked the issue as primary issue
#6 - c4-judge
2023-07-27T08:19:47Z
trust1995 marked the issue as selected for report
#7 - c4-sponsor
2023-07-27T08:40:10Z
0xBugsy marked the issue as sponsor confirmed
#8 - 0xLightt
2023-09-07T10:45:27Z
π Selected for report: 0xStalin
5707.2337 USDC - $5,707.23
depositNonce
is located can lead to set the status of the wrong deposit to Failed when the _clearDeposit() function is called.
depositNonce
to False can be:
depositNonce
that is sent to the anyFallback() because that depositNonce
won't be marked as Failed.depositNonces
that should not be marked as Failed.depositNonce
will be located at a different offset depending the flag.depositNonce
is located, is required to check the corresponding operation where the data was packed
depositNonce
for each flagflag == 0x00
depositNonce
is located at the data[1:5]bytes memory packedData = abi.encodePacked(bytes1(0x00), depositNonce, _params, gasToBridgeOut, _remoteExecutionGas); // data[0] ==> flag === 0x00 // data[1:5] ==> depositNonce
flag == 0x01
depositNonce
is located at the data[1:5]bytes memory packedData = abi.encodePacked(bytes1(0x01), depositNonce, _params, _gasToBridgeOut, _remoteExecutionGas); // data[0] ==> flag === 0x01 // data[1:5] ==> depositNonce
flag == 0x02
depositNonce
is located at the data[1:5]bytes memory packedData = abi.encodePacked( bytes1(0x02), depositNonce, _dParams.hToken, _dParams.token, _dParams.amount, _normalizeDecimals(_dParams.deposit, ERC20(_dParams.token).decimals()), _dParams.toChain, _params, _gasToBridgeOut, _remoteExecutionGas ); // data[0] ==> flag === 0x02 // data[1:5] ==> depositNonce
flag == 0x03
depositNonce
is located at the data[2:6]bytes memory packedData = abi.encodePacked( bytes1(0x03), uint8(_dParams.hTokens.length), depositNonce, _dParams.hTokens, _dParams.tokens, _dParams.amounts, deposits, _dParams.toChain, _params, _gasToBridgeOut, _remoteExecutionGas ); // data[0] ==> flag === 0x03 // data[1] ==> hTones.length // data[2:6] ==> depositNonce
flag == 0x04
depositNonce
is located at the data[21:25]bytes memory packedData = abi.encodePacked( bytes1(0x04), msg.sender, depositNonce, _params, msg.value.toUint128(), _remoteExecutionGas ); // data[0] ==> flag === 0x04 // data[1:21] ==> msg.sender // data[21:25] ==> depositNonce
flag == 0x05
depositNonce
is located at the data[21:25]bytes memory packedData = abi.encodePacked( bytes1(0x05), msg.sender, depositNonce, _dParams.hToken, _dParams.token, _dParams.amount, _normalizeDecimals(_dParams.deposit, ERC20(_dParams.token).decimals()), _dParams.toChain, _params, msg.value.toUint128(), _remoteExecutionGas ); // data[0] ==> flag === 0x05 // data[1:21] ==> msg.sender // data[21:25] ==> depositNonce
flag == 0x06
depositNonce
is located at the data[22:26]bytes memory packedData = abi.encodePacked( bytes1(0x06), msg.sender, uint8(_dParams.hTokens.length), depositNonce, _dParams.hTokens, _dParams.tokens, _dParams.amounts, _deposits, _dParams.toChain, _params, msg.value.toUint128(), _remoteExecutionGas ); // data[0] ==> flag === 0x06 // data[1:21] ==> msg.sender // data[21] ==> hTokens.length // data[22:26] ==> depositNonce
depositNonce
is located at for all the possible deposit options.For flags 0x00 , 0x01 & 0x02
, the depositNonce
is been read from the offset data[PARAMS_START:PARAMS_TKN_START]
, which is the same as data[1:5]
(PARAMS_START == 1 & PARAMS_TKN_START == 5), these 3 flags read correctly the depositNonce
For flag 0x03
, the depositNonce
is been read from the offset data[PARAMS_START + PARAMS_START:PARAMS_TKN_START + PARAMS_START]
, which is the same as data[2:6]
(PARAMS_START == 1 & PARAMS_TKN_START == 5), this flag also reads correctly the depositNonce
For flag 0x04 & 0x05
, the depositNonce
is been read from the offset data[PARAMS_START_SIGNED:PARAMS_START_SIGNED + PARAMS_TKN_START]
, which is the same as data[21:26]
(PARAMS_START_SIGNED == 21 & PARAMS_TKN_START == 5), these flags are reading INCORRECTLY the depositNonce
, from the above analyzis to detect where the depositNonce
is located at, we got that for flags 0x04 & 0x05, the depositNonce
is located at the offset data[21:25]
PoC to demonstrate the correct offset of the depositNonce when data is encoded similar to how flags 0x04 & 0x05 encode it (See the above analysis for more details)
generateData()
function and copy+paste the generated bytes on the rest of the functionsreadNonce()
returns the correct value of the nonce, and is reading the offset data[21:25]
pragma solidity 0.8.18; contract offset { uint32 nonce = 3; function generateData() external view returns (bytes memory) { bytes memory packedData = abi.encodePacked( bytes1(0x01), msg.sender, nonce ); return packedData; } function readFlag(bytes calldata data) external view returns(bytes1) { return data[0]; } function readMsgSender(bytes calldata data) external view returns (address) { return address(uint160(bytes20(data[1:21]))); } function readNonce(bytes calldata data) external view returns (uint32) { return uint32( bytes4(data[21:25]) ); } }
flag 0x06
, the depositNonce
is been read from the offset data[PARAMS_START_SIGNED + PARAMS_START:PARAMS_START_SIGNED + PARAMS_TKN_START + PARAMS_START]
, which is the same as data[22:27]
(PARAMS_START_SIGNED == 21, PARAMS_START == 1 & PARAMS_TKN_START == 5), this flag is also reading INCORRECTLY the depositNonce
, from the above analyzis to detect where the depositNonce
is located at, we got that for flag 0x06, the depositNonce
is located at the offset data[22:26]PoC to demonstrate the correct offset of the depositNonce when data is encoded similar to how flag 0x06 encode it (See the above analysis for more details)
generateData()
function and copy+paste the generated bytes on the rest of the functionsreadNonce()
returns the correct value of the nonce, and is reading the offset data[22:26]
pragma solidity 0.8.18; contract offset { uint32 nonce = 3; function generateData() external view returns (bytes memory) { bytes memory packedData = abi.encodePacked( bytes1(0x01), msg.sender, uint8(1), nonce ); return packedData; } function readFlag(bytes calldata data) external view returns(bytes1) { return data[0]; } function readMsgSender(bytes calldata data) external view returns (address) { return address(uint160(bytes20(data[1:21]))); } function readThirdParameter(bytes calldata data) external view returns(uint8) { return uint8(bytes1(data[21])); } function readNonce(bytes calldata data) external view returns (uint32) { return uint32( bytes4(data[22:26]) ); } }
Manual Audit
Make sure to read the depositNonce
from the correct offset, depending on the flag it will be the offset where depositNonce
is located at:
For flags 0x04 & 0x05
, read the offset as follows, either of the two options are correct:
depositNonce
is located at: data[21:25]
_depositNonce = uint32(bytes4(data[PARAMS_START_SIGNED : PARAMS_START_SIGNED])); _depositNonce = uint32(bytes4(data[21:25]));
flag 0x06
, read the offset as follows, either of the two options are correct:
depositNonce
is located at: data[22:26]
_depositNonce = uint32(bytes4(data[PARAMS_START_SIGNED + PARAMS_START : PARAMS_START_SIGNED + PARAMS_TKN_START])); _depositNonce = uint32(bytes4(data[22:26]));
en/de-code
#0 - c4-judge
2023-07-09T15:15:59Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-09T15:16:03Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-12T12:36:20Z
0xBugsy marked the issue as sponsor confirmed
#3 - c4-judge
2023-07-25T13:48:38Z
trust1995 marked the issue as selected for report
#4 - 0xLightt
2023-09-07T10:46:24Z
π Selected for report: 0xStalin
1712.1701 USDC - $1,712.17
The function clearTokens() is called from the BranchBridgeAgentExecutor::executeWithSettlementMultiple() function, which is used when the settlement flag is 2 "Multiple Settlements"
As per the documentation about the messaging layer written in the IBranchBridgeAgent
contract, when the flag is 2, the structure of the token info is as follows:
* - ht = hToken * - t = Token * - A = Amount * - D = Destination * - b = bytes * - n = number of assets * ________________________________________________________________________________________________________________________________ * | Flag | Deposit Info | Token Info | DATA | Gas Info | * | 1 byte | 4-25 bytes | (105 or 128) * n bytes | --- | 16 bytes | * | | | hT - t - A - D | | | * |_______________________________|__________________________________|____________________________________|__________|_____________| * | callOutMulti = 0x2 | 1b(n) + 20b(recipient) + 4b | 32b + 32b + 32b + 32b | --- | 16b |
Manual Audit
function clearTokens(bytes calldata _sParams, address _recipient) ... { ... _tokens[i] = address( uint160( bytes20( bytes32( _sParams[ PARAMS_TKN_START + PARAMS_ENTRY_SIZE * uint16(i + numOfAssets) + 12: PARAMS_TKN_START + PARAMS_ENTRY_SIZE * uint16(PARAMS_START + i + numOfAssets) ] ) ) ) ); ... }
en/de-code
#0 - c4-judge
2023-07-09T15:27:11Z
trust1995 marked the issue as unsatisfactory: Insufficient proof
#1 - c4-judge
2023-07-25T10:33:08Z
trust1995 marked the issue as satisfactory
#2 - c4-judge
2023-07-25T15:23:50Z
trust1995 marked the issue as primary issue
#3 - c4-sponsor
2023-07-25T18:00:38Z
0xBugsy marked the issue as disagree with severity
#4 - c4-sponsor
2023-07-25T18:00:50Z
0xBugsy marked the issue as sponsor confirmed
#5 - c4-judge
2023-07-27T11:06:20Z
trust1995 marked the issue as selected for report
#6 - 0xLightt
2023-09-07T10:58:03Z