infrastructure-upgrade/lib/openzeppelin-contracts/audit/2017-03.md
T-Hax 735546619e
init
Signed-off-by: T-Hax <>
2023-04-08 18:46:18 +00:00

19 KiB

OpenZeppelin Audit

March, 2017 Authored by Dennis Peterson and Peter Vessenes

Introduction

Zeppelin requested that New Alchemy perform an audit of the contracts in their OpenZeppelin library. The OpenZeppelin contracts are a set of contracts intended to be a safe building block for a variety of uses by parties that may not be as sophisticated as the OpenZeppelin team. It is a design goal that the contracts be deployable safely and "as-is".

The contracts are hosted at:

https://github.com/OpenZeppelin/zeppelin-solidity

All the contracts in the "contracts" folder are in scope.

The git commit hash we evaluated is: 9c5975a706b076b7000e8179f8101e0c61024c87

Disclaimer

The audit makes no statements or warrantees about utility of the code, safety of the code, suitability of the business model, regulatory regime for the business model, or any other statements about fitness of the contracts to purpose, or their bugfree status. The audit documentation is for discussion purposes only.

Executive Summary

Overall the OpenZeppelin codebase is of reasonably high quality -- it is clean, modular and follows best practices throughout.

It is still in flux as a codebase, and needs better documentation per file as to expected behavior and future plans. It probably needs more comprehensive and aggressive tests written by people less nice than the current OpenZeppelin team.

We identified two critical errors and one moderate issue, and would not recommend this commit hash for public use until these bugs are remedied.

The repository includes a set of Truffle unit tests, a requirement and best practice for smart contracts like these; we recommend these be bulked up.

Discussion

Big Picture: Is This A Worthwhile Project?

As soon as a developer touches OpenZeppelin contracts, they will modify something, leaving them in an un-audited state. We do not recommend developers deploy any unaudited code to the Blockchain if it will handle money, information or other things of value.

"In accordance with Unix philosophy, Perl gives you enough rope to hang yourself" --Larry Wall

We think this is an incredibly worthwhile project -- aided by the high code quality. Creating a framework that can be easily extended helps increase the average code quality on the Blockchain by charting a course for developers and encouraging containment of modifications to certain sections.

"Rust: The language that makes you take the safety off before shooting yourself in the foot" -- (@mbrubeck)

We think much more could be done here, and recommend the OpenZeppelin team keep at this and keep focusing on the design goal of removing rope and adding safety.

Most of the code uses Solidity 0.4.11, but some files under Ownership are marked 0.4.0. These should be updated.

Solidity 0.4.10 will add several features which could be useful in these contracts:

  • assert(condition), which throws if the condition is false

  • revert(), which rolls back without consuming all remaining gas.

  • address.transfer(value), which is like send but automatically propagates exceptions, and supports .gas(). See https://github.com/ethereum/solidity/issues/610 for more on this.

Error Handling: Throw vs Return False

Solidity standards allow two ways to handle an error -- either calling throw or returning false. Both have benefits. In particular, a throw guarantees a complete wipe of the call stack (up to the preceding external call), whereas false allows a function to continue.

In general we prefer throw in our code audits, because it is simpler -- it's less for an engineer to keep track of. Returning false and using logic to check results can quickly become a poorly-tracked state machine, and this sort of complexity can cause errors.

In the OpenZeppelin contracts, both styles are used in different parts of the codebase. SimpleToken transfers throw upon failure, while the full ERC20 token returns false. Some modifiers throw, others just wrap the function body in a conditional, effectively allowing the function to return false if the condition is not met.

We don't love this, and would usually recommend you stick with one style or the other throughout the codebase.

In at least one case, these different techniques are combined cleverly (see the Multisig comments, line 65). As a set of contracts intended for general use, we recommend you either strive for more consistency or document explicit design criteria that govern which techniques are used where.

Note that it may be impossible to use either one in all situations. For example, SafeMath functions pretty much have to throw upon failure, but ERC20 specifies returning booleans. Therefore we make no particular recommendations, but simply point out inconsistencies to consider.

Critical Issues

Stuck Ether in Crowdsale contract

CrowdsaleToken.sol has no provision for withdrawing the raised ether. We strongly recommend a standard withdraw function be added. There is no scenario in which someone should deploy this contract as is, whether for testing or live.

Recursive Call in MultisigWallet

Line 45 of MultisigWallet.sol checks if the amount being sent by execute is under a daily limit.

This function can only be called by the "Owner". As a first angle of attack, it's worth asking what will happen if the multisig wallet owners reset the daily limit by approving a call to resetSpentToday.

If a chain of calls can be constructed in which the owner confirms the resetSpentToday function and then withdraws through execute in a recursive call, the contract can be drained. In fact, this could be done without a recursive call, just through repeated execute calls alternating with the confirm calls.

We are still working through the confirmation protocol in Shareable.sol, but we are not convinced that this is impossible, in fact it looks possible. The flexibility any shared owner has in being able to revoke confirmation later is another worrisome angle of approach even if some simple patches are included.

This bug has a number of causes that need to be addressed:

  1. resetSpentToday and confirm together do not limit the days on which the function can be called or (it appears) the number of times it can be called.
  2. Once a call has been confirmed and executed it appears that it can be re-executed. This is not good.
  3. confirmandCheck doesn't seem to have logic about whether or not the function in question has been called.
  4. Even if it did, revoke would need updates and logic to deal with revocation requests after a function call had been completed.

We do not recommend using the MultisigWallet until these issues are fixed.

Moderate to Minor Issues

PullPayment

PullPayment.sol needs some work. It has no explicit provision for cancelling a payment. This would be desirable in a number of scenarios; consider a payee losing their wallet, or giving a griefing address, or just an address that requires more than the default gas offered by send.

asyncSend has no overflow checking. This is a bad plan. We recommend overflow and underflow checking at the layer closest to the data manipulation.

asyncSend allows more balance to be queued up for sending than the contract holds. This is probably a bad idea, or at the very least should be called something different. If the intent is to allow this, it should have provisions for dealing with race conditions between competing withdrawPayments calls.

It would be nice to see how many payments are pending. This would imply a bit of a rewrite; we recommend this contract get some design time, and that developers don't rely on it in its current state.

Shareable Contract

We do not believe the Shareable.sol contract is ready for primetime. It is missing functions, and as written may be vulnerable to a reordering attack -- an attack in which a miner or other party "racing" with a smart contract participant inserts their own information into a list or mapping.

The confirmation and revocation code needs to be looked over with a very careful eye imagining extraordinarily bad behavior by shared owners before this contract can be called safe.

No sanity checks on the initial constructor's required argument are worrisome as well.

Line by Line Comments

Lifecycle

Killable

Very simple, allows owner to call selfdestruct, sending funds to owner. No issues. However, note that selfdestruct should typically not be used; it is common that a developer may want to access data in a former contract, and they may not understand that selfdestruct limits access to the contract. We recommend better documentation about this dynamic, and an alternate function name for kill like completelyDestroy while kill would perhaps merely send funds to the owner.

Also note that a killable function allows the owner to take funds regardless of other logic. This may be desirable or undesirable depending on the circumstances. Perhaps Killable should have a different name as well.

Migrations

I presume that the goal of this contract is to allow and annotate a migration to a new smart contract address. We are not clear here how this would be accomplished by the code; we'd like to review with the OpenZeppelin team.

Pausable

We like these pauses! Note that these allow significant griefing potential by owners, and that this might not be obvious to participants in smart contracts using the OpenZeppelin framework. We would recommend that additional sample logic be added to for instance the TokenContract showing safer use of the pause and resume functions. In particular, we would recommend a timelock after which anyone could unpause the contract.

The modifers use the pattern if(bool){_;}. This is fine for functions that return false upon failure, but could be problematic for functions expected to throw upon failure. See our comments above on standardizing on throw or return(false).

Ownership

Ownable

Line 19: Modifier throws if doesn't meet condition, in contrast to some other inheritable modifiers (e.g. in Pausable) that use if(bool){_;}.

Claimable

Inherits from Ownable but the existing owner sets a pendingOwner who has to claim ownership.

Line 17: Another modifier that throws.

DelayedClaimable

Is there any reason to descend from Ownable directly, instead of just Claimable, which descends from Ownable? If not, descending from both just adds confusion.

Contactable

Allows owner to set a public string of contract information. No issues.

Shareable

This needs some work. Doesn't check if _required <= len(_owners) for instance, that would be a bummer. What if _required were like MAX - 1?

I have a general concern about the difference between owners, _owners, and owner in Ownable.sol. I recommend "Owners" be renamed. In general we do not recomment single character differences in variable names, although a preceding underscore is not uncommon in Solidity code.

Line 34: "this contract only has six types of events"...actually only two.

Line 61: Why is ownerIndex keyed by addresses hashed to uints? Why not use the addresses directly, so ownerIndex is less obscure, and so there's stronger typing?

Line 62: Do not love ++i) ... owners[2+ i]. Makes me do math, which is not what I want to do. I want to not have to do math.

There should probably be a function for adding a new operation, so the developer doesn't have to work directly with the internal data. (This would make the multisig contract even shorter.)

There's a revoke function but not a propose function that we can see.

Beware reordering. If propose allows the user to choose a bytes string for their proposal, bad things(TM) will happen as currently written.

Multisig

Just an interface. Note it allows changing an owner address, but not changing the number of owners. This is somewhat limiting but also simplifies implementation.

Payment

PullPayment

Safe from reentrance attack since ether send is at the end, plus it uses .send() rather than .call.value().

There's an argument to be made that .call.value() is a better option if you're sure that it will be done after all state updates, since .send will fail if the recipient has an expensive fallback function. However, in the context of a function meant to be embedded in other contracts, it's probably better to use .send. One possible compromise is to add a function which allows only the owner to send ether via .call.value.

If you don't use call.value you should implement a cancel function in case some value is pending here.

Line 14: Doesn't use safeAdd. Although it appears that payout amounts can only be increased, in fact the payer could lower the payout as much as desired via overflow. Also, the payer could add a large non-overflowing amount, causing the payment to exceed the contract balance and therefore fail when withdraw is attempted.

Recommendation: track the sum of non-withdrawn asyncSends, and don't allow a new one which exceeds the leftover balance. If it's ever desirable to make payments revocable, it should be done explicitly.

Tokens

ERC20

Standard ERC20 interface only.

There's a security hole in the standard, reported at Edcon: approve does not protect against race conditions and simply replaces the current value. An approved spender could wait for the owner to call approve again, then attempt to spend the old limit before the new limit is applied. If successful, this attacker could successfully spend the sum of both limits.

This could be fixed by either (1) including the old limit as a parameter, so the update will fail if some gets spent, or (2) using the value parameter as a delta instead of replacement value.

This is not fixable while adhering to the current full ERC20 standard, though it would be possible to add a "secureApprove" function. The impact isn't extreme since at least you can only be attacked by addresses you approved. Also, users could mitigate this by always setting spending limits to zero and checking for spends, before setting the new limit.

Edcon slides: https://drive.google.com/file/d/0ByMtMw2hul0EN3NCaVFHSFdxRzA/view

ERC20Basic

Simpler interface skipping the Approve function. Note this departs from ERC20 in another way: transfer throws instead of returning false.

BasicToken

Uses SafeSub and SafeMath, so transfer throws instead of returning false. This complies with ERC20Basic but not the actual ERC20 standard.

StandardToken

Implementation of full ERC20 token.

Transfer() and transferFrom() use SafeMath functions, which will cause them to throw instead of returning false. Not a security issue but departs from standard.

SimpleToken

Sample instantiation of StandardToken. Note that in this sample, decimals is 18 and supply only 10,000, so the supply is a small fraction of a single nominal token.

CrowdsaleToken

StandardToken which mints tokens at a fixed price when sent ether.

There's no provision for owner withdrawing the ether. As a sample for crowdsales it should be Ownable and allow the owner to withdraw ether, rather than stranding the ether in the contract.

Note: an alternative pattern is a mint() function which is only callable from a separate crowdsale contract, so any sort of rules can be added without modifying the token itself.

VestedToken

Lines 23, 27: Functions transfer() and transferFrom() have a modifier canTransfer which throws if not enough tokens are available. However, transfer() returns a boolean success. Inconsistent treatment of failure conditions may cause problems for other contracts using the token. (Note that transferableTokens() relies on safeSub(), so will also throw if there's insufficient balance.)

Line 64: Delete not actually necessary since the value is overwritten in the next line anyway.

Root level

Bounty

Avoids potential race condition by having each researcher deploy a separate contract for attack; if a research manages to break his associated contract, other researchers can't immediately claim the reward, they have to reproduce the attack in their own contracts.

A developer could subvert this intent by implementing deployContract() to always return the same address. However, this would break the researchers mapping, updating the researcher address associated with the contract. This could be prevented by blocking rewrites in researchers.

DayLimit

The modifier limitedDaily calls underLimit, which both checks that the spend is below the daily limit, and adds the input value to the daily spend. This is fine if all functions throw upon failure. However, not all OpenZeppelin functions do this; there are functions that returns false, and modifiers that wrap the function body in if (bool) {_;}. In these cases, _value will be added to spentToday, but ether may not actually be sent because other preconditions were not met. (However in the OpenZeppelin multisig this is not a problem.)

Lines 4, 11: Comment claims that DayLimit is multiowned, and Shareable is imported, but DayLimit does not actually inherit from Shareable. The intent may be for child contracts to inherit from Shareable (as Multisig does); in this case the import should be removed and the comment altered.

Line 46: Manual overflow check instead of using safeAdd. Since this is called from a function that throws upon failure anyway, there's no real downside to using safeAdd.

LimitBalance

No issues.

MultisigWallet

Lines 28, 76, 80: kill, setDailyLimit, and resetSpentToday only happen with multisig approval, and hashes for these actions are logged by Shareable. However, they should probably post their own events for easy reading.

Line 45: This call to underLimit will reduce the daily limit, and then either throw or return 0. So in this case there's no danger that the limit will be reduced without the operation going through.

Line 65: Shareable's onlyManyOwners will take the user's confirmation, and execute the function body if and only if enough users have confirmed. Whole thing throws if the send fails, which will roll back the confirmation. Confirm returns false if not enough have confirmed yet, true if the whole thing succeeds, and throws only in the exceptional circumstance that the designated transaction unexpectedly fails. Elegant design.

Line 68: Throw here is good but note this function can fail either by returning false or by throwing.

Line 92: A bit odd to split clearPending() between this contract and Shareable. However this does allow contracts inheriting from Shareable to use custom structs for pending transactions.

SafeMath

Another interesting comment from the same Edcon presentation was that the overflow behavior of Solidity is undocumented, so in theory, source code that relies on it could break with a future revision.

However, compiled code should be fine, and in the unlikely event that the compiler is revised in this way, there should be plenty of warning. (But this is an argument for keeping overflow checks isolated in SafeMath.)

Aside from that small caveat, these are fine.