Organization
@centrifugeEngagement Type
Cantina Reviews
Period
-
Repositories
N/A
Researchers
Findings
Low Risk
2 findings
2 fixed
0 acknowledged
Informational
13 findings
8 fixed
5 acknowledged
Gas Optimizations
3 findings
3 fixed
0 acknowledged
Low Risk2 findings
transferFrom() uses incorrect emit
State
- Fixed
PR #92
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
The
emit Transfer(receiver, address(0), amount)
intransferFrom()
simulates burningIOU
tokens from thereceiver
.However the I Owe You promise is settled from the
sender
perspective, so more logical to use thesender
.This can confuse the offchain logic.
Recommendation
Consider changing the code to:
- emit Transfer(receiver, address(0), amount);+ emit Transfer(sender, address(0), amount);Checks in _processRedeem() can be circumvented and is missing in claimCancelDepositRequest()
State
- Fixed
PR #96
Severity
- Severity: Low
Submitted by
Gerard Persoon
Description
In
_processRedeem()
,receiver
can be arbitrarily selected viaredeem()
andwithdraw()
.RestrictionManager
andRestrictedRedemptions
both have the following restrictions onfrom
:if (uint128(hookData.from).getBit(FREEZE_BIT) == true && !root.endorsed(from)) {// Source is frozen and not endorsedreturn false;}The caller of
redeem()
andwithdraw()
can choose areceiver
that doesn't have theFREEZE_BIT
set, so the_canTransfer()
check can be circumvented.Note: this assumes
address(0)
doesn't have theFREEZE_BIT
set, but if that would be the case_processRedeem()
would never work.Additionally
claimCancelDepositRequest()
also transfers assets but has no checks andreceiver
can also be arbitrarily selected.Recommendation
Consider adding a
_canTransfer()
check to both_processRedeem()
wherecontroller
is used as thefrom
.Also add similar checks in
claimCancelDepositRequest()
.Note: this requires adding
controller
as a parameter to_processRedeem()
.Note: this is similar to the check on
controller
inInvestmentManager::requestRedeem()
.A possible solution as suggested by the project is adding the following:
if (controller != receiver) {require(_canTransfer(vault, controller, receiver, convertToShares(vault, assetsDown)),}}
Informational13 findings
IERC20 imported from forge-std
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
InflationMinter
uses an import fromforge-std
, which make the source depend on the used toolset (e.g. foundry).Recommendation
If you don't want to rely on the toolset, consider importing
IERC20
from one of the following:Interface for burn isn't used
State
- Fixed
PR #1
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The interface for function
burn()
isn't used inInflationMinter
so it could be removed.Recommendation
Consider removing the interface for
burn()
:- function burn(address user, uint256 value) external;Constructor of InflationMinter doesn't have sanity checks
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The constructor of
InflationMinter
doesn't have sanity checks. Its very unlikely that the contract will be deployed with incorrect parameters and if it is, it can be redeployed, so the risk is low.Recommendation
If you do want to add sanity checks, the following can be checked:
target != address(0)
token != address(0)
decimals()
exists (isn't mandatory in ERC20)period != 0
(that would result in a revert)rate
is far smaller thanONE
(a high inflation rate could result in overflows)
The interface for function burn() is different than OZ version
State
- Confirmed
PR #92
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The interface for function
burn()
in contractIouCfg
is different than the standards from OpenZeppelin.Recommendation
Consider using the standards from OZ ERC20Burnable:
- function burn(uint256 value) external;
- function burnFrom(address account, uint256 value) external;
Note:
burnForm()
requires anallowance
andburn()
doesn'tNote: this assumes the contract for
newCfg
still has to be designed/isn't final yet.IouCfg and InflationMinter use different names for same interface
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The contracts
IouCfg
andInflationMinter
use different names for the same interface:IouCfg
usesInflationMinter
InflationMinter
usesIERC20Mintable
Recommendation
Consider using the same names in both contracts.
Auth and auth could be confused
State
- Fixed
PR #92
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The
constructor
in contractIouCfg
uses:Auth(auth)
. It is not immediately clear what the difference betweenAuth
andauth
is.Recommendation
Consider using
initialWard
instead ofauth
:- constructor(address auth, address escrow_, address newCfg_, address legacyCfg_) Auth(auth) {+ constructor(address initialWard , address escrow_, address newCfg_, address legacyCfg_) Auth(initialWard) {Unused bits are erased in updateMember()
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The functions
updateMember()
inRestrictedRedemptions
andRestrictionManager
update the timestamp and theFREEZE_BIT
. However they don't retreive and maintain the remaining 63 bits of thehookData
, so the unused bits are erased.If more bits would be used in the future it might be useful to keep them.
Recommendation
Consider keeping the unused bits from
hookData
.Different order of parameters in events Cancel...Claim
State
- Fixed
PR #96
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The following events start with
receiver
and thencontroller
. However most events start withcontroller
, followed byreceiver
.emit CancelDepositClaim(receiver, controller, REQUEST_ID, msg.sender, assets);emit CancelRedeemClaim(receiver, controller, REQUEST_ID, msg.sender, shares);Recommendation
Consider using the same order in all events.
Similar checks done in different places
State
- Fixed
PR #96
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
Function
ERC7540Vault::requestRedeem()
callscheckTransferRestriction()
andInvestmentManager::requestRedeem()
. FunctionInvestmentManager::requestRedeem()
calls_canTransfer()
, which is similar tocheckTransferRestriction()
.So two similar checks are done in different places.
Recommendation
Consider putting the checks in the same location, which is easier to read and maintain.
Comment in _canTransfer() can be improved to be easier to understand
Description
- The following comment in
InvestmentManager::_canTransfer()
can be improved to make it more readable:
- Sender (from) and receiver (to) have both to pass+ Sender (from) and receiver (to) have to both pass- The following comment in
Comment can be updated to provide more detail
Description
Comments in the
withdraw()
andredeem()
functions state:DOES NOT support controller != msg.sender
However, this is not completely accurate.
msg.sender
can be different than the controller ifmsg.sender
happens to be anoperator
ofcontroller
.Recommendation
Consider updating the comment to make this clear.
maxRedeem() and maxWithdraw() don't have _canTransfer() check
State
- Fixed
PR #96
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
The functions
Redeem()
andWithdraw()
call_processRedeem()
, which now has a_canTransfer()
check.The related functions
maxRedeem()
andmaxWithdraw()
don't have such a checkHowever the functions
maxMint()
andmaxDeposit()
, which are similar tomaxRedeem()
andmaxWithdraw()
, do have a_canTransfer()
check.Recommendation
Consider adding a
_canTransfer()
check tomaxRedeem()
andmaxWithdraw()
.Comment in IouCfg::transferFrom() can be improved to be easier to understand
State
- Fixed
PR #92
Severity
- Severity: Informational
Submitted by
Gerard Persoon
Description
A comment in
IouCfg::transferFrom()
can be improved to make it more readable:- "Ensures that only for Centrifuge to Eth tokens are minted"+ "Ensures that only for the direction "Centrifuge to Eth", tokens are minted"
Gas Optimizations3 findings
InflationMinter can be optimized
State
- Fixed
PR #1
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
Function
mint()
ofInflationMinter
will be used regularly (daily) on mainnet. It can be optimized to same gas costs.Recommendation
Consider using one or more of the following optimizations:
- replace
uint64
withuint256
; - replace
_rpow()
with solady rpow , which will save ~73 gas and saves duplicating code; - perform the calculation in periods, which will save ~1100 gas. This can be done in the following way:
uint256 public immutable period = ...;uint256 public lastMintingInPeriods = block.timestamp/period;function mint() external {uint256 elapsedPeriods = block.timestamp/period;uint256 cached = lastMintingInPeriods;require(elapsedPeriods > cached, "InflationMinter/already-inflated");uint256 mintingsElapsed = elapsedPeriods - cached;lastMintingInPeriods = elapsedPeriods;... // use lastMintingInPeriods}Function decimals() can be optimized
State
- Fixed
PR #92
Severity
- Severity: Gas optimization
Submitted by
Gerard Persoon
Description
Function
decimals()
queries thedecimals()
fromlegacyCfg
every time. This could be optimized.Recommendation
Consider retrieving the value for
IERC20Metadata(legacyCfg).decimals()
in theconstructor
and store the value in animmutable
variable.Functions in ERC7540Vault can be made external
Description
Several functions in
ERC7540Vault
as public while they are not called from the contract itself:function requestDeposit(uint256 assets, address controller, address owner) public returns (uint256) {function pendingDepositRequest(uint256, address controller) public view returns (uint256 pendingAssets) {function requestRedeem(uint256 shares, address controller, address owner) public returns (uint256) {function pendingRedeemRequest(uint256, address controller) public view returns (uint256 pendingShares) {function pendingCancelDepositRequest(uint256, address controller) public view returns (bool isPending) {function claimableCancelDepositRequest(uint256, address controller) public view returns (uint256 claimableAssets) {function pendingCancelRedeemRequest(uint256, address controller) public view returns (bool isPending) {function claimableCancelRedeemRequest(uint256, address controller) public view returns (uint256 claimableShares) {function setOperator(address operator, bool approved) public virtual returns (bool success) {function setEndorsedOperator(address owner, bool approved) public virtual {function convertToShares(uint256 assets) public view returns (uint256 shares) {function maxDeposit(address controller) public view returns (uint256 maxAssets) {function maxMint(address controller) public view returns (uint256 maxShares) {function mint(uint256 shares, address receiver) public returns (uint256 assets) {function maxWithdraw(address controller) public view returns (uint256 maxAssets) {function withdraw(uint256 assets, address receiver, address controller) public returns (uint256 shares) {function onRedeemRequest(address controller, address owner, uint256 shares) public auth {function onDepositClaimable(address controller, uint256 assets, uint256 shares) public auth {function onRedeemClaimable(address controller, uint256 assets, uint256 shares) public auth {function onCancelDepositClaimable(address controller, uint256 assets) public auth {function onCancelRedeemClaimable(address controller, uint256 shares) public auth {Note: the following functions are called from the contract, so should stay public:
function DOMAIN_SEPARATOR() public view returns (bytes32) {function convertToAssets(uint256 shares) public view returns (uint256 assets) {function deposit(uint256 assets, address receiver, address controller) public returns (uint256 shares) {function mint(uint256 shares, address receiver, address controller) public returns (uint256 assets) {function maxRedeem(address controller) public view returns (uint256 maxShares) {Recommendation
Consider making these functions
external
to save some gas.