-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC 4: Secure upgrades for contracts operating staked balances #446
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
:toc: macro | ||
|
||
= RFC 4: Secure upgrades for contracts operating staked balances | ||
|
||
.SUPERSEDED | ||
**** | ||
IMPORTANT: This RFC has been superseded by an alternative described | ||
in RFCs 9 and 11. It was never implemented as described. | ||
**** | ||
|
||
:icons: font | ||
:numbered: | ||
toc::[] | ||
|
||
== Background | ||
|
||
Following best practices for ERC-20 Tokens, we have non-upgradable and | ||
non-ownable Token and Token staking contracts, which means there is no | ||
backdoor to modify the contract storage or change the implementation. | ||
Amongst other things, these contracts only allow token transfers to be | ||
executed by the token holders. Since token staking functionality might | ||
need upgrading, it was decided to simply issue and deploy a new contract | ||
and advise stakers to migrate their balances to it. | ||
|
||
A different upgrade approach taken from Open Zeppelin was implemented for | ||
the rest of the contracts; this RFC states the pitfalls of the approach and | ||
proposes alternative way similar to what we already have for Token staking | ||
contracts. | ||
|
||
|
||
=== Current Functionality | ||
|
||
Current upgrading functionality influenced by Open Zeppelin libraries makes | ||
upgradable contract address and storage persistent and the logic can be | ||
upgraded by the contract owner. They do so by updating the implementation | ||
address in the persistent proxy contract. The method is also known as | ||
"Eternal Storage". This was considered a good approach for all the | ||
forthcoming Keep contracts including Group selection and Random beacon. | ||
A concern has been raised during implementation of stake slashing | ||
functionality where an upgradable contract such as Group Contract has to | ||
be authorized to modify stake balances. Since the address and storage are | ||
persistent in case of a compromised implementation all staked balances are | ||
at risk. Imagine a deploy key that is used to update the implementation is | ||
stolen, the hacker has full access to the contract storage immediately by | ||
updating contract with his implementation. Besides if the contract address | ||
was authorized to move stakers balances those will be lost as well. | ||
|
||
|
||
=== Goal | ||
|
||
Minimize the risk of lost/stolen staked tokens in the case of a hacked or | ||
bad implementation upgrade of the contracts that require full access to | ||
modify balance of a staker. | ||
|
||
=== Implementation | ||
|
||
Each new contract that does "slashing/reward" changes on staked token | ||
balances must be a non-upgradable and non-ownable one to minimize attack | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This really means any contract that affects staked token balances, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think what's not clear here is that even though the contract is non-upgradable, it can be using the eternal storage pattern with an associated proxy, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
correct There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
no, "eternal storage pattern" thats what we're trying to avoid now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible to make a layercake of:
The front-end can safely implement eternal storage as long as the work contracts don't grant it dangerous privileges. For example, in the random beacon the front-end contract could implement the beacon itself and its pricing/request process; the back-ends would handle signing the input they receive from the front-end, and creating new groups when the front-end asks them to. When a request comes in:
This way the interface presented to the customers can be unchanging while stakers' funds remain perfectly secure against unauthorized upgrades; in the worst-case scenario the frontend breaks, making the system unable to do useful work but not causing stakes to be stolen or lost. The exact API separation needs to be architected individually for each usecase, however. |
||
surface. Functionality upgrades are only possible by deploying a new | ||
implementation as a new contract. The address of this contract must be | ||
re-authorized by a staker. Stakers do so by calling the `authorize(address)` | ||
method on a staking contract. A client should only authorize official | ||
contract addresses from the metacontract list maintained by Keep | ||
organization along with the confirmation by two or more Keybase-proven | ||
channels. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can optionally be enforced in the staking contract, if Keep Org maintains a contract that tracks authorized versions. |
||
|
||
The contracts with `authorize()` method should also include a "panic button" | ||
- a method that cancels all authorizations in case of emergency. This | ||
should be restricted to be called by a Keep organization account used to | ||
deploy this contract. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can make the panic button act on a specific work contract, effectively freezing its functionality but protecting all (remaining) stakes. This makes it significantly more useful as the panic button doesn't have to stop absolutely everything, but only the unsafe contracts. Making panic buttons work against staking contracts, however, is rather difficult and probably not worth the effort. |
||
|
||
|
||
=== Limitations | ||
|
||
Not particularly a limitation but a move from being able to do a seamless | ||
and instant upgrade to a drawn-out one where stakers have to reauthorize | ||
a newly deployed version. | ||
|
||
=== Proof of Concept | ||
|
||
The basis for the concept can be seen in the current Staking contracts | ||
and StakingProxy.sol | ||
|
||
|
||
[bibliography] | ||
== Related Links | ||
|
||
- Discussion on Flowdock: | ||
https://www.flowdock.com/app/cardforcoin/tech/threads/6_Abd7qxhJrSNhQSrDxwgyvL0Pd |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With extended unstaking time, this may prove inconvenient in use; however, it's the most secure method and the staking contract can be completely upgrade-agnostic.
There's a risk of a "stakepocalypse" if a new staking contract is released and everybody rushes to unstake so they can migrate immediately.
However this means the work requested during the unstaking time gets distributed over those who stay in the old contract, making it disproportionately profitable to stay and unstake+restake later when the demand moves to the new contract.
Thus the stakepocalypse won't really happen, and the stakes will probably be migrated across over
2 * unstaking_time
time, by:If the demand for work on the old contract within the time
[T .. (T + unstaking_time)]
isA
and demand on the new contract from[(T + unstaking_time) .. (T + 2*unstaking_time)]
isB = pA
, and the fraction of stakers who unstake isu
, the revenue increase one gains from not unstaking immediately isA / (1 - u)
and the opportunity cost isB / u
. This means that it's profitable to wait on unstaking if:It's probably easiest to just do 50/50; if everybody does it the income distribution doesn't change (correctly anticipating increased demand can improve on this unless everybody does it), and it can be done by always splitting one's tokens across two stakers even if no specific provisions are made for partial unstaking.