Frankencoin - Proxy's results

A decentralized and fully collateralized stablecoin.

General Information

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

Frankencoin

Findings Distribution

Researcher Performance

Rank: 29/199

Findings: 2

Award: $304.87

Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xWeiss

Also found by: Breeje, Proxy, V_B

Labels

bug
2 (Med Risk)
satisfactory
duplicate-155

Awards

283.8353 USDC - $283.84

External Links

Lines of code

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

Vulnerability details

Impact

Attacker can steal funds via reorg attack if position is funded within a few block of being created

Proof of Concept

PositionFactory.sol#L30-L34

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.

PositionFactory.sol#L37-L46

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.

Tools Used

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

Gas Optimizations

Summary

IssueInstancesTotal Gas Saved
[G-01]Cheaper Comparison1751
[G-02]Consider using assembly for math (add, sub, mul, div)43~1800
[G-03]Consider using assembly to check for address(0)318
[G-04]Consider using assembly to write storage values10660

Total: 73 instances over 4 issues with ~2529 gas saved. Per instance gas savings are listed in the individual issue description.

[G-01] Cheaper Comparison

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:

[G-02] Consider using assembly for math (add, sub, mul, div)

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:

  • add: 40 gas
  • sub: 37 gas
  • mul: 60 gas
  • div: 60 gas

Lines:

[G-04] Consider using assembly to check for address(0)

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:

[G-05] Consider using assembly to write storage values

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter