Platform: Code4rena
Start Date: 30/09/2021
Pot Size: $75,000 ETH
Total HM: 9
Participants: 15
Period: 7 days
Judge: 0xean
Total Solo HM: 2
Id: 39
League: ETH
Rank: 6/15
Findings: 4
Award: $4,340.54
🌟 Selected for report: 6
🚀 Solo Findings: 0
pauliax
Not every ERC20 token returns true on transfer success. To support different tokens, the current best practice is to use SafeERC20 (safeTransfer, safeTransferFrom, etc): https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol Similarly, some ERC20 tokens like USDT require resetting the approval to 0 first before being able to reset it to another value, so you should also consider replacing: uToken.approve(cTokenAddr, a); with: uToken.safeApprove(cTokenAddr, 0); uToken.safeApprove(cTokenAddr, a);
Consider using SafeERC20 library.
#0 - 0xean
2021-10-16T23:24:59Z
dupe of #155
pauliax
When calling function createMarket an admin can override an existing market by specifying the same underlying and maturity: markets[u][m] = Market(c, zctAddr, vAddr); it does not check if the market for these parameters already exists, so technically it is possible to re-create it but this function is only callable by admin so I am not sure if that's a supposed behavior or a potential bug, but because a track of old zcTokenAddr and vaultAddr is not kept, values and tokens held in them may become inaccessible forever.
Consider adding a check that the market does not exist before creating it.
#0 - 0xean
2021-10-16T23:17:31Z
dupe of #97
pauliax
Different declared and the actual order of parameters for the Mature event:
event Mature(address indexed underlying, uint256 indexed maturity, uint256 maturityRate, uint256 matured); emit Mature(u, m, block.timestamp, currentExchangeRate);
maturityRate and matured are in the wrong order.
This may trick the outside consumers of this event so consider fixing this.
#0 - 0xean
2021-10-16T23:26:03Z
dupe of #90
#1 - JTraversa
2021-11-03T19:32:57Z
pauliax
Another potential issue with this setFee function is that an admin can grief by frontrunning users and setting arbitrary high fees. The protocol should enforce lower and upper bounds for this fenominator. While this is a very theoretical issue and depends on the honesty of the owner, a similar issue was reported in a previous contest and assigned a score of medium: https://github.com/code-423n4/2021-05-nftx-findings/issues/51
Introduce reasonable boundaries for the denominator values.
#0 - 0xean
2021-10-15T01:35:46Z
downgrading. While this is possible, funds are not at risk.
pauliax
I know this contract is not in scope but you should consider replacing your own Sig library that has some ToDos left with a battle-tested ECDSA library for signature verifications: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol
This is just a recommendation to use ECDSA from OZ, didn't find a direct issue with your current approach.
#0 - 0xean
2021-10-16T23:28:32Z
dupe of #104
🌟 Selected for report: 0xRajeev
Also found by: 0xsanson, nikitastupin, pauliax
pauliax
The variable domain in contract Swivel is cached in the contract storage and will not change after being initialized. However, if a hard fork happens after the contract deployment, the domain would become invalid on one of the forked chains due to the block.chainid has changed. A similar issue was reported in a previous contest and was assigned a severity of low: https://github.com/code-423n4/2021-06-realitycards-findings/issues/166
An elegant solution that you may consider applying is from Sushi Trident: https://github.com/sushiswap/trident/blob/concentrated/contracts/pool/concentrated/TridentNFT.sol#L47-L62
🌟 Selected for report: pauliax
pauliax
function setFee should validate that t is a valid index (0-3) and d > 0, as denominator must not be zero, it may produce division by 0 runtime error otherwise.
Consider adding proposed validations.
pauliax
There are places where the same storage variable is accessed multiple times in the same function, e.g. function p2pZcTokenExchange accesses markets[u][m].zcTokenAddr 2 times, function matureMarket accesses markets[u][m] 3 times, and so on.
It would be more gas efficient to cache these variables and re-use them where necessary.
pauliax
Consider using a fixed-size array for fenominator as the contract only assumes 4 fees anyway: /// @dev holds the fee demoninators for [zcTokenInitiate, zcTokenExit, vaultInitiate, vaultExit] uint16[] public fenominator; Also, there is no gas benefit of using lower than uint256 size variables here, actually quite the opposite as EVM operates on 32-byte values so it needs a conversion.
Proposed solution: uint256 constant private NUMBER_OF_FEES = 4; uint256[NUMBER_OF_FEES] public fenominator;
🌟 Selected for report: pauliax
0.0941 ETH - $278.98
pauliax
It is unclear why there are many functions that always return a boolean value of true. While this may be your agreed practice that you try to follow, it also incurs more gas consumption as the caller needs to receive and check these returned values.
If you want to optimize for gas, consider dropping return values for functions that actually do not need them.
🌟 Selected for report: pauliax
0.0941 ETH - $278.98
pauliax
boolean flag 'matured' could be removed if you agree to accept maturityRate > 0 as a matured vault, basically replacing: require(!matured, 'already matured'); with: require(maturityRate == 0, 'already matured'); This would eliminate one storage variable and thus reduce gas usage. The risk is that exchangeRateCurrent can never be 0 as this would mean an immature state.
Consider getting rid of 'matured' as per suggestion.
#0 - JTraversa
2021-10-24T19:19:03Z
🌟 Selected for report: pauliax
0.0941 ETH - $278.98
pauliax
function matureVault could accept maturityRate as a parameter as this value was already fetched in Marketplace and can be passed to Vault to avoid a duplicate external call.
function matureVault(uint256 maturityRate) {...} require(VaultTracker(markets[u][m].vaultAddr).matureVault(currentExchangeRate), 'maturity not reached');
#0 - JTraversa
2021-10-08T05:32:10Z
Need to double check if duplicate.
#1 - JTraversa
2021-10-24T20:26:34Z
duplicate of #30
🌟 Selected for report: pauliax
0.0941 ETH - $278.98
pauliax
I do not see a reason why 'mature' and 'maturityRate' need to be stored in separate mappings, why they can't be included in the Market struct for better maintainability and gas reduction?
before: struct Market { address cTokenAddr; address zcTokenAddr; address vaultAddr; } mapping (address => mapping (uint256 => Market)) public markets; mapping (address => mapping (uint256 => bool)) public mature; mapping (address => mapping (uint256 => uint256)) public maturityRate;
after: struct Market { address cTokenAddr; address zcTokenAddr; address vaultAddr; bool mature; uint256 maturityRate; } mapping (address => mapping (uint256 => Market)) public markets;
#0 - JTraversa
2021-10-08T05:29:31Z
Is there gas reduction? I figured it would effectively be the same amount of SLOAD words. Will review.
#1 - 0xean
2021-10-15T20:54:46Z
Yes, this is a valid optimization
#2 - JTraversa
2021-10-24T19:20:02Z
🌟 Selected for report: pauliax
0.0941 ETH - $278.98
pauliax
Having both modifiers 'onlyAdmin' and 'onlySwivel' is not only more expensive but also misleading as these modifiers basically do the same job of checking an address against msg.sender.
Better have a generalized modifier, something like onlyAddress(address a), and re-use it with both admin and swivel: modifier onlyAddress(address a) { require(msg.sender == a, 'sender not authorized'); _; } onlyAddress(admin) onlyAddress(swivel)
#0 - JTraversa
2021-10-24T19:19:26Z