NOTE ON 2021-07-19: This report makes reference to Zeppelin, OpenZeppelin, OpenZeppelin Contracts, the OpenZeppelin team, and OpenZeppelin library. Many of these things have since been renamed and know that this audit applies to what is currently called the OpenZeppelin Contracts which are maintained by the OpenZeppelin Contracts Community.
March, 2017 Authored by Dennis Peterson and Peter Vessenes
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
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.
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.
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 likesend
but automatically propagates exceptions, and supports.gas()
. See ethereum/solidity#610 for more on this.
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.
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.
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:
resetSpentToday
andconfirm
together do not limit the days on which the function can be called or (it appears) the number of times it can be called.- Once a call has been confirmed and
execute
d it appears that it can be re-executed. This is not good. confirmandCheck
doesn't seem to have logic about whether or not the function in question has been called.- 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.
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.
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.
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.
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.
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 modifiers 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)
.
Line 19: Modifier throws if doesn't meet condition, in contrast to some other inheritable modifiers (e.g. in Pausable) that use if(bool){_;}
.
Inherits from Ownable but the existing owner sets a pendingOwner who has to claim ownership.
Line 17: Another modifier that throws.
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.
Allows owner to set a public string of contract information. No issues.
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 uint
s? 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.
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.
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.
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
Simpler interface skipping the Approve function. Note this departs from ERC20 in another way: transfer throws instead of returning false.
Uses SafeSub
and SafeMath
, so transfer throw
s instead of returning false. This complies with ERC20Basic but not the actual ERC20 standard.
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.
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.
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.
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.
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
.
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.
No issues.
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.
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.