Skip to content

Commit

Permalink
feat: initial code skeleton and working setup
Browse files Browse the repository at this point in the history
  • Loading branch information
jac18281828 committed Nov 6, 2024
1 parent ee492e9 commit a789125
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 10 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/commit-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: commitlint
on:
workflow_dispatch:
pull_request:
branches:
- main
push:
branches:
- "*"
jobs:
run-commitlint-on-pull-request:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 22.x
- name: Install dependencies
run: npm install -g @commitlint/{cli,config-conventional}
- name: Validate all commits from PR
run: npx commitlint --from HEAD~${{ github.event.pull_request.commits }} --to HEAD --verbose
File renamed without changes.
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"editor.formatOnSave": true,
"[solidity]": {
"editor.defaultFormatter": "JuanBlanco.solidity"
},
"solidity.formatter": "forge"
}
81 changes: 81 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "install",
"type": "shell",
"command": "forge install",
"options": {
"cwd": "${workspaceFolder}"
},
"group": {
"kind": "build"
}
},
{
"label": "fmt",
"type": "shell",
"command": "forge fmt --check",
"options": {
"cwd": "${workspaceFolder}"
},
"dependsOn": "install",
"group": {
"kind": "build"
}
},
{
"label": "hint",
"type": "shell",
"command": "yarn hint",
"options": {
"cwd": "${workspaceFolder}"
},
"dependsOn": "fmt",
"group": {
"kind": "build"
}
},
{
"label": "build",
"type": "shell",
"command": "forge build --sizes",
"options": {
"cwd": "${workspaceFolder}"
},
"dependsOn": "fmt",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "clean",
"type": "shell",
"command": "forge clean && forge cache clean",
"options": {
"cwd": "${workspaceFolder}"
},
"dependsOn": "build",
"group": {
"kind": "build",
"isDefault": false
}
},
{
"label": "test",
"type": "shell",
"command": "forge test -vvv",
"options": {
"cwd": "${workspaceFolder}"
},
"dependsOn": "fmt",
"group": {
"kind": "test",
"isDefault": true
}
},
]
}
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };
227 changes: 221 additions & 6 deletions contracts/PlayChicken.sol
Original file line number Diff line number Diff line change
@@ -1,23 +1,238 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.20;

import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";

import {AddressSet} from "./AddressSet.sol";

contract PlayChicken {
contract PlayChicken is ReentrancyGuardUpgradeable, AccessControlUpgradeable {
using SafeERC20 for IERC20;

bytes32 public constant PROTOCOL_ROLE = keccak256("PROTOCOL_ROLE");

uint256 public constant BPS = 10000;

uint256 public constant MINIMUM_REWARD_AMOUNT = 1e18; // 1 token
uint256 public constant MINIMUM_DEPOSIT_AMOUNT = 1e18; // 1 token

event ProtocolFeeChanged(uint256 protocolFee);
event ProtocolFeeWithdrawn(uint256 amount, address token);
event PlayerClaimedReward(uint256 chickenId, address player, uint256 reward);
event PlayerChickendOut(uint256 chickenId, address player, uint256 amount);
event ChickenStarted(
uint256 chickenId, uint256 start, uint256 end, uint256 reward, address token, address createdBy
);
event PlayerJoined(uint256 chickenId, address player, uint256 totalBalance);

error ChickenMustEndInFuture();
error ChickenMustStartInFuture();
error ChickenStartAndEndMustBeDifferent();
error ChickenRunning();
error ChickenRewardMustBeGreaterThanMinimum(uint256 _minimum);
error ChickenMinimumDepositMustBeLarger(uint256 _minimum);
error ChickenRewardAndProtocolFeeNotMet(uint256 requiredReward, uint256 protocolFee);
error ChickenNotFinished();
error ChickenFinished();
error ChickenIdInvalid(uint256 _chickenId);
error ChickenMinimumDepositNotMet(uint256 _minimum);
error PlayerIsNotInChickenPool(address player);

struct Chicken {
address token;
uint256 start;
uint256 end;
uint256 reward_amount;
uint256 total_balance;
uint256 rewardAmount;
uint256 totalBalance;
uint256 minimumDeposit;
AddressSet players;
mapping(address => uint256) player_balance;
mapping(address => uint256) playerBalance;
}

uint256 public protocolFee; // protocol fee in bps
mapping(uint256 => Chicken) public chickens;
uint256 public chickenCount;

modifier onlyValidChickenPool(uint256 _chickenId) {
require(_chickenId < chickenCount, ChickenIdInvalid(_chickenId));
_;
}

constructor() {
_disableInitializers();
}

function initialize(address _owner) public initializer {
__ReentrancyGuard_init();
__AccessControl_init();
_grantRole(DEFAULT_ADMIN_ROLE, _owner);
chickenCount = 0;
protocolFee = 100; // 1%
}

/**
* Start a new chicken pool. Requires transfer of reward amount + protocol fee to the contract
* @param _token address of the token to be used as reward
* @param _start block number when the chicken will start
* @param _end block number when the chicken will end
* @param _rewardAmount amount of reward to be distributed
*/
function start(address _token, uint256 _start, uint256 _end, uint256 _rewardAmount, uint256 _minimumDeposit)
external
nonReentrant
{
require(_start > block.number, ChickenMustStartInFuture());
require(_end > block.number, ChickenMustEndInFuture());
require(_start < _end, ChickenStartAndEndMustBeDifferent());
require(_rewardAmount > MINIMUM_REWARD_AMOUNT, ChickenRewardMustBeGreaterThanMinimum(MINIMUM_REWARD_AMOUNT));
require(_minimumDeposit > MINIMUM_DEPOSIT_AMOUNT, ChickenMinimumDepositMustBeLarger(MINIMUM_DEPOSIT_AMOUNT));

IERC20 poolToken = IERC20(_token);
uint256 feeRequiredByProtocol = (_rewardAmount * protocolFee) / BPS;
uint256 depositAmount = feeRequiredByProtocol + _rewardAmount;
require(
depositAmount < poolToken.allowance(msg.sender, address(this)),
ChickenRewardAndProtocolFeeNotMet(depositAmount, protocolFee)
);
SafeERC20.safeTransferFrom(IERC20(_token), msg.sender, address(this), depositAmount);

Chicken storage chicken = chickens[chickenCount];
chicken.token = _token;
chicken.start = _start;
chicken.end = _end;
chicken.rewardAmount = _rewardAmount;
chicken.totalBalance = 0;
chicken.minimumDeposit = _minimumDeposit;
chicken.players = new AddressSet();
chickenCount++;

emit ChickenStarted(chickenCount, _start, _end, _rewardAmount, _token, msg.sender);
}

/**
* Claim reward for successfully completed chicken pool
* @dev reward is sent to msg.sender, msg.sender must be part of the chicken pool
* @param _chickenId id of the chicken pool
*/
function claim(uint256 _chickenId) external nonReentrant onlyValidChickenPool(_chickenId) {
Chicken storage chicken = chickens[_chickenId];
require(chicken.end < block.number || chicken.players.size() == 1, ChickenNotFinished());
require(chicken.players.contains(msg.sender), PlayerIsNotInChickenPool(msg.sender));

uint256 playerDeposit = chicken.playerBalance[msg.sender];
uint256 playerPortion = playerDeposit * BPS / chicken.totalBalance;

uint256 rewardAmount = (chicken.rewardAmount * playerPortion) / BPS;
SafeERC20.safeTransfer(IERC20(chicken.token), msg.sender, rewardAmount);
emit PlayerClaimedReward(_chickenId, msg.sender, rewardAmount);
}

/**
* Chicken out! Retrieve your deposit prior to expiration of the chicken pool
* @param _chickenId id of the chicken pool
*/
function withdraw(uint256 _chickenId) external nonReentrant onlyValidChickenPool(_chickenId) {
Chicken storage chicken = chickens[_chickenId];
require(chicken.end < block.number, ChickenFinished());
require(chicken.players.contains(msg.sender), PlayerIsNotInChickenPool(msg.sender));

uint256 playerBalance = chicken.playerBalance[msg.sender];
chicken.totalBalance -= playerBalance;
chicken.playerBalance[msg.sender] = 0;
delete chicken.playerBalance[msg.sender];
chicken.players.erase(msg.sender);

SafeERC20.safeTransfer(IERC20(chicken.token), msg.sender, playerBalance);
emit PlayerChickendOut(_chickenId, msg.sender, playerBalance);
}

/**
* join the chicken pool
* @param _chickenId id of the chicken pool
*/
function join(uint256 _chickenId) external payable nonReentrant onlyValidChickenPool(_chickenId) {
Chicken storage chicken = chickens[_chickenId];
require(chicken.start < block.number, ChickenRunning());

uint256 depositAmount = msg.value;
require(depositAmount >= chicken.minimumDeposit, ChickenMinimumDepositNotMet(chicken.minimumDeposit));

SafeERC20.safeTransferFrom(IERC20(chicken.token), msg.sender, address(this), depositAmount);
chicken.totalBalance += depositAmount;
chicken.playerBalance[msg.sender] += depositAmount;

if (!chicken.players.contains(msg.sender)) {
chicken.players.add(msg.sender);
}
emit PlayerJoined(_chickenId, msg.sender, chicken.totalBalance);
}

/**
* Withdraw protocol fee
* @dev only protocol can withdraw protocol fee
* @param _chickenId id of the chicken pool
*/
function withdrawProtocolFee(uint256 _chickenId)
external
onlyRole(PROTOCOL_ROLE)
nonReentrant
onlyValidChickenPool(_chickenId)
{
Chicken storage chicken = chickens[_chickenId];
uint256 feeBalance = getProtocolFeeBalance(_chickenId);
SafeERC20.safeTransfer(IERC20(chicken.token), msg.sender, feeBalance);
emit ProtocolFeeWithdrawn(feeBalance, chicken.token);
}

/**
* Get balance of the player in the chicken pool
* @param _chickenId id of the chicken
*/
function balance(uint256 _chickenId) external view onlyValidChickenPool(_chickenId) returns (uint256) {
Chicken storage chicken = chickens[_chickenId];
if (chicken.players.contains(msg.sender)) {
return chicken.playerBalance[msg.sender];
}
return 0;
}

/**
* Get number of players in the chicken pool
* @param _chickenId id of the chicken pool
*/
function remainingPlayers(uint256 _chickenId) external view onlyValidChickenPool(_chickenId) returns (uint256) {
Chicken storage chicken = chickens[_chickenId];
return chicken.players.size();
}

/**
* Get total balance of the chicken pool
* @param _chickenId id of the chicken pool
*/
function chickenPoolBalance(uint256 _chickenId) external view onlyValidChickenPool(_chickenId) returns (uint256) {
Chicken storage chicken = chickens[_chickenId];
return chicken.totalBalance;
}

/**
* Set protocol fee in bps for the PlayChicken contract
* @dev only the protocol can set the protocol fee
* @param _protocolFee protocol fee in bps
*/
function setProtocolFee(uint256 _protocolFee) external onlyRole(PROTOCOL_ROLE) {
protocolFee = _protocolFee;
emit ProtocolFeeChanged(_protocolFee);
}

/**
* Get protocol fee balance for a chicken pool
* @param _chickenId id of the chicken pool
*/
function getProtocolFeeBalance(uint256 _chickenId) public view returns (uint256) {
Chicken storage chicken = chickens[_chickenId];
return chicken.rewardAmount * protocolFee / BPS;
}
}
3 changes: 2 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[profile.default]
src = "src"
src = "contracts"
out = "out"
libs = ["lib"]
via-ir = true

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
2 changes: 1 addition & 1 deletion test/AddressSet.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ contract AddressTest is Test {
vm.expectRevert(abi.encodeWithSelector(AddressSet.IndexInvalid.selector, _maxIndex));
_set.get(_maxIndex);
}
}
}
Loading

0 comments on commit a789125

Please sign in to comment.