-
Notifications
You must be signed in to change notification settings - Fork 115
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
Content directory - initial smart contracts #1752
base: development
Are you sure you want to change the base?
Content directory - initial smart contracts #1752
Conversation
Here is my response to your notes Not yet coveredDon't do any of this except tests, the upgradebility can wait, and it requires a deeper review of alternatives. Things to consider
|
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.
A very nice kickoff on the whole thing, Leszek! Some notes:
-
We should use
int256
anduint256
instead of a smaller number types by default and use smaller types only when it brings some concrete advantage. In our use case, (only?) Rust/Solidity interface and events should share a smaller number type. In Ethereum, 256bit number is default Word and downscaling it costs extra gas - storinguint8
costs more than storinguint256
, the same goes for calculations. This is one of the (possibly significant) causes of too expensive contract deployment (~6871595 gas). -
In general, it might be a good idea to split the smart contract into multiple contract definitions that each focus on individual aspects. It will make code inspection easier as the contract gets bigger. In our case,
ContentDirectory
contract could be split into one part focusing on council-controlled mechanics (onlyCouncil
functions), second focusing on channels, third on videos, etc.; and then joined viacontract ContentDirectory is ContentDirectoryCouncil, ContentDirectoryChannels, ... {
. In a sense, this is mentioned in the original PR description. -
Right now, events are emitted from the main
ContentDirectory
contract, and storage is saved in a changeable contract(s). One small con of this is the lack of event emits in the storage - we can't simply walkthrough/audit storage operations using Ethereum logs. It might also create confusion/problems when a new storage contract is set - events may not necessarily correlate to storage contents. Another thing we need to keep in mind when doing the next iteration on upgradability is that when new storage is introduced, it needs to somehow include the old storage content in its inner workings because copying the whole storage content to the new contract will be expensive. We should definitely create two versions of storage contracts and a test for an actual migration and afterward querying data before the release.
using ChannelOwnershipDecoder for ChannelOwnership; | ||
|
||
// Access/validation utils: | ||
function _validateOwnership(ChannelOwnership memory _ownership) internal view 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.
This function is never used.
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.
Nicely spotted! It was supposed to be used instead of ownership.isValid
in createChannel
/updateChannelOwnership
. I'll fix it and add a related test case
|
||
mapping(uint64 => Video) private videoById; | ||
mapping(uint64 => uint64) public videoCountByChannelId; | ||
uint64 nextVideoId = 1; |
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.
Is there a reason why to use a 1-based index here? It seems to me that a regular 0-based index is used in the content directory pallet.
This topic is relevant to nextChannelId
, nextGroupId
, and nextVideoId
.
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'm used to 1-based index when it comes to "entity ids", since that's usually how ids are assigned in MySQL
/PostgresSQL
databases etc. (and since query node uses PostgresSQL database it seems to make sense).
Also it looks like currently we're using 1-based index in the runtime too:
next_entity_id: 1, |
// A helper library to parse ChannelOwnership. | ||
// New ownership types can be added if needed without the need for migration | ||
// (but changing/removing existing ones would still require migration to new storage) | ||
enum ChannelOwnerType {Address, Member, CuratorGroup} |
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.
Not a problem on its own, but we should be careful when using enums in multiple contracts as there are non-intuitive issues while upgrading contracts. See https://hackernoon.com/beware-the-solidity-enums-9v1qa31b2 .
Either we should limit their existence to only one contract or write a WARNING
commentary next to each such enum to prevent this kind of error as it might not be revealed by regular tests (as stated in the article).
event Migrated(address _logic, address _videoStorage, address _channelStorage, address _curatorGroupStorage); | ||
|
||
// Channel-related events | ||
event ChannelCreated(uint64 _id, ChannelOwnership _ownership, string[2][] _metadata); |
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.
What string[2][]
actually represents? I'm a little bit puzzled on what is the motivation behind the [2][]
part (even number of characters?). Could we replace it with struct so we can use a more expressive data type name like Metadata
through the code?
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's a dynamic-size array of arrays containing exactly 2 strings. Initially it was meant to represent cusomizable metadata with dynamic key-value
pairs, ie.:
[
[ 'title', 'Some video title' ],
[ 'description', 'Some video description' ]
]
But since this is not very flexible, in #1816 I explored an alternative possiblity to replace it with just a string
containing json
metadata representation (I think that's probably the best way to solve this if the contracts don't need to care about metadata validity and structure)
// Access/validation utils: | ||
function _validateOwnership(ChannelOwnership memory _ownership) internal view returns (bool) { | ||
require(_ownership.isValid(), "Invalid ownership data"); | ||
if (_ownership.isMember()) { |
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.
Since _ownership.isMember() != _ownership.isCuratorGroup()
is always true, we can return right after require
check
if (_ownership.isMember()) {
require(...);
return;
}
curatorGroupStorage.groupExists(_ownership.asCuratorGroup()), | ||
"CuratorGroup ownership - group does not exist!" | ||
); | ||
} |
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.
Is it ok that the check for .isAddress()
is not here with the rest of the ownership checks?
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.
In case of membership/group we can additionaly check if it exists, but with address I don't think we need any additional checks (except maybe disallowing zero-address)
pragma solidity ^0.6.0; | ||
|
||
// uint16 version of OpenZeppelin's SafeMath | ||
library SafeMath16 { |
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.
Instead of type-specific implementations of SafeMath
(SafeMath16
, SafeMath32
, ...) not originating from OpenZeppelin, we should use SafeMath
(256bit) in combination with OpenZeppelin's SafeCast
. Maintaining multiple math libraries that are not supported by OpenZeppelin
would create for us an unnecessary maintenance burden and security weak point, especially when (eventually) updating the Solidity version. See OpenZeppelin/openzeppelin-contracts#1625 (comment)
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 stumbled upon this link when trying to figure out the best solution, but it seemed to add a lot of burden to perform this casting after every operation. Since this is related to the general u256
comment, I'll address it further below.
} | ||
|
||
function _hasOwnerAccess(address _address, ChannelOwnership memory _ownership) internal view returns (bool) { | ||
if (_ownership.isAddress()) { |
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.
Just a code style note for your consideration. As the Object Calisthenics recommends, it is good to evade writing else
and else if
and instead of such branching style use process of elimination style instead.
if (_ownership.isAddress()) {
return _address == _ownership.asAddress();
}
// easily commentable
if (_ownership.isMember()) {
return membershipBridge.isMemberController(_address, _ownership.asMember());
}
...
After thinking it through I agree. At the beginning I could see a few benefits of using smaller types, ie.:
But considering the major drawbacks on the other side, ie. the need to either use custom |
This PR includes the initial code of new content directory Solidity smart contracts.
The implementation is based on #1520, the initial design was described in #1520 (comment)
I also icluded a few simple tests that can be run against a Ganache node:
Of course there is also a possibility to run any other Truffle command, ie.:
Currently we rely on Ganache to run the tests, but once the evm pallet is enabled in the runtime it would be possible to run some test against the actual Joystream node (this was further described here: #1681)
Covered:
ContentDirectory.sol
contract currently containing all the logic related to managing channels, curator groups and videos. It probably needs to be split into separate contracts / libraries as described in the Things to consider section belowNot yet covered:
migrate
method inContentDirectory.sol
but it's not yet fully functional)Things to consider:
_curatorId
as context in case the method is executed by curator. There are a few alternatives, but each of them has some drawbacks:_curatorId
arg part of the original methods likeupdateChannelMetadata
,removeVideo
- in this case this value could just be ignored on the smart-contract side if it's not needed (ie. sender has owner access to channel), but it would have to still be provided on the frontend (where it can be just set to0
if it's not needed). This saves us some code in the smart contract, but seems like a bit less "cleaner" approach.curatorIdsByAddress
map inContentWorkingGroupBridge
- we could then derive the curator context from themsg.sender
address, but it would require a loop and could potentially cost a lot of gas in case the address controlls many curatorsActor
to all content directory contract methods - this wouldn't look very good in Solidity due to lack of enums that can hold additional values depending on the variant. It also adds complexity on the frontend.Actor
argument toevm.call
extrinsic in the runtime and use specialmsg.sender
address format for members/curators - this would also allow us to get rid of bridge-contracts, but at the expense of not beeing able to verify whether a curator / member id is valid from insde of the evm environement (we would only know that the currentmsg.sender
is valid curator/member)ContentDirectory.sol
- currently it's a very big contract that costs a lot of gas to deploy (exceeds the default gas limit) and requires compiler optimalization specifically for deployment in order to prevent other issues. It needs to be split into libraries / other contracts or optimalized in some other way.Channel
/Video
fields likehandle
orcontentId
to validate whether they are unique, prevent them from beeing updated etc. There is also an open question w.r.t. how to handle the metadata that is clearly invalid (and in which case, if any, it should be handled on the smart contracts side)