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
Rank: 2/11
Findings: 8
Award: $35,784.57
🌟 Selected for report: 4
🚀 Solo Findings: 4
1557.1694 NOTE - $1,557.17
4671.5083 USDC - $4,671.51
leastwood
Notional's governance framework utilises a fork of Compound's Governor Alpha and ERC20 token. These are denoted specifically as the GovernorAlpha.sol
and NoteERC20.sol
contracts. However, the GovernorAlpha.sol
has a key difference when compared to Compound's implementation of the same contract. Notional's Governor Alpha contract inherits Openzeppelin's TimelockController.sol
contract. This enables Notional's governance framework to enforce a timelock on any proposals, giving users time to exit before a potentially dangerous maintenance operation is applied. TimelockController.sol
's executeBatch()
function is vulnerable to reentrancy as the check to see if the previous call has been executed is done after an external call is made. This allows an executor to hijack the contract and make unexpected changes to the Notional smart contract system.
Bug outlined here. Fix is outline in this commit.
Sourced from publicly disclosed post by Immnuefi.
Update Openzeppelin
library to a version containing the commit fixing the bug (mentioned above). Tag v3.4.2-solc-0.7
in Openzeppelin
's Github repository is an example of a compatible library that contains the aforementioned bug fix.
#0 - jeffywu
2021-09-11T18:16:32Z
Duplicate #58
🌟 Selected for report: leastwood
3460.3765 NOTE - $3,460.38
10381.1295 USDC - $10,381.13
leastwood
The scripts/
folder outlines a number of deployment scripts used by the Notional team. Some of the contracts deployed utilise the ERC1967 upgradeable proxy standard. This standard involves first deploying an implementation contract and later a proxy contract which uses the implementation contract as its logic. When users make calls to the proxy contract, the proxy contract will delegate call to the underlying implementation contract. NoteERC20.sol
and Router.sol
both implement an initialize()
function which aims to replace the role of the constructor()
when deploying proxy contracts. It is important that these proxy contracts are deployed and initialized in the same transaction to avoid any malicious frontrunning. However, scripts/deployment.py
does not follow this pattern when deploying NoteERC20.sol
's proxy contract. As a result, a malicious attacker could monitor the Ethereum blockchain for bytecode that matches the NoteERC20
contract and frontrun the initialize()
transaction to gain ownership of the contract. This can be repeated as a Denial Of Service (DOS) type of attack, effectively preventing Notional's contract deployment, leading to unrecoverable gas expenses.
https://github.com/code-423n4/2021-08-notional/blob/main/scripts/deployment.py#L44-L60 https://github.com/code-423n4/2021-08-notional/blob/main/scripts/mainnet/deploy_governance.py#L71-L105
Manual code review
As the GovernanceAlpha.sol
and NoteERC20.sol
are co-dependent contracts in terms of deployment, it won't be possible to deploy the governance contract before deploying and initializing the token contract. Therefore, it would be worthwhile to ensure the NoteERC20.sol
proxy contract is deployed and initialized in the same transaction, or ensure the initialize()
function is callable only by the deployer of the NoteERC20.sol
contract. This could be set in the proxy contracts constructor()
.
🌟 Selected for report: leastwood
3460.3765 NOTE - $3,460.38
10381.1295 USDC - $10,381.13
leastwood
There are a number of contracts which inherit UUPSUpgradeable.sol
, namely; GovernanceAction.sol
, PauseRouter.sol
and NoteERC20.sol
. All these contracts are deployed using a proxy pattern whereby the implementation contract is used by the proxy contract for all its logic. The proxy contract will make delegate calls to the implementation contract. This helps to facilitate future upgrades by pointing the proxy contract to a new and upgraded implementation contract. However, if the implementation contract is left uninitialized, it is possible for any user to gain ownership of the onlyOwner
role in the implementation contract for NoteERC20.sol
. Once the user has ownership they are able to perform an upgrade of the implementation contract's logic contract and delegate call into any arbitrary contract, allowing them to self-destruct the proxy's implementation contract. Consequently, this will prevent all NoteERC20.sol
interactions until a new implementation contract is deployed.
Initial information about this issue was found here.
Consider the following scenario:
NoteERC20.sol
.initialize()
on the NoteERC20.sol
implementation contract.NoteERC20.sol
's implementation contract, they can bypass the _authorizeUpgrade
check used to restrict upgrades to the onlyOwner
role.UUPSUpgradeable.upgradeToAndCall()
shown here which in turn calls this function. The new implementation contract then points to their own contract containing a self-destruct call in its fallback function.NoteERC20.sol
proxy contract until a new implementation contract has been deployed.Manual code review
Consider initializing the implementation contract for NoteERC20.sol
and checking the correct permissions before deploying the proxy contract or performing any contract upgrades. This will help to ensure the implementation contract cannot be self-destructed.
#0 - jeffywu
2021-09-11T13:44:18Z
Acknowledged, I don't think this should be categorized high risk because the worst case is a denial of service and a redeployment of the ERC20 contract. As it stands, we've already successfully deployed our ERC20 contract so this is a non-issue.
I would categorize as 0 (Non-critical)
#1 - sockdrawermoney
2021-09-11T14:21:34Z
Warden leastwood added this proof of concept to illustrate the vulnerability https://gist.github.com/leastwood/b23d9e975883c817780116c2ceb785b8
#2 - jeffywu
2021-09-11T14:24:40Z
Ok I retract my previous statement, I misread the issue description. Up to you guys but do you want to pay out a full amount to someone who is reporting issues discovered elsewhere? OZ has already called initialize on our deployed contract for us.
#3 - sockdrawermoney
2021-09-12T20:37:57Z
@jeffywu I think the question is whether the issue is valid based on the original code base. Given your initial response and change after his proof of concept, my read was there was value here in what he reported. Is that a correct understanding?
#4 - jeffywu
2021-09-12T20:42:53Z
There was value added here but perhaps not at the same level as the other high risk issues.
#5 - sockdrawermoney
2021-09-13T00:27:50Z
@jeffywu Thanks for the input. As per our rules, awards are determined strictly based on the judge's assessment of the validity and severity, so we'll see how our judge chooses to score this.
155.7169 NOTE - $155.72
467.1508 USDC - $467.15
leastwood
The current ownership transfer process involves the current owner calling NoteERC20.transferOwnership()
. This function checks the new owner is not the zero address and proceeds to write the new owner's address into the owner's state variable. If the nominated EOA account is not a valid account, it is entirely possible the owner may accidentally transfer ownership to an uncontrolled account, breaking all functions with the onlyOwner()
modifier.
Manual code review
Consider implementing a two step process where the owner nominates an account and the nominated account needs to call an acceptOwnership()
function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.
🌟 Selected for report: leastwood
312.5 NOTE - $312.50
937.5 USDC - $937.50
leastwood
The StorageLayoutV1.sol
contract is inherited by several contracts and represents a shared common state that ensures the slot layout of certain contracts are the same. It is possible to minimise the number of storage slots used by rearranging the state variables in the most efficient way.
https://github.com/code-423n4/2021-08-notional/blob/main/contracts/global/StorageLayoutV1.sol
Manual code review
Arrange the uint16
and bytes1
variables such that they fit into the same slot.