Platform: Code4rena
Start Date: 21/06/2022
Pot Size: $55,000 USDC
Total HM: 29
Participants: 88
Period: 5 days
Judge: gzeon
Total Solo HM: 7
Id: 134
League: ETH
Rank: 29/88
Findings: 2
Award: $311.72
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: defsec
Also found by: 0x1f8b, 0x29A, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xkowloon, 0xmint, Bnke0x0, BowTiedWardens, Chom, ElKu, Funen, GalloDaSballo, GimelSec, IllIllI, JC, Kenshin, Kulk0, Lambda, Limbooo, MadWookie, Metatron, Picodes, Soosh, StErMi, TomJ, WatchPug, Waze, Yiko, _Adam, ak1, asutorufos, aysha, bardamu, catchup, datapunk, delfin454000, dipp, fatherOfBlocks, grGred, hake, hansfriese, hyh, joestakey, kebabsec, kenzo, kirk-baird, oyc_109, pashov, poirots, rfa, robee, saian, sashik_eth, shenwilly, simon135, slywaters, z3s, zeesaw, zer0dot
126.2189 USDC - $126.22
Few vulnerabilities were found, the main concerns are with the lack of boundaries on fees amount:
constructors should check the address written in an immutable address variable is not the zero address.
Note: while it has been indicated by the sponsor input validation will be on the front-end side to relieve users from unnecessary gas spendings, this issue here concerns constructor functions, ie when the contract is deployed by the team.
Low
Instances include:
Manual Analysis
Add a zero address check for the immutable variables aforementioned.
MarketPlace
uses the Safe
library transferFrom
method in its minting functions. But there is no transferFrom
method in marketplace/Safe.sol
.
Low
Instances include:
Safe.transferFrom(ERC20(address(pool.base())), msg.sender, address(pool), uA)
Safe.transferFrom(ERC20(address(pool.fyToken())), msg.sender, address(pool), ptA)
Safe.transferFrom(ERC20(address(pool.base())), msg.sender, address(pool), a)
Manual Analysis
Declare transferFrom
in marketplace/Safe.sol
constructors and setters should check the input value - ie make revert if it is the zero address or zero
Note: while it has been indicated by the sponsor input validation will be on the front-end side to relieve users from unnecessary gas spendings, this issue here concerns constructor functions or setters called by authorized admins, not users, and most likely by interacting with the contract directly (for instance using Etherscan or Hardhat) and not with a front-end.
Low
Instances include:
swivelAddr = s
admin = a
marketPlace = m
swivelAddr = s
lender = l
swivelAddr = s
apwineAddr = a
admin = a
marketPlace = m
lender = l
swivelAddr = s
Manual Analysis
Add non-zero checks - address - to the setters aforementioned.
There is no upper boundary to f
in setFee
. This means a malicious admin could set feenominator
to 100%, resulting in any call to lend
essentially draining users from their tokens without giving them any PT token in exchange.
Low
Instances include:
Manual Analysis
Add a reasonable upper boundary in setFee
.
Some of the function comments are missing function parameters or returns
Non-Critical
Instances include:
Manual Analysis
Add a comment for this parameter
There are portions of commented code in some files.
Non-Critical
Instances include:
// _purchaseReturn = _buySecondaryCurve(_to, _lowerCurveDiff)
// buyoutValuationDeposit = _currentValuation - ((primaryReserveBalance - fictitiousPrimaryReserveBalance) + secondaryReserveBalance)
Manual Analysis
Remove commented code
Events should use indexed fields to make it easier to filter them in logs.
Non-Critical
Instances include:
event Lend(uint8 principal, address indexed underlying, uint256 indexed maturity, uint256 returned)
event Mint(uint8 principal, address indexed underlying, uint256 indexed maturity, uint256 amount)
event ScheduleWithdrawal(address indexed token, uint256 hold)\
event Redeem(uint8 principal, address indexed underlying, uint256 indexed maturity, uint256 amount)
Manual Analysis
Add indexed fields to these events so that they have the maximum number of indexed fields possible.
Setters should emit an event so that Dapps can detect important changes to storage.
Again, this does not go against the will of the sponsor to alleviate gas costs from users, as these setters are called by admins.
Non-Critical
Instances include:
function setPrincipal
function setAdmin
function setPool
function setAdmin
function setFee
function setMarketPlace
function setSwivel
function setAdmin
function setMarketPlace
function setLender
function setSwivel
Manual Analysis
emit an event in all setters
Functions should be ordered following the Soldiity conventions.
Non-Critical
In MarketPlace.sol
and Lender.sol
, several external
functions are declared after public
and even internal
functions.
Manual Analysis
Place the external
functions before public
and internal
functions.
Decoupling an interface from its implementation can lead to code drift and incomplete or incorrect interfaces/implementations
Non-Critical
Instances include:
contract Redeemer is not inheriting from IRedeemer
.
Manual Analysis
Redeemer
should inherit from IRedeemer
( which is by the way defined in marketplace/Interfaces
, but not in redeemer/Interfaces
- add IRedeemer
to redeemer/Interfaces
for consistency)
It is good practice to mark functions as external
instead of public
if they are not called by the contract where they are defined.
Non-Critical
Instances include:
Note: the lend
functions listed below have the same name but are separate functions with different parameters.
function mint
function lend
function lend
function lend
function lend
function lend
function lend
function lend
function lend
Note: the redeem
functions listed below have the same name but are separate functions with different parameters.
function redeem
function redeem
function redeem
function redeem
function authRedeem
Manual Analysis
Declare these functions as external
instead of public
For readability, it is best to use scientific notation (e.g 10e5
) rather than decimal literals(100000
) or exponentiation(10**5
). Underscores are used throughout the contracts and do improve readability too, so this is more of a suggestion.
Non-Critical
Instances include:
Manual Analysis
It is good practice to add timelock to critical parameters changes, such as admin changes, to give users time to react.
Non-Critical
All the admin setters across all three contracts are missing a timelock.
Manual Analysis
Consider adding a timelock to admin setters.
🌟 Selected for report: BowTiedWardens
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 0xkowloon, Bnke0x0, ElKu, Fitraldys, Funen, GalloDaSballo, IllIllI, JC, Kaiziron, Lambda, MadWookie, Noah3o6, Nyamcil, RoiEvenHaim, TomJ, Tomio, UnusualTurtle, Waze, _Adam, ajtra, asutorufos, bardamu, c3phas, catchup, datapunk, defsec, delfin454000, fatherOfBlocks, grGred, hake, hansfriese, hyh, ignacio, joestakey, kebabsec, ladboy233, oyc_109, pashov, poirots, rfa, robee, sach1r0, samruna, sashik_eth, simon135, slywaters, z3s, zer0dot
185.4987 USDC - $185.50
Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.
Instances include:
scope: calculateFee()
feeNominator
is read twice if feeNominator > 0
. It may be worth caching it as it is quite likely to be the case.return feenominator > 0 ? a / feenominator : 0
scope: redeem(uint8 p,address u,uint256 m,address o)
lender
is read twice.uint256 amount = IERC20(principal).balanceOf(lender)
Safe.transferFrom(IERC20(u), lender, address(this), amount)
scope: redeem(uint8 p,address u,uint256 m)
lender
is read twice.uint256 amount = IERC20(principal).balanceOf(lender)
Safe.transferFrom(IERC20(u), lender, address(this), amount)
scope: redeem(uint8 p,address u,uint256 m, bytes32 i)
lender
is read twice.uint256 amount = token.balanceOf(lender)
Safe.transferFrom(token, lender, address(this), amount)
scope: redeem(uint8 p,address u,uint256 m, address d,address o)
lender
is read twice.uint256 amount = token.balanceOf(lender)
Safe.transferFrom(token, lender, address(this), amount)
Manual Analysis
cache these storage variables in memory
If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory. Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.
Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.
Instances include:
Manual Analysis
Replace memory
with calldata
In the EVM, there is no opcode for >=
or <=
.
When using greater than or equal, two operations are performed: >
and ==
.
Using strict comparison operators hence saves gas, approximately 3
gas in require
and if
statements
Instances include:
require (block.timestamp >= when, 'withdrawal still on hold')
Manual Analysis
Replace <=
with <
, and >=
with >
. Do not forget to increment/decrement the compared variable
example:
-block.timestamp >= when +block.timestamp > when - 1;
However, if 1
is negligible compared to the value of the variable, we can omit the increment.
-block.timestamp >= when +block.timestamp > when;
Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters.
Instances include:
swivelAddr = s
pendleAddr = p
tempusAddr = t
feenominator = 1000, this one is not a construction parameter, this initial value can be hardcoded.
lender = l
swivelAddr = s
pendleAddr = p
tempusAddr = t
apwineAddr = a
Manual Analysis
Hardcode storage variables with their initial value instead of writing it during contract deployment with constructor parameters.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here.
It not only saves gas upon deployment - ~5500
gas saved per custom error instead of a require statement, but it is also cheaper in a function call, 22
gas saved per require statement replaced with a custom error.
Custom errors are defined using the error statement
Instances include:
require (when != 0, 'no withdrawal scheduled')
require (block.timestamp >= when, 'withdrawal still on hold')
Manual Analysis
Replace require and revert statements with custom errors.
For instance, in Lender.sol
:
Replace
require (when != 0, 'no withdrawal scheduled')
with
if (when == 0) { revert NoWithdrawalScheduled(); }
and define the custom error in the contract
error NoWithdrawalScheduled();
If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type).
Explicitly initializing it with its default value is an anti-pattern and wastes 3
gas per variable initialized.
Instances include:
Manual Analysis
Remove explicit initialization for default values.
If a variable is set in the constructor and never modified afterwards, marking it as immutable
can save a storage operation - 20,000
gas.
Instances include:
address public apwineAddr is only set in the constructor but can never be modified.
Manual Analysis
Mark it as immutable
.
X += Y costs 3
more gas than X = X + Y.
Instances include:
totalFee += fee
lent += amountLent
returned += amountLent * order.premium / order.principal
fees[u] += totalFee
fees[u] += calculateFee(a)
fees[u] += fee
fees[u] += fee
fees[u] += fee
fees[u] += fee
fees[u] += fee
Manual Analysis
use X = X + Y
instead of X += Y
(same with -
)
Prefix increments are cheaper than postfix increments: it returns the incremented variable instead of returning a temporary variable storing the initial value of the variable. It saves 5
gas per iteration
Instances include:
Manual Analysis
change variable++
to ++variable
.
The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.
if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked
block will save gas
Instances include:
a - calculateFee(a), calculateFee(a) <= a, so it cannot underflow
a - calculateFee(a), calculateFee(a) <= a, so it cannot underflow
uint256 amountLent = amount - fee, fee = calculateFee(amount) <= amount, so it cannot underflow
a - fee, fee = calculateFee(a) <= a, so it cannot underflow
a - fee, fee = calculateFee(a) <= a, so it cannot underflow
a - fee, fee = calculateFee(a) <= a, so it cannot underflow
a - fee, fee = calculateFee(a) <= a, so it cannot underflow
Manual Analysis
Place the arithmetic operations in an unchecked
block
Gas can be saved by avoiding using local variables when they are not necessary.
There are two instances:
uint256 max = 2**256 - 1, is only used here so it can be inlined
uint256 max = 2**256 - 1, is only used here so it can be inlined
Manual Analysis
max
with its value to avoid the use of a local variabletype(uint256).max
instead of 2**256 - 1