Platform: Code4rena
Start Date: 01/09/2023
Pot Size: $36,500 USDC
Total HM: 4
Participants: 70
Period: 6 days
Judge: kirk-baird
Id: 281
League: ETH
Rank: 5/70
Findings: 3
Award: $1,605.87
🌟 Selected for report: 0
🚀 Solo Findings: 0
1333.1052 USDC - $1,333.11
https://github.com/code-423n4/2023-09-ondo/blob/b88271d64112234b7a7273cd7f3cea73c350e6a7/contracts/bridge/SourceBridge.sol#L79 https://github.com/code-423n4/2023-09-ondo/blob/b88271d64112234b7a7273cd7f3cea73c350e6a7/contracts/bridge/DestinationBridge.sol#L349
The bridge contracts assume that the source and destination chains have the same address encoding, which may not be true and cause the users to lose their tokens in the destination chain.
In SourceBridge, the source sender is set to msg.sender and encoded into the payload.
bytes memory payload = abi.encode(VERSION, msg.sender, amount, nonce++);
In destination chain, the DestinationBridge will mint the new tokens to the same address as the source sender.
TOKEN.mint(txn.sender, txn.amount);
This will work fine if the bridges are only used in the chains that are EVM compatible and have the same address encoding.
However, if the bridges are used in the chains that are EVM compatible but have different address encoding, the users will not receive the tokens in the destination chain because there may be no address that matches the source sender in the destination chain.
For example, TRON is EVM compatible but has different address encoding from Ethereum. If the bridges are used in Ethereum and TRON, the users will not receive the tokens in TRON. Currently, TVM is compatible with EVM.
https://tronprotocol.github.io/documentation-en/contracts/tvm/#features-of-tvm: https://stackoverflow.com/questions/74974850/creating-a-wallet-on-ether-creates-it-on-all-evn-supported-networks
Because the contracts are deployed to any EVM compatible chains, it is possible that the bridges are used in the chains that have different address encoding.
Manual
Allow users to set receiver address instead of using msg.sender address from source chain as the receiver address.
en/de-code
#0 - c4-pre-sort
2023-09-08T06:20:34Z
raymondfam marked the issue as duplicate of #406
#1 - c4-pre-sort
2023-09-08T06:20:39Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2023-09-17T06:11:05Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: Udsen
Also found by: 0xDING99YA, 0xpiken, Inspecktor, SpicyMeatball, adriro, ast3ros, bin2chen, bowtiedvirus, kutugu, pep7siup, seerether
265.675 USDC - $265.68
https://github.com/code-423n4/2023-09-ondo/blob/b88271d64112234b7a7273cd7f3cea73c350e6a7/contracts/bridge/DestinationBridge.sol#L102-L104 https://github.com/code-423n4/2023-09-ondo/blob/b88271d64112234b7a7273cd7f3cea73c350e6a7/contracts/bridge/SourceBridge.sol#L77
The destination bridge does not distinguish the source chain of the transactions, which may cause nonce collision and revert the minting of the bridged tokens. The destination bridge will revert some valid transactions because the nonce is already spent.
In the destination bridge, the nonce is checked to ensure that no transaction is replayed. The isSpentNonce is the mapping of hash of approved sender => nonce => bool
and set to true when the transaction is executed.
mapping(bytes32 => mapping(uint256 => bool)) public isSpentNonce;
The problem is that the mapping does not specify which chain the transaction is from. Therefore, if two transactions from different chains have the same approved sender address and the same nonce, the second transaction will fail because the nonce is already spent.
if (isSpentNonce[chainToApprovedSender[srcChain]][nonce]) { revert NonceSpent(); }
This can happen because the address of the contract is defined by the address of the deployer and nonce. If the deployer address is the same in both chains, the address of the contract can be the same.
The second transaction failure will cause the user to lose their bridged tokens because the minting is reverted and the tokens are already burnt in the source chain
TOKEN.burnFrom(msg.sender, amount);
Manual
The isSpentNonce
should be specified by which chain the transaction is from.
For example, the mapping can be changed to mapping(bytes32 => mapping(uint256 => mapping(uint256 => bool))) public isSpentNonce;
where the first uint256 is the chain id.
Other
#0 - c4-pre-sort
2023-09-08T00:08:02Z
raymondfam marked the issue as duplicate of #162
#1 - c4-pre-sort
2023-09-08T00:08:17Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2023-09-17T06:41:41Z
kirk-baird marked the issue as satisfactory
#3 - c4-judge
2023-09-26T03:03:40Z
kirk-baird changed the severity to 2 (Med Risk)
🌟 Selected for report: adriro
Also found by: 0x6980, 0xStalin, 0xanmol, 0xmystery, 0xpanicError, Arz, Aymen0909, BenRai, Breeje, Lalanda, MohammedRizwan, Raihan, SovaSlava, Stormreckson, Udsen, ast3ros, bin2chen, castle_chain, catellatech, codegpt, dev0cloo, gkrastenov, hals, klau5, kutugu, ladboy233, matrix_0wl, nirlin, ohm, peanuts, pipidu83, sandy, wahedtalash77
7.08 USDC - $7.08
Contract that inherit from PausableUpgradeable should call the __Pausable_init()
function in their initialize function. This is not done in the rUSDY contract.
) internal onlyInitializing { __BlocklistClientInitializable_init(blocklist); __AllowlistClientInitializable_init(allowlist); __SanctionsListClientInitializable_init(sanctionsList); __rUSDY_init_unchained(_usdy, guardian, _oracle); + __Pausable_init(); }
In the DestinationBridge, the number of approvals needed is always one less than the threshold. This is because there is an automatic approval when _execute
function is called.
_approve(txnHash);
This means that the number of approvals needed is always reduced by one. For example, if the threshold is 2, then the number of approvals needed is 1. If the threshold is 3, then the number of approvals needed is 2.
In _rmul
function, the x * y operation is executed in the _mul(x, y) function. The _mul function performs the overflow checking by require(y == 0 || (z = x * y) / y == x);
. However since solidity 0.8, overflow checking is done by default. Therefore, the overflow checking in _mul function is unnecessary and can be removed.
In the destination bridge, after the transaction is executed, not all the state variables related to the transaction are cleared. There are two state variables:
Gas can be saved by clearing the txnToThresholdSet state variable.
In the unwrap function, the usdyAmount
is not the amount of USDY to be unwrapped, but the amount of shares to be burned. Therefore, the variable name should be changed to sharesAmount
instead of usdyAmount
to avoid confusion. It can mislead the reader to think that the amount of USDY.
#0 - c4-pre-sort
2023-09-08T08:16:07Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-09-23T00:59:07Z
kirk-baird marked the issue as grade-b