Platform: Code4rena
Start Date: 07/07/2022
Pot Size: $75,000 USDC
Total HM: 32
Participants: 141
Period: 7 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 144
League: ETH
Rank: 21/141
Findings: 4
Award: $939.85
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0xA5DF
Also found by: 0x, 0xsanson, 242, Critical, sorrynotsorry, unforgiven, zzzitron
https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/VaultFactory.sol#L19-L22 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/Vault.sol#L23-L29
VaultFactory
is Factory contract for deploying fractional vaults. in the constructor()
it creates a Vault
implementation contract but don't initialize it. then it uses this implementation address to creates clone of Vault
contract. attacker can call init()
for that Vault
implementation address then it can use delegatecall
functionality in Vault
to perform selfdestruct
in the implementation contract which would case other deployed contract or new deployments to fail.
This is constructor()
code in VaultFactory
:
/// @notice Initializes implementation contract constructor() { implementation = address(new Vault()); }
As you can see it creates new Vault
contract and set the implementation
variable. but it doesn't call init()
function of that Vault
. in deployFor()
code uses implementation
to clone and create new vaults.
This is init()
code in Vault()
:
/// @dev Initializes nonce and proxy owner function init() external { if (nonce != 0) revert Initialized(owner, msg.sender, nonce); nonce = 1; owner = msg.sender; emit TransferOwnership(address(0), msg.sender); }
As you can see if anyone who calls this function become the owner
of the Vault
and can perform authorized action in Vault
like installing plugins.
because VaultFactory
don't call init()
for implementation
contract, so attacker can call init()
on implementation contract and become the owner
of that contract and install plugins and then call them and preform selfdestruct
on the implementation contract. this would cause implementation contract to be destroyed and all the Vault
contract based on proxy pattern to fail and users funds would be lost, also new deployment of Vault
would fail too and protocol would be in broken stage.
VIM
call init()
for implementation
contract.
#0 - ecmendenhall
2022-07-15T03:14:34Z
π Selected for report: byterocket
Also found by: 0x1f8b, 242, ACai, BradMoon, Chom, Lambda, PwnedNoMore, Twpony, __141345__, ayeslick, berndartmueller, cccz, giovannidisiena, infosec_us_team, minhtrng, nine9, oyc_109, reassor, slywaters, sseefried, tofunmi, unforgiven
4.9607 USDC - $4.96
Function _execute()
Executes plugin transactions through delegatecall, but code don't have any mechanism to prevent delegatecall from executing selfdestruct
. if selfdestruct
get executed in delegatecall, then Vault
storage would be removed and users funds would be lost.
This is _execute()
function code:
function _execute(address _target, bytes calldata _data) internal returns (bool success, bytes memory response) { // Check that the target is a valid contract uint256 codeSize; assembly { codeSize := extcodesize(_target) } if (codeSize == 0) revert TargetInvalid(_target); // Save the owner address in memory to ensure that it cannot be modified during the DELEGATECALL address owner_ = owner; // Reserve some gas to ensure that the function has enough to finish the execution uint256 stipend = gasleft() - MIN_GAS_RESERVE; // Delegate call to the target contract (success, response) = _target.delegatecall{gas: stipend}(_data); if (owner_ != owner) revert OwnerChanged(owner_, owner); // Revert if execution was unsuccessful if (!success) { if (response.length == 0) revert ExecutionReverted(); _revertedWithReason(response); } }
As you can see code calls _target
contract with delegatecall
, if that contract executes selfdestruct
by mistake or intentionally then Vault
state would be removed and users funds would be lost.
VIM
add some mechanism to detect and prevent selfdestruct
execution in delegatecall
.
#0 - 0x0aa0
2022-07-19T15:05:29Z
Duplicate of #200
#1 - HardlyDifficult
2022-07-26T14:59:48Z
Duping to #487
π Selected for report: zzzitron
Also found by: unforgiven
604.4936 USDC - $604.49
https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/FERC1155.sol#L74-L87 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/FERC1155.sol#L52-L63
Function mint()
mints new fractions for an ID and is only callable by VaultRegistry
. code mints tokens then updates totalSupply
value. when minting contract may make external call to target address, in that external call contract state is wrong, tokens are minted but totalSupply
don't show correct value. totalSupply
is used in Buyout
and Migration
modules for calculating price and other important stuff, attacker can call those logics and exploit the issue and steal funds(because totalSupply
don't show correct number, the calculations of Buyout
and Migration
would be wrong)
This is mint()
and burn()
code in FERC1155
:
function mint( address _to, uint256 _id, uint256 _amount, bytes memory _data ) external onlyRegistry { _mint(_to, _id, _amount, _data); totalSupply[_id] += _amount; } function burn( address _from, uint256 _id, uint256 _amount ) external onlyRegistry { _burn(_from, _id, _amount); totalSupply[_id] -= _amount; }
As you can see it doesn't follow check-effect-interact pattern, _mint()
may call the target address, int that external call contract state is wrong and totalSupply
is not showing the correct number. attacker can use this issue to steal other users funds, because other contracts uses totalSupply
in their logics for calculating important stuff like price and ...
For example in Buyout
contract start()
function:
// Calculates price of buyout and fractions // @dev Reverts with division error if called with total supply of tokens uint256 buyoutPrice = (msg.value * 100) / (100 - ((depositAmount * 100) / totalSupply)); uint256 fractionPrice = buyoutPrice / totalSupply;
totalSupply
is used for calculating price.
VIM
follow check-effect-intract pattern
#0 - HardlyDifficult
2022-08-08T19:32:44Z
π Selected for report: xiaoming90
Also found by: 0x1f8b, 0x29A, 0x52, 0xA5DF, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xsanson, 0xsolstars, 242, 8olidity, Amithuddar, Aymen0909, Bnke0x0, BowTiedWardens, David_, Deivitto, ElKu, Funen, Hawkeye, IllIllI, JC, Kaiziron, Keen_Sheen, Kthere, Kulk0, Kumpa, Lambda, MEP, ReyAdmirado, Rohan16, Ruhum, Sm4rty, TomJ, Tomio, Treasure-Seeker, TrungOre, Tutturu, Viksaa39, Waze, _Adam, __141345__, ak1, apostle0x01, asutorufos, async, ayeslick, aysha, bbrho, benbaessler, berndartmueller, c3phas, cccz, chatch, cloudjunky, codexploder, cryptphi, delfin454000, dipp, durianSausage, dy, exd0tpy, fatherOfBlocks, hake, hansfriese, horsefacts, hubble, joestakey, jonatascm, kebabsec, kenzo, kyteg, mektigboy, neumo, oyc_109, pashov, pedr02b2, peritoflores, rajatbeladiya, rbserver, robee, rokinot, s3cunda, sach1r0, sahar, sashik_eth, scaraven, shenwilly, simon135, sorrynotsorry, sseefried, svskaushik, unforgiven, z3s, zzzitron
62.6864 USDC - $62.69
deployFor()
function in VaultFactory
creates vault for tx.origin
and transfer ownership to specified address in the call. attacker can redirect others transactions to deployFor()
and creates a vault for user address but set the owner as attacker.
This is deployFor()
code:
function deployFor(address _owner) public returns (address payable vault) { bytes32 seed = nextSeeds[tx.origin]; // Prevent front-running the salt by hashing the concatenation of tx.origin and the user-provided seed. bytes32 salt = keccak256(abi.encode(tx.origin, seed)); bytes memory data = abi.encodePacked(); vault = implementation.clone(salt, data); Vault(vault).init(); // Transfer the ownership from this factory contract to the specified owner. Vault(vault).transferOwnership(_owner); // Increment the seed. unchecked { nextSeeds[tx.origin] = bytes32(uint256(seed) + 1); } // Log the vault via en event. emit DeployVault( tx.origin, msg.sender, _owner, seed, salt, address(vault) ); }
because it uses tx.origin
it's possible to perform this attack. attacker only need to create some smart contracts and make users to call attacker controlled address. in general attacker don't need to create a smart contract and ask users to create a transaction and call attacker contract, for any user transaction that reach to attacker controlled address, attacker can perform this action (for example in ERC721
transfer).
VIM
don't use tx.origin
#0 - Ferret-san
2022-07-21T16:54:20Z
Duplicate of #68
#1 - HardlyDifficult
2022-07-28T17:31:45Z
Lowering these to Low risk and converting this into a QA report for the warden. See https://github.com/code-423n4/2022-07-fractional-findings/issues/501 for context.
#2 - HardlyDifficult
2022-08-03T21:43:33Z