Skip to content

Commit

Permalink
feat: cleanup (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
deluca-mike committed Feb 22, 2022
1 parent 6d8a9b3 commit 9872470
Show file tree
Hide file tree
Showing 14 changed files with 596 additions and 304 deletions.
33 changes: 17 additions & 16 deletions README.md
Expand Up @@ -2,31 +2,32 @@

## Description

This contract provides a mechanisms for users to lock XDEFI, resulting in non-fungible locked positions, since each position is only un-lockable in its entirety after a certain time from locking. Locked positions have a right to withdraw at least the respective amount of XDEFI deposited, as well as a portion of XDEFI that was airdropped to this contract, and thus dispersed to all locked positions. This portion is based on the relative portion of locked XDEFI in comparison to all locked XDEFI, and the bonus multiplier of the locked position, which is assigned at lock-time based on the lock duration. Further, the locked and unlocked positions exist as NFTs with a score, in which several can be merged/burned to create new NFTs of a larger score.
This contract provides a mechanism for users to lock XDEFI, resulting in non-fungible locked positions, since each position is only un-lockable in its entirety after a certain time. Locked positions have a right to withdraw at least the respective amount of XDEFI deposited, as well as a portion of XDEFI that was airdropped to this contract, and thus dispersed to all locked positions. This portion is based on the relative portion of locked XDEFI in comparison to all locked XDEFI, and the bonus multiplier of the locked position, which is assigned at lock-time, based on the lock duration. Further, the locked and unlocked positions exist as NFTs with a number of "credits", in which several can be merged/burned to consolidate them into one NFT.

## Features and Functionality

- Users can lock in an amount of XDEFI for a duration and cannot unlock/withdraw during the specified duration
- Lock durations and their respective bonus multiplier are definable by the admin, and can be changed. (0-second durations cannot be enabled). "No bonus" is effectively a bonus multiplier of 1x, which still receives a "normal" share of future distributed rewards.
- User can lock in any amount of XDEFI that results in at least 1e18 (1 with 18 decimals) "units" (i.e. 1 XDEFI at a 1x bonus multiplier).
- The lockup becomes a “locked position”, which is an NFT (similar to Uniswap v3's liquidity position NFTs, but simpler).
- The "locked position" is transferable as a NFT during lockup and after it is unlocked/withdrawn.
- After a locked position's lockup time expires, the owner of the NFT can re-lock the amount into a new stake position, or withdraw it, or some combination, in one tx.
- Rewards are accrued while locked up, with a bonus multiplier based on the lockup time.
- Accruing of rewards/revenue with the bonus multiplier persists after the lockup time expires. This is fine since the goal is to reward the initial commitment. Further, one would be better off re-locking their withdrawable token, to compound.
- Upon locking, the NFT locked position is given a “score”, which is some function of amount and lockup time (i.e. `amount * duration`).
- The NFT's score is embedded in the `tokenId`, so the chain enforces it (first/leftmost 128 bits is the score, last/rightmost 128 bits is a sequential identifier, for uniqueness).
- The NFT points to some off-chain server that will serve the correct metadata given the NFTs points (i.e. `tokenId`). This is a stateless process off-chain.
- Once the NFT position has been unlocked and the XDEFI withdrawn, the NFT still exists simply as a transferable loyalty NFT, with its same score, but without any withdrawable XDEFI.
- Users can combine several of these amount-less loyalty NFTs into one, where the resulting NFT’s points is the sum of those burned to produce it.
- Users can lock in an amount of XDEFI for a duration and cannot unlock/withdraw during the specified duration.
- Lock durations and their respective bonus multiplier are definable by the admin, and can be changed. 0-second durations cannot be enabled. "No bonus" is effectively a bonus multiplier of 1x, which still receives a "normal" share of future distributed rewards. Changes to the lock durations do not retroactively affect existing locked positions.
- Users can lock in any amount of XDEFI that results in at least 1e18 (1 with 18 decimals) "units" (i.e. 1 XDEFI at a 1x bonus multiplier).
- Upon creating a locked position, an NFT is minted which is the owner of that locked position. In order words, owning that NFT gives the user the right to eventually withdraw the locked position.
- The NFT is transferable at any time, regardless if the original locked position still exists.
- After a locked position's lockup time expires, the owner of the NFT can re-lock the amount into a new stake position, or withdraw it, or some combination, in one transaction.
- XDEFI Rewards are accrued while locked up, with a bonus multiplier based on the lockup time.
- Accruing of XDEFI rewards with the bonus multiplier persists after the lockup time expires. This is fine since the goal is to reward the initial commitment. Further, one is still better off re-locking their withdrawable token, to compound.
- Upon locking, the NFT locked position is also given "credits”, which is some function of amount and lockup time (`amount * duration`).
- The NFT points to some off-chain server that will serve the correct metadata given the `tokenId`. The metadata (`tier`, `credits`, etc) are enforced by the smart contract.
- Once the locked position has been unlocked and the XDEFI withdrawn, the NFT still exists simply as a transferable loyalty NFT, and retains its credits, but without any withdrawable XDEFI.
- Users can combine several of these position-less loyalty NFTs into one, where the resulting NFT’s credits is the sum of those burned to consolidate.
- Contract supports ERC20 Permit, which avoids the need to do ERC20 approvals for XDEFI locking.
- A "no-going-back" emergency mode exists where the contract admin can prevent new locks, allow immediate unlocks of all locked positions, and users to remove just their deposits in the event of severe issues.
- A "no-going-back" emergency mode exists where the contract admin can prevent new locks, allow immediate unlocks of all locked positions, as well as an emergency unlock to alow users to remove just their deposits in the event of severe issues.
- NFT credits can be consumed by the owner, or by anyone via a ConsumePermit, similar to ERC20 Permits.
- Contract support token and account approvals, so any access control logic that is limited to the owner of the NFTs are actually also enabled for approved operators.

## Contracts

### XDEFIDistribution

This contract contains the standalone logic for locking, unlocking, re-locking, batched unlocking, batched re-locking, and merging, as well as the ERC721Enumerable functionality.
This contract contains the standalone logic for locking, unlocking, re-locking, batched unlocking, batched re-locking, merging, and consuming, as well as the ERC721Enumerable functionality.

### XDEFIDistributionHelper

Expand Down
125 changes: 75 additions & 50 deletions backend/index.js
Expand Up @@ -2,25 +2,40 @@ const http = require('http');
const url = require('url');
const fs = require('fs');
const path = require('path');

// TODO: add trait/attribute indicating if the NFT is backed by withdrawable XDEFI, and how much
// TODO: env for `fee_recipient` and chain read api key
const axios = require('axios');

const host = 'localhost';
const port = 8000;
const contract = '0x0000000000000000000000000000000000000000';

const CREATURES = {
1: { name: 'Ikalgo', file: 'ikalgo' },
2: { name: 'Oxtopus', file: 'oxtopus' },
3: { name: 'Nautilus', file: 'nautilus' },
4: { name: 'Kaurna', file: 'kaurna' },
5: { name: 'Haliphron', file: 'haliphron' },
6: { name: 'Kanaloa', file: 'kanaloa' },
7: { name: 'Taniwha', file: 'taniwha' },
8: { name: 'Cthulhu', file: 'cthulhu' },
9: { name: 'Yacumama', file: 'yacumama' },
10: { name: 'Hafgufa', file: 'hafgufa' },
11: { name: 'Akkorokamui', file: 'akkorokamui' },
12: { name: 'Nessie', file: 'nessie' },
13: { name: 'The Kraken', file: 'thekraken' },
};

const errorResponse = (res) => {
const errorResponse = (res, error = '') => {
res.writeHead(400);
res.end();
res.end(error);
};

const infoResponse = (res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });

const metadata = JSON.stringify({
name: 'XDEFI Distribution Creatures',
description: 'XDEFI Distribution Creatures are tiered NFTs born from the creation of XDEFI Distribution Positions.',
image: 'https://s2.coinmarketcap.com/static/img/coins/64x64/13472.png',
name: 'XDEFI Badges',
description: 'XDEFI Badges are tiered NFTs born from the creation of XDEFI Distribution Positions.',
image: 'http://localhost:8000/media/xdefi.png',
external_link: 'https://www.xdefi.io/',
seller_fee_basis_points: 100, // 1% in basis points
fee_recipient: '0x0000000000000000000000000000000000000000',
Expand All @@ -29,65 +44,75 @@ const infoResponse = (res) => {
res.end(metadata);
};

const getCreature = (tier) => {
if (tier === '1') return { name: 'Ikalgo', file: 'ikalgo' };

if (tier === '2') return { name: 'Oxtopus', file: 'oxtopus' };

if (tier === '3') return { name: 'Nautilus', file: 'nautilus' };

if (tier === '4') return { name: 'Kaurna', file: 'kaurna' };

if (tier === '5') return { name: 'Haliphron', file: 'haliphron' };
const getAttributes = async (tokenId) => {
const {
data: { result },
} = await axios.post(
'http://127.0.0.1:7545',
{
jsonrpc: '2.0',
method: 'eth_call',
params: [
{
to: contract,
data: `0x09363c44${BigInt(tokenId).toString(16).padStart(64, '0')}`,
},
'latest',
],
id: 1,
},
{
headers: {
'Content-Type': 'application/json',
},
},
);

return {
tier: BigInt('0x' + result.slice(2, 66)).toString(),
credits: BigInt('0x' + result.slice(66, 130)).toString(),
withdrawable: BigInt('0x' + result.slice(130, 194)).toString(),
expiry: Number(BigInt('0x' + result.slice(194, 258))),
};
};

if (tier === '6') return { name: 'Kanaloa', file: 'kanaloa' };
const metadataResponse = async (tokenIdParam, res) => {
if (!tokenIdParam || !/^[0-9]+$/.test(tokenIdParam)) return errorResponse(res, 'INVALID TOKEN ID');

if (tier === '7') return { name: 'Taniwha', file: 'taniwha' };
const tokenId = BigInt(tokenIdParam);

if (tier === '8') return { name: 'Cthulhu', file: 'cthulhu' };
if (tokenId >= 2n ** 256n) return errorResponse(res, 'INVALID TOKEN ID');

if (tier === '9') return { name: 'Yacumama', file: 'yacumama' };
const { tier, credits, withdrawable, expiry } = await getAttributes(tokenId).catch(() => errorResponse(res, 'FETCH FAIL'));

if (tier === '10') return { name: 'Hafgufa', file: 'hafgufa' };
const creature = CREATURES[tier];

if (tier === '11') return { name: 'Akkorokamui', file: 'akkorokamui' };
if (!creature) return errorResponse(res, 'INVALID TIER');

if (tier === '12') return { name: 'Nessie', file: 'nessie' };
const { name, file } = creature;

if (tier === '13') return { name: 'The Kraken', file: 'thekraken' };
const attributes = [
{ display_type: 'number', trait_type: 'Tier', value: tier },
{ display_type: 'number', trait_type: 'Credits', value: credits },
{ trait_type: 'Has Locked Position', value: expiry ? 'yes' : 'no' },
];

throw Error('Invalid Tier');
};
if (expiry) {
attributes.push({ display_type: 'number', trait_type: 'Withdrawable XDEFI', value: withdrawable });
attributes.push({ display_type: 'date', trait_type: 'Lock Expiry', value: expiry });
}

const getMetadata = (tokenId) => {
const mintSequence = (tokenId & (2n ** 128n - 1n)).toString();
const score = ((tokenId >> 128n) & (2n ** 124n - 1n)).toString();
const tier = (tokenId >> 252n).toString();
const { name, file } = getCreature(tier);

return JSON.stringify({
attributes: [
{ display_type: 'number', trait_type: 'score', value: score },
{ display_type: 'number', trait_type: 'tier', value: tier },
{ display_type: 'number', trait_type: 'sequence', value: mintSequence },
],
description: `${name} is a tier ${tier} XDEFI Distribution Creature.`,
const data = JSON.stringify({
attributes,
description: `${name} is a tier ${tier} XDEFI Badge`,
name,
background_color: '2040DF',
image: `http://localhost:8000/media/${file}.png`,
animation_url: `http://localhost:8000/media/${file}.mp4`,
});
};

const metadataResponse = (tokenIdParam, res) => {
if (!tokenIdParam || !/^[0-9]+$/.test(tokenIdParam)) return errorResponse(res);

const tokenId = BigInt(tokenIdParam);

if (tokenId >= 2n ** 256n) return errorResponse(res);

res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(getMetadata(tokenId));
res.end(data);
};

const mediaResponse = (fileName, res) => {
Expand Down

0 comments on commit 9872470

Please sign in to comment.