Skip to content

Instantly share code, notes, and snippets.

@iamendy
Created May 18, 2023 16:04
Show Gist options
  • Save iamendy/3675f8ec897296b1eca3d4b33acc4086 to your computer and use it in GitHub Desktop.
Save iamendy/3675f8ec897296b1eca3d4b33acc4086 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
//import "contracts/QuickSort.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/automation/AutomationCompatible.sol";
//TODO: optimize, struct packing
//TODO: optimize, uint correct values
contract AgreementVault is AutomationCompatibleInterface, Ownable {
//aiarena NFT contract address
address private nftToken;
//aiarena Erc20 token contract address
address private nrnToken;
//will remove later
constructor(address _nftToken, address _nrnToken) {
nftToken = _nftToken;
nrnToken = _nrnToken;
}
enum RequirementStatus {
ACTIVE,
CANCELLED,
ACCEPTED,
SETTLED,
EXPIRED //when bots has run
}
// enum ServiceToken {
// WETH,
// USDC
// //NRN
// }
struct CasualGamerTerms {
uint256 termId;
address casualGamer;
address proGamer;
uint256 tokenId;
uint256 duration;
uint256 pricePerElo;
uint256 maxRenumeration;
uint256 proGamersStake;
uint8 serviceToken;
uint256 timeAccepted;
RequirementStatus reqStatus;
uint256 currentEloScore; //change uint size;
//the elo score at the end of the term duration
uint256 endingEloScore; //change uint size;
address[] privateListing;
//mapping(address => bool) proGamersAllowed;
}
CasualGamerTerms[] private terms;
mapping (uint8 => address) private serviceTokens;
uint256 termsCount = 0;
struct ActiveTerms {
uint256 termId;
uint256 timeToExpire;
}
ActiveTerms[] private activeTerms;
//a test event will replace later
event RequirementCreated(
address indexed casualGamer,
uint256 indexed tokenId,
uint256 indexed termId,
uint256 duration,
uint256 pricePerElo,
uint256 maxRenumeration,
uint256 proGamersStake,
uint256 currentEloScore);
event RequirementAccepted(
address indexed proGamer,
uint256 indexed termId,
uint256 timeAccepted,
uint256 timeToExpire);
event RequirementCancelled(uint256 termId, uint256 cancelledAt);
function createRequirement(
//duration in seconds
uint256 _duration,
uint256 _pricePerElo,
uint256 _maxRenumeration,
uint256 _proGamersStake,
uint8 _serviceToken,
uint256 _tokenId,
uint256 _currentEloScore,
address[] memory _privateListing) external {
require(msg.sender != address(0), "zero address");
//require owner of NFT
//might remove if safeTransfer already does the check
require(IERC721(nftToken).ownerOf(_tokenId) == msg.sender, "Not NFT owner");
terms.push(
CasualGamerTerms({
termId: termsCount,
casualGamer: msg.sender,
proGamer: address(0),
tokenId: _tokenId,
duration: _duration,
pricePerElo: _pricePerElo,
maxRenumeration: _maxRenumeration,
proGamersStake: _proGamersStake,
serviceToken: _serviceToken,
privateListing: _privateListing,
timeAccepted: 0,
currentEloScore: _currentEloScore,
endingEloScore: 0, //might recheck to see if there is any logical error
reqStatus: RequirementStatus.ACTIVE
}));
//300, 10, 500, 500, 0, 1, 1450, [0x79d899379844d35a1a1f5d51d3185dd821f44dc1, 0x89a6a01f58fb5dbed9fb1c10140f7a096c4430a0]
//500000000000000000000
//transfer maxRenumeration to the contract
//TODO: should we add a sentinel value check? although it returns an error if it the proper value is not keyed in
//TODO: if we pass in the value * 1e18, then we won't do the multiplication here.
//TODO: decide later if we want the multiplication in, or the multiplication out
IERC20(getServiceToken(_serviceToken)).transferFrom(msg.sender, address(this), _maxRenumeration * (10 ** 18));
//transfer the NFT to the contract
IERC721(nftToken).transferFrom(msg.sender, address(this), _tokenId);
//a test emit, will replace later
emit RequirementCreated(
msg.sender,
_tokenId,
termsCount++,
_duration,
_pricePerElo,
_maxRenumeration,
_proGamersStake,
_currentEloScore);
//termsCount = termsCount + 1;
}
function acceptRequirements(uint256 _termsId) external {
//TODO: decide if to use memory
//TODO: check if it's a private listing
CasualGamerTerms memory _terms = terms[_termsId];
//TODO: ensure that termsId is valid
//ensure that the casual gamer can't accept his own terms.
require(msg.sender != _terms.casualGamer, "Can't accept your own term");
//require that terms haven't been accepte4d by anyone else
require(_terms.reqStatus == RequirementStatus.ACTIVE, "Not active");
//kickoff duration after accepting a term
_terms.timeAccepted = block.timestamp;
//associate address to the tokenId
_terms.proGamer = msg.sender;
//make the requirement active
_terms.reqStatus = RequirementStatus.ACCEPTED;
//deposit stake into the contract
IERC20(nrnToken).transferFrom(msg.sender, address(this), _terms.proGamersStake * (10 ** 18));
activeTerms.push(ActiveTerms({
termId: _termsId,
timeToExpire: _terms.timeAccepted + _terms.duration
}));
//TODO: check if this replaces all the previous values of the previous terms, or only the modifiied values
terms[_termsId] = _terms;
//emit event
emit RequirementAccepted(
msg.sender,
_termsId,
_terms.timeAccepted,
//TODO: see if storing timeAc plus duration in a variable is cheaper
_terms.timeAccepted + _terms.duration);
}
function test(uint256 _termsId, uint256 endScore) external {
CasualGamerTerms storage cgTerms = terms[_termsId];
cgTerms.endingEloScore = endScore;
}
function getTerm(uint256 _termsId) external view returns(CasualGamerTerms memory cterm){
return terms[_termsId];
}
function settleTerms(uint256 _termsId) external {
//TO-ASK: can the elo ever get to 0?
//TODO: try using safeTransfer instead of transfer
//TODO: condition for when progamer's stake is 0
//TODO: check if converting to memory is cheaper than orking with storage directly
CasualGamerTerms storage cgTerms = terms[_termsId];
//ensures that the ELO has been set by the keeper bots
//assumes that the minimum elo score is 0
require(cgTerms.endingEloScore > 0, "keeper-bots haven't updated score");
require(cgTerms.reqStatus == RequirementStatus.EXPIRED, "!EXPIRED");
//only casual gamer or pro gamer can settle terms
require(cgTerms.casualGamer == msg.sender
|| cgTerms.proGamer == msg.sender, "Not allowed to settle terms");
//require that the duration has ended
require(block.timestamp - cgTerms.timeAccepted >= cgTerms.duration, "Not Settlement time");
cgTerms.reqStatus = RequirementStatus.SETTLED;
//require that the transfer(NFT and erc20 token) was successful
//recalculate servicePayment and stake to the pro gamer or the causal gamer
//calculation assumes proGamer scores a positive elo for the casual gamer
//might check for below zero
//also we might have to multiply by 18 decimal places
int256 maxRenumeration = int256(cgTerms.maxRenumeration);
//ending eloscore can not be less than zero because it was declared as a uint
int256 eloDifference = int256(cgTerms.endingEloScore) - int256(cgTerms.currentEloScore);
//assuming that we have a loss
if(eloDifference < 0) {
//TODO: maximum acceptable loss
//use a better formula later
int256 proGamersPenalty = ((eloDifference * -1) * int256(cgTerms.proGamersStake))/int256(cgTerms.currentEloScore);
int256 whatsLeftForProgamer = int256(cgTerms.proGamersStake) - proGamersPenalty;
int256 totalForCasualGamer = proGamersPenalty + maxRenumeration;
IERC20(nrnToken).transfer(cgTerms.casualGamer, uint256(totalForCasualGamer*1e18));
IERC20(nrnToken).transfer(cgTerms.proGamer, uint256(whatsLeftForProgamer*1e18));
}else{
int256 reward = int256(cgTerms.pricePerElo) * eloDifference;
int256 pGamerRewardShare = reward >= maxRenumeration ? maxRenumeration : reward;
//update the money that will be sent back to the casual gamer if there's a remainder
int256 remnantForCasualGamer = maxRenumeration - pGamerRewardShare;
//we assume it's the same currency. If it isn't the same currency then we will have two transfer functions
int256 totalMoneyReceived = pGamerRewardShare + int256(cgTerms.proGamersStake);
IERC20(nrnToken).transfer(cgTerms.proGamer, uint256(totalMoneyReceived*1e18));
IERC20(nrnToken).transfer(cgTerms.casualGamer, uint256(remnantForCasualGamer*1e18));
}
//send NFT back to the casual gamer
//TODO: check if safeTransfer works instead
IERC721(nftToken).safeTransferFrom(address(this), cgTerms.casualGamer, cgTerms.tokenId);
// //if the gamer loses the entire elo point
// if(eloDifference == int256(cgTerms.currentEloScore) * -1) {
// }
//call recalculate service payment function
//if progamer exceeds or gets to the maxRenumeration, send everything to him
//if he gains some ELO but doesn't get to the MaxRenumeration, pay him based on pro-rata basis
//if ELO stays thesame refund both parties
//if progamer loses more than the maxAcceptable loss, calculate stake fee to be deducted based on the pro-rata fee
//stop chainlink keepers from tracking this termId
//sets isSettled to true
}
//cancels a term if it hasn't been matched/accepted
function cancelRequirement(uint256 _termsId) external {
CasualGamerTerms storage cgTerms = terms[_termsId];
require(cgTerms.reqStatus == RequirementStatus.ACTIVE, "!ACTIVE");
cgTerms.reqStatus = RequirementStatus.CANCELLED;
//TODO: return NFT and token back to the casual gamer
emit RequirementCancelled(_termsId, block.timestamp);
}
function checkUpkeep(bytes calldata /* checkData */) external view override
returns (bool upkeepNeeded, bytes memory /* performData */) {
//best implementationshould be to sort the array by time remaining to expire
for(uint256 i = 0; i < activeTerms.length;) {
if(block.timestamp >= activeTerms[i].timeToExpire) {
return(true, abi.encode(i, activeTerms[i].termId, block.timestamp, activeTerms[i].timeToExpire));
}
unchecked{
i++;
}
}
return (false, bytes(""));
}
function performUpkeep(bytes calldata performData ) external override {
//It's highly recommend to revalidate the upkeep in the performUpkeep function
(uint256 termIndex, uint256 termId, uint256 timeStamp, uint256 timeToExpire) = abi.decode(performData, (uint256, uint256, uint256, uint256));
//TODO: Include accepted state check
//revalidation
if(timeStamp >= timeToExpire) {
//TODO: set the elo of the current term id's NFT
//remember the term must be in an accepted state
//TODO: remove the term from active arrays
CasualGamerTerms storage cgTerms = terms[termId];
cgTerms.reqStatus = RequirementStatus.EXPIRED;
cgTerms.endingEloScore = 3000;
activeTerms[termIndex] = activeTerms[activeTerms.length - 1];
activeTerms.pop();
//emit DurationReached(termId);
}
}
//function setApprovedNFTs(uint8 _srvcToken, address _tokenAddr) external onlyOwner {
//serviceTokens[_srvcToken] = _tokenAddr;
//}
function setServiceToken(uint8 _srvcToken, address _tokenAddr) external onlyOwner {
//TODO: ensure that weth and usdc are not overriden
serviceTokens[_srvcToken] = _tokenAddr;
}
function getServiceToken(uint8 _srvcToken) public view returns (address) {
//TODO: return error if the service token is not whitelisted
return serviceTokens[_srvcToken];
}
function getTokenAddress() external view returns(address) {
return nrnToken;
}
function getNFTAddress() external view returns(address) {
return nftToken;
}
function getActiveTerms() external view returns(ActiveTerms[] memory) {
return activeTerms;
}
function getTerms() external view returns(CasualGamerTerms[] memory) {
return terms;
}
//TODO: should multiply or not multiply
}
contract MyToken is ERC20, Ownable {
constructor() ERC20("MyToken", "MTK") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract MyNFT is ERC721 {
uint _tokenIdCounter = 0;
constructor() ERC721("MyNFT", "MTK") {}
function safeMint(address to) public {
_tokenIdCounter = _tokenIdCounter + 1;
_safeMint(to, _tokenIdCounter);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment