Platform: Code4rena
Start Date: 07/07/2023
Pot Size: $121,650 USDC
Total HM: 36
Participants: 111
Period: 7 days
Judge: Picodes
Total Solo HM: 13
Id: 258
League: ETH
Rank: 67/111
Findings: 2
Award: $40.22
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: bin2chen
Also found by: 0x11singh99, 0xWaitress, 0xbepresent, ABAIKUNANBAEV, ArmedGoose, Bauchibred, DadeKuma, GREY-HAWK-REACH, GalloDaSballo, Inspecktor, Jeiwan, Kaysoft, MohammedRizwan, Rolezn, Vagner, alexzoid, alymurtazamemon, ayden, banpaleo5, catellatech, dacian, erebus, eyexploit, fatherOfBlocks, grearlake, joaovwfreire, keccak123, kutugu, lanrebayode77, markus_ether, nadin, naman1778, rvierdiiev, squeaky_cactus, volodya, yixxas
15.9228 USDC - $15.92
You can use the delete keyword instead of setting the variable as zero.
There are 3 instances of this issue in 1 file:
File: prize-pool/src/PrizePool.sol 369: claimCount = 0; 370: canaryClaimCount = 0; 371: largestTierClaimed = 0;
There are 6 instances of this issue in 1 file:
File: twab-controller/src/libraries/OverflowSafeComparatorLib.sol 19: uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32; 20: uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32; 35: uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32; 36: uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32; 52: uint256 aAdjusted = _a > _timestamp ? _a : _a + 2 ** 32; 53: uint256 bAdjusted = _b > _timestamp ? _b : _b + 2 ** 32;
There is 1 instance of this issue in 1 file:
File: prize-pool/src/PrizePool.sol 206: mapping(address => mapping(address => mapping(uint16 => mapping(uint8 => mapping(uint32 => bool)))))
Consider using a modifier to implement access control instead of inlining the condition/requirement in the function’s body.
There are 2 instances of this issue in 1 file:
File: vault/src/Vault.sol 558: if (msg.sender != address(_liquidationPair)) 559: revert LiquidationCallerNotLP(msg.sender, address(_liquidationPair)); 614: if (msg.sender != _claimer) revert CallerNotClaimer(msg.sender, _claimer);
The owner of the contracts has too many privileges relative to standard users. The consequence is disastrous if the contract owner’s private key has been compromised. And, in the event the key was lost or unrecoverable, no implementation upgrades and system parameter updates will ever be possible.
For a project this grand, it increases the likelihood that the owner will be targeted by an attacker, especially given the insufficient protection on sensitive owner private keys. The concentration of privileges creates a single point of failure; and, here are some of the incidents that could possibly transpire:
Transfer ownership and mess up with all the setter functions, hijacking the entire protocol.
Consider:
-- splitting privileges (e.g. via the multisig option) to ensure that no one address has excessive ownership of the system, -- clearly documenting the functions and implementations the owner can change, -- documenting the risks associated with privileged users and single points of failure, and -- ensuring that users are aware of all the risks associated with the system.
#0 - c4-judge
2023-07-18T19:08:21Z
Picodes marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0x11singh99, 0xAnah, 0xn006e7, LeoS, Rageur, Raihan, ReyAdmirado, Rolezn, SAAJ, SAQ, SM3_SS, Udsen, alymurtazamemon, hunter_w3b, koxuan, naman1778, petrichor, ybansal2403
24.2984 USDC - $24.30
When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution. Note that even if an interface defines a function as having memory arguments, it’s still valid for implementation contracs to use calldata arguments instead.
If the array is passed to an internal function which passes the array to another internal function where the array is modified and therefore memory is used in the external call, it’s still more gass-efficient to use calldata when the external function uses modifiers, since the modifiers may prevent the internal functions from being called. Structs have the same overhead as an array of length one
Note that I’ve also flagged instances where the function is public but can be marked as external since it’s not called by the contract, and cases where a constructor is involved
There are 2 instances of this issue in 2 files:
File: vault/src/Vault.sol 653: function setHooks(VaultHooks memory hooks) external {
File: vault/src/VaultFactory.sol 55: function deployVault( 56: IERC20 _asset, 57: string memory _name, 58: string memory _symbol, 59: TwabController _twabController, 60: IERC4626 _yieldVault, 61: PrizePool _prizePool, 62: address _claimer, 63: address _yieldFeeRecipient, 64: uint256 _yieldFeePercentage, 65: address _owner 66: ) external returns (address) {
contract GasTest is DSTest { Contract0 c0; Contract1 c1; function setUp() public { c0 = new Contract0(); c1 = new Contract1(); } function testGas() public { c0.not_optimized("Naman"); c1.optimized("Naman"); } } contract Contract0 { function not_optimized(string memory a) public returns(string memory){ return a; } } contract Contract1 { function optimized(string calldata a) public returns(string calldata){ return a; } }
Contract0 contract | |||||
---|---|---|---|---|---|
Deployment Cost | Deployment Size | ||||
100747 | 535 | ||||
Function Name | min | avg | median | max | # calls |
not_optimized | 790 | 790 | 790 | 790 | 1 |
Contract1 contract | |||||
---|---|---|---|---|---|
Deployment Cost | Deployment Size | ||||
66917 | 366 | ||||
Function Name | min | avg | median | max | # calls |
optimized | 556 | 556 | 556 | 556 | 1 |
Changing abi.encode function to abi.encodePacked can save gas since the abi.encode function pads extra null bytes at the end of the call data, which is unnecessary. Also, in general, abi.encodePacked is more gas-efficient (see Solidity-Encode-Gas-Comparison).
Consider using abi.encodePacked() here:
Use abi.encodePacked() where possible to save gas
There is 1 instance of this issue in 1 file:
File: prize-pool/src/libraries/TierCalculationLib.sol 110: return uint256(keccak256(abi.encode(_user, _tier, _prizeIndex, _winningRandomNumber)));
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)); } }
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 |
There are 4 instances of this issue 2 files:
File: prize-pool/src/PrizePool.sol 278: drawManager = params.drawManager; 303: drawManager = _drawManager;
File: vault/src/Vault.sol 1210: _claimer = claimer_; 1230: _yieldFeeRecipient = yieldFeeRecipient_;
contract GasTest is DSTest { Contract0 c0; Contract1 c1; function setUp() public { c0 = new Contract0(); c1 = new Contract1(); } function testGas() public { c0.setOwnerAssembly(0xFD2dabe9DFcc4d88a12A9D0D40D834E81217Cccf); c1.setOwner(0xFD2dabe9DFcc4d88a12A9D0D40D834E81217Cccf); } } contract Contract0 { address owner; function setOwnerAssembly(address _owner) public { assembly{ sstore(owner.slot,_owner) } } } contract Contract1 { address owner; function setOwner(address _owner) public { owner = _owner; } }
Contract0 contract | |||||
---|---|---|---|---|---|
Deployment Cost | Deployment Size | ||||
35287 | 207 | ||||
Function Name | min | avg | median | max | # calls |
setOwnerAssembly | 22324 | 22324 | 22324 | 22324 | 1 |
Contract1 contract | |||||
---|---|---|---|---|---|
Deployment Cost | Deployment Size | ||||
48499 | 273 | ||||
Function Name | min | avg | median | max | # calls |
setOwner | 22363 | 22363 | 22363 | 22363 | 1 |
On Remix, given only uint256 types, the following are logical equivalents, but don’t cost the same amount of gas:
(a != b || c != d || e != f) costs 571 ((a ^ b) | (c ^ d) | (e ^ f)) != 0 costs 498 (saving 73 gas) Consider rewriting as following to save gas:
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
There are 2 instances of this issue in 1 file:
File: twab-controller/src/TwabController.sol 624: if (_toDelegate == address(0) || _toDelegate == SPONSORSHIP_ADDRESS) { 635: if (_fromDelegate == address(0) || _fromDelegate == SPONSORSHIP_ADDRESS) {
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; } }
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 |
Counting down is more gas efficient than counting up.
There are 7 instances of this issue in 4 files:
File: claimer/src/Claimer.sol 67: for (uint i = 0; i < winners.length; i++) { 144: for (uint i = 0; i < _claimCount; i++) {
File: prize-pool/src/abstract/TieredLiquidityDistributor.sol 361: for (uint8 i = start; i < end; i++) { 630: for (uint8 i = start; i < end; i++) {
File: prize-pool/src/libraries/TierCalculationLib.sol 138: for (uint8 i = 0; i < _numberOfTiers; i++) {
File: vault/src/Vault.sol 618: for (uint w = 0; w < _winners.length; w++) { 620: for (uint p = 0; p < prizeIndicesLength; p++) {
contract GasTest is DSTest { Contract0 c0; Contract1 c1; function setUp() public { c0 = new Contract0(); c1 = new Contract1(); } function testGas() public { c0.AddNum(); c1.AddNum(); } } contract Contract0 { uint256 num = 3; function AddNum() public { uint256 _num = num; for(uint i=0;i<=9;i++){ _num = _num +1; } num = _num; } } contract Contract1 { uint256 num = 3; function AddNum() public { uint256 _num = num; for(uint i=9;i>=0;i--){ _num = _num +1; } num = _num; } }
Contract0 contract | |||||
---|---|---|---|---|---|
Deployment Cost | Deployment Size | ||||
77011 | 311 | ||||
Function Name | min | avg | median | max | # calls |
AddNum | 7040 | 7040 | 7040 | 7040 | 1 |
Contract1 contract | |||||
---|---|---|---|---|---|
Deployment Cost | Deployment Size | ||||
73811 | 295 | ||||
Function Name | min | avg | median | max | # calls |
AddNum | 3819 | 3819 | 3819 | 3819 | 1 |
If a function modifier such as onlyOwner 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.
There are 6 instances of this issue in 2 files:
File: prize-pool/src/PrizePool.sol 335: function withdrawReserve(address _to, uint104 _amount) external onlyDrawManager { 348: function closeDraw(uint256 winningRandomNumber_) external onlyDrawManager returns (uint16) {
File: vault/src/Vault.sol 641: function setClaimer(address claimer_) external onlyOwner returns (address) { 665: function setLiquidationPair( 666: LiquidationPair liquidationPair_ 667: ) external onlyOwner returns (address) { 691: function setYieldFeePercentage(uint256 yieldFeePercentage_) external onlyOwner returns (uint256) { 704: function setYieldFeeRecipient(address yieldFeeRecipient_) external onlyOwner returns (address) {
#0 - c4-judge
2023-07-18T18:54:35Z
Picodes marked the issue as grade-b
#1 - PierrickGT
2023-09-09T00:03:36Z
G-01: fixed in this PR: https://github.com/GenerationSoftware/pt-v5-vault/pull/45
G-02: we can't use abi.encodePacked
G-03, 04, 05: we would lose in code legibility
G-06: non payable functions shouldn't be marked as payable