-
Notifications
You must be signed in to change notification settings - Fork 46
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
An example of a shortened ONE Wallet contract with equivalent functionalities #4
Conversation
code/contracts/ONEWallet2.sol
Outdated
|
||
// Management of daily spendings | ||
uint256 dailyLimit; // uint128 is sufficient, but uint256 is more efficient since EVM works with 32-byte words. | ||
mapping(uint => uint) dailySpendings; // day => amount ; can be cleaned up once in a while to save storage |
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.
We shouldn't need to track this by day since only the latest matter and the information can be reconstructed on-demand by querying past transactions of the contract address anyway.
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.
I agree and the goal is not to track all days but the current one. Everything that is older than the current day is stale and thus can be cleaned up immediately, but I do it just once in a while. Using the mapping is just an implementation detail and has a big advantage over using two vars for tracking a current day spending + the timestamp of a day it corresponds to. The advantage is that mapping has data valid all the time, while two vars can get stale as soon as the day changes and nobody sends tx to contract, modifying these two vars, so getters for these vars would return an invalid value (which can be handle in the client of course, but it is not as intuitive as mapping).
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.
That's true. I am avert to using mapping unless necessary because it stores data at pseudo-random location and requires random access, also costs more gas. But in practice it should not make much difference.
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.
The gas cost of using maps is really negligible. That I'd not worry about. I slightly updated it in the recent version, though I still kept the map there. However, it can have only a single element at the time (except for the reveal execution which checks it for the current day and deletes the last day with the transfer). So far, it is still the clearest solution I see.
address payable addr; | ||
uint amount; | ||
} | ||
mapping(uint32 => Commit[]) public commits; // idx of OTP valid during a commitment => commitment data |
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.
I noticed only revealed commits are deleted (line 103). The commits would grow unbounded if an attacker spams it.
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.
Yes, correct. However, it is on the attacker's expense of submitting commits. Anyway, clean-up method would definitely help. Thanks for pointing out it.
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.
It clutters the blockchain. But another potential danger is if we are to use an upgrade pattern in this contract. The attacker who is allowed to make unlimited commits may find a way to override key storage locations through spamming commits, though the exact formulation of the attack is unknown at this time due to the assumed reliability of pseudo-randomness of SHA3.
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.
I'd say we can safely assume that sha3 is collision-resistant and thus provides enough pseudo-randomness.
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.
Proper clean-up method was already added.
code/contracts/ONEWallet2.sol
Outdated
/** | ||
* This is a general reveal for all types of operation. | ||
*/ | ||
function reveal(bytes32[] calldata neighbors, uint32 otpIdx, bytes32 eotp) external returns (bool) { |
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.
I think this is a nice way of unifying the threes reveal functions into a single one. As mentioned above if we want to hide the nature of the commit, we would then need to add another parameter here (a struct that unifies parameters of all kinds of operations)
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.
Actually, the unified parameters are already in the struct Commit, which is submitted during commit(). Therefore, here are no operation-specific parameters. Anyway, I will think about the use case that you mention if it is really something that we should be concerned about (I still do not see any serious example).
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.
IMPORTANT: My conclusion is that we have to store parameters of the operation along with the hash of the commit because the smart contract has to verify in reveal() that h(submitted parameters || eotp) == submitted hash. This is how the first matching entry is found. Actually, this approach is the simplified 3-stage protocol for replacing the root hash in SmartOTPs. While I was designing our commit/reveal, I realized that 3-stages can be reduced to 2-stage, where in 1st stage, you submit hash of commit + params.
This has an implication of showing the parameters of the operation, as mentioned above (which should not be an issue).
I will incorporate some good practices into the contract on master branch |
Let's reopen this when Ivan is back from holiday |
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.
Reactions of comments of the simple design. There are two issues to me: 1) reverting in execute transfer needs to be removed and added return statement instead. 2) if the commit really needs to hide the nature of the operation (type, value etc), the design needs to be modified. However, so far I do not see any use case that would require it.
code/contracts/ONEWallet2.sol
Outdated
|
||
// Management of daily spendings | ||
uint256 dailyLimit; // uint128 is sufficient, but uint256 is more efficient since EVM works with 32-byte words. | ||
mapping(uint => uint) dailySpendings; // day => amount ; can be cleaned up once in a while to save storage |
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.
I agree and the goal is not to track all days but the current one. Everything that is older than the current day is stale and thus can be cleaned up immediately, but I do it just once in a while. Using the mapping is just an implementation detail and has a big advantage over using two vars for tracking a current day spending + the timestamp of a day it corresponds to. The advantage is that mapping has data valid all the time, while two vars can get stale as soon as the day changes and nobody sends tx to contract, modifying these two vars, so getters for these vars would return an invalid value (which can be handle in the client of course, but it is not as intuitive as mapping).
address payable addr; | ||
uint amount; | ||
} | ||
mapping(uint32 => Commit[]) public commits; // idx of OTP valid during a commitment => commitment data |
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.
Yes, correct. However, it is on the attacker's expense of submitting commits. Anyway, clean-up method would definitely help. Thanks for pointing out it.
code/contracts/ONEWallet2.sol
Outdated
/** | ||
* This is a general reveal for all types of operation. | ||
*/ | ||
function reveal(bytes32[] calldata neighbors, uint32 otpIdx, bytes32 eotp) external returns (bool) { |
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.
Actually, the unified parameters are already in the struct Commit, which is submitted during commit(). Therefore, here are no operation-specific parameters. Anyway, I will think about the use case that you mention if it is really something that we should be concerned about (I still do not see any serious example).
uint32 currentIdx = uint32(block.timestamp) / interval - t0; // check whether idx of OTP is timely | ||
require(currentIdx - otpIdx <= MAX_REVEAL_DELAY, "Reveal is out of max allowed delay."); // here is OK to revert, coz if revert fails, submitted eotp + proof are invalidated already and cannot be misused anymore. | ||
|
||
// 2) scan all the commits made within otpIdx interval and execute the first matching one |
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.
Assume a user made a commit and revealed within the same otpIdx interval (30 seconds in general, which is necessary for good user experience). After observing the revealed EOTP but before it is confirmed and executed by the blockchain nodes, an attacker could make another commit with the correct EOTP using different parameters of operType
, addr
, and amount
and produce a valid hash in that commit. The attacker can then reveal its own commit at a higher gas cost before the user's reveal is processed. The attacker's reveal will succeed because it meets the requirement here: under the attacker's parameters, it will be the first matching commit within the otpIdx interval
.
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.
That is true. It was very likely the reason why I put the parameters of an operation into the commit() in the original version instead of reveal(). The first matching one will be always the user's operation. Unfortunately, I did not make any comment to the code about it. So, with parameters in the commit(), it is secure but reveals user intention about the operation, which I still think does cause any problems in the scope of the standard wallet use cases.
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.
Then the current implementation (#v0.4.1, also current master branch) would be similar, except keys of commits
are encoded as commitHash
to hide time information and to prevent attacker from committing at future slots, and parameters are also encoded as paramsHash
to hide user's intent, and to make the structure work for all types of operations in addition to transfer
. The hash
here would be similar to verificationHash
there
code/contracts/ONEWallet2.sol
Outdated
*/ | ||
function cleanUpCommits() external { | ||
for (uint32 i = lastCleanUpOfCommitsIdx; i <= lastRevealedOTPIdx; i++){ // IH: in theory, this might lead to out-of-gas exception for settings of TOTP with very long validity but currently I'd not be affraid. Moreover, we are getting the gas back for successfull deletions. | ||
delete commits[i]; // NOTE: I'd say it makes no difference whether it was initialized or not, so we can delete any entry. |
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.
For primitive arrays, a deletion on array itself is sufficient to delete everything. But I am uncertain whether it would be sufficient for struct arrays.
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.
The compiler did not complain about it.
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.
The spec reads:
delete a
assigns the initial value for the type toa
. I.e. for integers it is equivalent toa = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements set to their initial value
(https://docs.soliditylang.org/en/v0.8.4/types.html#delete)
It is unclear from the language whether a dynamic array's elements would be set to their initial value
code/contracts/ONEWallet2.sol
Outdated
} | ||
|
||
function _drain() internal returns (bool) { | ||
return lastResortAddress.send(address(this).balance); |
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.
similar issue with send
as above
//////////////////////// Internal Functions ////////////////////////// | ||
|
||
/** | ||
* It cleans up the previous value of daily spending from the map (this ensures that map has always only a single value -- except the execution time of the reveal()) |
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.
This procedure essentially reduces dailySpendings to a single value, so it needs not to be a mapping anymore
I am merging this into a separate folder - |
No description provided.