diff --git a/contracts/README.md b/contracts/README.md index ed2f1a2..67c5f11 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -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) diff --git a/contracts/ScoutGameProtocol.sol b/contracts/ScoutGameProtocol.sol new file mode 100644 index 0000000..dd40b48 --- /dev/null +++ b/contracts/ScoutGameProtocol.sol @@ -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; + _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) { + require( + _unclaimedBalances[msg.sender] >= amount, + "Insufficient unclaimed balance" + ); + uint256 contractHolding = token.balanceOf(address(this)); + require(contractHolding >= amount, "Insufficient balance in contract"); + + _unclaimedBalances[msg.sender] -= amount; + token.transfer(msg.sender, amount); + return true; + } + + // Deposit funds to the contract + function depositFunds(uint256 amount) public { + token.transferFrom(msg.sender, address(this), amount); + } + + function decodeValue( + 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; + } +} diff --git a/contracts/ScoutToken.sol b/contracts/ScoutToken.sol new file mode 100644 index 0000000..4205cb1 --- /dev/null +++ b/contracts/ScoutToken.sol @@ -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 { + 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 { + 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 { + 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); + } +} diff --git a/contracts/ScoutVesting.sol b/contracts/ScoutVesting.sol new file mode 100644 index 0000000..0f1624e --- /dev/null +++ b/contracts/ScoutVesting.sol @@ -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; + } +} diff --git a/package-lock.json b/package-lock.json index 9b7ac92..1674a78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,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", @@ -3671,9 +3671,9 @@ } }, "node_modules/@openzeppelin/contracts": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.2.tgz", - "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.1.0.tgz", + "integrity": "sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA==", "dev": true }, "node_modules/@openzeppelin/contracts-upgradeable": { diff --git a/package.json b/package.json index 803ec0c..8088b58 100644 --- a/package.json +++ b/package.json @@ -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",