Platform: Code4rena
Start Date: 12/04/2023
Pot Size: $60,500 USDC
Total HM: 21
Participants: 199
Period: 7 days
Judge: hansfriese
Total Solo HM: 5
Id: 231
League: ETH
Rank: 29/199
Findings: 2
Award: $304.87
🌟 Selected for report: 0
🚀 Solo Findings: 0
283.8353 USDC - $283.84
https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/PositionFactory.sol#L30-L34 https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/PositionFactory.sol#L37-L46
Attacker can steal funds via reorg attack if position is funded within a few block of being created
function clonePosition(address _existing) external returns (address) { Position existing = Position(_existing); Position clone = Position(createClone(existing.original())); return address(clone); }
The PositionFactory
contract clones positions via clonePosition
which calls the createClone
function.
function createClone(address target) internal returns (address result) { bytes20 targetBytes = bytes20(target); assembly { let clone := mload(0x40) mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) mstore(add(clone, 0x14), targetBytes) mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) result := create(0, clone, 0x37) } }
The createClone
function uses the create
opcode. This means the address of the new position is only dependent on the nonce of the factory. An attacker can abuse this to steal funds via reorg.
Example: Imagine a user creates a position and funds it. This now allows an attacker to steal the funds via a reorg attack. They would maliciously insert their own transaction which they would use to create a clone with their own malicious parameters. This contract would have the same address as the legitimate user's did before the reorg attack. Now the users funds are in the attacker's malicious contract which can be taken.
Manual Review
Deploy the position contract via the create2
opcode which uses salt
.
Example OpenZeppelin cloneDeterministic
function
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) { /// @solidity memory-safe-assembly assembly { // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes // of the `implementation` address with the bytecode before the address. mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000)) // Packs the remaining 17 bytes of `implementation` with the bytecode after the address. mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) instance := create2(0, 0x09, 0x37, salt) } require(instance != address(0), "ERC1167: create2 failed"); }
#0 - c4-pre-sort
2023-04-20T12:18:39Z
0xA5DF marked the issue as duplicate of #155
#1 - c4-judge
2023-05-18T10:49:31Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: c3phas
Also found by: 0xDACA, 0xRB, 0xSmartContract, 0xhacksmithh, 0xnev, Aymen0909, BenRai, Breeje, DishWasher, Erko, EvanW, JCN, MohammedRizwan, NoamYakov, Polaris_tow, Proxy, Rageur, Raihan, RaymondFam, ReyAdmirado, SAAJ, Sathish9098, Satyam_Sharma, Udsen, __141345__, aria, codeslide, decade, fatherOfBlocks, hunter_w3b, karanctf, matrix_0wl, nadin, naman1778, niser93, pavankv, petrichor, pfapostol, sebghatullah, slvDev, trysam2003, xmxanuel
21.0255 USDC - $21.03
Issue | Instances | Total Gas Saved | |
---|---|---|---|
[G-01] | Cheaper Comparison | 17 | 51 |
[G-02] | Consider using assembly for math (add, sub, mul, div) | 43 | ~1800 |
[G-03] | Consider using assembly to check for address(0) | 3 | 18 |
[G-04] | Consider using assembly to write storage values | 10 | 660 |
Total: 73 instances over 4 issues with ~2529 gas saved. Per instance gas savings are listed in the individual issue description.
When comparing integers, it is cheaper to use strict >
& <
operators over >=
& <=
operators, even if you must increment or decrement one of the operands. Note: before using this technique, it's important to consider whether incrementing/decrementing one of the operators could result in an over/underflow.
This optimization is applicable when the optimizer is turned off.
Example:
function gte() external pure returns (bool) { return 2 >= 2; } function gtPlusMinusOne() external pure returns (bool) { return 2 > 2 - 1; } function lte() external pure returns (bool) { return 2 <= 2; } function ltPlusOne() external pure returns (bool) { return 2 < 2 + 1; }
Saved per instance: 3 gas
Lines:
Use assembly for math instead of Solidity. You can check for overflow/underflow in assembly to ensure safety. If using Solidity versions < 0.8.0 and you are using Safemath, you can gain significant gas savings by using assembly to calculate values and checking for overflow/underflow.
Example:
//addition in Solidity function addTest(uint256 a, uint256 b) public pure { uint256 c = a + b; } //addition in assembly function addAssemblyTest(uint256 a, uint256 b) public pure { assembly { let c := add(a, b) if lt(c, a) { mstore(0x00, "overflow") revert(0x00, 0x20) } } } //subtraction in Solidity function subTest(uint256 a, uint256 b) public pure { uint256 c = a - b; } //subtraction in assembly function subAssemblyTest(uint256 a, uint256 b) public pure { assembly { let c := sub(a, b) if gt(c, a) { mstore(0x00, "underflow") revert(0x00, 0x20) } } } //multiplication in Solidity function mulTest(uint256 a, uint256 b) public pure { uint256 c = a * b; } //multiplication in assembly function mulAssemblyTest(uint256 a, uint256 b) public pure { assembly { let c := mul(a, b) if lt(c, a) { mstore(0x00, "overflow") revert(0x00, 0x20) } } } //division in Solidity function divTest(uint256 a, uint256 b) public pure { uint256 c = a * b; } //division in assembly function divAssemblyTest(uint256 a, uint256 b) public pure { assembly { let c := div(a, b) if gt(c, a) { mstore(0x00, "underflow") revert(0x00, 0x20) } } }
Saved per instance:
Lines:
Example:
contract Contract0 { function ownerNotZero(address _addr) public pure { require(_addr != address(0), "zero address)"); } } function assemblyOwnerNotZero(address _addr) public pure { assembly { if iszero(_addr) { mstore(0x00, "zero address") revert(0x00, 0x20) } } }
Saved per instance: 6 gas
Lines:
Example:
contract Contract0 { address owner = 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84; function updateOwner(address newOwner) public { owner = newOwner; } } contract Contract1 { address owner = 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84; function assemblyUpdateOwner(address newOwner) public { assembly { sstore(owner.slot, newOwner) } } }
Saved per instance: 66 gas
Lines:
#0 - c4-judge
2023-05-16T13:27:55Z
hansfriese marked the issue as grade-b