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
Rank: 66/77
Findings: 1
Award: $22.00
🌟 Selected for report: 0
🚀 Solo Findings: 0
21.9995 USDC - $22.00
https://github.com/open-dollar/od-contracts/blob/dev/src/contracts/proxies/Vault721.sol#L85-L88 https://github.com/open-dollar/od-contracts/blob/dev/src/contracts/proxies/actions/BasicActions.sol#L226-L228 https://github.com/open-dollar/od-contracts/blob/dev/src/contracts/proxies/ODSafeManager.sol#L105-L109
A user is meant to interact with the protocol through the use of its personal ODProxy
which will execute different actions on the ODSafeManager
.
In the following scenario, User A wants to create a new safe and allow his friend, User B to access the same safe.
https://github.com/open-dollar/od-contracts/blob/dev/src/contracts/proxies/Vault721.sol#L85-L88
Based upon how the protocol is supposed to work, in order to do that a user would :
ODProxy
using the build()
function in Vault721.sol
(this will work as intended)ODProxy
to execute()
the openSAFE()
function on BasicActions.sol
which is going to call the openSAFE()
function in ODSafeManager
(this will work as intended)ODProxy
to execute()
the allowSAFE()
function on ODSafeManager.sol
directly (this will fail everytime)The problem here comes from the fact that ODProxy
uses a delegatecall()
to execute the given functions on a target contract
https://github.com/open-dollar/od-contracts/blob/dev/src/contracts/proxies/ODProxy.sol#L26-L35
function execute(address _target, bytes memory _data) external payable onlyOwner returns (bytes memory _response) { if (_target == address(0)) revert TargetAddressRequired(); bool _succeeded; (_succeeded, _response) = _target.delegatecall(_data); if (!_succeeded) { revert TargetCallFailed(_response); } }
Thus, these functions will be executed in the context of the ODProxy
including its state and storage variables BUT these ODProxy
don't have any state variable other than the variable OWNER
To summerize, as soon as an ODProxy
tries to interact directly with another contract in order to change this contract's state (such as ODSafeManager.sol
) this will fail because of the delegatecall
In this PoC, we are going to do the exact same thing that was explained previously in order to show that the proxy can't interact with the ODSafeManager
This file should be located in test/RevertManager.t.sol
and can be executed with
forge test --match-contract RevertManager --match-test testRevertManager
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test, console} from "forge-std/Test.sol"; import {Vault721} from "src/contracts/proxies/Vault721.sol"; import {ODProxy} from '@contracts/proxies/ODProxy.sol'; import {BasicActions} from '@contracts/proxies/actions/BasicActions.sol'; import {ODSafeManager} from '@contracts/proxies/ODSafeManager.sol'; import {SAFEEngine} from '@contracts/SAFEEngine.sol'; import '@interfaces/ISAFEEngine.sol'; contract RevertManager is Test { address public deployer = address(1); address public friend = address(2); address public governor = address(3); address public legitUser = address(4); Vault721 public vault721; ODSafeManager public safeManager; SAFEEngine public safeEngine; BasicActions public basicActions; ISAFEEngine.SAFEEngineParams public safeEngineParameters = ISAFEEngine.SAFEEngineParams( 10 ** 18, // WAD 10 ** 45 // RAD ); function setUp() public { vm.startPrank(deployer); vault721 = new Vault721(governor); safeEngine = new SAFEEngine(safeEngineParameters); safeManager = new ODSafeManager(address(safeEngine), address(vault721)); basicActions = new BasicActions(); vm.stopPrank(); } function testRevertManager() public { // first we build a proxy for a user vm.startPrank(legitUser); vault721.build(legitUser); ODProxy userProxy = ODProxy(vault721.getProxy(legitUser)); bytes memory payload = abi.encodeWithSelector( basicActions.openSAFE.selector, address(safeManager), "", address(userProxy) ); bytes memory safeData = userProxy.execute( address(basicActions), payload ); uint256 userSafeId = abi.decode(safeData, (uint256)); // userProxy owns the SAFE : the state of the ODSafeManager has been updated assertEq( safeManager.safeData(userSafeId).owner, address(userProxy) ); // here we try to allow the address of our "friend" by setting the value 1 in the // `safeCan` mapping on ODSafeManager payload = abi.encodeWithSelector( safeManager.allowSAFE.selector, userSafeId, friend, 1 ); // this will revert as the proxy tries to interact directly with the ODSafeManager // but using the proxy's context/state (which is empty) vm.expectRevert(); userProxy.execute( address(safeManager), payload ); // we can see that the mapping has not been changed and is still 0 assertEq( safeManager.safeCan(legitUser, userSafeId, friend), 0 ); } }
Foundry
ODProxy
to call
(and not delegatecall
) on the target contractcall/delegatecall
#0 - c4-pre-sort
2023-10-26T06:32:09Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-10-26T06:32:24Z
raymondfam marked the issue as duplicate of #76
#2 - c4-pre-sort
2023-10-26T19:04:48Z
raymondfam marked the issue as duplicate of #380
#3 - c4-judge
2023-11-02T18:12:28Z
MiloTruck marked the issue as not a duplicate
#4 - c4-judge
2023-11-02T18:12:39Z
MiloTruck marked the issue as duplicate of #294
#5 - c4-judge
2023-11-08T00:21:46Z
MiloTruck changed the severity to 2 (Med Risk)
#6 - MiloTruck
2023-11-08T00:26:07Z
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:08Z
MiloTruck marked the issue as unsatisfactory: Insufficient proof
#8 - c4-judge
2023-11-11T08:18:43Z
MiloTruck removed the grade
#9 - c4-judge
2023-11-11T08:19:02Z
MiloTruck marked the issue as satisfactory
#10 - MiloTruck
2023-11-11T08:19:24Z