-
Notifications
You must be signed in to change notification settings - Fork 0
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
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 |
---|---|---|
@@ -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) |
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; | ||
|
||
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; | ||
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 would be a good place to add validations on who can attest Example here 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 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) { | ||
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. 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"); | ||
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 raises an interesting product question of users having claimable balances that can't be claimed, something to look at 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. 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 { | ||
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. What is the use case for depositFunds? |
||
token.transferFrom(msg.sender, address(this), amount); | ||
} | ||
|
||
function decodeValue( | ||
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. 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; | ||
} | ||
} |
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 { | ||
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. 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 { | ||
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. 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 { | ||
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 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); | ||
} | ||
} |
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; | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
Would be preferable to read this value directly from the ERC20 token