Created
May 18, 2023 16:04
-
-
Save iamendy/3675f8ec897296b1eca3d4b33acc4086 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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