Ondo Finance - adriro's results

Institutional-Grade Finance. On-Chain. For Everyone.

General Information

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

Ondo Finance

Findings Distribution

Researcher Performance

Rank: 1/70

Findings: 4

Award: $4,057.52

QA:
grade-a
Gas:
grade-a

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: adriro

Also found by: gkrastenov, lsaudit

Labels

bug
2 (Med Risk)
low quality report
primary issue
satisfactory
selected for report
sponsor acknowledged
M-01

Awards

3565.9193 USDC - $3,565.92

External Links

Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/SourceBridge.sol#L121-L129 https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L234-L240

Vulnerability details

Chain support cannot be removed or cleared in bridge contracts

Due to how addresses are handled and stored in the configuration settings, it is not possible to remove chain support in both source and destination bridge contracts.

Impact

The Axelar bridge service uses strings to represent addresses: messages sent to another chain need to specify its destination contract as a string. The protocol decided to follow the same representation and contract store addresses as strings as part of their configuration.

Chain support in the bridge contracts is then represented by associating the chain name with the address of the contract, stored as a string. This can be seen in the implementation of setDestinationChainContractAddress() for the SourceBridge contract, which stores the string, and the implementation of addChainSupport() in the DestinationBridge contract, which stores the hash of the string:

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/SourceBridge.sol#L121-L129

121:   function setDestinationChainContractAddress(
122:     string memory destinationChain,
123:     address contractAddress
124:   ) external onlyOwner {
125:     destChainToContractAddr[destinationChain] = AddressToString.toString(
126:       contractAddress
127:     );
128:     emit DestinationChainContractAddressSet(destinationChain, contractAddress);
129:   }

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L234-L240

234:   function addChainSupport(
235:     string calldata srcChain,
236:     string calldata srcContractAddress
237:   ) external onlyOwner {
238:     chainToApprovedSender[srcChain] = keccak256(abi.encode(srcContractAddress));
239:     emit ChainIdSupported(srcChain, srcContractAddress);
240:   }

This also means that checks need to be done based on the stored representation. SourceBridge checks the length of the string, while DestinationBridge checks for a bytes32(0) value:

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/SourceBridge.sol#L68-L71

68:     if (bytes(destContract).length == 0) {
69:       revert DestinationNotSupported();
70:     }
71: 

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L96-L98

96:     if (chainToApprovedSender[srcChain] == bytes32(0)) {
97:       revert ChainNotSupported();
98:     }

Note that this implies that there is no way to clear these settings and remove chain support. In the case of SourceBridge, any address sent to setDestinationChainContractAddress() will be converted to their string representation which will always have length greater than zero. For the DestinationBridge, addChainSupport() will hash the address parameter and it will be impossible for that hash value to be zero (since it will imply knowing the preimage of zero).

Proof of Concept

  1. Admin configs destination address in SourceBridge by calling setDestinationChainContractAddress("optimism", destinationAddress).
  2. Admin decides to remove support for Optimism.
  3. Admin calls setDestinationChainContractAddress("optimism", address(0)), but this will actually store the string for the zero address "0x0000....000".
  4. The check bytes(destContract).length == 0 will fail and messages will still be routed.

Provide functions in both contracts to allow the owner to clear the settings by resetting their configuration to the default value.

function removeDestinationChainContractAddress(
  string memory destinationChain
) external onlyOwner {
  delete destChainToContractAddr[destinationChain];
  emit DestinationChainContractAddressRemoved(destinationChain);
}
function removeChainSupport(
  string calldata srcChain
) external onlyOwner {
  delete chainToApprovedSender[srcChain];
  emit ChainIdRemoved(srcChain);
}

Assessed type

Other

#0 - c4-pre-sort

2023-09-08T05:11:57Z

raymondfam marked the issue as duplicate of #467

#1 - c4-pre-sort

2023-09-08T05:12:01Z

raymondfam marked the issue as low quality report

#2 - c4-judge

2023-09-19T10:18:44Z

kirk-baird marked the issue as unsatisfactory: Invalid

#3 - c4-judge

2023-09-27T00:52:41Z

kirk-baird removed the grade

#4 - c4-judge

2023-09-27T00:53:13Z

kirk-baird marked the issue as selected for report

#5 - c4-judge

2023-09-27T00:53:51Z

kirk-baird marked the issue as satisfactory

#6 - tom2o17

2023-09-28T14:40:38Z

@kirk-baird

Can I not set acceptable srcAddr to address(0) and then given address(0) cannot have a contract deployed to it, this value should not be w/n the possible ranges of srcAddr w/n the _execute function?

Granted not super ideal given it will hit a diff error msg, but functionally the result is the same.

#7 - kirk-baird

2023-09-28T21:50:14Z

@kirk-baird

Can I not set acceptable srcAddr to address(0) and then given address(0) cannot have a contract deployed to it, this value should not be w/n the possible ranges of srcAddr w/n the _execute function?

Granted not super ideal given it will hit a diff error msg, but functionally the result is the same.

@tom2o17 my concern here is that there's no way to stop calling burnAndCallAxelar() which would burn tokens on the source chain. These would not be minted on the destination chain and so are essentially lost.

e.g. If you do setDestinationChainContractAddress(dstChain, address(0)) then Address.toString(address(0)) will encode that to 42 hex characters.

So this check in burnAndCallAxelar() will always pass since bytes(destContract).length is 42 bytes.

#8 - tom2o17

2023-09-29T15:50:53Z

@kirk-baird

Ah I see. I am inclined to accept this issue, apologies thought this was referencing the dstBridge not the srcBridge. As mitigation we would be able to pause the contract, and prevent this functionality but agree with the auditor that this is not ideal.

#9 - c4-sponsor

2023-10-11T13:57:20Z

tom2o17 (sponsor) acknowledged

Findings Information

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
duplicate-391

Awards

265.675 USDC - $265.68

External Links

Lines of code

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L109

Vulnerability details

Bridged messages containing the same information coming from different source chains may clash in the destination bridge and lead to loss of tokens.

Impact

Tokens are bridged by encoding some information that is sent as the payload of the bridged message through the Axelar service. The destination bridge then recovers this information and uses its hash as the key value to identify the transaction while it is under review by the approvers.

The SourceBridge contract encodes the current version, the sender, amount and current nonce as the payload of the message:

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/SourceBridge.sol#L79-L81

79:     bytes memory payload = abi.encode(VERSION, msg.sender, amount, nonce++);
80: 
81:     _payGasAndCallContract(destinationChain, destContract, payload);

The DestinationBridge receives this payload and hashes it using keccak256 that serves as the key of both mappings txnHashToTransaction (which holds the transaction information) and txnToThresholdSet (which holds the current state of approvals):

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L108-L109

108:     bytes32 txnHash = keccak256(payload);
109:     txnHashToTransaction[txnHash] = Transaction(srcSender, amt);

Note that the payload doesn't contain the source bridge address, and the mapping isn't scoped to the source chain, the key is just the hash of the payload. If an account wants to bridge the same amount of tokens from different source chains, which also happen to have by coincidence the same current nonce in their source bridge, both messages will end up having the same payload and the same value for their hash, causing a clash in these mappings.

Since it is likely that bridged messages will need confirmation by approvers (i.e. minting won't happen automatically in _execute()), the described will lead to loss of funds for the user: while the first transaction is pending to be executed as it is being reviewed by the approvers, the next transaction will overwrite all the data from the first, leading to information loss and preventing the minting of tokens from the first transaction that arrived.

Proof of Concept

User has the same amount of tokens in both source chain 1 (SC1) and source chain 2 (SC2) and wants to bridge them to the destination chain (DC). State of source bridge in both SC1 and SC2 currently contains the same nonce value.

  1. User calls burnAndCallAxelar(amount, DC) in SC1.
  2. DC receives message through execute(). This will hash the payload as txnHash and store this transaction in txnHashToTransaction[txnHash] while it is under review to be approved.
  3. User calls burnAndCallAxelar(amount, DC) in SC2.
  4. Again, DC receives message through execute() and hashes the payload. Since the information is the same (same version, same sender, same amount and same nonce) the hash happens to be the same as before, and will end up overriding the values in the txnHashToTransaction mapping as the key is the same.
  5. The first transaction will be lost as all of the information will be overwritten.

Scope the txnHashToTransaction mapping to include the address of the source bridge, or alternatively include the source address as part of the bridged payload.

Assessed type

Other

#0 - c4-pre-sort

2023-09-08T01:39:58Z

raymondfam marked the issue as duplicate of #162

#1 - c4-pre-sort

2023-09-08T01:40:08Z

raymondfam marked the issue as sufficient quality report

#2 - c4-judge

2023-09-17T06:40:22Z

kirk-baird marked the issue as satisfactory

#3 - c4-judge

2023-09-26T03:03:40Z

kirk-baird changed the severity to 2 (Med Risk)

Awards

113.855 USDC - $113.85

Labels

bug
grade-a
QA (Quality Assurance)
selected for report
sponsor confirmed
sufficient quality report
edited-by-warden
Q-09

External Links

Report

Summary

Low Issues

Total of 10 issues:

IDIssue
L-1Confusing semantics for bridged messages approvals
L-2No default threshold configuration
L-3Validate number of approvers in setThresholds()
L-4Missing safe wrapper for ERC20 transfer in rescueTokens()
L-5Account may get blacklisted during the bridge process
L-6Missing validation for index parameter in overrideRange()
L-7Oracle assumes asset has 18 decimals
L-8Wrong argument in Transfer event of wrap() function
L-9Wrong argument in TransferShares event of wrap() function
L-10Contracts can be re-deployed in rUSDY factory

Non Critical Issues

Total of 2 issues:

IDIssue
NC-1Missing calls to base initializers in rUSDY
NC-2Missing event to notify oracle has changed in rUSDY

Low Issues

<a name="L-1"></a>[L-1] Confusing semantics for bridged messages approvals

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L111

Bridged messages get an "automatic" approval while the message is being executed in the destination bridge contract, which means that all messages effectively get one approval as soon as they are bridged.

This implies that "real" approvals would require setting a threshold with a numberOfApprovalsNeeded of at least two.

This brings a lot of unnecessary confusing semantics to the approvals scheme and could potentially lead to errors in configuration settings that would allow messages to go through automatically when actually they are expected to be approved.

It is recommended to remove this automatic approval and treat each approval as an explicit approval action of the set of enabled approvers.

<a name="L-2"></a>[L-2] No default threshold configuration

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L128

The implementation of _attachThreshold() loops through each of the configured set of thresholds searching for the element that adjusts to the bridged amount.

If no configuration is found the function will revert, the message will be lost and it will require manual handling.

It is recommended setting a default value (a high value number of approvals) to be used when no threshold configuration matches.

<a name="L-3"></a>[L-3] Validate number of approvers in setThresholds()

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L255

The setThresholds() function should validate that elements in the numOfApprovers array are greater than zero.

<a name="L-4"></a>[L-4] Missing safe wrapper for ERC20 transfer in rescueTokens()

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L322

Use a "safe" wrapper to execute ERC20 transfer for better compatibility.

<a name="L-5"></a>[L-5] Account may get blacklisted during the bridge process

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L349

Accounts that bridge tokens may get blacklisted in the source chain after the bridging process had been initiated. This could lead to different issues:

  • If the account is not blacklisted in the destination chain, the user may still operate their funds.
  • If the account is blacklisted, then the function will revert and could be treated as a failure that would require manual intervention.

<a name="L-6"></a>[L-6] Missing validation for index parameter in overrideRange()

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L186

The overrideRange() function should validate that indexToModify is within bounds, i.e. require(indexToModify < rangeLength).

<a name="L-7"></a>[L-7] Oracle assumes asset has 18 decimals

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L282

The roundUpTo8() implementation assumes the asset has 18 decimals as the rounding is done using 1e10 (since 18 - 8 = 10). It is recommended to use a constant to properly state this dependency and for better understanding.

<a name="L-8"></a>[L-8] Wrong argument in Transfer event of wrap() function

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L438

The third argument to the Transfer event is expecting the amount of rUSDY tokens but it is wrongly called with getRUSDYByShares(_USDYAmount), which lacks the proper scaling to convert the USDY amount to shares.

The correct version should be:

emit Transfer(address(0), msg.sender, getRUSDYByShares(_USDYAmount * BPS_DENOMINATOR));

<a name="L-9"></a>[L-9] Wrong argument in TransferShares event of wrap() function

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L439

The third argument to the TransferShares event is expecting the amount of shares but it is wrongly called with _USDYAmount, which is the amount of USDY tokens, not the shares.

The correct version should be:

emit TransferShares(address(0), msg.sender, _USDYAmount * BPS_DENOMINATOR);

<a name="L-10"></a>[L-10] Contracts can be re-deployed in rUSDY factory

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L75

The rUSDY contract can be redeployed by the guardian by calling deployrUSDY() in the rUSDYFactory contract. This will deploy a new set of contracts and overwrite the storage variables that link to the contract.

It is recommended to add a check to avoid redeployment once contracts have been deployed.

Non Critical Issues

<a name="NC-1"></a>[NC-1] Missing calls to base initializers in rUSDY

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L120

The __rUSDY_init() function doesn't call the initializers for some of the base contracts:

  • Initializable
  • ContextUpgradeable
  • PausableUpgradeable
  • AccessControlEnumerableUpgradeable

<a name="NC-2"></a>[NC-2] Missing event to notify oracle has changed in rUSDY

https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L662

The setOracle() function should emit an event to signal off-chain actors that the oracle has been updated.

#0 - c4-pre-sort

2023-09-08T08:34:07Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-09-21T10:21:06Z

kirk-baird marked the issue as grade-a

#2 - c4-judge

2023-09-24T07:32:15Z

kirk-baird marked the issue as selected for report

#3 - kirk-baird

2023-09-24T07:39:03Z

The issues raised in this report are all valid and contain quality analysis. They correctly determine the severity except L-01 which I would consider a non-critical rather than low.

#4 - c4-sponsor

2023-09-25T13:55:12Z

ali2251 (sponsor) confirmed

Awards

112.0737 USDC - $112.07

Labels

bug
G (Gas Optimization)
grade-a
sponsor acknowledged
sufficient quality report
edited-by-warden
G-01

External Links

DestinationBridge contract

RWADynamicOracle contract

rUSDY contract

rUSDYFactory contract

#0 - c4-pre-sort

2023-09-08T14:40:07Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-09-24T06:01:54Z

kirk-baird marked the issue as grade-a

#2 - c4-sponsor

2023-09-27T21:28:10Z

ali2251 (sponsor) acknowledged

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter