npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@le7el/rewards_engine

v0.6.6

Published

Smart contracts that distribute reward tokens according to conditional oracle or a merkle root

Downloads

47

Readme

@le7el/rewards_engine

Rewards engine includes three contracts for reward distribution: VirtualDistributor, ConditionalDistributor and MerkleDistrbutor. All distributors support ERC20 and ERC1155 as reward tokens.

VirtualDistributor allows to allocate staking rewards to NFT metadata. It uses ideas and code from MasterChefV2 and TribalChef.

ConditionalDistributor is used to automate reward distribution based on onchain data supplied by oracle contract. The current version includes ERC721Holder oracle which distribute a fixed reward to holder of ERC721 NFT once and OneTimeOffchainTickets which allows issuing arbitary rewards to some address, authorized by off-chain signature of a validator wallet.

MerkleDistrbutor is an adapted fork of @uniswap/merkle-distributor.

This version is adopted for web usage (instead of orginal NodeJS) and has the concept of "rounds". As change of round usually implies the change of a root hash, all unclaimed rewards from a previous round could be expired in a next round.

This version of merkle distributor also has admin controls to declare new rounds, withdraw an unclaimed tokens and pausing / unpausing of the claim process.

JS

Installation

npm install @le7el/rewards_engine

Usage

```js
import {
    ethers,
    BalanceTree,
    Le7elEvents,
    contracts,
    getWeb3Provider,
    MerkleDistributor,
    ConditionalDistributor,
    ERC721Holder,
    OneTimeOffchainTickets,
    VirtualDistributor
} from "@le7el/rewards_engine"
```

Each contract function with exception to abi, bytecode, deployedAddress and prepareOffchainClaim supports web3Provider and contractKey as the last 2 arguments. web3Provider should be ethers.js v5 compliant Web3Provider or JsonRpcSigner. contractKey should be a deployed address of the relevant contract. By default windows.ethereum will be used as web3Provider and canonic deployment of the relevant contract would be used as a contractKey.

Le7elEvents

Le7elEvents are used to track off-chain activity, which later can be rewarded with MerkleDistributor or other distributor smart contract. You can think about it as "Google analitics" for gaming activity. Publishing events require private key which is generated when you create API filter for Le7el project and as simple as that:

Filter creation illustration

const le7el_api = new Le7elEvents(PRIVATE_KEY) // 
le7el_api.sendEvent(
    "0x6ecB6C62f723dC20fd9d44d95DeCC1f8AE655444",
    "WonMatch",
    {
        score: 34,
        totalParticipants: 5
    }
)

Curl-based CLI primitive, which can be integrated with any other language. To generate signature you would likely use existing Ethereum or Secp256k1 library in your language of choice, which would do Ethereum signed message prefixing and keccak digest before actual signing with a private key:

export DATA='{"nonce":1678294084655,"user_id":"0x6ecB6C62f723dC20fd9d44d95DeCC1f8AE655444","user_id_type":"wallet","context_type":"filter","platform":"web","event":"WonMatch","payload":{"score":34,"totalParticipants":5}}'
export SIGNATURE=$(echo -n "0x"; printf "\x19Ethereum Signed Message:\n%d%s" "$(echo -n "${DATA}" | wc -c)" "${DATA}" | keccak-256sum -l | xxd -r -p | openssl pkeyutl -sign -inkey private_key.pem | xxd -p)
curl -X POST -H "Content-Type: application/json" -H "Nonce: 1678294084655" -H "Authorization: Bearer ${SIGNATURE}" --data "${DATA}" "https://tools.le7el.com/v1/events"

Example of Ethereum signing in Elixir language using ex_keccak and ex_secp256k1 libraries:

digest = ExKeccak.hash_256("\x19Ethereum Signed Message:\n#{byte_size(data)}#{data}")
{:ok, {r, s, v}} = ExSecp256k1.sign(digest, Base.decode16!(private_key, case: :lower))
v = Integer.to_string((if v in [0, 1], do: v + 27, else: v), 16) |> String.downcase()
signature = "0x" <> Base.encode16(r <> s, case: :lower) <> v

sendEvent(user_id string, event string, payload JSON, nonce = 0 integer, context_type = 'filter' string, user_id_type = 'wallet' string, platform = 'web' string)

For now only EVM addresses are supported as user_id, but we plan to introduce mappers to make it possible to connect wallets with internal game ids. event is an arbitary string intended to label specific gaming activity. payload is JSON metadata which can be queried and used to customise specific reward distribution. context_type and user_id_type should use default values for now and platform can be used as additonal filtering criteria. Each sendEvent request should have a unique nonce to prevent replay attacks, by default current timestamp in milliseconds is used as nonce but any integer UID would work.

MerkleDistributor

Main interface to claim rewards distributed with the help of merkle tree.

Unless explicitly specified, all functions accept custom web3 provider and contract address as the last 2 arguments, if none specified window.ethereum will be used as provider and canonic LE7EL deployment as contract.

abi() returns (object)

ABI to interact with MerkleDistributor smart contract.

bytecode() returns (string)

Bytecode to deploy your own version of MerkleDistributor smart contract.

deployedAddress(integer chainId) returns (address | null)

Returns canonic deployment of MerkleDistributor on some network or null if it wasn't deployed there.

owner() returns (address promise)

Admin address for MerkleDistributor contract.

token() returns (address promise)

Reward token address.

tokenId() returns (integer promise)

Reward token id for ERC1155 NFTs, always 0 for ERC20 token rewards.

claimInterface() returns (bytes4 promise)

4-bytes signature in a hex form, which defines how reward will be claimed by the users as mint or transfer.

ipfsCid() returns (bytes32 promise)

IPFS cid where the merkle tree with current reward distribution is stored. It's returned in a hex form without static 1220 prefix.

currentRound() returns (integer promise)

Current distribution round, new round invalidates rewards distributed in the previous one.

adminSetNewRound(integer newRound, bytes32 newMerkleRoot, bytes32 ipfsCid) returns (transaction promise)

Admin can start a new reward distribution to wallets defined in ipfsCid with a merkle root of newMerkleRoot.

isClaimed(integer index) return (boolean promise)

Returns true if reward was already claimed for the index position in a merkle tree, specified by the on-chain merkle root.

claim(integer index, address account, integer amount, [string] merkleProof) returns (transaction promise)

Claim reward of amount for account for the index position in a merkle tree, specified by the on-chain merkle root, validated by a merkleProof. To generate merkleProof you can use BalanceTree.getProof(index, account, amount), BalanceTree data can be populated from a published ipfs cid.

ConditionalDistributor

Main interface to claim oracle based rewards.

abi() returns (object)

ABI to interact with ConditionalDistributor smart contract.

bytecode() returns (string)

Bytecode to deploy your own version of ConditionalDistributor smart contract.

deployedAddress(integer chainId) returns (address | null)

Returns canonic deployment of ConditionalDistributor on some network or null if it wasn't deployed there.

owner() returns (address promise)

Admin address for ConditionalDistributor contract.

isClaimed(address oracle, string claim) returns (boolean promise)

Checks if the supplied claim is no longer valid for an oracle.

```js
getWeb3Provider()
    .then((provider) => {
        return ERC721Holder.prepareClaim("0x731133e9", "0x2D3F5666bB1713B13E8fb24F39aA20256Cee2F8F", 0, BigNumber.from("3723987324234324"), provider)
            .then((claim) => ConditionalDistributor.isClaimed(ERC721Holder.deployedAddress(4), claim, provider))
    })
```

claim(address account, address oracle, bytes4 claimInterface, address rewardToken, integer rewardTokenId, bytes claim) returns (transaction promise)

Claim reward for an account in rewardToken for the provided claim validaded by oracle. ERC20 rewards should use 0 as rewardTokenId, specific token id is useful for ERC1155 rewards. Rewards can be either minted or transfered from the ConditionalDistributor address, make sure to fill contract beforehands if you use transfer claim interfaces. Keep in mind that if you use a proxy contract to manage minting rights for your token (e.g. MultiMinter) you should use the address of that proxy as rewardToken. The following claimInterface are supported:

  • Mint ERC20: 0x40c10f19
  • Mint ERC1155: 0x731133e9
  • Transfer ERC1155: 0xd9b67a26
  • Transfer ERC20: 0xffffffff

To generate claim use prepareOffchainClaim or prepareClaim of the relevant oracle contract (e.g. ERC721Holdwer).

```js
getWeb3Provider()
    .then((provider) => {
        return ERC721Holder.prepareClaim("0x731133e9", "0x2D3F5666bB1713B13E8fb24F39aA20256Cee2F8F", 0, BigNumber.from("3723987324234324"), provider)
            .then((claim) => {
                return ConditionalDistributor.claim(
                    "0xc4adcF8814a1da13522716A23331Ce4d48A1414d",
                    ERC721Holder.deployedAddress(4), // Rinkeby
                    "0x731133e9",
                    "0x2D3F5666bB1713B13E8fb24F39aA20256Cee2F8F",
                    0,
                    claim,
                    provider
                )
                .then((tr) => provider.waitForTransaction(tr.hash))
                .then(() => {
                    console.log('reward claimed!')
                })
            })
    })
```

batchedClaims(address account, address oracle, bytes4 claimInterface, address rewardToken, integer rewardTokenId, bytes[] claims) returns (transaction promise)

The same as claim, but executes several claims in a batch. This function expects that account, oracle, claimInterface, rewardToken and rewardTokenId are the same for all claims.

```js
nftIds = ["3723987324234324", "233232", "7973223"]
claims = nftIds.map((nftId) => ERC721Holder.prepareOffchainClaim("0x731133e9", "0x2D3F5666bB1713B13E8fb24F39aA20256Cee2F8F", 0, BigNumber.from(nftId)))
getWeb3Provider()
    .then((provider) => {
        return ConditionalDistributor.claim(
            "0xc4adcF8814a1da13522716A23331Ce4d48A1414d",
            ERC721Holder.deployedAddress(4), // Rinkeby
            "0x731133e9",
            "0x2D3F5666bB1713B13E8fb24F39aA20256Cee2F8F",
            0,
            claims,
            provider
        )
        .then((tr) => provider.waitForTransaction(tr.hash))
        .then(() => {
            console.log('reward claimed!')
        })
    })
```

adminWithdrawUnclaimed(address beneficiary) returns (transaction promise)

Admin can withdraw all unclaimed reward tokens currently stored on this merkle distributor to beneficiary address. Token information would be taken automatically from state variables. Withdrawal is only viable for 0xffffffff and 0xd9b67a26 claim interfaces.

ERC721Holder

Used to generate claims and checking rewards for specific NFTs.

abi() returns (object)

ABI to interact with ERC721Holder smart contract.

bytecode() returns (string)

Bytecode to deploy your own version of ERC721Holder smart contract.

deployedAddress(integer chainId) returns (address | null)

Returns canonic deployment of ERC721Holder on some network or null if it wasn't deployed there.

owner() returns (address promise)

Admin address for ERC721Holder contract.

getReward(integer nftId) returns (integer promise)

Checks reward for a specific NFT, keep in mind that it doesn't validate the existance of that NFT so can be false positive.

hasClaim(address account, integer nftId) returns (boolean promise)

Checks if a specific account can claim a reward for his NFT.

```js
claim = ERC721Holder.prepareOffchainClaim("0x731133e9", "0x2D3F5666bB1713B13E8fb24F39aA20256Cee2F8F", 0, BigNumber.from("3723987324234324"))
Promise.all([ERC721Holder.getReward("3723987324234324"), ERC721Holder.hasClaim("0xc4adcF8814a1da13522716A23331Ce4d48A1414d", claim)])
  .then(([reward, valid]) => {
    if (!valid) {
      console.log('invalid claim')
    } else if (reward.eq(BigNumber.from(0))) {
      console.log('no reward')
    } else {
      console.log(reward.toString())
    }
  })
```

prepareClaim(bytes4 claimInterface, address rewardToken, integer rewardTokenId, integer nftId) returns (bytes promise)

Generate claim of rewardToken for specific nftId which should be delived by claimInterface. Check ConditionalDistributor.claim for more details.

prepareOffchainClaim(bytes4 claimInterface, address rewardToken, integer rewardTokenId, integer nftId) returns (bytes)

The same as above but synchronous and done offchain with ethers.js.

OneTimeOffchainTickets

Allows distribution of fixed rewards based on tickets signed by off-chain validator wallet. Tickets with a lower nonce are invalidated when higher nonce is claimed. Tickets are also invalided if current claimed amount differs from the value when the ticket was generated. It's done to prevent double rewarding with pre-generated, but unclaimed tickets.

The good practise is to always issue a ticket for the full reward at the moment of ticket issuance, if the claimant would use one of older tickets the later ticket would be automatically invalidated, because of correction in a claimed amount, so a claimant would have to generate a new ticket for the remaining pending debt.

abi() returns (object)

ABI to interact with ERC721Holder smart contract.

bytecode() returns (string)

Bytecode to deploy your own version of ERC721Holder smart contract.

deployedAddress(integer chainId) returns (address | null)

Returns canonic deployment of ERC721Holder on some network or null if it wasn't deployed there.

owner() returns (address promise)

Admin address for OneTimeOffchainTickets contract.

getDomainSeparator() returns (string promise)

Part of seed to generate off-chain ticket for the current contract.

nextNonce(address account) return (integer promise)

Return next valid nonce to generate next off-chain ticket for the account.

claimedAmount(address account) return (integer promise)

Return currently claimed reward amount by account to generate next off-chain ticket for the account.

isAllowed(address account, integer amount, integer claimedAmount, integer nonce, string callData) return (boolean promise)

Used to validate ticket signature, but doesn't do the rest of important validations. Most likely you should use hasClaim instead, which ensures all the validity constraints.

hasClaim(address account, string claim) return (boolean promise)

Checks if specific claim is valid for account.

signTicket(Wallet signer, string domainSeparator, address user, integer amount, integer claimedAmount, integer nonce) retirn (string promise)

Use ethers wallet class to sign off-chain ticket. Unless you use node.js on your backend, most likely you'll have to re-implement this function in your backend language.

prepareOffchainClaim(string claimInterface, address rewardToken, integer rewardTokenId, address user, integer amount, integer claimedAmount, integer nonce, string ticketSignature) return (string promise)

Prepare off-chain claim for specific amount. user, amount, claimedAmount and nonce should be the same as your passed to signTicket to generate ticketSignature argument. claimInterface, rewardToken and rewardTokenId should be the same as configured by oracle owner.

Full claim example:

```
const TICKET_ORACLE = '0x23Fe1Ef7c30c2007559216B2C766A9f10608d61b'
const EXP_TOKEN_MINTER = '0xF526929CF357842Eb0aEB76Ff58d3010EF35bB62'
const ERC1155_MINT_INTERFACE = '0x731133e9'
const user = '0x30dc9ba4e5e0047848e4291ec448b1576582654e'
Promise.all([
    nextNonce(user),
    claimedAmount(user),
    getDomainSeparator()
]).then(([nonce, claimed, separator]) => {
    const signer = new ethers.Wallet('888fa71d782f31e9d1c952ab74d23a0f8f3f4dc189b8165a94810cf62c805af8') // '0x11169009E2E4956205632177ba1d2F2603342D91'
    return signTicket(signer, separator, user, 100, claimed, nonce)
        .then((ticketSignature) => {
            return prepareOffchainClaim(ERC1155_MINT_INTERFACE, EXP_TOKEN_MINTER, 0, user, 100, claimed, nonce, ticketSignature)
        })
}).then((claim) => {
    return claim(user, TICKET_ORACLE, ERC1155_MINT_INTERFACE, EXP_TOKEN_MINTER, 0, claim)
})
```

VirtualDistributor

Used to generate claims and checking rewards for specific NFTs.

abi() returns (object)

ABI to interact with VirtualDistributor smart contract.

bytecode() returns (string)

Bytecode to deploy your own version of VirtualDistributor smart contract.

deployedAddress(integer chainId) returns (address | null)

Returns canonic deployment of VirtualDistributor on some network or null if it wasn't deployed there.

join(address nftContract, integer nftId) returns (transaction promise)

Join specific NFT to rewards program. Repeatable joins are allowed, in case your NFT level is the same from the last join nothing will happen, will also update your rewards according to your new level.

pendingRewards(address nftContract, integer nftId) returns (integer promise)

Returns total amount of accumulated reward tokens for specific NFT. Keep in mind that unlocked rewards are shown as 0 in NFT metadata to prevent abuse on marketplaces.

nftInfo(address nftContract, integer nftId) returns (object promise)

Returns information about specific NFT metadata inside the pool. rewardDebt is a technical value used for reward correction based on join time, virtualAmount is a share of reward for the NFT and lockedUntil is an optional timestamp for reward unlocking.

rewardPerBlock() returns (integer promise)

Returns reward per block for VirtualDistributor contract

lockHarvest(address nftContract, integer nftId) returns (transaction promise)

NFT owner can lock harvesting of rewards to make it safe to buy (see unlockHarvest below).

unlockHarvest(address nftContract, integer nftId) returns (transaction promise)

NFT owner can unlock harvestng of L7L rewards, unlock takes 1 hour.

harvest(address nftContract, integer nftId) returns (transaction promise)

Claim L7L rewards accumulated on this NFT, it requires additional claim through VestingRewarder contract if not fully vested.

VestingRewarder

Used with VirtualDistributor to claim rewards with vesting.

abi() returns (object)

ABI to interact with VestingRewarder smart contract.

bytecode() returns (string)

Bytecode to deploy your own version of VestingRewarder smart contract.

deployedAddress(integer chainId) returns (address | null)

Returns canonic deployment of VestingRewarder on some network or null if it wasn't deployed there.

vestingStarts() returns (integer promise)

UNIX timestamp when vesting starts.

vestingEnds() returns (integer promise)

UNIX timestamp when vesting ends.

claimableAmount(address wallet) returns (integer promise)

Total amount of vested tokens for the address.

claimVested(address wallet) returns (transaction promise)

Claim vested tokens for the wallet.

vestingLedger(address wallet) returns (integer promise)

Returns amount of tokens being vested for the wallet (will also include already claimed tokens).

claimedVestedLedger(address wallet) returns (integer promise)

Returns amount of already claimed vested tokens for the wallet.

OneSidedStaking

Staking contract to issue rewards proportionally to the amount of staking tokens staked. Owner of staking contract can take uncollatarised loan from this smart contract. Staking and reward tokens can be different.

abi() returns (object)

ABI to interact with OneSidedStaking smart contract.

bytecode() returns (string)

Bytecode to deploy your own version of OneSidedStaking smart contract.

deployedAddress(integer chainId) returns (address | null)

Returns canonic deployment of OneSidedStaking on some network or null if it wasn't deployed there.

owner() returns (address promise)

Admin address for OneSidedStaking contract.

pendingRewards(address wallet) returns (integer promise)

Returns total amount of accumulated staking rewards for the wallet address.

claim() returns (transaction promise)

Claim all unclaimed staking rewards for the current wallet address.

claimAllUnstaked() returns (transaction promise)

Claim all staking tokens which are ready to be withdrawn for the current wallet address.

claimUnstaked(integer ticket) returns (transaction promise)

Claim specific unstaking request.

stake(integer amount) returns (transaction promise)

Stake certain amount of tokens from the current wallet address. Keep in mind that ERC20.approve() should be called first to allow the transfer of relevant amount of tokens.

unstake(integer amount) returns (transaction promise)

Withdraw certain amount of previously staked tokens to the current wallet address. Default unlock period is 30 days, so they are not immediatly available.

depositInfo(address wallet) returns ([integer, integer] promise)

Return previously claimed rewards and staked token for the wallet address.

unstakingRequests(address wallet, integer ticket) returns ([integer, integer] promise)

Return unstake amount and timestamp until that amount is locked for the wallet address.

getWalletUnstakingRequests(address wallet) returns ([Event] promise)

Return all unclaimed unstaking Events.

Solidity

Install packages

$ npm install --dev

Install Hardhat

$ npm install --save-dev hardhat

Launch the local Ethereum client e.g. Ganache:

Testing

Install local ganache: npm install --global ganache

Run it in cli: ganache, you may need to change network_id for develop network in truffle-config.js

Run tests with truffle: yarn test

Integration

Run webpack development server: npx webpack serve --open or npm run webpack:watch

Check http://localhost:8080/ for Merkle proof generation and validation UX.

Implementation example entrypoints can be found here: src/index.ts and dist/index.html.

Verification

To try out Etherscan verification, you first need to deploy a contract to an Ethereum network that's supported by Etherscan, such as Rinkeby.

In this project, copy the .example file to a file named .secret, and then edit it to fill in the details. Enter your Etherscan API key, your Rinkeby node URL (eg from Infura), and the private key of the account which will send the deployment transaction. With a valid .secret file in place, first deploy your contract:

npx hardhat run --network live_goerli scripts/1_deploy_merkle_distributor.js
npx hardhat run --network live_goerli scripts/2_deploy_conditional_distributor.js
npx hardhat run --network live_goerli scripts/3_deploy_virtual_distributor.js

Then, copy the deployment address and paste it in to replace DEPLOYED_CONTRACT_ADDRESS in this command:

npx hardhat verify --network live_goerli DEPLOYED_CONTRACT_ADDRESS ...CONSTRUCTOR_ARGS

Deployments

Rinkeby

  • MerkleDistributor (DAI token) deployed to: 0x1B1d03B59233243cb43844e930a6a1B181077cD9
  • ERC721Holder deployed to: 0xBE1eFff4F86dB8226620126B02Ba2e334d682378
  • ConditionalDistributor deployed to: 0x5d014dAA8688DB97B3B65138782920faEBBb32C3

Goerli

  • MerkleDistributor (PXP token) deployed to: 0x9E7baB365BcA758681c6ee44bc38BFAf121B6a7d
  • ERC721Holder deployed to: 0xe9589a535cbDDF6aF50a7AC162DEc1dFa1adA188
  • OneTimeOffchainTickets deployed to: 0x23Fe1Ef7c30c2007559216B2C766A9f10608d61b
  • ConditionalDistributor deployed to: 0xb898262910C4A585AbC8be366D6102fc77519ec7
  • VirtualDistributor deployed to: 0x2628D5e8fB8D95454ceE66A82Ffc512A5F14D6DC

Sepolia

  • MerkleDistributor (PXP token) deployed to: 0x5C1C7511f6d90b00bF23b12c25dA9dbD93C369B5
  • ERC721Holder deployed to: 0xCF5eE082a77C5005De433e96a20CC626F29f754D
  • OneTimeOffchainTickets deployed to: 0x49008D8c8d0f1EFe9d819Ee1AE1d11c49dDD0390
  • ConditionalDistributor deployed to: 0xC972af85DbD3d770D5E8668e1f81Fd45D52D2aaa
  • VirtualDistributor deployed to: 0x47D3e90827dFED8Ef5d73dA83D06282e5012d82E
  • OneSidedStaking deployed to: 0xd693f887D6Dc28D6bE8B39cc0E920c26233d3022
  • VestingRewarder deployed to: 0x7a1a9f14c323e04e443E7F19d259393e3fa20829

Polygon

  • ERC721Holder deployed to: 0xd373a0fDf749f8fC28B913014aFc0BE0c17490C6
  • OneTimeOffchainTickets deployed to: 0xb3E7F55d98F499c97A1DD9B585D76e10624ca429
  • ConditionalDistributor deployed to: 0x276FE941757C93c4A916B985C59613692e0f551f
  • VirtualDistributor deployed to: 0x73A699D74734023aE4945FaE6205dfe383347f21