Skip to content
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

Kirill Review and some small fixes #1

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions contracts/zxp/Games.sol
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IGames} from "./interfaces/IGames.sol";
import {ObjectRegistry} from "./ObjectRegistry.sol";
import { IGames } from "./interfaces/IGames.sol";
import { ObjectRegistry } from "./ObjectRegistry.sol";


// why does this need to be ObjectRegistry? where is this functionality used?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inheritance of the ObjectRegistry and its use below are all remnants of a rework.
Fixed, and it will give me an opportunity to discuss why this change was made.

// mb also change the inheritance order. go with interface last
contract Games is IGames, ObjectRegistry {
struct Game {
string metadata;
// this should be named better. it's confusing what this is
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to registeredObjects?

ObjectRegistry objects;
}

mapping(bytes32 name => Game game) public games;

constructor() ObjectRegistry(msg.sender) {}

function createGame(
bytes32 name,
// why is this not an msg.sender? to set someone else as an owner?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, compatibility for contracts so they can run this and set someone as the owner

address owner,
string calldata metadata,
// who deploys these contracts and who pays for each deployment of
// these sets of objects per EVERY game?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not us. Users, who want to launch game utility for an NFT collection. These are the "game creators"

bytes32[] calldata objectNames,
address[] calldata objectAddresses
) external override {
// what is the difference between this ObjectRegistry and the one we inherited here, initialized in the constructor?
// also, does there HAVE to be a new registry for every game?
// what are the advantages of this?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inherited ObjectRegistry is removed.
It was changed to one registry per game because this allows the data association with the mapping -> struct to work generically with the ObjectRegistry, which wasn't it's own contract previously. Instead, code was repeated with very slight variation on the Games and the Seasons, in order to achieve the spec. However this is bad practice - this change consolidates the repeated code and makes the ObjectRegistry reusable.

ObjectRegistry newRegistry = new ObjectRegistry(owner);
games[name].metadata = metadata;
games[name].objects = newRegistry;
if (objectNames.length > 0) {
// this call will always fail because msg.sender is the contract and not the owner
// that is set above on L31
// this is also never tested, and it seems to be a crucial function
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it used to work and I used to have a test for it before the change.

newRegistry.registerObjects(objectNames, objectAddresses);
}
}
Expand Down
11 changes: 10 additions & 1 deletion contracts/zxp/ObjectRegistry.sol
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IObjectRegistry} from "./interfaces/IObjectRegistry.sol";
import { IObjectRegistry } from "./interfaces/IObjectRegistry.sol";


contract ObjectRegistry is IObjectRegistry {
bytes32 internal constant OWNER = "Owner";
// what is an object? is it a contract?
// the name is too generic and is not clear what it is
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is either a contract or EOA. it is a thing registered in an ObjectRegistry, thus it an Object. perhaps there is a better name scheme for the ObjectRegistry itself. but they are called objects as a reference to "Game Objects" which is a common nomenclature

mapping(bytes32 name => address object) public objects;

constructor(address owner) {
Expand All @@ -15,14 +18,20 @@ contract ObjectRegistry is IObjectRegistry {
bytes32[] calldata objectNames,
address[] calldata objectAddresses
) public override {
// does this work? if it comes from Games.createGame() then msg.sender is that contract and not the owner wallet.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah think this is ok, it is not supposed to happen there anyway.

require(msg.sender == objects[OWNER], "ZXP Not game owner");
// should we check that the length of both of these arrays are equal?
// otherwise we can get wrong opcode error. but it can be a way to prevent passing arrays of different lengths...
// need to test this
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I considered this but it should just fail right?
Will write test for it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just removed this for simplicity. it was just a convenience anyway, can be re-added if wanted again.

require(objectNames.length > 0, "ZXP Objects empty");
for (uint256 i = 0; i < objectNames.length; i++) {
objects[objectNames[i]] = objectAddresses[i];
}
}

function addressOf(
// where are these names stored?
// is it up to the owner of the game to store them somewhere off-chain?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are stored in each ObjectRegistryClient

bytes32 objectName
) external view override returns (address) {
return objects[objectName];
Expand Down
3 changes: 2 additions & 1 deletion contracts/zxp/ObjectRegistryClient.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IObjectRegistry} from "./interfaces/IObjectRegistry.sol";
import { IObjectRegistry } from "./interfaces/IObjectRegistry.sol";


/**
* @dev Base contract for Registry clients
Expand Down
15 changes: 10 additions & 5 deletions contracts/zxp/game/GameVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,40 @@ import {IObjectRegistry} from "../interfaces/IObjectRegistry.sol";
import {ObjectRegistryClient} from "../ObjectRegistryClient.sol";
import {ISeasons} from "./interfaces/ISeasons.sol";


contract GameVault is ERC721Wrapper, ObjectRegistryClient, IGameVault {
bytes32 internal constant SEASONS = "Seasons";
mapping(uint id => uint block) public stakedAt;

constructor(
IERC721 underlyingToken,
IERC20 _rewardToken,
string memory name,
string memory symbol,
string memory underlyingTokenName,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is incorrect. The underlyingToken and the Staked Token name are not the same. I will name them differently to be clearer.

string memory underlyingTokenSymbol,
IObjectRegistry registry,
bytes32 game
)
ObjectRegistryClient(registry)
ERC721(name, symbol)
ERC721(underlyingTokenName, underlyingTokenSymbol)
ERC721Wrapper(underlyingToken)
{}

// what is this `id` for? how do we get it? maybe a clearer name?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its the NFT token ID

function _mint(address to, uint id) internal override {
stakedAt[id] = block.number;
super._mint(to, id);
}

function _burn(uint id) internal override {
// moved this for checks-effects pattern to avoid reentrancy
uint stakedAt = stakedAt[id];
stakedAt[id] = 0;

ISeasons(registry.addressOf(SEASONS)).onUnstake(
id,
msg.sender,
block.number - stakedAt[id]
block.number - stakedAt
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice catch yeah that was a bug

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry after looking at this and making the change myself i realized it's not actually a bug or a re-entrancy. You know that there's no re-entrancy because it's not just doing some arbitrary tx to an address, it is specifically calling the Seasons, which does not re-enter (or could if it really wanted to).

So it's a bit of a waste of gas to do this, but it is best practices in general and avoids bugs that might get introduced later so will keep.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this change verbatim causes a compiler error, but i fixed that and added this

);
stakedAt[id] = 0;
super._burn(id);
}
}
8 changes: 8 additions & 0 deletions contracts/zxp/game/XP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import {QuadraticLevelCurve} from "./QuadraticLevelCurve.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IXP} from "./interfaces/IXP.sol";


// where is QuadraticLevelCurve logic used here? why do we inherit it?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it's just inherited because I was compartmentalizing and writing some different leveling curves to use interchangeably.
But ended up simplifying the LevelCurve stuff for now. WIP

contract XP is ObjectRegistryClient, QuadraticLevelCurve, ERC20, IXP {
bytes32 internal constant SEASONS = "Seasons";

constructor(
string memory name,
string memory symbol,
IObjectRegistry registry,
// why is this passed if it's not used?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another rework remnant

bytes32 game
) ObjectRegistryClient(registry) ERC20(name, symbol) {}

Expand All @@ -22,10 +25,15 @@ contract XP is ObjectRegistryClient, QuadraticLevelCurve, ERC20, IXP {
address to,
uint amount
) internal virtual override {
// does this need to support burning and minting? is that why it checks for `from` and `to`?
// can minting be even done through transfer()? if not, then do we need `from` check?
// could it just be a single `revert("ZXP: Token soulbound")`?
// or it may even be on `transfer()` method instead of `_beforeTokenTransfer()`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will all have to change anyway due to OZ updates

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to actually answer your question though, the way this OZ code used to work is that when from is 0 then it's minting and when to is 0 it's burning, so this makes it so you can only mint and burn, not transfer

require(from == address(0) || to == address(0), "ZXP: Token soulbound");
super._beforeTokenTransfer(from, to, amount);
}

// does this mean this will not work if Seasons contract is not used?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep it won't

function awardXP(address to, uint amount) external override only(SEASONS) {
_mint(to, amount);
}
Expand Down
8 changes: 8 additions & 0 deletions contracts/zxp/game/seasons/Seasons.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ contract Seasons is ObjectRegistryClient, ISeasons, Ownable {
bytes32 internal constant STAKER_REWARDS = "StakerRewards";
bytes32 internal constant XP = "XP";
uint public currentSeason;
// is this a constant or a var? if a constant, make it a constant to save gas in tx,
// if a var, we need a setter for it
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add setter

uint public stakerXPReward = 100;

struct Season {
Expand All @@ -21,6 +23,7 @@ contract Seasons is ObjectRegistryClient, ISeasons, Ownable {
uint end;
ObjectRegistry objects;
}

mapping(uint season => Season data) public seasons;

modifier preseason(uint season) {
Expand Down Expand Up @@ -65,6 +68,8 @@ contract Seasons is ObjectRegistryClient, ISeasons, Ownable {
require(seasons[currentSeason].start != 0, "ZXP season not started");
seasons[currentSeason].end = block.number;
currentSeason++;
// will this always be the case? we assume here that the next season will always start at the end of the current one
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's done here to save gas, no reason to make them run another TX just to increment the currentSeason.

// user also pays to start a new season when he may not need it
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incrementing the season and making a new registry is a gas optimization.
These two things could be done separately but then it's a whole other TX. This may be a small extra expense 1 time, but this should save much more over the life of the game.

seasons[currentSeason].objects = new ObjectRegistry(msg.sender);
}

Expand All @@ -73,8 +78,11 @@ contract Seasons is ObjectRegistryClient, ISeasons, Ownable {
address to,
uint stakedAt
) external override only(GAME_VAULT) {
// what if this type of contract is not in the .objects? can other contracts be passed there?
// and if it always have to be present, where are the checks that a correct contract has been set?
IStakerRewards(seasons[currentSeason].objects.addressOf(STAKER_REWARDS))
.onUnstake(id, to, stakedAt);
// same here, what if this contract is not added or other contract is added that has a different ABI?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this will not work. The point of these sets of contracts is to be able to work together, without having to put everything in a single contract. If a contract expects some other one to be there but it isn't, then it needs to be added to get that functionality to work.
This should not be an issue to a user, as it should always get handled for them in launch UI.

IXP(registry.addressOf(XP)).awardXP(
to,
stakerXPReward * (block.number - stakedAt)
Expand Down
9 changes: 9 additions & 0 deletions contracts/zxp/game/seasons/mechanics/PlayerRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {ObjectRegistryClient} from "../../../ObjectRegistryClient.sol";
import {IObjectRegistry} from "../../../interfaces/IObjectRegistry.sol";
import {ISeasons} from "../../interfaces/ISeasons.sol";


contract PlayerRewards is ObjectRegistryClient, Ownable, IPlayerRewards {
bytes32 internal constant name = "PlayerRewards";
IERC20 public rewardToken;
Expand All @@ -22,6 +23,8 @@ contract PlayerRewards is ObjectRegistryClient, Ownable, IPlayerRewards {
ISeasons seasonManager,
uint xpRewarded
)
// this seems to be called on every single contract. what is the point of it?
// if we already have access to this value, why do we need to write it in every single state?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every contract that get registered as an object uses it. They need to know the registry they are on so they can call their fellow contracts.

ObjectRegistryClient(
IObjectRegistry(
seasonManager.getRegistryAddress(seasonManager.currentSeason())
Expand All @@ -38,6 +41,12 @@ contract PlayerRewards is ObjectRegistryClient, Ownable, IPlayerRewards {
rewardToken.transfer(to, rewards[to]);
}

// when is this called? why 3 results?
// why not addresses and uints arrays?
// also, what is the point of this contract if the decision to disperse
// rewards is made by the caller by an external call at arbitrary time?
// we can just do this all off-chain and the result seems the same
// from the trust standpoint
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function just demonstrates using the system in the most simple possible way. it award the tokens and the XP, which you can't do with just a transfer.

More functionality similar to this should be added, but it is nice how this one creates a 0 tx reward pattern for players though.

function submitTop3Results(
address first,
address second,
Expand Down
15 changes: 13 additions & 2 deletions contracts/zxp/game/seasons/mechanics/SecretRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ contract SecretRewards is ObjectRegistryClient, ISecretRewards {
ISeasons seasonManager,
uint xpRewarded
)
// here another double-external call to set the variable that's already available
ObjectRegistryClient(
IObjectRegistry(
// is there a different registry contract for every season?
seasonManager.getRegistryAddress(seasonManager.currentSeason())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Season contract is a game object. it gets registered on the game

)
)
Expand All @@ -33,6 +35,7 @@ contract SecretRewards is ObjectRegistryClient, ISecretRewards {
string reveal;
}

// what is the reason there's no nonce counter on the contract?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, could store it, just no good reason to on-contract since it is not getting used anywhere.
This does mean you have to store these offchain, but even if you had them on-chain you'd still have some work to do there managing these.
And I don't think we'd want to spend gas on making them like enumerable or something.

mapping(uint nonce => Commitment commit) public secrets;
mapping(address player => mapping(uint nonce => Commitment commit))
public guesses;
Expand All @@ -47,7 +50,7 @@ contract SecretRewards is ObjectRegistryClient, ISecretRewards {
function commitGuess(uint nonce, bytes32 guessHash) public override {
require(
bytes(secrets[nonce].reveal).length == 0,
"No overwrite after reveal"
"No overwrite after reveal" // seems like a wrong message
);
guesses[msg.sender][nonce] = Commitment({
secretHash: guessHash,
Expand All @@ -62,13 +65,15 @@ contract SecretRewards is ObjectRegistryClient, ISecretRewards {

require(
guessHash == guesses[msg.sender][nonce].secretHash,
"Invalid reveal"
"Invalid reveal" // unclear message
);
require(
bytes(secrets[nonce].reveal).length != 0,
"Answer not revealed"
);
require(
// why is this necessary to hash these here and not just compare string directly?
// what do we miss when just comparing strings?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple compare does not work due to differing data types storage vs mem

keccak256(abi.encode(guess)) ==
keccak256(abi.encode(secrets[nonce].reveal)),
"Wrong answer"
Expand All @@ -88,6 +93,12 @@ contract SecretRewards is ObjectRegistryClient, ISecretRewards {
emit SecretCommitted(nonce, secretHash);
}

// this whole flow seems redundant. an user that has the hash
// (that is available on the contract by the time a user commits a guess,
// he can locally keep hashing his guesses to get the same hash,
// once he gets the same hash he (and we) already know that his guess is correct,
// so we should be able to reward him at guess commit time and this whole reveal secret flow
// seems redundant
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aw but then, only one person would be able to guess and then everybody else would know that it's the right answer
without having to do the quest.

Actually there is an issue with the contract related to this though. that is WIP. either salt is needed here, or the nonce needs to be abstracted in the guess. in order to prevent copying.

function revealSecret(
uint nonce,
string memory secret
Expand Down
3 changes: 3 additions & 0 deletions contracts/zxp/game/seasons/mechanics/StakerRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {ISeasons} from "../../interfaces/ISeasons.sol";
import {ObjectRegistryClient} from "../../../ObjectRegistryClient.sol";
import {IObjectRegistry} from "../../../interfaces/IObjectRegistry.sol";


contract StakerRewards is ObjectRegistryClient, IStakerRewards {
IERC20 public rewardToken;
uint public rewardPerBlock;
Expand All @@ -19,6 +20,7 @@ contract StakerRewards is ObjectRegistryClient, IStakerRewards {
mapping(address awardee => uint amount) public rewards;
mapping(uint nft => uint block) public claimedAt;

// incorrect naming here. what is even registry in zXP? is it seasons? is it ObjectRegistry?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes was incorrect, fixed

modifier onlyRegistry(address checked) {
require(checked == address(seasons), "ZXP: not registry");
_;
Expand Down Expand Up @@ -49,6 +51,7 @@ contract StakerRewards is ObjectRegistryClient, IStakerRewards {
function onUnstake(
uint id,
address to,
// why don't we check this value against a mapping in GameVault ?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because this is a Season object, it doesn't actually know about the game vault. it is not on the same registry as it.

uint stakedAt
) external override onlyRegistry(msg.sender) {
uint numBlocks;
Expand Down
Loading