Giter VIP home page Giter VIP logo

Comments (17)

BillSchumacher avatar BillSchumacher commented on June 9, 2024 1

Indeed, thanks again.

Ran 1 test for test/HelloWorld.t.sol:OverCapTest
[PASS] testHelloOverCap() (gas: 19073)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 920.50µs (139.40µs CPU time)

Ran 1 test for test/WorldHello.t.sol:OverCapAfterTest
[PASS] testWorldOverCap() (gas: 19065)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 807.10µs (117.40µs CPU time)

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

Another example is the capped implementation:

function _update(address from, address to, uint256 value) internal virtual override {
    super._update(from, to, value);

    if (from == address(0)) {
        uint256 maxSupply = cap();
        uint256 supply = totalSupply();
        if (supply > maxSupply) {
            revert ERC20ExceededCap(supply, maxSupply);
        }
    }
}

This could be implemented slightly more efficiently, by being located before the super in this instance, but also by being able to add this check before the total supply increment.

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

image
image
image

from openzeppelin-contracts.

ernestognw avatar ernestognw commented on June 9, 2024

Hi @BillSchumacher,

Let's say I want to customize the burn to increment the balance of the 0 adresss and decrement the total supply, It's not currently possible.

Can you elaborate what would be the problem with the following approach?

abstract contract MyERC20 is ERC20 {
    uint256 addressZeroBalance;

    function balanceOf(address account) public view virtual returns (uint256) {
        if(account === address(0)) return addressZeroBalance;
        return _balances[account];
    }

    function _update(address from, address to, uint256 value) internal virtual override {
        super._update(from, to, value);

        if (to == address(0)) {
            addressZeroBalance++;
        }
    }
}

The variables are private intentionally so users DO NOT BREAK the invariants the contract assumes.

This could be implemented slightly more efficiently, by being located before the super in this instance, but also by being able to add this check before the total supply increment.

Could you elaborate on how it's more efficient? At most you'll be saving one if statement, which is about a couple of gas units (JUMPI costs 10 units). The contract is optimized to avoid the most possible SLOADs, which is one of the most expensive operations.

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

Hi @BillSchumacher,

Let's say I want to customize the burn to increment the balance of the 0 adresss and decrement the total supply, It's not currently possible.

Can you elaborate what would be the problem with the following approach?

abstract contract MyERC20 is ERC20 {
    uint256 addressZeroBalance;

    function balanceOf(address account) public view virtual returns (uint256) {
        if(account === address(0)) return addressZeroBalance;
        return _balances[account];
    }

This is broken because _balances is private and unavailable. A super to balanceOf does workaround that though.
image

function _update(address from, address to, uint256 value) internal virtual override {
    super._update(from, to, value);

    if (to == address(0)) {
        addressZeroBalance++;
    }
}

}


The variables are private intentionally so users DO NOT BREAK the invariants the contract assumes.

> This could be implemented slightly more efficiently, by being located before the super in this instance, but also by being able to add this check before the total supply increment.

Could you elaborate on how it's more efficient? At most you'll be saving one `if` statement, which is about a couple of gas units (JUMPI costs 10 units). The contract is optimized to avoid the most possible SLOADs, which is one of the most expensive operations.

In the scenario that we are at the cap two state writes are avoided saving quite a bit of gas, as displayed in the gas remaining screenshots.

With the suggested workaround fixed:
image

~10000 more gas than required.

image

Anyhow, I can submit a PR to fix capped if you like with the addition of course so we don't go over the cap before capping,

I appreciate the insight.

from openzeppelin-contracts.

ernestognw avatar ernestognw commented on June 9, 2024

This is broken because _balances is private and unavailable. A super to balanceOf does workaround that though.

Right, this is what I meant:

abstract contract MyERC20 is ERC20 {
  uint256 addressZeroBalance;
  
  function balanceOf(address account) public view virtual returns (uint256) {
      if(account === address(0)) return addressZeroBalance;
      return super.balanceOf(account);
  }
  
  ...
}

A super to balanceOf does workaround that though.

I assume should be fine now.

~10000 more gas than required.

This is straight false, you're measuring the gas just before executing the _update function, which of course will change dramatically the gas consumption.

Please, share a detailed gas comparison using either Foundry or the Hardhat gas reporter for the whole function execution, including the optimization settings and other stuff.

Without optimizations, I'd expect to save only 1 JUMPI on average for your use case. With optimizations enabled and via-ir I'd expect the difference to be waaay smaller. Note that 10 gas units saved is around 0.003 cents in mainnet at 100 gwei, that's the kind of optimizations we won't implement because it compromises other security guarantees of the contract. Variables are private for a reason.

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

Why would you execute the update function if you are going to go over the cap?

from openzeppelin-contracts.

ernestognw avatar ernestognw commented on June 9, 2024

Why would you execute the update function if you are going to go over the cap?

Gas measurement should take the full execution of the function into account. Why would you optimize for reverts if you can avoid it with a gas estimation?

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

To avoid the state writes

from openzeppelin-contracts.

ernestognw avatar ernestognw commented on June 9, 2024

There are no state writes if you revert, sir.

from openzeppelin-contracts.

ernestognw avatar ernestognw commented on June 9, 2024

The current implementation of the ERC20 already tracks the balance of the zero address. Also, I prepared a Foundry project with both the current implementation and another custom implementation having the cap check before the super._update(...) call.

You can see both implementations here.

After running the following gas measurement:

forge snapshot --via-ir --optimizer-runs 1000000000 --use 0.8.24 --match-test testTransfer -vvvv

The result is exactly the same for both implementations:

MyERC20CappedModifiedTest:testTransfer() (gas: 10316)
MyERC20CappedTest:testTransfer() (gas: 10316)

Closing

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

Actually I forgot the addition, it does save gas.

image

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
error ERC20ExceededCap(uint256, uint256);


abstract contract MyERC20 is ERC20, ERC20Burnable, ERC20Capped {
    function _update(address from, address to, uint256 value) internal virtual override(ERC20Capped, ERC20) {
        if (from == address(0)) {
            uint256 maxSupply = cap();
            uint256 supply = totalSupply();
            if (supply + value > maxSupply) {
                revert ERC20ExceededCap(supply, maxSupply);
            }
        }
        ERC20._update(from, to, value);
    }
}

abstract contract MyERC20After is ERC20, ERC20Burnable, ERC20Capped {
    function _update(address from, address to, uint256 value) internal virtual override(ERC20Capped, ERC20) {
        ERC20._update(from, to, value);
        if (from == address(0)) {
            uint256 maxSupply = cap();
            uint256 supply = totalSupply();
            if (supply > maxSupply) {
                revert ERC20ExceededCap(supply, maxSupply);
            }
        }
    }
}

contract HelloWorld is MyERC20 {
    constructor() payable MyERC20() ERC20("hello", "wrld") ERC20Capped(42)  {
    }

    function mint(uint256 value) public {
        _mint(msg.sender, value);
    }
}

contract WorldHello is MyERC20After {
    constructor() payable MyERC20After() ERC20("hello", "wrld") ERC20Capped(42)  {
    }

    function mint(uint256 value) public {
        _mint(msg.sender, value);
    }
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../contracts/HelloWorld.sol";

contract OverCapTest is Test {
    HelloWorld public helloWorld;
    function setUp() public {
        helloWorld = new HelloWorld();
    }
    function testHelloOverCap() public {
        try helloWorld.mint(1000000 * 10 ** helloWorld.decimals()) {
        } catch {
        }
    }
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../contracts/HelloWorld.sol";

contract OverCapAfterTest is Test {
    WorldHello public worldHello;

    function setUp() public {
        worldHello = new WorldHello();
    }
    function testWorldOverCap() public {
        try worldHello.mint(1000000 * 10 ** worldHello.decimals()) {
        } catch {
        }
    }
}

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

this output seems super unreliable...

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

Is there a setting that I should have a certain way?

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

Here's the result with 2 million runs:
image

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

If your test doesn't fail to a revert I don't think you're hitting the condition btw.

from openzeppelin-contracts.

BillSchumacher avatar BillSchumacher commented on June 9, 2024

I matched your 1 billion runs, which seems absurd.
image

from openzeppelin-contracts.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.