40,000 USDC
View results
Submission Details
Severity: gas

Gas Report

GAS Summary

Gas Optimizations

Issue Contexts Estimated Gas Saved
GAS‑1 abi.encode() is less efficient than abi.encodepacked() 1 53
GAS‑2 Consider activating via-ir for deploying 1 250
GAS‑3 Use assembly to emit events 4 152
GAS‑4 Setting the constructor to payable 1 13
GAS‑5 Functions guaranteed to revert when called by normal users can be marked payable 3 63
GAS‑6 Optimize names to save gas 2 44
GAS‑7 Use solidity version 0.8.20 to gain some gas boost 4 352
GAS‑8 Using XOR (^) and AND (&) bitwise equivalents 1 13

Total: 17 contexts over 8 issues

Gas Optimizations

[GAS‑1] abi.encode() is less efficient than abi.encodepacked()

See for more information: https://github.com/ConnorBlockchain/Solidity-Encode-Gas-Comparison

Proof Of Concept

File: EscrowFactory.sol
77: byteCode, abi.encode(price, tokenContract, buyer, seller, arbiter, arbiterFee)

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/EscrowFactory.sol#L77

Test Code

contract GasTest is DSTest {
    Contract0 c0;
    Contract1 c1;
    function setUp() public {
        c0 = new Contract0();
        c1 = new Contract1();
    }
    function testGas() public {
        c0.not_optimized();
        c1.optimized();
    }
}
contract Contract0 {
    string a = "Code4rena";
    function not_optimized() public returns(bytes32){
        return keccak256(abi.encode(a));
    }
}
contract Contract1 {
    string a = "Code4rena";
    function optimized() public returns(bytes32){
        return keccak256(abi.encodePacked(a));
    }
}

Gas Test Report

Contract0 contract
Deployment Cost Deployment Size
101871 683
Function Name min avg median max # calls
not_optimized 2661 2661 2661 2661 1
Contract1 contract
Deployment Cost Deployment Size
99465 671
Function Name min avg median max # calls
optimized 2608 2608 2608 2608 1

[GAS‑2] Consider activating via-ir for deploying

The IR-based code generator was introduced with an aim to not only allow code generation to be more transparent and auditable but also to enable more powerful optimization passes that span across functions.

You can enable it on the command line using --via-ir or with the option {"viaIR": true}.

This will take longer to compile, but you can just simple test it before deploying and if you got a better benchmark then you can add --via-ir to your deploy command

More on: https://docs.soliditylang.org/en/v0.8.17/ir-breaking-changes.html

[GAS‑3] Use assembly to emit events

We can use assembly to emit events efficiently by utilizing scratch space and the free memory pointer. This will allow us to potentially avoid memory expansion costs.
Note: In order to do this optimization safely, we will need to cache and restore the free memory pointer.

For example, for a generic emit event for eventSentAmountExample:

// uint256 id, uint256 value, uint256 amount
emit eventSentAmountExample(id, value, amount);

We can use the following assembly emit events:

assembly {
let memptr := mload(0x40)
mstore(0x00, calldataload(0x44))
mstore(0x20, calldataload(0xa4))
mstore(0x40, amount)
log1(
0x00,
0x60,
// keccak256("eventSentAmountExample(uint256,uint256,uint256)")
0xa622cf392588fbf2cd020ff96b2f4ebd9c76d7a4bc7f3e6b2f18012312e76bc3
)
mstore(0x40, memptr)
}

Proof Of Concept

File: Escrow.sol
96: emit Confirmed(i_seller);

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L96

File: Escrow.sol
105: emit Disputed(msg.sender);

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L105

File: Escrow.sol
117: emit Resolved(i_buyer, i_seller);

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L117

File: EscrowFactory.sol
51: emit EscrowCreated(address(escrow), msg.sender, seller, arbiter);

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/EscrowFactory.sol#L51

[GAS‑4] Setting the constructor to payable

Saves ~13 gas per instance

Proof Of Concept

File: Escrow.sol
32: constructor(
uint256 price,
IERC20 tokenContract,
address buyer,
address seller,
address arbiter,
uint256 arbiterFee
)

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L32

[GAS‑5] Functions guaranteed to revert when called by normal users can be marked payable

If a function modifier or require such as onlyOwner/onlyX is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.

Proof Of Concept

File: Escrow.sol
94: function confirmReceipt() external onlyBuyer inState(State.Created) {

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L94

File: Escrow.sol
102: function initiateDispute() external onlyBuyerOrSeller inState(State.Created) {

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L102

File: Escrow.sol
109: function resolveDispute(uint256 buyerAward) external onlyArbiter nonReentrant inState(State.Disputed) {

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L109

Recommended Mitigation Steps

Functions guaranteed to revert when called by normal users can be marked payable.

[GAS‑6] Optimize names to save gas

Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.

See more here

Proof Of Concept

All in-scope contracts

Recommended Mitigation Steps

Find a lower method ID name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas
For example, the function IDs in the Gauge.sol contract will be the most used; A lower method ID may be given.

[GAS‑7] Use solidity version 0.8.20 to gain some gas boost

Upgrade to the latest solidity version 0.8.20 to get additional gas savings.
See latest release for reference:
https://blog.soliditylang.org/2023/05/10/solidity-0.8.20-release-announcement/

Proof Of Concept

File: Escrow.sol
pragma solidity 0.8.18;

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L2

File: EscrowFactory.sol
pragma solidity 0.8.18;

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/EscrowFactory.sol#L2

File: IEscrow.sol
pragma solidity 0.8.18;

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/IEscrow.sol#L2

File: IEscrowFactory.sol
pragma solidity 0.8.18;

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/IEscrowFactory.sol#L2

[GAS‑8] Using XOR (^) and AND (&) bitwise equivalents

Given 4 variables a, b, c and d represented as such:

0 0 0 0 0 1 1 0 <- a
0 1 1 0 0 1 1 0 <- b
0 0 0 0 0 0 0 0 <- c
1 1 1 1 1 1 1 1 <- d

To have a == b means that every 0 and 1 match on both variables. Meaning that a XOR (operator ^) would evaluate to 0 ((a ^ b) == 0), as it excludes by definition any equalities.Now, if a != b, this means that there’s at least somewhere a 1 and a 0 not matching between a and b, making (a ^ b) != 0.Both formulas are logically equivalent and using the XOR bitwise operator costs actually the same amount of gas.However, it is much cheaper to use the bitwise OR operator (|) than comparing the truthy or falsy values.These are logically equivalent too, as the OR bitwise operator (|) would result in a 1 somewhere if any value is not 0 between the XOR (^) statements, meaning if any XOR (^) statement verifies that its arguments are different.

Proof Of Concept

File: Escrow.sol
103: if (i_arbiter == address(0)) revert Escrow__DisputeRequiresArbiter();

https://github.com/Cyfrin/2023-07-escrow/tree/main/src/Escrow.sol#L103

Test Code

contract GasTest is DSTest {
    Contract0 c0;
    Contract1 c1;
    function setUp() public {
        c0 = new Contract0();
        c1 = new Contract1();
    }
    function testGas() public {
        c0.not_optimized(1,2);
        c1.optimized(1,2);
    }
}

contract Contract0 {
    function not_optimized(uint8 a,uint8 b) public returns(bool){
        return ((a==1) || (b==1));
    }
}

contract Contract1 {
    function optimized(uint8 a,uint8 b) public returns(bool){
        return ((a ^ 1) & (b ^ 1)) == 0;
    }
}

Gas Test Report

Contract0 contract
Deployment Cost Deployment Size
46099 261
Function Name min avg median max # calls
not_optimized 456 456 456 456 1
Contract1 contract
Deployment Cost Deployment Size
42493 243
Function Name min avg median max # calls
optimized 430 430 430 430 1

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.