Platform: Code4rena
Start Date: 09/12/2021
Pot Size: $50,000 USDC
Total HM: 19
Participants: 21
Period: 7 days
Judge: 0xean
Total Solo HM: 14
Id: 61
League: ETH
Rank: 11/21
Findings: 3
Award: $1,269.70
π Selected for report: 6
π Solo Findings: 1
π Selected for report: 0xngndev
845.0261 USDC - $845.03
0xngndev
In AaveYield.sol
the functions:
liquidityToken
_withdrawETH
_depositETH
Make a conditional call to IWETHGateway(wethGateway).getAWETHAddress()
This function does not exist in the wethGateway
contract, causing these function to fail with the error "Fallback not allowed"
.
The function they should be calling is getWethAddress()
without the "A".
Small yet dangerous typo.
Simply modify:
IWETHGateway(wethGateway).getAWETHAddress()
to:
IWETHGateway(wethGateway).getWETHAddress()
In the functions mentioned above.
#0 - ritik99
2021-12-27T05:45:07Z
We were using an older version of the contracts that had this definition, will be updated accordingly
10.9563 USDC - $10.96
0xngndev
Reduce code size and gas expenditure by upgrading the compiler version to version 0.8.0 or higher in order to remove SafeMath from your contracts.
I was curious to test how much more expensive both in gas and code size was to use SafeMath
contrary to simply using a compiler version higher than 0.8.0. So I wrote a small contract with a function that performed:
And using Dapptools I tested gas usage and code size first using 0.8.10 compiler version without SafeMath, and then using SafeMath.
Results: Without SafeMath:
Using SafeMath:
With these results in mind, I would suggest upgrading your compiler version if possible and stop using SafeMath as it's not necessary with compiler version higher than 0.8.0. You can also make use of unchecked in higher versions to save even more gas if you are certain the arithmetic operation you
Here is the example contract I ran to test this:
pragma solidity 0.8.10; contract SafeMath { function add(uint256 x, uint256 y) public pure returns ( uint256, uint256, uint256, uint256 ) { uint256 addResult = x + y; uint256 mulResult = x * y; uint256 divResult = x / y; uint256 substractResult = x - y; return (addResult, mulResult, divResult, substractResult); } }
pragma solidity ^0.7.6; import "@openzeppelin/contracts/math/SafeMath.sol"; //inlined saves gas in the calling function contract SafeMath { using SafeMath for uint256; function add(uint256 x, uint256 y) public pure returns ( uint256, uint256, uint256, uint256 ) { uint256 addResult = x.add(y); uint256 mulResult = x.mul(y); uint256 divResult = x.div(y); uint256 substractResult = x.sub(y); return (addResult, mulResult, divResult, substractResult); } }
And here is the test file
//SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; import "ds-test/test.sol"; import "../SafeMath.sol"; contract SafeMathTest is DSTest { SafeMath safeMathContract; function setUp() public { safeMathContract = new SafeMath(); } //concrete test function testAdd() public logs_gas { safeMathContract.add(10, 5); } }
DappTools
#0 - ritik99
2021-12-26T17:12:14Z
We use Uniswap's oracle library which in turn uses a library that is not yet compatible with >=0.8, hence we cannot upgrade at the moment
37.573 USDC - $37.57
0xngndev
Compiler detecs unused variables, function parameters and a function that can be restricted to pure. I'll enumerate them here:
SavingsAccount.sol
L209
β> _receivedToken
is unusedSavingsAccount.sol
L246
β> _receivedToken
is unusedNoYield.sol
L152
β> function parameter address _asset
is unusedNoYield.sol
L162
β> function parameter address _asset
is unusedNoYield.sol
L53
β> liquidityToken
function visibility can be restricted to pureSimply remove the unused variables and functions parameters, and change the function's visibility.
#0 - ritik99
2021-12-27T13:13:55Z
Gas optimizations label seems more apt, hence the disagreeing with severity
π Selected for report: 0xngndev
Also found by: Jujic, WatchPug, robee, sirhashalot
10.9563 USDC - $10.96
0xngndev
Some of your contracts are quite large byte-wise and require the optimizer with low runs to not reach the code size limit of 24576 bytes. The code size of these contracts can be drastically reduced by shortening the length of your error messages, reducing their deployment cost.
The best example of contracts having unnecessary long erorr messages are CreditLine.sol
and PoolFactory.sol
. When it comes to factories, whose primary goal is to deploy contracts, reducing the cost of doing so is something to keep in mind.
Reduce the length of your error strings. Error messages like:
'PoolFactory::createPool - Repayment interval not within limits'
Could be reduced to:
Interval out of limits
You could save even more by doing something similar to what Uniswap does. They have very short error messages like: ST
, and they expand on what they mean in their documentation.
Another approach is to use revert with CustomErrors, something like this:
if (....) revert CustomError()
Following the example I used above, you could have a custom error message that says:
OutOfBounds()
and expand what it means in the natspec.
Custom error messages are cheaper than strings error messages. Here's a snippet of Solidity's documentation about this:
Using aΒ customΒ errorΒ instance will usually be much cheaper than a string description, because you can use the name of the error to describe it, which is encoded in only four bytes. A longer description can be supplied via NatSpec which does not incur any costs.
π Selected for report: 0xngndev
83.4956 USDC - $83.50
0xngndev
Reduce code size and gas expenditure by inlining internal functions that are only used once throughout your contract.
In some of your contracts, like AaveYield.sol
, you have an initialize
function that calls the internal functions updateSavingsAccount
and updateAaveAddresses
. These two functions are only called in the initialize
function, so you can save some gas and code size by simply inlining their logic inside initialize
. The tradeoff, of course, is that you lose readability, and because initialize
will only be called once, perhaps it's not worth the tradeoff.
To test the difference in code size and gas expenditure I wrote a example contract replicating the behaviour to see how much cheaper inlining the logic was.
The results:
Inlining the logic:
Separating into internal functions:
Contracts I used to test the gas expenditure and code size differences:
//SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.10; contract InliningLogic { address public someRandomAddress; address public anotherRandomAddress; function assignAddresses( address _someRandomAddress, address _anotherRandomAddress ) public { someRandomAddress = _someRandomAddress; anotherRandomAddress = _anotherRandomAddress; } }
//SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; import "ds-test/test.sol"; import "../InliningLogic.sol"; contract InliningLogicTest is DSTest { InliningLogic inliningLogicContract; function setUp() public { inliningLogicContract = new InliningLogic(); } function testInliningLogic() public logs_gas { inliningLogicContract.assignAddresses(address(0x1), address(0x1)); } }
//SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; contract NotInliningLogic { address public someRandomAddress; address public anotherRandomAddress; function assignAddresses( address _someRandomAddress, address _anotherRandomAddress ) public { _assignAddresses(_someRandomAddress, _anotherRandomAddress); } function _assignAddresses( address _someRandomAddress, address _anotherRandomAddress ) internal { someRandomAddress = _someRandomAddress; anotherRandomAddress = _anotherRandomAddress; } }
//SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; import "ds-test/test.sol"; import "../NotInliningLogic.sol"; contract NotInliningLogicTest is DSTest { NotInliningLogic notInliningLogicContract; function setUp() public { notInliningLogicContract = new NotInliningLogic(); } function testNotInliningLogic() public logs_gas { notInliningLogicContract.assignAddresses(address(0x1), address(0x1)); } }
DappTools
π Selected for report: 0xngndev
281.6754 USDC - $281.68
0xngndev
In NoYield.sol
the natspec of function liquidityToken
says it's used to query the liquidity token for a given asset. However, the function simply returns the address of the passed in asset, not its liquidity token address. This could be because NoYield.sol
does not have liquidity tokens and so it simply returns the same asset address the function is called with. Either way, I found it slightly confusing, and I'm not fully sure if I understand why this function is there other than to fulfill the interface.
There's also a small typo in the @return of the natspec:
@return _tokenAddress address of the lqiudity token for the asset
should be:
@return _tokenAddress address of the liquidity token for the asset
#0 - ritik99
2021-12-23T07:42:42Z
As mentioned by the warden, the function has been included just to maintain consistency with the other yield strategies. The return value is what would be expected given NoYield.sol
does not have any associated liquidity token. Maintaining consistency with other yield strategies is important to allow for a common interface in case these strategies are utilized elsewhere, so removing it would not make sense
#1 - 0xean
2022-01-21T21:58:07Z
I think the warden is rather suggesting the nat spec documentation be updated.