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: 1/70
Findings: 4
Award: $4,057.52
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: adriro
Also found by: gkrastenov, lsaudit
3565.9193 USDC - $3,565.92
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
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.
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: }
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).
setDestinationChainContractAddress("optimism", destinationAddress)
.setDestinationChainContractAddress("optimism", address(0))
, but this will actually store the string for the zero address "0x0000....000".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); }
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
toaddress(0)
and then givenaddress(0)
cannot have a contract deployed to it, this value should not be w/n the possible ranges ofsrcAddr
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
🌟 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/main/contracts/bridge/DestinationBridge.sol#L109
Bridged messages containing the same information coming from different source chains may clash in the destination bridge and lead to loss of tokens.
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):
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.
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.
burnAndCallAxelar(amount, DC)
in SC1
.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.burnAndCallAxelar(amount, DC)
in SC2
.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.Scope the txnHashToTransaction
mapping to include the address of the source bridge, or alternatively include the source address as part of the bridged payload.
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)
🌟 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
113.855 USDC - $113.85
Total of 10 issues:
ID | Issue |
---|---|
L-1 | Confusing semantics for bridged messages approvals |
L-2 | No default threshold configuration |
L-3 | Validate number of approvers in setThresholds() |
L-4 | Missing safe wrapper for ERC20 transfer in rescueTokens() |
L-5 | Account may get blacklisted during the bridge process |
L-6 | Missing validation for index parameter in overrideRange() |
L-7 | Oracle assumes asset has 18 decimals |
L-8 | Wrong argument in Transfer event of wrap() function |
L-9 | Wrong argument in TransferShares event of wrap() function |
L-10 | Contracts can be re-deployed in rUSDY factory |
Total of 2 issues:
ID | Issue |
---|---|
NC-1 | Missing calls to base initializers in rUSDY |
NC-2 | Missing event to notify oracle has changed in rUSDY |
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.
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.
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.
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.
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:
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)
.
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.
Transfer
event of wrap()
functionhttps://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));
TransferShares
event of wrap()
functionhttps://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);
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.
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:
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
🌟 Selected for report: c3phas
Also found by: 0x11singh99, 0xhex, 0xta, Eurovickk, K42, MohammedRizwan, SAAJ, SAQ, SY_S, adriro, albahaca, castle_chain, jeffy, kaveyjoe, matrix_0wl, naman1778, petrichor, wahedtalash77, ybansal2403, zabihullahazadzoi
112.0737 USDC - $112.07
Store value of chainToApprovedSender[srcChain]
as a local variable instead of reading it multiple times from storage. The same value is read 4 times:
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L96
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L99
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L102
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L106
Validation in line 96 (chainToApprovedSender[srcChain] == bytes32(0)
) is not needed as it is already covered by the validation in line 99 (chainToApprovedSender[srcChain] != keccak256(abi.encode(srcAddr))
).
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L96
Avoid copying the full array of Treshold
struct to memory while reading chainToThresholds[srcChain]
in _attachThreshold()
function. Alias the variable to storage
instead.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L135
Improve storage of approvers by using a mapping instead of an array. Currently the array of approvers needs to traverse each time an approver grants approval to check for duplicate entries. By using a mapping, the same operation can be done in O(1).
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L160-L164
Avoid copying the TxnThreshold
struct to memory while reading txnToThresholdSet[txnHash]
in _checkThresholdMet()
function. Alias the variable to storage
instead.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L178
The check in line 270 can use the local amounts
array instead of fetching the entry from storage.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L270
Both branches of the if in line 265 have the same duplicated code that push the entry to the chainToThresholds[srcChain]
. The code can be refactored to execute the operation outside the if statement and avoid code duplication.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L265
In rescueTokens()
, transfer the tokens to msg.sender
instead of reading the owner from storage.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L324
Avoid copying the Transaction
struct to memory while reading txnHashToTransaction[txnHash]
in _mintIfThresholdMet()
function. Alias the variable to storage
instead.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/bridge/DestinationBridge.sol#L339
In getPrice()
loop using the indices in revese order (i = length - 1; i >=0l; --i
) instead of calculating the (length - 1) - i
expression in every iteration.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L75
In getPrice()
, the range.end
variable is read from storage two times. Consider using a local variable cache.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L80
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L81
In setRange()
, the length of the ranges
array is fetched from storage two times. Consider using a local variable cache.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L155
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L165
Avoid copying the Range
struct to memory while reading currentRange
in derivePrice()
function. Alias the variable to storage
instead.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L263
Reduce size of variables in Range
struct to enable efficient storage packing.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L295-L300
The _mul
doesn't need to check for overflow since checked arithmetic is already provided in Solidity 0.8
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/rwaOracles/RWADynamicOracle.sol#L404-L406
Consider not updating allowance if it the current allowance is set to an "infinite" (type(uint256).max
) value.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L310
Calculation in line 310 can be unchecked since the condition has been already checked in line 307.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L310
Calculation in line 362 can be unchecked since the condition has been already checked in line 359.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L362
The wrap()
function doesn't require the whenNotPaused
since the implementation of _mintShares
already checks this.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L434
The unwrap()
function doesn't require the whenNotPaused
since the implementation of _burnShares
already checks this.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L449
Calculation in line 530 can be unchecked since the condition has been already checked in line 526.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L530
Calculation in line 531 can be unchecked since overflow is already covered by the totalShares
variable.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L531
The totalShares
variable is re-fetched from storage in _mintShares
. Consider using a local variable cache.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L555
Calculation in line 590 can be unchecked since the condition has been already checked in line 584.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L590
The totalShares
variable is re-fetched from storage in _burnShares
. Consider using a local variable cache.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDY.sol#L601
The rUSDYImplementation
is read multiple times from storage in deployrUSDY()
. Consider using a local variable cache.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L82
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L85
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L104
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L111
The rUSDYProxyAdmin
is read multiple times from storage in deployrUSDY()
. Consider using a local variable cache.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L83
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L86
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L99
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L100
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L103
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L110
The rUSDYProxy
is read multiple times from storage in deployrUSDY()
. Consider using a local variable cache.
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L84
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L89
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L102
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L109
Remove unnecessary assert()
in deployrUSDY()
https://github.com/code-423n4/2023-09-ondo/blob/main/contracts/usdy/rUSDYFactory.sol#L100
#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