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: 37/70
Findings: 2
Award: $25.93
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L197-L203 https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L156-L167 https://github.com/code-423n4/2023-09-ondo/blob/47d34d6d4a5303af5f46e907ac2292e6a7745f6c/contracts/bridge/DestinationBridge.sol#L337-L353
The purpose of adding pausability feature in a contract is to prevent the execution of its functions in certain cases; which are mostly emergency cases (when calls to some contracts/tokens start to act maliciously).
The DestinationBridge
contract implements this feature; where transactions initiation (calling execute
) is disabled when the contract is paused .
But even if the contract is paused; approvers can still approve transactions (by calling approve
that doesn't check if the contract is paused) and eventually leading these transactions to be executed if they met the approvals threshold.
This will break the main purpose of adding the pausability feature; which is protecting the contract in the cases of emergancies; such when the bridged token contract starts to act in a malicious way that will lead to harming the destination bridge.
Code: approve function
File:2023-09-ondo/contracts/bridge/DestinationBridge.sol Line 197-203: function approve(bytes32 txnHash) external { if (!approvers[msg.sender]) { revert NotApprover(); } _approve(txnHash); _mintIfThresholdMet(txnHash); }
File:2023-09-ondo/contracts/bridge/DestinationBridge.sol Line 156-167: function _approve(bytes32 txnHash) internal { // Check that the approver has not already approved TxnThreshold storage t = txnToThresholdSet[txnHash]; if (t.approvers.length > 0) { for (uint256 i = 0; i < t.approvers.length; ++i) { if (t.approvers[i] == msg.sender) { revert AlreadyApproved(); } } } t.approvers.push(msg.sender); }
File:2023-09-ondo/contracts/bridge/DestinationBridge.sol Line 156-167: function _mintIfThresholdMet(bytes32 txnHash) internal { bool thresholdMet = _checkThresholdMet(txnHash); Transaction memory txn = txnHashToTransaction[txnHash]; if (thresholdMet) { _checkAndUpdateInstantMintLimit(txn.amount); if (!ALLOWLIST.isAllowed(txn.sender)) { ALLOWLIST.setAccountStatus( txn.sender, ALLOWLIST.getValidTermIndexes()[0], true ); } TOKEN.mint(txn.sender, txn.amount); delete txnHashToTransaction[txnHash]; emit BridgeCompleted(txn.sender, txn.amount); } }
Foundry PoC:
test_Transaction_Executed_When_Contract_Is_Paused()
test is added to forge-tests/bridges/DestinationBridge.t.sol
file; with the following scenario:
DestinationBridge
contract owner pauses the contract.function test_Transaction_Executed_When_Contract_Is_Paused() public { string memory srcChain = "arbitrum"; string memory srcAddress = "0xce16F69375520ab01377ce7B88f5BA8C48F8D666"; address srcSender = alice; uint256 amt = 100e18; address nonApprover = address(500); //2. a transaction of 100e18 amount is initiated and then executed by the nonApprover as it need one approver only: assertEq(destinationBridge.approvers(nonApprover), false); assertEq(usdy.balanceOf(srcSender), 0); //the srcSender doesnt have any usdc on the destination chain befor transaction execution bytes32 cmdId = bytes32( 0x9991faa1f435675159ffae64b66d7ecfdb55c29755869a18db8497b4392347e0 ); uint256 nonce = 1; bytes memory payload = abi.encode(VERSION, srcSender, amt, nonce); approve_message( cmdId, srcChain, srcAddress, address(destinationBridge), keccak256(payload) ); destinationBridge.execute(cmdId, srcChain, srcAddress, payload); //2. Then the admin pauses the destination bridge contract : assertEq(destinationBridge.paused(), false); vm.prank(guardian); destinationBridge.pause(); assertEq(destinationBridge.paused(), true); //3. the transaction needs two approvals; the first approval is given by the relayer who initiated the transaction execution,and the second approval is given by the approved ondo signer: vm.prank(ondoSigner0); destinationBridge.approve(keccak256(payload)); assertEq(destinationBridge.getNumApproved(keccak256(payload)), 2); //4. the transaction executed : assertEq(usdy.balanceOf(srcSender), amt); }
Test result:
$ forge test --fork-url $(grep -w MAINNET_RPC_URL .env | cut -d '=' -f2) --fork-block-number $(grep -w FORK_FROM_BLOCK_NUMBER_MAINNET .env | cut -d '=' -f2) --nmc Test_Prod --mc '(Test_DestinationBridge)' [PASS] test_Transaction_Executed_When_Contract_Is_Paused() (gas: 306194)
Manual Testing & Foundry.
Update approve
function to be called only when the contract is not paused:
- function approve(bytes32 txnHash) external { + function approve(bytes32 txnHash) external whenNotPaused {
Context
#0 - c4-pre-sort
2023-09-08T15:43:21Z
raymondfam marked the issue as duplicate of #529
#1 - c4-pre-sort
2023-09-08T15:43:26Z
raymondfam marked the issue as low quality report
#2 - c4-judge
2023-09-19T10:15:31Z
kirk-baird marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-09-21T10:20:31Z
kirk-baird changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-09-24T05:44:22Z
kirk-baird marked the issue as grade-c
#5 - c4-judge
2023-09-27T00:06:24Z
kirk-baird marked the issue as grade-b
🌟 Selected for report: catellatech
Also found by: 0xAsen, 0xE1D, 0xStalin, 0xmystery, Breeje, Bube, DedOhWale, JayShreeRAM, K42, Krace, castle_chain, hals, hunter_w3b, kaveyjoe, m4ttm, mahdikarimi, nirlin, peanuts, sandy
18.8458 USDC - $18.85
The Ondo finance project under audit scope consists of tokenomics part; where the rUSDY interest bearing token is managed, and the bridging service where USDY tokens are bridged between approved chains. The audited codebase consits of appx. 965 nSLoC distributed over 6 contracts.
SourceBridge
& DestinationBridge
contracts:
Where bridging of USDY tokens transaction is initiated in the source bridge (by burning the bridged tokens); and then executed after it gets approved in the source chain (by minting the bridged tokens back to the user); this feature is built on the top of AXELAR service.rUSDY
,rUSDYFactory
& RWADynamicOracle
contracts:
Where deployment of rUSDY token is managed on different service in rUSDYFactory
; token management in rUSDY
and token price settup in RWADynamicOracle
.SourceBridge
contract:multiexcall
function .DestinationBridge
contract:rescueTokens
function that's meant to transfer the contract tokns balances to the owner at anytime.DestinRWADynimcOracle
contract:rUSDY
contract:DestinationBridge
contract: users will lose their bridged tokens if the approved contract address of the source chain is changed or removed.DestinationBridge::setThresholds
: the number of approvers threshold can be greater than the number of availble approvers.rUSDY::burn
function: USDY tokens are refunded to the owner address instead of the address that has his tokens burned by the owner.DestinationBridge
contract: non approved approver can give the transaction the first approval when he initiates the transaction execution (by invoking DestinationBridge::execute
function).DestinationBridge
contract can't be rescued if the owner is a blocklisted account in the rescued tokens; so this will leads to funds being stuck and never being retreived.DestinationBridge
contract: transactions can be executed even if the contract is paused; which contradicts the purpose of adding this feature in the contract.The approved chains on both the source and destination chains must be ensured to be synchronized; since any call can be initiated by the source contract and then get stuck as this source contract became invalid in the destination chain.
A refund mechanism in the source contract must be implemented in order to enable users from getting back their burnt tokens if the transaction on the destination chain has failed due to any reason outside the users control; as not meeting the minimum approvals threshold or the source contract that started the call became invalid in the destination chain.
It's Recommended to add an expiration for each transaction alongside with the refund mechanism mentioned above.
Checking the number of required approvers required for each amount if it's acheivable with the current available approvers; as the admin can set this threshold to a value greater than the currently available approvers which will result in making the initiated transactions never being executed as the current approvers number will never meet the minimu threshold for that transaction to be executed.
It's Recommended to synchronize pausing the RWADynamicOracle
and rUSDY
contracts; since pausing the oracle will make the rUSDY
dysfunctional.
The rUSDY
token price is vulnerable to manipulation: since the price of USDY is set manually by the oracle contract admin/setter; then if the admin/setter accounts got compromised this will lead to manipulate the price; so it's recommended to extract the token price from a chainlink oracle.
Approximately 18 hours; divided between manually reviewing the codebase, reading documentation, foundry testing, and documenting my findings.
18 hours
#0 - c4-pre-sort
2023-09-08T14:41:01Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-09-24T07:18:08Z
kirk-baird marked the issue as grade-b