Platform: Code4rena
Start Date: 18/11/2021
Pot Size: $50,000 USDC
Total HM: 18
Participants: 26
Period: 7 days
Judge: leastwood
Total Solo HM: 12
Id: 54
League: ETH
Rank: 18/26
Findings: 2
Award: $175.40
🌟 Selected for report: 4
🚀 Solo Findings: 0
🌟 Selected for report: HardlyDifficult
HardlyDifficult
Save 4.5k gas on createLock
.
Replace https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/Unlock.sol#L223:
locks[newLock] = LockBalances({ deployed: true, totalSales: 0, yieldedDiscountTokens: 0 });
with:
locks[newLock].deployed = true;
yarn test
and diff gas report
Remove any instance explicitly assigning new variables to 0 - only do so when purposefully overwriting data that may or may not already be there.
There is no concept of null or undefined in Solidity, everything is 0 if it has not otherwise been defined. When writing to storage, there is a non-trivial cost to write 0 even though it's effectively a no-op.
#0 - 0xleastwood
2022-01-17T08:19:00Z
Tested this in remix. There are actual considerable gas savings by making this change. 27989
-> 23557
🌟 Selected for report: HardlyDifficult
Also found by: TomFrenchBlockchain, cmichel
HardlyDifficult
Saves 2k gas on upgradeLock
Rename proxyAdmin
https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/contracts/Unlock.sol#L99 to __gap_was_proxyAdmin
and replace references to use ProxyAdmin(proxyAdminAddress)
instead.
In the upgradeLock
function, both proxyAdminAddress and proxyAdmin are read. There is a fairly high gas cost to read from storage, so using the same variable casted instead saves gas.
yarn test
and diff the gas report.
Remove proxyAdmin
since it was a private variable, and use the public proxyAdminAddress
instead. Be sure to put a gap variable in that slot so upgrades are not broken.
#0 - 0xleastwood
2022-01-17T08:14:57Z
Agreed! It doesn't seem necessary to store a casted ProxyAdmin
address and a normal address
type.
🌟 Selected for report: HardlyDifficult
HardlyDifficult
Save up to 45k gas on purchase
key (27k gas on average per the test suite).
grantKeys
, shareKey
, and safeTransferFrom
have similar savings.
Remove this line https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/contracts/mixins/MixinKeys.sol#L50 and replace with a gap variable instead.
yarn test
and diff gas report
Replace address[] public owners
with a gap so to preserve upgrade compatibility.
There is a high cost to push into an array. Arrays are generally not useful for other contracts because they will not be able to walk entries once there's a non-trivial number of owners. Off-chain, indexers such as The Graph are better suited to track data like this.
If there is an on-chain need for this, it may be better to be explicit about the value and maybe optimize the data for what's required -- e.g. maybe it's a way to check if an address currently or has previously owned a key, that could be handy on-chain but this array does not allow a contract to determine that.
#0 - 0xleastwood
2022-01-17T08:12:55Z
I'll let @julien51 comment on the usefulness of this parameter.
#1 - julien51
2022-01-17T15:22:59Z
This is actually something we have been wondering about as well and indeed, may be worth considering. This is a big change though we use that array to keep track of the number of owners.
#2 - 0xleastwood
2022-01-17T20:32:15Z
Considering the sponsors comment, I think it would be useful to remove.
🌟 Selected for report: HardlyDifficult
HardlyDifficult
Save 1.7k gas on a key purchase when there's no discount, and 3.2k gas when discounts are added. A minor improvement, but this also seems easier to follow in trace logs or with a tool like Tenderly.
In the PublicLock purchase
remove the calls to _purchasePriceFor
and recordKeyPurchase
and replace it with uint inMemoryKeyPrice = _purchase(_recipient, _referrer, _data);
which was implemented as:
function _purchase( address _recipient, address _referrer, bytes memory _data ) internal returns (uint minKeyPrice) { if(address(onKeyPurchaseHook) != address(0)) { minKeyPrice = onKeyPurchaseHook.keyPurchasePrice(msg.sender, _recipient, _referrer, _data); } else { minKeyPrice = keyPrice; } return unlockProtocol.recordKeyPurchaseAndGetDiscount(_recipient, minKeyPrice, _referrer); }
Then in Unlock, add a new function:
function recordKeyPurchaseAndGetDiscount( address recipient, uint minKeyPrice, address referrer ) public returns (uint inMemoryKeyPrice) { if(minKeyPrice > 0) { (uint unlockDiscount, uint unlockTokens) = computeAvailableDiscountFor(recipient, minKeyPrice); inMemoryKeyPrice = minKeyPrice; if (unlockDiscount > 0) { // INVALID_DISCOUNT_FROM_UNLOCK error assumed from safe math inMemoryKeyPrice -= unlockDiscount; recordConsumedDiscount(unlockDiscount, unlockTokens); } } // Record price without any tips recordKeyPurchase(inMemoryKeyPrice, referrer); }
This approach clones the current behavior, but moves logic into Unlock instead of going back and forth between PublicLock and Unlock.
yarn test
and diff the gas reporter table.
#0 - 0xleastwood
2022-01-17T08:19:55Z
I'll let @julien51 comment on this one.
#1 - julien51
2022-01-17T15:18:53Z
Makes sense!