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

add example contracts for scout game #15

Merged
merged 1 commit into from
Oct 23, 2024
Merged
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
31 changes: 21 additions & 10 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
# Builder NFT Contracts
# Contracts

## contracts/BuilderNFTSeasonOneUpgradeable.sol
## ScoutToken.sol

ERC20 tokens for purchasing Builder NFT

# Testing
We use hardhat/viem and jest for our unit tests.
## StargateVesting.sol

Contract to enable the Stargate Protocol to receive funds on a schedule.

## StargateProtocol.sol

Allows players to claim their points. Receives tokens and EAS attestations on a weekly basis which contain information how to distribute funds. See: [EAS Resolver Contract](https://docs.attest.org/docs/core--concepts/resolver-contracts).

## BuilderNFTSeasonOneUpgradeable.sol

ERC1155 tokens for builders

## Lock.sol

Simple locking contract that we use to set up simple set of working jest tests

## USDC Contracts

We use the official USDC contracts from Optimism so that our unit tests are accurately using the underlying contract.

USDC Contracts valid as of October 16th 2024

### contracts/FiatTokenProxy
Proxy for USDC
https://optimistic.etherscan.io/token/0x0b2c639c533813f4aa9d7837caf62653d097ff85#code
### FiatTokenProxy

Proxy for [USDC](https://optimistic.etherscan.io/token/0x0b2c639c533813f4aa9d7837caf62653d097ff85#code)

### contracts/FiatTokenV2_2
Implementation for USDC
https://optimistic.etherscan.io/address/0xdEd3b9a8DBeDC2F9CB725B55d0E686A81E6d06dC#code
### FiatTokenV2_2

Implementation for [USDC](https://optimistic.etherscan.io/address/0xdEd3b9a8DBeDC2F9CB725B55d0E686A81E6d06dC#code)
85 changes: 85 additions & 0 deletions contracts/ScoutGameProtocol.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import "@ethereum-attestation-service/eas-contracts/contracts/resolver/SchemaResolver.sol";
import "@ethereum-attestation-service/eas-contracts/contracts/IEAS.sol";
import "./ScoutToken.sol";

/// @title ScoutGameProtocol
/// @notice A schema resolver that manages unclaimed balances based on EAS attestations.
contract ScoutGameProtocol is SchemaResolver {
ScoutToken public token;
mapping(address => uint256) private _unclaimedBalances;

// Define a constant for 18 decimals
uint256 constant DECIMALS = 10 ** 18;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be preferable to read this value directly from the ERC20 token


constructor(IEAS eas, address _token) SchemaResolver(eas) {
token = ScoutToken(_token);
}

// Method that is called by the EAS contract when an attestation is made
//
function onAttest(
Attestation calldata attestation,
uint256 /*value*/
) internal override returns (bool) {
uint256 addedBalance = 10 * DECIMALS;
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be a good place to add validations on who can attest

Example here
https://docs.attest.org/docs/tutorials/resolver-contracts#attester-resolver

Copy link
Contributor

@motechFR motechFR Oct 23, 2024

Choose a reason for hiding this comment

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

We will need some upgradeable way to evolve the balances calculation

_unclaimedBalances[attestation.recipient] += addedBalance;
return true;
}

// Method that is called by the EAS contract when an attestation is revoked
function onRevoke(
Attestation calldata attestation,
uint256 /*value*/
) internal pure override returns (bool) {
return true;
}

function getUnclaimedBalance(
address account
) public view returns (uint256) {
return _unclaimedBalances[account];
}

function getTokenBalance(address account) public view returns (uint256) {
return token.balanceOf(account);
}

// Allow the sender to claim their balance as ERC20 tokens
function claimBalance(uint256 amount) public returns (bool) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why add an amount here? We could just make it claimBalance, and zero out the user's balance

require(
_unclaimedBalances[msg.sender] >= amount,
"Insufficient unclaimed balance"
);
uint256 contractHolding = token.balanceOf(address(this));
require(contractHolding >= amount, "Insufficient balance in contract");
Copy link
Contributor

Choose a reason for hiding this comment

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

This raises an interesting product question of users having claimable balances that can't be claimed, something to look at

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, we could just build the revert logic into the transfer method of the ERC20


_unclaimedBalances[msg.sender] -= amount;
token.transfer(msg.sender, amount);
return true;
}

// Deposit funds to the contract
function depositFunds(uint256 amount) public {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the use case for depositFunds?

token.transferFrom(msg.sender, address(this), amount);
}

function decodeValue(
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused code so far?

bytes memory attestationData
) internal pure returns (uint256) {
uint256 value;

// Decode the attestation data
assembly {
// Skip the length field of the byte array
attestationData := add(attestationData, 0x20)

// Read the value (32 bytes)
value := mload(add(attestationData, 0x00))
}
return value;
}
}
61 changes: 61 additions & 0 deletions contracts/ScoutToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol";
import "@openzeppelin/contracts/utils/Context.sol";

contract ScoutToken is Context, AccessControlEnumerable, ERC20Pausable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Noticing we don't have a max total supply here, that would be a useful safeguard to add

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
bytes32 public constant VESTING_ROLE = keccak256("VESTING_ROLE");

constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());

_grantRole(MINTER_ROLE, _msgSender());
_grantRole(PAUSER_ROLE, _msgSender());
}

function mint(address to, uint256 amount) public virtual {
require(
hasRole(MINTER_ROLE, _msgSender()),
"Must have minter role to mint"
);
_mint(to, amount);
}

function mintForVesting(address to, uint256 amount) public virtual {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering if this could be simplified, and the vesting contract hardcoded

require(
hasRole(VESTING_ROLE, _msgSender()),
"Must have vesting role to mint for vesting"
);
_mint(to, amount);
}

function pause() public virtual {
require(
hasRole(PAUSER_ROLE, _msgSender()),
"Must have pauser role to pause"
);
_pause();
}

function unpause() public virtual {
Copy link
Contributor

Choose a reason for hiding this comment

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

I see alot of use of virtuals here, which mainly applies for overriding from inheriting contracts.

What is the use case for having virtuals everywhere?

require(
hasRole(PAUSER_ROLE, _msgSender()),
"Must have pauser role to unpause"
);
_unpause();
}

function grantVestingRole(address vestingContract) public virtual {
require(
hasRole(DEFAULT_ADMIN_ROLE, _msgSender()),
"Must have admin role to grant vesting role"
);
_grantRole(VESTING_ROLE, vestingContract);
}
}
150 changes: 150 additions & 0 deletions contracts/ScoutVesting.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract ScoutVesting is Ownable {
IERC20 public scoutToken;
address public scoutGameProtocol;

struct VestingSchedule {
uint256 totalAmount;
uint256 startTime;
uint256 duration;
uint256 releasedAmount;
}

mapping(address => VestingSchedule) public vestingSchedules;
address[] public employees;
VestingSchedule public protocolVestingSchedule;

event TokensVested(address indexed beneficiary, uint256 amount);
event ProtocolVestingScheduleAdded(
uint256 totalAmount,
uint256 startTime,
uint256 duration
);

constructor(
address _scoutToken,
address _scoutGameProtocol
) Ownable(msg.sender) {
scoutToken = IERC20(_scoutToken);
scoutGameProtocol = _scoutGameProtocol;
}

function addEmployee(
address employee,
uint256 totalAmount,
uint256 startTime,
uint256 duration
) external onlyOwner {
require(
vestingSchedules[employee].totalAmount == 0,
"Employee already exists"
);

vestingSchedules[employee] = VestingSchedule({
totalAmount: totalAmount,
startTime: startTime,
duration: duration,
releasedAmount: 0
});

employees.push(employee);
}

function addProtocolVestingSchedule(
uint256 totalAmount,
uint256 startTime,
uint256 duration
) external onlyOwner {
require(
protocolVestingSchedule.totalAmount == 0,
"Protocol vesting schedule already exists"
);

protocolVestingSchedule = VestingSchedule({
totalAmount: totalAmount,
startTime: startTime,
duration: duration,
releasedAmount: 0
});

emit ProtocolVestingScheduleAdded(totalAmount, startTime, duration);
}

function vest() external {
VestingSchedule storage schedule = vestingSchedules[msg.sender];
require(schedule.totalAmount > 0, "No vesting schedule found");

uint256 vestedAmount = calculateVestedAmount(schedule);
uint256 claimableAmount = vestedAmount - schedule.releasedAmount;

require(claimableAmount > 0, "No tokens available for vesting");

schedule.releasedAmount += claimableAmount;
require(
scoutToken.transfer(msg.sender, claimableAmount),
"Token transfer failed"
);

emit TokensVested(msg.sender, claimableAmount);
}

function vestForProtocol() external {
require(
msg.sender == scoutGameProtocol,
"Only ScoutGameProtocol can call this function"
);
require(
protocolVestingSchedule.totalAmount > 0,
"No protocol vesting schedule found"
);

uint256 vestedAmount = calculateVestedAmount(protocolVestingSchedule);
uint256 claimableAmount = vestedAmount -
protocolVestingSchedule.releasedAmount;

require(claimableAmount > 0, "No tokens available for vesting");

protocolVestingSchedule.releasedAmount += claimableAmount;
require(
scoutToken.transfer(scoutGameProtocol, claimableAmount),
"Token transfer failed"
);

emit TokensVested(scoutGameProtocol, claimableAmount);
}

function calculateVestedAmount(
VestingSchedule memory schedule
) internal view returns (uint256) {
if (block.timestamp < schedule.startTime) {
return 0;
} else if (block.timestamp >= schedule.startTime + schedule.duration) {
return schedule.totalAmount;
} else {
return
(schedule.totalAmount *
(block.timestamp - schedule.startTime)) / schedule.duration;
}
}

function getVestedAmount(address employee) external view returns (uint256) {
VestingSchedule memory schedule = vestingSchedules[employee];
return calculateVestedAmount(schedule);
}

function getProtocolVestedAmount() external view returns (uint256) {
return calculateVestedAmount(protocolVestingSchedule);
}

function updateScoutGameProtocol(
address _newScoutGameProtocol
) external onlyOwner {
scoutGameProtocol = _newScoutGameProtocol;
}
}
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@nomicfoundation/hardhat-ethers": "^3.0.6",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/hardhat-viem": "^2.0.3",
"@openzeppelin/contracts": "^5.0.2",
"@openzeppelin/contracts": "^5.1.0",
"@openzeppelin/contracts-upgradeable": "^5.0.2",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
Expand Down
Loading