Notional contest - cmichel's results

Fixed rates, now in crypto.

General Information

Platform: Code4rena

Start Date: 26/08/2021

Pot Size: $200,000 USDC

Total HM: 17

Participants: 11

Period: 14 days

Judge: ghoulsol

Total Solo HM: 12

Id: 23

League: ETH

Notional

Findings Distribution

Researcher Performance

Rank: 1/11

Findings: 25

Award: $114,668.11

🌟 Selected for report: 22

πŸš€ Solo Findings: 18

Findings Information

🌟 Selected for report: cmichel

Also found by: leastwood

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

1557.1694 NOTE - $1,557.17

4671.5083 USDC - $4,671.51

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The GovernorAlpha inherits from a vulnerable TimelockController. This TimelockController allows an EXECUTOR role to escalate privileges and also gain the proposer role. See details on OZ and the fix here. The bug is that _executeBatch checks if the proposal was scheduled only after the transactions have been executed. This allows inserting a call into the batch that schedules the batch itself, and the entire batch will succeed. As the custom GovernorAlpha.executeProposal function removed the original "queued state check" (require(state(proposalId) == ProposalState.Queued), the attack can be executed by anyone, even without the EXEUCTOR_ROLE.

POC

  1. Create a proposal using propose. The calldata will be explained in the next step. (This can be done by anyone passing the min proposalThreshold)
  2. Call executeProposal(proposalId, ...) such that the following calls are made:
call-0: grantRole(TIME_LOCK_ADMIN, attackerContract) call-1: grantRole(EXECUTOR, attackerContract) call-2: grantRole(PROPOSER, attackerContract) call-3: updateDelay(0) // such that _afterCall "isOperationReady(id): timestamp[id] = block.timestamp + minDelay (0) <= block.timestamp" passes call-4: attackerContract.hello() // this calls timelock.schedule(args=[targets, values, datas, ...]) where args were previously already stored in contract. (this is necessary because id depends on this function's args and we may not be self-referential) // attackerContract is proposer & executor now and can directly call scheduleBatch & executeBatch without having to create a proposal

ℹ️ I already talked to Jeff Wu about this and he created a test case for it confirming this finding

Impact

Anyone who can create a proposal can become Timelock admin (proposer & executor) and execute arbitrary transactions as the DAO-controlled GovernorAlpha. Note that this contract has severe privileges and an attacker can now do anything that previously required approval of the DAO. For example, they could update the globalTransferOperator and steal all tokens.

We recommend updating the vulnerable contract to TimelockController v3.4.2. It currently uses OpenZeppelin/openzeppelin-contracts@3.4.0-solc-0.7

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

3460.3765 NOTE - $3,460.38

10381.1295 USDC - $10,381.13

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

Some tokens (like USDT) don't correctly implement the EIP20 standard and their transfer/transferFrom function return void instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert.

See CompoundToNotionalV2.notionalCallback's IERC20(underlyingToken).transferFrom call.

Impact

Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value. As there is a cToken with USDT as the underlying this issue directly applies to the protocol.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

3460.3765 NOTE - $3,460.38

10381.1295 USDC - $10,381.13

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The CompoundToNotionalV2.notionalCallback is supposed to only be called from the verified contract that calls this callback but the access restrictions can be circumvented by simply providing sender = this as sender is a parameter of the function that can be chosen by the attacker.

function notionalCallback(
    address sender,
    address account,
    bytes calldata callbackData
) external returns (uint256) {
// @audit sender can be passed in by the attacker
require(sender == address(this), "Unauthorized callback");

Impact

An attacker can call the function passing in an arbitrary account whose tokens are then transferred to the contract. The account first has to approve this contract but this can happen with accounts that legitimately want to call the outer function and have to send a first transaction to approve the contract, but then an attacker frontruns the actual transaction.

It's at least a griefing attack: I can pass in a malicious cTokenBorrow that returns any token of my choice (through the .underlying() call) but whose repayBorrowBehalf is a no-op. This will lead to any of the victim's approved tokens becoming stuck in the contract, essentially burning them:

// @audit using a malicious contract, this can be any token
address underlyingToken = CTokenInterface(cTokenBorrow).underlying();
bool success = IERC20(underlyingToken).transferFrom(account, address(this), cTokenRepayAmount);
require(success, "Transfer of repayment failed");

// Use the amount transferred to repay the borrow
// @audit using a malicious contract, this can be a no-op
uint code = CErc20Interface(cTokenBorrow).repayBorrowBehalf(account, cTokenRepayAmount);

Note that the assumption at the end of the function "// When this exits a free collateral check will be triggered" is not correct anymore but I couldn't find a way to make use of it to lead to an invalid account state.

Fix the authorization check.

Findings Information

🌟 Selected for report: cmichel

Also found by: pauliax

Labels

bug
3 (High Risk)

Awards

1557.1694 NOTE - $1,557.17

4671.5083 USDC - $4,671.51

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The NotionalV1ToNotionalV2.notionalCallback is supposed to only be called from the verified contract that calls this callback but the access restrictions can be circumvented by simply providing sender = this as sender is a parameter of the function that can be chosen by the attacker.

function notionalCallback(
    address sender,
    address account,
    bytes calldata callbackData
) external returns (uint256) {
    require(sender == address(this), "Unauthorized callback");

Impact

An attacker can call the function passing in an arbitrary account whose tokens can then be stolen. The account first has to approve this contract but this can happen with accounts that legitimately want to migrate their tokens and therefore have to send a first transaction to approve the contract, but then an attacker frontruns the actual migration transaction.

The attacker can steal the tokens by performing an attack similar to the following:

  • first transaction is used to withdraw the victim's funds to the contract. This can be done by choosing account=victim, v1RepayAmount=0, v1CollateralId=WBTC, v2CollateralId=DAI. The NotionalV1Erc1155.batchOperationWithdraw (not part of this contest) will withdraw the victim's funds to this contract. Note that the attacker has to deposit the same v2CollateralBalance = uint256(collateralBalance) for the victim into the V2 version, but they can choose different cheaper collateral (for example, withdraw WBTC, deposit same amount of DAI).
  • second transaction is now used to deposit the victim funds in the contract into the user's account. They use account=attacker, v1DebtCurrencyId=WBTC, v1RepayAmount=amount to deposit it into Notional V1. (They need to have a small collateralBalance, etc. to pass all checks).

Fix the authorization check.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

3460.3765 NOTE - $3,460.38

10381.1295 USDC - $10,381.13

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The TokenHandler.safeTransferOut function uses the standard IERC20 function for the transfer call and proceeds with a checkReturnCode function to handle non-standard compliant tokens that don't return a return value. However, this does not work as calling token.transfer(account, amount) already reverts if the token does not return a return value, as token's IERC20.transfer is defined to always return a boolean.

Impact

When using any non-standard compliant token like USDT, the function will revert. Deposits for these tokens are broken, which is bad as USDT is a valid underlying for the cUSDT cToken.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handles the return value check as well as non-standard-compliant tokens.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

3460.3765 NOTE - $3,460.38

10381.1295 USDC - $10,381.13

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The TokenHandler.safeTransferIn function uses the standard IERC20 function for the transfer call and proceeds with a checkReturnCode function to handle non-standard compliant tokens that don't return a return value. However, this does not work as calling token.transferFrom(account, amount) already reverts if the token does not return a return value, as token's IERC20.transferFrom is defined to always return a boolean.

Impact

When using any non-standard compliant token like USDT, the function will revert. Withdrawals for these tokens are broken, which is bad as USDT is a valid underlying for the cUSDT cToken.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handles the return value check as well as non-standard-compliant tokens.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
3 (High Risk)
disagree with severity
sponsor confirmed

Awards

3460.3765 NOTE - $3,460.38

10381.1295 USDC - $10,381.13

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The liquidity token value (AssetHandler.getLiquidityTokenValue) is the sum of the value of the individual claims on cash (underlying or rather cTokens) and fCash. The amount to redeem on each of these is computed as the LP token to redeem relative to the total LP tokens, see AssetHandler.getCashClaims / AssetHandler.getHaircutCashClaims:

// @audit token.notional are the LP tokens to redeem
assetCash = market.totalAssetCash.mul(token.notional).div(market.totalLiquidity);
fCash = market.totalfCash.mul(token.notional).div(market.totalLiquidity);

This means the value depends on the current market reserves which can be manipulated. You're essentially computing a spot price (even though the individual values use a TWAP price) because you use the current market reserves which can be manipulated.

See the "How do I tell if I’m using spot price?" section on https://shouldiusespotpriceasmyoracle.com/.

However, by doing this you’re actually incorporating the spot price because you’re still dependent on the reserve balances of the pool. This is an extremely subtle detail, and more than one project has been caught by it. You can read more about this footgun in this writeup by @cmichelio.

Impact

The value of an LP token is computed as assetCashClaim + assetRate.convertFromUnderlying( presentValue(fCashClaim) ) where (assetCashClaim, fCashClaim) depend on the current market reserves which can be manipulated by an attacker via flashloans. Therefore, an attacker trading large amounts in the market either increases or decreases the value of an LP token.

If the value decreases, they can try to liquidate users borrowing against their LP tokens / nTokens. If the value increases, they can borrow against it and potentially receive an under-collateralized borrow this way, making a profit.

The exact profitability of such an attack depends on the AMM as the initial reserve manipulation and restoring the reserves later incurs fees and slippage. In constant-product AMMs like Uniswap it's profitable and several projects have already been exploited by this, like warp.finance. However, Notional Finance uses a more complicated AMM and the contest was too short for me to do a more thorough analysis. It seems like a similar attack could be possible here as described by the developers when talking about a different context of using TWAP oracles:

"Oracle rate protects against short term price manipulation. Time window will be set to a value on the order of minutes to hours. This is to protect fCash valuations from market manipulation. For example, a trader could use a flash loan to dump a large amount of cash into the market and depress interest rates. Since we value fCash in portfolios based on these rates, portfolio values will decrease and they may then be liquidated." - Market.sol L424

Recommendation

Do not use the current market reserves to determine the value of LP tokens. Think about how to implement a TWAP oracle for the LP tokens themselves, instead of combining it from the two TWAPs of the claimables.

#0 - T-Woodward

2021-09-11T15:08:02Z

It is true that a flash loan could be used to manipulate the value of a liquidity token’s cash and fCash claims. This issue can potentially cause accounts to be liquidated which shouldn’t be, but not for the reasons stated in this issue. I’ll explain what actually can go wrong, and why the fix is simple and non-invasive.

First, to restate the issue: The manipulator could borrow or lend a large amount to a liquidity pool, which would change the amount of cash and fCash sitting in that pool and the corresponding cash and fCash claims of a liquidity token associated with that pool. This could change the liquidity token’s net value within the space of a transaction despite the fact that the oracleRate used to value fCash is lagged and manipulation resistant.

But it is not true that this manipulation could decrease the value of a liquidity token - in fact it could only increase a liquidity token’s value. By borrowing or lending a large amount using a flash loan, the interest rate that the attacker would receive would deviate from the oracleRate in favor of the liquidity provider. If the attacker executed a large lend order, the interest rate on the loan would be significantly below the oracleRate. This would mean that the liquidity providers had borrowed at a below-market rate and that the net value of that trade would be positive for them. Conversely if the attacker executed a large borrow order, the interest rate on the loan would be significantly above the oracleRate. Again, this would mean that the net value of that trade would be positive for the liquidity providers because they would effectively be lending at an above-market rate. In either case, the value of the liquidity token would increase, not decrease.

However, even though the value of a liquidity token could only increase during such an attack, the collateral value of the liquidity token could decrease once the haircuts were applied in the free collateral calculation. The reason for this is that fCash claims are effectively double-haircut (once by the liquidity token haircut and once by the fCash haircut), whereas cash claims are only haircut once (by the liquidity token haircut). This means that even though the attack would increase the value of the liquidity token without haircuts, once you consider the haircuts applied in the free collateral calculation, the collateral value of the liquidity token can be decreased and accounts could become undercollateralized and eligible for liquidation.

Remediation: The immediate remediation for this issue is to restrict providing liquidity to the nToken account exclusively. In the longer term, we will plan to add TWAPs to determine the collateral value of liquidity token cash and fCash claims. This immediate remediation will be fine for now though, and will not degrade the system for two reasons:

  1. The team does not anticipate users providing liquidity directly outside of the nToken (we don’t even offer a way to do it within the UI for example). Only nToken holders receive NOTE incentives, not direct liquidity providers.

  2. The nToken accounts are safe from this attack because the maximum amount that an attacker could temporarily decrease the collateral value of liquidity tokens could never be enough to cause the nToken accounts to become undercollateralized, and therefore they would never be at risk of liquidation due to this attack. The TLDR here is that this attack can’t actually decrease the collateral value of liquidity tokens all that much, and so for an account to be vulnerable they would have to be running quite close to minimum collateralization. This will never happen for the nToken because it doesn’t borrow, it just provides liquidity and always maintains very healthy collateralization levels.

#1 - ghoul-sol

2021-09-19T00:51:35Z

Again, I gave it some thought and I think that this is high risk. Keeping as is.

Findings Information

🌟 Selected for report: a_delamo

Also found by: JMukesh, cmichel, defsec, tensors

Labels

bug
duplicate
2 (Med Risk)
disagree with severity
sponsor acknowledged

Awards

136.2212 NOTE - $136.22

408.6635 USDC - $408.66

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

There is no check in ExchangeRate.buildExchangeRate if the return values indicate stale data. This could lead to stale prices according to the Chainlink documentation:

Impact

Stale prices that do not reflect the current market price anymore could be used which would influence the exchange rate of the assets to ETH. This could lead to issues with free collateral and lead to wrong liquidations/borrows.

Recommendation

Add the recommended checks:

(
    uint80 roundID,
    int256 price,
    ,
    uint256 timeStamp,
    uint80 answeredInRound
) = chainlink.latestRoundData();
require(
    timeStamp != 0,
    β€œChainlinkOracle::getLatestAnswer: round is not complete”
);
require(
    answeredInRound >= roundID,
    β€œChainlinkOracle::getLatestAnswer: stale data”
);
require(price != 0, "Chainlink Malfunction”);

#0 - jeffywu

2021-09-11T13:55:19Z

Duplicate of #92

#1 - jeffywu

2021-09-14T11:38:42Z

After some discussion with @T-Woodward we've downgraded how we feel about the severity of this issue (and its duplicates). I think this is a 0 Non-Critical. While true that Chainlink prices can be stale, the alternative solution is to revert when that is the case. That would mean in times of high price volatility and high gas prices we would be unable to liquidate for periods of time while waiting for the Chainlink price to update.

This would be an adverse result for the protocol because liquidations must happen in a timely manner in order for the protocol to stay solvent. If we were to wait for the Chainlink price to update, the prices may continue to fall and cause accounts to fall into insolvency. We believe it is safer to accept a stale price than to prevent liquidations.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1038.113 NOTE - $1,038.11

3114.3389 USDC - $3,114.34

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The nTokenAction implements two token approvals, the nTokenWhitelist which is always used first, and the nTokenAllowance which is checked second. If the nTokenWhitelist does not have enough allowance for the transfer, the transaction fails, even in the case where nTokenAllowance still has enough allowance.

Impact

Transfers that have sufficient allowance fail in certain cases.

Instead of reverting if the nTokenWhitelist allowance is not enough, default to the nTokenAllowance case:

// something like this

uint256 requiredAllowance = amount;

uint256 allowance = nTokenWhitelist[from][spender];
// use whitelist allowance first
if (allowance > 0) {
    uint256 min = amount < allowance ? amount : allowance;
    requiredAllowance -= min;
    allowance = allowance.sub(min);
    nTokenWhitelist[from][spender] = allowance;
}

// use currency-specific allowance now
if(requiredAllowance > 0)
    // This is the specific allowance for the nToken.
    allowance = nTokenAllowance[from][spender][currencyId];
    require(allowance >= requiredAllowance, "Insufficient allowance");
    allowance = allowance.sub(requiredAllowance);
    nTokenAllowance[from][spender][currencyId] = allowance;
}

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1038.113 NOTE - $1,038.11

3114.3389 USDC - $3,114.34

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The enableToken function performs an ERC20.approve() call but does not check the success return value. Some tokens do not revert if the approval failed but return false instead.

Impact

Tokens that don't actually perform the approve and return false are still counted as a correct approve.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handles the return value check as well as non-standard-compliant tokens.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1038.113 NOTE - $1,038.11

3114.3389 USDC - $3,114.34

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The nTokenERC20Proxy functions emit events all the time, even if the return value from the inner call returns false indicating an unsuccessful action.

Impact

An off-chain script scanning for Transfer or Approval events can be tricked into believing that an unsuccessful transfer was indeed successful. This happens in the approve, transfer and transferFrom functions.

Only emit evens on success.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1038.113 NOTE - $1,038.11

3114.3389 USDC - $3,114.34

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The setToken function performs an ERC20.approve() call but does not check the success return value. Some tokens do not revert if the approval failed but return false instead.

Impact

Tokens that don't actually perform the approve and return false are still counted as a correct approve.

We recommend using OpenZeppelin’s SafeERC20 versions with the safeApprove function that handles the return value check as well as non-standard-compliant tokens.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The initialize function that initializes important contract state can be called by anyone. Occurences:

  • NoteERC20.initialize
  • Router.initialize

Impact

The attacker can initialize the contract before the legitimate deployer, hoping that the victim continues to use the same contract. In the best case for the victim, they notice it and have to redeploy their contract costing gas.

Use the constructor to initialize non-proxied contracts. For initializing proxy contracts deploy contracts using a factory contract that immediately calls initialize after deployment or make sure to call it immediately after deployment and verify the transaction succeeded.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

As the return value of ERC1155.balanceOf was changed to a signed integer, the nERC1155Interface does not implement the ERC1155 interface and the supportsInterface call will return false if people call it with the actual ERC1155 interface ID.

Impact

Not all users of the contract might care about the balance function and call supportsInterface with the original EIP1155 interface. The contract will still deny the

It is indeed debatable if this contract should be considered implementing ERC1155 and what the correct return value of supportsInterface(ERC1155.interface) should be for compatibility. Users need to be aware that this contract is not standard compliant and the supportsInterface call will fail.

Findings Information

🌟 Selected for report: tensors

Also found by: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

155.7169 NOTE - $155.72

467.1508 USDC - $467.15

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The ERC1155Action.safeTransferFrom / ERC1155Action.safeBatchTransferFrom functions do not follow the recommended re-entrancy protection guidelines and allow a re-entrancy through the onERC1155Received while state still has to be written.

Impact

The re-entrancy doesn't seem to open any attacks currently as the re-entrancy call happens right at the beginning and no interesting variables are set yet.

While no immediate re-entrancy issues could be found, it's better to add these checks, especially, as calling this function from another Notional finance function in the future might lead to unintended issues.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The ERC1155Action._checkPostTransferEvent has open TODOs:

// TODO: retrieve revert string
require(status, "Call failed");

Impact

Open TODOs can hint at programming or architectural errors that still need to be fixed.

Resolve the TODO and bubble up the error.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor disputed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The Router forwards nTokenTransferApprove calls to the nTokenAction implementation. However, these always fail due to the msg.sender == nTokenAddress check.

This call failing seems to be the intended behavior but it shouldn't even be forwarded in the Router. Remove sig == nTokenAction.nTokenTransferApprove.selector from the getRouterImplementation as it indicates that this is a valid function call.

#0 - jeffywu

2021-09-12T19:55:40Z

Calling approve on the nToken will forward the call to the Router which will then delegate call to the nTokenTransferApprove method. This is the intended functionality and will pass the require statement because the delegate call does not change the msg.sender

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The cTokenAggregator.decimals value is set to 18 but cTokens only have 8 decimals. It's unclear what this decimals field refers to.

If it should refer to the cToken decimals, it's wrong and should be set to 8. This value is not used inside the contract but it's public and anyone can read it.

#0 - jeffywu

2021-09-11T14:19:38Z

Decimals refers to the decimals in the exchange rate, but we should add a comment here. Agree it is confusing.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The GovernorAlpha.MIN_VOTING_PERIOD_BLOCKS = 6700 value indicates an average block time of 12.8956s which was correct a year ago, but at the moment a more accurate block time would be 13.2s, see blocktime.

Use a MIN_VOTING_PERIOD_BLOCKS of 6545.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The NoteERC20.initialize function does not emit an initial OwnershipTransferred event.

In initialize, emit OwnershipTransferred(address(0), owner_).

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The TokenHandler.transfer should handle the if (token.tokenType == TokenType.Ether) case first, as if the token type is Ether but netTransferExternal <= 0 it treats the token as an ERC20 token and tries to call ERC20 functions on it.

Impact

Luckily, trying to call ERC20 functions on the invalid token address will revert which is the desired behavior.

We still recommend reordering the branches and adding a netTransferExternal <= 0 check. The code becomes cleaner and it's more obvious that the transaction will fail.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

DateTime.isValidMarketMaturity can be called with a maxMarketIndex < 10 but the inner DateTime.getTradedMarket(i) function will revert for any values i > 7.

Impact

"Valid" maxMarketIndex values above 7 will break and return with an error.

The upper bound on maxMarketIndex should be set to 7.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

DateTime.getMarketIndex can be called with a maxMarketIndex < 10 but the inner DateTime.getTradedMarket(i) function will revert for any values i > 7.

Impact

"Valid" maxMarketIndex values above 7 will break and return with an error.

The upper bound on maxMarketIndex should be set to 7.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
disagree with severity
sponsor confirmed

Awards

346.0377 NOTE - $346.04

1038.113 USDC - $1,038.11

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The NoteERC20.getPriorVotes function is supposed to return the voting strength of an account at a specific block in the past. This should be a static value but it directly includes the current unclaimed incentives due to the getUnclaimedVotes(account) call.

Impact

Users that didn't even have tokens at the time of proposal creation but are now interested in voting on the proposal can farm unclaimed incentives and impact the outcome of the proposal.

Adding checkpoints for all unclaimed incentives would be the correct solution but was probably not done because it'd cost too much gas. It also needs to be ensured that incentives cannot be increased through flash-loaning of assets.

#0 - T-Woodward

2021-09-11T16:35:13Z

It is true that getPriorVotes returns an inaccurate value for the reason you have stated.

In practice, this issue is not very severe though. Any meaningful attack would require an enormous amount of capital, and could only gain access to at most a relatively small number of votes. The attack would be to provide liquidity at the moment a proposal is introduced, accrue incentives for the duration of the voting period, and then vote for them. So the maximum damage that could be done would assume that you have provided all liquidity on the platform (not gonna happen) for the entire voting period (5 days). Given that the highest annual emission rate for NOTE incentives is 20M, this would imply that the maximum amount of incentives earned in the five day period is 277,778 NOTE. This is .277% of the total NOTE supply. So the worst case is still not really a big deal.

Claimable incentives can't be manipulated via flash loans.

Remediation: We'll remove the getUnclaimedVotes function. It is a vestige of an earlier design that we have since moved away from anyway, so there's no real impact on the system or reason not to remove it.

#1 - ghoul-sol

2021-09-19T01:22:12Z

per sponsor comment, the impact is indeed low

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter