Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors the code for gas optimisation. #36

Merged
merged 7 commits into from
Sep 12, 2022
Merged

Conversation

afmfe-iul
Copy link
Contributor

@afmfe-iul afmfe-iul commented Aug 19, 2022

Results

(all values, except "runs" column, expressed in gas)

Deployments

                            initial          final         saving
Fast                   |   2_341_663   ·   2_341_663   ·    0,00%
FastAccessFacet        |   1_624_627   ·   1_202_978   ·   25,95%
FastFrontendFacet      |   1_266_962   ·     968_394   ·   23,57%
FastHistoryFacet       |   1_880_857   ·   1_298_675   ·   30,95%
FastTokenFacet         |   4_580_376   ·   3_508_391   ·   23,40%
FastTopFacet           |     442_135   ·     312_393   ·   29,34%
IssuerAccessFacet      |     958_949   ·     631_827   ·   34,11%
IssuerFrontendFacet    |     755_673   ·     556_301   ·   26,38%
IssuerTopFacet         |     945_724   ·     652_858   ·   30,97%
MarketplaceAccessFacet |   1_713_025   ·   1_223_002   ·   28,61%
MarketplaceTopFacet    |     118_207   ·      97_073   ·   17,88%

Function calls

                           runs      initial        final       saving
addGovernor            |    32   ·   108_956   ·   106_684   ·   2,09%
addMember              |    82   ·    93_244   ·    91_519   ·   1,85%
addTransferCredits     |    60   ·    72_357   ·    70_592   ·   2,44%
approve                |    64   ·   161_419   ·   158_650   ·   1,72%
beforeRemovingMember   |     5   ·    87_583   ·    84_300   ·   3,75%
burn                   |     5   ·   163_632   ·   160_652   ·   1,82%
burnt                  |     1   ·   139_005   ·   138_577   ·   0,31%
diamondCut             |    43   ·   213_374   ·   213_374   ·   0,00%
disapprove             |     6   ·    58_487   ·    56_567   ·   3,28%
drainTransferCredits   |     5   ·    51_796   ·    50_327   ·   2,84%
emitDetailsChanged     |     2   ·    40_033   ·    39_244   ·   1,97%
mint                   |    66   ·   181_042   ·   177_734   ·   1,83%
minted                 |    13   ·   108_579   ·   108_140   ·   0,40%
removeGovernor         |     5   ·    58_987   ·    56_925   ·   3,50%
removeMember           |    13   ·    57_812   ·    55_719   ·   3,62%
setIsSemiPublic        |     4   ·    60_364   ·    58_373   ·   3,30%
transfer               |     1   ·   275_438   ·   267_326   ·   2,95%
transfered             |    33   ·   232_399   ·   231_427   ·   0,42%
transferFrom           |    88   ·   322_647   ·   311_963   ·   3,31%
transferFromWithRef    |    14   ·   287_540   ·   276_707   ·   3,77%
transferWithRef        |     1   ·   276_270   ·   268_245   ·   2,90%
initialize             |     5   ·   199_899   ·   197_918   ·   0,99%
registerFast           |    27   ·   112_201   ·   110_685   ·   1,35%
activateMember         |     3   ·    50_579   ·    48_337   ·   4,43%
deactivateMember       |    11   ·    92_136   ·    90_107   ·   2,20%
memberAddedToFast      |     5   ·    79_745   ·    78_785   ·   1,20%
memberRemovedFromFast  |     1   ·    37_878   ·    36_864   ·   2,68%

Types and comments about optmisations done

Enabling the Solidity optimizer

Not much to say here, it's advisable to use the optimizer even with the Diamond pattern, but there's room for tweaking the actual value: a smaller one will lead to smaller bytecode (cheaper contract deployments) and a bigger one will lead to cheaper function calls, right now I've used a really big number for cheaper calls, since we didn't exceed the max bytecode size on any deployed contract.

Use of custom errors with revert

Solidity 0.8.4 added custom errors and after reading about it, which cost less gas and open the door for some interesting stuff like having arguments passed to the error or having better testing (soon with TypeChain!). Here are some links that explain it way better than I can:

Unchecked Math where applicable

Mainly for looping and when updating balances after a check was already done. Lots of tips taken from this post
Maybe this one is a better way of doing it, for readability.

Better caching of variables

Stopped storing some values on variables that weren't used more than once, and started storing array lengths on variables before looping.

Extra notes

  1. Broke lots of tests because the previous revert messages don't match the Custom errors. Didn't fix them because I'm not sure if we want to go for this change.
  2. Left LibDiamond.sol un-touched but there are 14 require statements there that we could update (the contract was written prior to 0.8.4)
  3. I'm not sure if we looked at the different Diamond implementations and picked the best suited for us (Diamond Implementations, or even if with the tooling we use if we can choose one. As an example Dark Forest went for the diamond-2 implementation and have a note on their readme saying:

Note: The implementations of diamondCut and loupe functions can vary based on complexity and gas cost choices. Dark Forest has chosen the "diamond-2" implementation because it allows cheaper gas cost of the diamondCut function, which is useful for Lobbies. This results in a higher cost loupe function call.

@afmfe-iul afmfe-iul requested a review from hickscorp August 19, 2022 16:23
@@ -81,46 +81,45 @@ contract FastFrontendFacet is AFastFacet {

function detailedMember(address member)
public view returns(MemberDetails memory) {
LibFastToken.Data storage tokenStorage = LibFastToken.data();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

});
}

function detailedGovernor(address governor)
public view returns(GovernorDetails memory) {
LibFastAccess.Data storage accessStorage = LibFastAccess.data();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

});
}

function paginateDetailedMembers(uint256 index, uint256 perPage)
external view returns(MemberDetails[] memory, uint256) {
LibFastAccess.Data storage accessStorage = LibFastAccess.data();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

values[i] = detailedMember(members[i]);
unchecked { ++i; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
return (values, nextCursor);
}

function paginateDetailedGovernors(uint256 index, uint256 perPage)
external view returns(GovernorDetails[] memory, uint256) {
LibFastAccess.Data storage accessStorage = LibFastAccess.data();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

values[i] = detailedGovernor(governors[i]);
unchecked { ++i; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

values[i] = s.transferProofs[collection[cursor + i]];
unchecked { ++i; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -29,10 +29,9 @@ contract FastTokenFacet is AFastFacet, IERC20, IERC1404 {
// We want to make sure that either of these two is true:
// - The token doesn't have fixed supply.
// - The token has fixed supply but has no tokens yet (First and only mint).
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -50,11 +49,17 @@ contract FastTokenFacet is AFastFacet, IERC20, IERC1404 {
onlyIssuerMember {
LibFastToken.Data storage s = LibFastToken.data();

require(!FastTopFacet(address(this)).hasFixedSupply(), LibConstants.REQUIRES_CONTINUOUS_SUPPLY);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -50,11 +49,17 @@ contract FastTokenFacet is AFastFacet, IERC20, IERC1404 {
onlyIssuerMember {
LibFastToken.Data storage s = LibFastToken.data();

require(!FastTopFacet(address(this)).hasFixedSupply(), LibConstants.REQUIRES_CONTINUOUS_SUPPLY);
require(balanceOf(address(0)) >= amount, LibConstants.INSUFFICIENT_FUNDS);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// Remove the minted amount from the zero address.
s.balances[address(0)] -= amount;
unchecked {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, it could be that we should remove all the reverts around bound checking, so that we could not use unchecked and rely on the compiler to throw when overflowing...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, that's even better probably, I'll double check!

@@ -154,7 +159,9 @@ contract FastTokenFacet is AFastFacet, IERC20, IERC1404 {
// If the allowance being queried is from the zero address and the spender
// is a governor, we want to make sure that the spender has full rights over it.
if (owner == address(0)) {
require(FastAccessFacet(address(this)).isGovernor(spender), LibConstants.REQUIRES_FAST_GOVERNORSHIP);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -228,8 +235,7 @@ contract FastTokenFacet is AFastFacet, IERC20, IERC1404 {

function detectTransferRestriction(address from, address to, uint256 amount)
external view override returns(uint8) {
LibFastToken.Data storage s = LibFastToken.data();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -253,7 +259,7 @@ contract FastTokenFacet is AFastFacet, IERC20, IERC1404 {
} else if (restrictionCode == LibFastToken.REQUIRES_DIFFERENT_SENDER_AND_RECIPIENT_CODE) {
return LibConstants.REQUIRES_DIFFERENT_SENDER_AND_RECIPIENT;
}
revert(LibConstants.UNKNOWN_RESTRICTION_CODE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if(s.balances[p.from] < p.amount){
revert ICustomErrors.InsufficientFunds();
}
if(p.amount <= 0){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be p.amount == 0 as it can never be less than zero - it's unsigned. Slight gas optimization.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted!


// If the from account isn't the zero address...
if (p.from != address(0)) {
// Decrease allowance.
uint256 newAllowance = s.allowances[p.from][p.spender] -= p.amount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

s.balances[p.from] -= p.amount;
unchecked {
s.balances[p.from] -= p.amount;
}
s.balances[p.to] += p.amount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - overflow case here.

if(s.transferCredits < p.amount){
revert ICustomErrors.InsufficientTransferCredits();
}
unchecked {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -406,17 +415,15 @@ contract FastTokenFacet is AFastFacet, IERC20, IERC1404 {
if (candidate != address(0)) {
// FAST is semi-public - the only requirement to hold tokens is to be an marketplace member.
if (IFast(address(this)).isSemiPublic()) {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
// FAST is private, the requirement to hold tokens is to be a member of that FAST.
else {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -37,10 +37,11 @@ contract FastTopFacet is AFastFacet {
function setIsSemiPublic(bool flag)
external
onlyIssuerMember {
LibFast.Data storage s = LibFast.data();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a rework. For now, no cigar :)

@@ -23,82 +24,74 @@ abstract contract AFastFacet is IFastEvents {

/// @dev Ensures that a method can only be called by another facet of the same diamond.
modifier onlyDiamondFacet() {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

_;
}

/// @dev Ensures that a method can only be called by the owner of this diamond.
modifier onlyDiamondOwner() {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

_;
}

/// @dev Ensures that a method can only be called by the singleton deployer contract factory.
modifier onlyDeployer() {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -9,8 +9,9 @@ library LibPaginate {
internal view returns(address[] memory, uint256) {
uint256 length = (perPage > collection.length - cursor) ? collection.length - cursor : perPage;
address[] memory values = new address[](length);
for (uint256 i = 0; i < length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -19,8 +20,9 @@ library LibPaginate {
internal view returns(uint256[] memory, uint256) {
uint256 length = (perPage > collection.length - cursor) ? collection.length - cursor : perPage;
uint256[] memory values = new uint256[](length);
for (uint256 i = 0; i < length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -29,8 +31,9 @@ library LibPaginate {
internal view returns(LibFastHistory.SupplyProof[] memory, uint256) {
uint256 length = (perPage > collection.length - cursor) ? collection.length - cursor : perPage;
LibFastHistory.SupplyProof[] memory values = new LibFastHistory.SupplyProof[](length);
for (uint256 i = 0; i < length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -39,8 +42,9 @@ library LibPaginate {
internal view returns(LibFastHistory.TransferProof[] memory, uint256) {
uint256 length = (perPage > collection.length - cursor) ? collection.length - cursor : perPage;
LibFastHistory.TransferProof[] memory values = new LibFastHistory.TransferProof[](length);
for (uint256 i = 0; i < length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -70,7 +71,9 @@ contract MarketplaceAccessFacet is AMarketplaceFacet, IHasMembers, IHasActiveMem
onlyIssuerMember {
LibMarketplaceAccess.Data storage s = LibMarketplaceAccess.data();
// Ensure that member doesn't have any FAST membership.
require(s.fastMemberships[member].values.length == 0, LibConstants.REQUIRES_NO_FAST_MEMBERSHIPS);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -93,27 +96,23 @@ contract MarketplaceAccessFacet is AMarketplaceFacet, IHasMembers, IHasActiveMem
function memberAddedToFast(address member)
external {
// Verify that the given address is in fact a registered FAST contract.
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// Keep track of the member's FAST membership.
LibAddressSet.Data storage memberFasts = LibMarketplaceAccess.data().fastMemberships[member];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

/** @dev Callback from FAST contracts allowing the Marketplace contract to keep track of FAST memberships.
* @param member The member for which a FAST membership has been removed.
*/
function memberRemovedFromFast(address member)
external {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// Remove the tracked membership.
LibAddressSet.Data storage memberFasts = LibMarketplaceAccess.data().fastMemberships[member];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -132,10 +131,9 @@ contract MarketplaceAccessFacet is AMarketplaceFacet, IHasMembers, IHasActiveMem
onlyIssuerMember
onlyMember(member) {
// Guard against attempting to activate an already active member.
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -153,10 +151,9 @@ contract MarketplaceAccessFacet is AMarketplaceFacet, IHasMembers, IHasActiveMem
onlyIssuerMember
onlyMember(member) {
// Guard against attempting to deactivate an already deactivated member.
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -22,7 +22,9 @@ contract MarketplaceInitFacet is AMarketplaceFacet {
external
onlyDeployer {
// Make sure we haven't initialized yet.
require(LibMarketplace.data().version < LibMarketplace.STORAGE_VERSION, LibConstants.ALREADY_INITIALIZED);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -42,7 +44,6 @@ contract MarketplaceInitFacet is AMarketplaceFacet {
// ------------------------------------- //

// Initialize access storage.
LibMarketplaceAccess.Data storage accessData = LibMarketplaceAccess.data();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -21,31 +22,28 @@ abstract contract AMarketplaceFacet is IMarketplaceEvents {

/// @dev Ensures that a method can only be called by the singleton deployer contract factory.
modifier onlyDeployer() {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

_;
}

/** @dev Requires that the message sender is a member of the linked Issuer.
*/
modifier onlyIssuerMember() {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

_;
}

/** @dev Requires that the given address is a member of the marketplace.
* @param candidate is the address to be checked.
*/
modifier onlyMember(address candidate) {
require(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
optimizer: {
enabled: true,
runs: 1000000
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch.

Copy link
Member

@hickscorp hickscorp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All very nice optimizations. However, none of the ones requiring the unchecked block should be accepted. I'll explain tomorrow :)

MemberDetails[] memory values = new MemberDetails[](members.length);
for (uint256 i = 0; i < members.length; ++i) {
uint256 length = members.length;
for (uint256 i = 0; i < length;) {
Copy link
Contributor

@jamesduncombe jamesduncombe Aug 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (uint256 i; i < length;) { ? using default 0 for uint256... would this be cheaper still? That's getting a bit crazy of course... might not be worth it.

Copy link
Member

@hickscorp hickscorp Aug 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well if it's a gas optimisation, @afmfe-iul can have a look.
Just so you know - I'm against some of them.
If we're really after gas optimisations, we shouldn't have the FastFacetHistory - which is what consumes half of the gas during transfers - and could be removed if our users were tech-savy ;)

The unchecked keyword is to be avoided, unless we have the money to hire super-soldiers in solidity.
I only use unchecked in very specific circumstances. For example consider the following:

require(balance >= amount, "Not enough funds or somethn'");
unchecked {
  balance -= amount; # Doesn't crash on underflow, but we're protected by the `require`.
}

So far so good. But as the complexity increases, and as multiple developers work and solve conflicts on the file, you could end up with:

require(balance >= amount, "Not enough funds or somethin");
balance -= fee;
unchecked {
  balance -= amount; # Won't crash on underflow, and there could be one now.
}

Or even, why not:

balance -= fee;
unchecked {
  balance -= amount; # Won't crash on overflow, and no more require to guard us!
}

As much as I like gas optimizations, the compiler isn't smart enough to fence the developer against these problems. So I'd rather have our users spend a bit more gas on the first snippet, or on:

balance -= fee; # Will crash on underflow, even without the `require` which was acting as documentation for misusing the API.

If I'm not mistaken, solidity > 0.8 checks for overflow. In this context, lots of the require that we have in the source code add as "documentation" when someone misuses the API. So in a way, as long as we use solidity 0.8+ to compile - yes - we're consuming a bit more gas - but given how poor the compiler is at warning us, I'd rather.

Copy link
Member

@hickscorp hickscorp Aug 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamesduncombe FYI I've started telling @afmfe-iul about being careful around over-optimisations. IMO most of these are over-optimisations. I agree that they are optimisations - but a bit too far on the spectrum of where I consider a codebase "safe to play with" for future developers.

If the solidity compiler reaches a point where it performs static analysis properly, and at least warns you of underflows, I'd be happy using unchecked all over the place. But until then, the risk outweighs the reward IMO - especially if we're targeting private chains / L2.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, sounds good to me... if we're happy with the cost of leaving some things under optimised it's all good. Readability + security at the cost of few extra cents makes sense 👍

@hickscorp hickscorp force-pushed the features/gas-optimizations branch from 1311fbc to 59e3c62 Compare September 6, 2022 10:03
@hickscorp hickscorp force-pushed the features/gas-optimizations branch from 59e3c62 to 001d690 Compare September 6, 2022 10:46
@hickscorp hickscorp merged commit cc371b1 into main Sep 12, 2022
@hickscorp hickscorp deleted the features/gas-optimizations branch September 12, 2022 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants