Platform: Code4rena
Start Date: 03/11/2022
Pot Size: $115,500 USDC
Total HM: 17
Participants: 120
Period: 7 days
Judge: LSDan
Total Solo HM: 1
Id: 174
League: ETH
Rank: 30/120
Findings: 2
Award: $669.35
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xRoxas, 0xSmartContract, Awesome, Aymen0909, B2, BClabs, Bnke0x0, Deekshith99, Deivitto, Diana, Dinesh11G, Funen, HE1M, HardlyCodeMan, Josiah, Nyx, Rahoz, RaymondFam, RedOneN, ReyAdmirado, Rolezn, Saintcode_, TomJ, Trust, __141345__, a12jmx, adriro, ajtra, aphak5010, apostle0x01, brgltd, btk, bulej93, c3phas, carlitox477, catwhiskeys, ch0bu, chaduke, chrisdior4, cryptonue, cryptostellar5, csanuragjain, ctf_sec, delfin454000, djxploit, durianSausage, erictee, fatherOfBlocks, gogo, i_got_hacked, immeas, joestakey, jumpdest7d, lukris02, martin, mcwildy, merlin, minhquanym, oyc_109, pashov, peanuts, pedr02b2, rbserver, rotcivegaf, rvierdiiev, sakman, saneryee, seyni, shark, slowmoses, tnevler, trustindistrust, w0Lfrum, yurahod, zaskoh
620.1198 USDC - $620.12
Context:
return id;
L453return state.claimRevenue(revenueContract, token, data);
L74return state.claimEscrow(token);
L90return (c, principal, interest);
L97return credit;
L160return claimed;
L57return claimed;
L101return claimed;
L121Recommendation:
Choose named return variable or return statement. It is unnecessary to use both.
Context:
Description:
According to official solidity documentation functions should be grouped according to their visibility and ordered:
constructor
receive function (if exists)
fallback function (if exists)
external
public
internal
private
Within a grouping, place the view and pure functions last.
Recommendation:
Put the functions in the correct order according to the documentation.
Context:
require(uint(status) >= uint( LineLib.STATUS.ACTIVE));
L112require(interestRate.setRate(id, drate, frate));
L241require(interestRate.setRate(id, drate, frate));
L259require(amount <= credit.principal + credit.interestAccrued);
L326require(defaultRevenueSplit_ <= SpigotedLineLib.MAX_SPLIT);
L62require(amount <= unusedTokens[credit.token]);
L143require(msg.sender == borrower);
L160require(msg.sender == arbiter);
L239require(escrow_.liquidate(amount, targetToken, to));
L64require(escrow.updateLine(newLine));
L90require(amount > 0);
L91require(msg.sender == ILineOfCredit(self.line).arbiter());
L105require(amount > 0);
L161require(amount > 0);
L198require(msg.sender == self.line);
L216require(LineLib.sendOutTokenOrETH(token, self.treasury, claimed - escrowedAmount));
L96require(revenueContract != address(this));
L128require(self.settings[revenueContract].transferOwnerFunction == bytes4(0));
L130require(success);
L155require(newOwner != address(0));
L180require(newOperator != address(0));
L189require(newTreasury != address(0));
L201require(ISpigot(spigot).updateOwner(newLine));
L147Context:
function init() external virtual returns(LineLib.STATUS) {
L64function _init() internal virtual returns(LineLib.STATUS) {
L69modifier whileActive() {
L78modifier whileBorrowing() {
L83modifier onlyBorrower() {
L88function counts() external view returns (uint256, uint256) {
L117function _healthcheck() internal virtual returns (LineLib.STATUS) {
L121function declareInsolvent() external whileBorrowing returns(bool) {
L143function _canDeclareInsolvent() internal virtual returns(bool) {
L157function updateOutstandingDebt() external override returns (uint256, uint256) {
L163function _updateOutstandingDebt()
L167function accrueInterest() external override returns(bool) {
L200function addCredit(
L223function setRates(
L247function increaseCredit(bytes32 id, uint256 amount)
L265function depositAndClose()
L292function depositAndRepay(uint256 amount)
L315function borrow(bytes32 id, uint256 amount)
L340function withdraw(bytes32 id, uint256 amount)
L370function close(bytes32 id) external payable override returns (bool) {
L388function unused(address token) external view returns (uint256) {
L78function claimAndRepay(address claimToken, bytes calldata zeroExTradeData)
L93function useAndRepay(uint256 amount) external whileBorrowing returns(bool) {
L137function claimAndTrade(address claimToken, bytes calldata zeroExTradeData)
L154function updateOwnerSplit(address revenueContract) external returns (bool) {
L213function addSpigot(
L223function updateWhitelist(bytes4 func, bool allowed)
L235function releaseSpigot(address to) external returns (bool) {
L244function sweep(address to, address token) external nonReentrant returns (uint256) {
L255constructor(
L13function rollover(address newLine)
L48function _healthcheck() internal override(EscrowedLine, LineOfCredit) returns(LineLib.STATUS) {
L97constructor(address _escrow) {
L16function owner() external view returns (address) {
L41function operator() external view returns (address) {
L45function treasury() external view returns (address) {
L49function updateOwnerSplit(address revenueContract, uint8 ownerSplit)
L146function getSetting(address revenueContract)
L220constructor(address _registry) {
L15function accrueInterest(
L34function _accrueInterest(
L42function setRate(
L74constructor(
L20function deployEscrow(
L42function deploySpigot(
L51function deploySecuredLine(address borrower, uint256 ttl)
L59function deploySecuredLineWithConfig(CoreLineParams calldata coreParams)
L87event AddCredit(
L16function getOutstandingDebt(
L73function addCollateral(EscrowState storage self, address oracle, uint256 amount, address token)
L87function enableCollateral(EscrowState storage self, address oracle, address token) external returns (bool) {
L104function releaseCollateral(
L152function getCollateralRatio(EscrowState storage self, address oracle) external returns (uint256) {
L182function getCollateralValue(EscrowState storage self, address oracle) external returns (uint256) {
L187function liquidate(
L192function isLiquidatable(EscrowState storage self, address oracle, uint256 minimumCollateralRatio) external returns(bool) {
L210function updateLine(EscrowState storage self, address _line) external returns(bool) {
L215function _claimRevenue(SpigotState storage self, address revenueContract, address token, bytes calldata data)
L29function operate(SpigotState storage self, address revenueContract, bytes calldata data) external returns (bool) {
L61function claimRevenue(SpigotState storage self, address revenueContract, address token, bytes calldata data)
L83function claimEscrow(SpigotState storage self, address token)
L105function addSpigot(SpigotState storage self, address revenueContract, ISpigot.Setting memory setting) external returns (bool) {
L125function removeSpigot(SpigotState storage self, address revenueContract)
L143function updateOwnerSplit(SpigotState storage self, address revenueContract, uint8 ownerSplit)
L164function updateOwner(SpigotState storage self, address newOwner) external returns (bool) {
L178function updateOperator(SpigotState storage self, address newOperator) external returns (bool) {
L187function updateTreasury(SpigotState storage self, address newTreasury) external returns (bool) {
L196function updateWhitelistedFunction(SpigotState storage self, bytes4 func, bool allowed) external returns (bool) {
L208function getEscrowed(SpigotState storage self, address token) external view returns (uint256) {
L216function isWhitelisted(SpigotState storage self, bytes4 func) external view returns(bool) {
L221function getSetting(SpigotState storage self, address revenueContract)
L227function trade(
L120function canDeclareInsolvent(address spigot, address arbiter) external view returns (bool) {
L151function _mutualConsent(address _signerOne, address _signerTwo) internal returns(bool) {
L38function _getNonCaller(address _signerOne, address _signerTwo) internal view returns(address) {
L65event DeployedSecuredLine(
L8event DeployedSpigot(
L16event DeployedEscrow(
L23function transferModulesToLine(address line, address spigot, address escrow) external {
L57function deploySecuredLine(
L77Context:
// Line Financials aggregated accross all existing Credit
L37 (Change accross to across)* @param arbiter_ - A neutral party with some special priviliges on behalf of Borrower and Lender.
L45 (Change priviliges to privileges)* @dev - updates `status` variable in storage if current status is diferent from existing status
L107 (Change diferent to different)@notice - accrues token demoninated interest on a lender's position.
L213 (Change demoninated to denominated)// ensure that borrowing doesnt cause Line to be LIQUIDATABLE
L355 (Change doesnt to doesn't)// ensure all money owed is accounted for. Accrue facility fee since prinicpal was paid off
L395 (Change prinicpal to principal)// Internal funcs //
L412 (Change funcs to functions)* @notice - updates `status` variable in storage if current status is diferent from existing status.
L416 (Change diferent to different)* @notice - Insert `p` into the next availble FIFO position in the repayment queue
L510 (Change availble to available)* @param arbiter_ - neutral party with some special priviliges on behalf of borrower and lender
L46 (Change priviliges to privileges)* @notice requires Spigot contract itselgf to be transfered to Arbiter and sold off to a 3rd party before declaring insolvent
L84 (Change itselgf to itself)* @notice requires Spigot contract itselgf to be transfered to Arbiter and sold off to a 3rd party before declaring insolvent
L84 (Change transfered to transferred)spigot.claimEscrow(claimToken) : // same asset. dont trade
L107 (Change dont to don't)spigot.claimEscrow(claimToken) : // same asset. dont trade
L164 (Change dont to don't)* @dev - priviliged internal function
L180 (Change priviliged to privileged)// we dont use revenue after this so can store now
L204 (Change dont to don't)// we dont check borrower is same on both lines because borrower might want new address managing new line
L58 (Change dont to don't)* @return isInsolvent - if the entire Line including all collateral sources is fuly insolvent.
L110 (Change fuly to fully)* @notice helper function to allow borrower to easily swithc collateral to a new Line after repyment
L84 (Change swithc to switch)* @notice helper function to allow borrower to easily swithc collateral to a new Line after repyment
L84 (Change repyment to repayment)// ##### Claimoooor #####
L54 (Change Claimoooor to Claimor)// ##### OPERATOOOR #####
L96 (Change OPERATOOOR to OPERATOR)// ##### OPERATOOOR #####
L97 (Change OPERATOOOR to OPERATOR)// ##### Maintainooor #####
L115 (Change to Maintainor)* @dev - revenuContract's transfer func MUST only accept one paramteter which is the new owner.
L133 (Change paramteter to parameter)* @dev - Used if we setup Escrow before Line exists. Line has no way to interface with this function so once transfered `line` is set forever
L71 (Change transfered to transferred)* @notice - allows the lines arbiter to enable thdeposits of an asset
L94 (Change thdeposits to the deposits)* - gives better risk segmentation forlenders
L95 (Change forlenders to for lenders)* @notice Interest rate / acrrued interest calculation contract for Line of Credit contracts
L17 (Change acrrued to accrued)* @param oracle - interset rate contract used by line that will calculate interest owed
L123 (Change interset to interest)* @param credit - The lender position that is being bwithdrawn from
L200 (Change bwithdrawn to withdrawn)// emit events before seeting to 0
L221 (Change seeting to setting)* @param interest - interset rate contract used by line that will calculate interest owed
L237 (Change interset to interest)// get token demoninated interest accrued
L250 (Change demoninated to denominated)// cap so uint doesnt overflow in split calculations.
L53 (Change doesnt to doesn't)* @dev priviliged internal function
L44 (Change priviliged to privileged)* @param spigot - The Spigot to claim from. Must be owned by adddress(this)
L48 (Change adddress(this) to address(this))* @notice - Transfers ownership of the entire Spigot and its revenuw streams from its then Owner to either
L188 (Change revenuw to revenue)Context:
Context:
function _updateStatus(LineLib.STATUS status_) internal returns(LineLib.STATUS) {
L421 (param tag status_ is missing)function _createCredit(
L435 (return tag id is missing)function _repay(Credit memory credit, bytes32 id, uint256 amount)
L465 (param tag credit is missing)function _close(Credit memory credit, bytes32 id) internal virtual returns (bool) {
L483 (param tags credit and id are missing)function liquidate(
L80 (return tag is missing)function _init() internal virtual returns(LineLib.STATUS) {
L25 (return tag is missing)function _healthcheck() virtual internal returns(LineLib.STATUS) {
L34 (return tag is missing)function _rollover(address newLine) internal virtual returns(bool) {
L89 (param tag newLine is missing)function claimRevenue(address revenueContract, address token, bytes calldata data)
L70 (param tag token is missing)function operate(address revenueContract, bytes calldata data) external returns (bool) {
L108 (return tag is missing)function addSpigot(address revenueContract, Setting memory setting) external returns (bool) {
L125 (return tag is missing)function removeSpigot(address revenueContract)
L137 (return tag is missing)function updateOwner(address newOwner) external returns (bool) {
L159 (return tag is missing)function updateOperator(address newOperator) external returns (bool) {
L170 (return tag is missing)function updateTreasury(address newTreasury) external returns (bool) {
L181 (return tag is missing)function updateWhitelistedFunction(bytes4 func, bool allowed) external returns (bool) {
L194 (return tag is missing)function getEscrowed(address token) external view returns (uint256) {
L206 (return tag is missing)function isWhitelisted(bytes4 func) external view returns(bool) {
L216 (return tag is missing)function line() external view override returns(address) {
L57 (return tag is missing)function updateLine(address _line) external returns(bool) {
L74 (param tag _line is missing)function enableCollateral(address token) external returns (bool) {
L100 (return tag is missing)function getLatestAnswer(address token) external returns (int) {
L22 (param tag token is missing)function deploySecuredLineWithModules(
L135 (params tag coreParams, mSpigot, mEscrow are missing)function deploySecuredLineWithModules(
L135 (return tag line is missing)function create(
L125 (param tags are missing)function create(
L125 (return tag is missing)function repay(
L168 (param tags are missing)function withdraw(
L202 (param tags are missing)function accrue(
L239 (param tags are missing)function _getLatestCollateralRatio(EscrowState storage self, address oracle) public returns (uint256) {
L34 (param self is missing)function rollover(address spigot, address newLine) external returns(bool) {
L146 (param tags are missing)function updateSplit(address spigot, address revenueContract, LineLib.STATUS status, uint8 defaultSplit) external returns (bool) {
L169 (param tags spigot, status, defaultSplit are missing)function releaseSpigot(address spigot, LineLib.STATUS status, address borrower, address arbiter, address to) external returns (bool) {
L194 (param tags are missing)function sweep(address to, address token, uint256 amount, LineLib.STATUS status, address borrower, address arbiter) external returns (bool) {
L217 (param tags are missing)modifier mutualConsent(address _signerOne, address _signerTwo) {
L31 (param tags are missing)function rolloverSecuredLine(
L41 (param tag oracle is missing)Context:
mapping(bytes32 => Credit) public credits; // id -> Reference ID for a credit line provided by a single Lender for a given token on a Line of Credit
L35* @dev - A Borrower and a first Lender agree on terms. Then the Borrower deploys the contract using the constructor below.
L42* Later, both Lender and Borrower must call _mutualConsent() during addCredit() to actually enable funds to be deposited.
L43* @param ttl_ - The time to live for all credit lines for the Line of Credit facility (sets the maturity/term of the Line of Credit)
L47deadline = block.timestamp + ttl_; //the deadline is the term/maturity/expiry date of the Line of Credit facility
L58/// @notice exchange aggregator (mainly 0x router) to trade revenue tokens from a Spigot for credit tokens owed to lenders
L28* @notice - excess unsold revenue claimed from Spigot to be sold later or excess credit tokens bought from revenue but not yet used to repay debt
L36* - needed because the Line of Credit might have the same token being lent/borrower as being bought/sold so need to separate accounting.
L37* @param swapTarget_ - 0x protocol exchange address to send calldata for trades to exchange revenue tokens for credit tokens
L49* @param ttl_ - time to live for line of credit contract across all lenders set at deployment in order to set the term/expiry date
L50* @param defaultRevenueSplit_ - The % of Revenue Tokens that the Spigot escrows for debt repayment if the Line is healthy.
L51* @notice requires Spigot contract itselgf to be transfered to Arbiter and sold off to a 3rd party before declaring insolvent
L84* - current implementation just sends "liquidated" tokens to Arbiter to sell off how the deem fit and then manually repay with DepositAndRepay
L72* @notice - a contract allowing the revenue stream of a smart contract to be split between two parties, Owner and Treasury
L12* @param _owner - An address that controls the Spigot and owns rights to some or all tokens earned by owned revenue contracts
L26* @param _treasury - A non-active address for non-Owner that receives revenue tokens that aren't allocated and escrowed for the Owner
L27* @param _operator - An active address for non-Owner that can execute whitelisted functions to manage and maintain product operations
L28* @notice - Claims revenue tokens from the Spigot (push and pull payments) and escrows them for the Owner withdraw later.
L59owner
and `treasury```` L68* @notice - uses predefined function in revenueContract settings to transfer complete control and ownership from this Spigot to the Operator
L131* @notice - Ownable contract that allows someone to deposit ERC20 and ERC4626 tokens as collateral to back a Line of Credit
L17* @param _minimumCollateralRatio - In bps, 3 decimals. Cratio threshold where liquidations begin. see Escrow.isLiquidatable()
L36* @param _line - Initial owner of Escrow contract. May be non-Line contract at construction before transferring to a Line.
L38* @param _borrower - borrower on the _line contract. Cannot pull from _line because _line might not be a Line at construction.
L40* @notice - Checks Line's outstanding debt value and current Escrow collateral value to compute collateral ratio and checks that against minimum.
L62* @dev - Used if we setup Escrow before Line exists. Line has no way to interface with this function so once transfered `line` is set forever
L71* @notice - simple contract that wraps Chainlink's Feed Registry to get asset prices for any tokens without needing to know the specific oracle address
L10@notice sets up new line based of config of old line. Old line does not need to have REPAID status for this call to succeed.
L171/// @notice After accrueInterest runs, emits the amount of interest added to a Borrower's outstanding balance of interest due
L31(N.B. results in an increase in interestRepaid, i.e. interest not yet withdrawn by a Lender). There is no corresponding function
L43* @dev - Creates a deterministic hash id for a credit line provided by a single Lender for a given token on a Line of Credit facility
L55* @notice - Calculates value of tokens. Used for calculating the USD value of principal and of interest during getOutstandingDebt()
L101* @dev assumes that `id` of a single credit line within the Line of Credit facility (same lender/token) is stored only once in the `positions` array
L12* This means cleanup on _close() and checks on addCredit() are CRITICAL. If `id` is duplicated then the position can't be closed
L14* @return newPositions - all active credit lines on the Line of Credit facility after the `id` has been removed [Bob - consider renaming to newIds
L17function isLiquidatable(EscrowState storage self, address oracle, uint256 minimumCollateralRatio) external returns(bool) {
L210// Configurations for revenue contracts related to the split of revenue, access control to claiming revenue tokens and transfer of Spigot ownership
L15// cant claim revenue via operate() because that fucks up accounting logic. Owner shouldn't whitelist it anyway but just in case
L69function addSpigot(SpigotState storage self, address revenueContract, ISpigot.Setting memory setting) external returns (bool) {
L125* @notice Allows revenue tokens in 'escrowed' to be traded for credit tokens that aren't yet used to repay debt.
L39The newly exchanged credit tokens are held in 'unusedTokens' ready for a Lender to withdraw using useAndRepay
L40This feature allows a Borrower to take advantage of an increase in the value of the revenue token compared
L41to the credit token and to in effect use less revenue tokens to be later used to repay the same amount of debt.
L42* @notice Changes the revenue split between a Borrower's treasury and the LineOfCredit based on line health, runs with updateOwnerSplit()
L164function updateSplit(address spigot, address revenueContract, LineLib.STATUS status, uint8 defaultSplit) external returns (bool) {
L169function releaseSpigot(address spigot, LineLib.STATUS status, address borrower, address arbiter, address to) external returns (bool) {
L194* @notice - Sends any remaining tokens (revenue or credit tokens) in the Spigot to the Borrower after the loan has been repaid.
L212- In case of a Borrower default (loan status = liquidatable), this is a fallback mechanism to withdraw all the tokens and send them to the Arbiter
L213function sweep(address to, address token, uint256 amount, LineLib.STATUS status, address borrower, address arbiter) external returns (bool) {
L217@notice sets up new line based of config of old line. Old line does not need to have REPAID status for this call to succeed.
L34Description:
Maximum suggested line length is 120 characters.
#0 - c4-judge
2022-12-06T21:39:58Z
dmvt marked the issue as grade-a
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xRajkumar, Awesome, Aymen0909, B2, Bnke0x0, Deivitto, Diana, JC, Metatron, Rahoz, RaymondFam, RedOneN, ReyAdmirado, Rolezn, Saintcode_, TomJ, __141345__, ajtra, aphak5010, brgltd, c3phas, ch0bu, chrisdior4, cryptonue, durianSausage, emrekocak, erictee, exolorkistis, gogo, karanctf, lukris02, martin, me_na0mi, oyc_109, peanuts, rotcivegaf, saneryee, seyni, tnevler, zaskoh
49.2315 USDC - $49.23
Context:
for (uint256 i; i < len; ++i) {
L179for (uint256 i; i < len; ++i) {
L203for (uint256 i; i <= lastSpot; ++i) {
L520for(uint256 i; i < len; ++i) {
L23for(uint i = 1; i < len; ++i) {
L51for (uint256 i; i < length; ++i) {
L57Description:
This can save 30-40 gas per loop iteration.
Recommendation:
Example how to fix. Change:
for (uint256 i = 0; i < orders.length; ++i) { // Do the thing }
To:
for (uint256 i = 0; i < orders.length;) { // Do the thing unchecked { ++i; } }
Context:
unusedTokens[credit.token] -= repaid - newTokens;
L122 (repaid - newTokens)unusedTokens[credit.token] += newTokens - repaid;
L125 (newTokens - repaid)unusedTokens[credit.token] -= amount;
L144ids[i - 1] = ids[i]; // could also clean arr here like in _SortIntoQ
L52 (i-1)self.deposited[token].amount -= amount;
L164self.deposited[token].amount -= amount;
L202require(LineLib.sendOutTokenOrETH(token, self.treasury, claimed - escrowedAmount));
L96 (claimed - escrowedAmount)uint256 diff = oldClaimTokens - newClaimTokens;
L101unused - diff
L109Description:
Some gas can be saved by using an unchecked {} block if an underflow isn't possible because of a previous require() or if-statement.
Context:
Description:
If they are not inlined, they cost an additional 20 to 40 gas because of 2 extra jump instructions and additional stack operations needed for function calls.
Context:
https://github.com/debtdao/Line-of-Credit/blob/audit/code4rena-2022-11-03/contracts/utils/SpigotLib.sol#L34-L49 (self.settings[revenueContract].claimFunction)
Description:
If you read value from mapping/array more than once within a function then it is cheaper to cache it in local memory and then read it from memory wnen it is neaded. This will save about 40 gas.
Recommendation:
Example how to fix. Change:
if(self.settings[revenueContract].claimFunction == bytes4(0)) { // push payments // claimed = total balance - already accounted for balance claimed = existingBalance - self.escrowed[token]; // underflow revert ensures we have more tokens than we started with and actually claimed revenue } else { // pull payments if(bytes4(data) != self.settings[revenueContract].claimFunction) { revert BadFunction(); } (bool claimSuccess,) = revenueContract.call(data); if(!claimSuccess) { revert ClaimFailed(); } // claimed = total balance - existing balance claimed = LineLib.getBalance(token) - existingBalance; // underflow revert ensures we have more tokens than we started with and actually claimed revenue }
to:
bytes4 _claimFunction = self.settings[revenueContract].claimFunction; if(_claimFunction == bytes4(0)) { // push payments // claimed = total balance - already accounted for balance claimed = existingBalance - self.escrowed[token]; // underflow revert ensures we have more tokens than we started with and actually claimed revenue } else { // pull payments if(bytes4(data) != _claimFunction) { revert BadFunction(); } (bool claimSuccess,) = revenueContract.call(data); if(!claimSuccess) { revert ClaimFailed(); } // claimed = total balance - existing balance claimed = LineLib.getBalance(token) - existingBalance; // underflow revert ensures we have more tokens than we started with and actually claimed revenue }
Context:
function _accrue(Credit memory credit, bytes32 id) internal returns(Credit memory) {
L218 (credit)function _close(Credit memory credit, bytes32 id) internal virtual returns (bool) {
L483 (credit)function addSpigot(address revenueContract, Setting memory setting) external returns (bool) {
L125 (setting)ILineOfCredit.Credit memory credit,
L74function addSpigot(SpigotState storage self, address revenueContract, ISpigot.Setting memory setting) external returns (bool) {
L125 (setting)Description:
If a reference type function parameter is read-only, it is recommended to use calldata instead of memory because this provides significant gas savings. Since Solidity v0.6.9, memory and calldata are allowed in all functions regardless of their visibility type (ie external, public, etc).
#0 - c4-judge
2022-11-17T22:57:22Z
dmvt marked the issue as grade-b