Skip to content

pyk/LlamaLocker

Repository files navigation

Llama Locker

Llama Locker allows users to lock their LLAMA tokens and earn a share of the yield generated by the treasury over time. It manages epochs, reward token distribution, and token locking/unlocking, ensuring fair and transparent reward distribution.

Lock Mechanism

  • Locked NFTs cannot be withdrawn for 4 epochs (4 weeks) and are eligible to receive a proportionate share of yields during this period.
  • Unlike the CVX Lock style, which requires active kicking out of tokens after the lock duration ends, LlamaLocker offers a more user-friendly approach.
  • NFT owners can withdraw their NFTs in the epoch after the lock duration ends. If not withdrawn, the NFTs will automatically re-lock for the subsequent lock duration, streamlining the process and saving users on gas costs.

Example of Lock Mechanism

  1. Alice locks Llama #1 on January 28, 2024, at 22:49:42 GMT.
  2. Llama #1 starts accruing yields from February 1, 2024, at 00:00:00 GMT (next epoch).
  3. Withdrawal of Llama #1 is possible anytime from February 29, 2024, at 00:00:00 GMT to March 7, 2024, at 00:00:00 GMT (one-week epoch).
  4. If Llama #1 remains unwithdrawn during this window, it will automatically re-lock starting March 7, 2024, at 00:00:00 GMT.

Getting Started

Ensure you are using the latest version of Foundry:

foundryup

Install dependencies:

forge install

Run the tests:

forge test

Example output:

$ forge test
[⠊] Compiling...
[⠑] Compiling 1 files with Solc 0.8.23
[⠘] Solc 0.8.23 finished in 1.71s
Compiler run successful!

Ran 1 test for test/LlamaLocker.t.sol:LlamaLockerTest
[PASS] test_renounceOwnership_InvalidAction() (gas: 13389)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.44ms (93.38µs CPU time)

Ran 4 tests for test/AddRewardTokens.t.sol:AddRewardTokensTest
[PASS] test_addRewardTokens_InvalidRewardToken() (gas: 92195)
[PASS] test_addRewardTokens_InvalidRewardTokenCount() (gas: 13954)
[PASS] test_addRewardTokens_Unauthorized() (gas: 14191)
[PASS] test_addRewardTokens_Valid() (gas: 133255)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.60ms (179.63µs CPU time)

Ran 8 tests for test/Whitelist.t.sol:RewardDistributionTest
[PASS] test_disableWhitelist_InvalidAction() (gas: 20211)
[PASS] test_disableWhitelist_Unauthorized() (gas: 13655)
[PASS] test_disableWhitelist_Valid() (gas: 19409)
[PASS] test_lock_InvalidAction() (gas: 115192)
[PASS] test_lock_Valid() (gas: 225023)
[PASS] test_setRoot_InvalidAction() (gas: 15583)
[PASS] test_setRoot_Unauthorized() (gas: 13695)
[PASS] test_setRoot_Valid() (gas: 19625)
Suite result: ok. 8 passed; 0 failed; 0 skipped; finished in 1.65ms (751.29µs CPU time)

Ran 6 tests for test/LockMechanism.t.sol:LockMechanismTest
[PASS] test_lock_InvalidTokenCount() (gas: 11454)
[PASS] test_lock_Valid() (gas: 496076)
[PASS] test_unlock_InvalidLockOwner() (gas: 225991)
[PASS] test_unlock_InvalidTokenCount() (gas: 9265)
[PASS] test_unlock_InvalidUnlockWindow() (gas: 246840)
[PASS] test_unlock_ValidUnlockWindow() (gas: 222618)
Suite result: ok. 6 passed; 0 failed; 0 skipped; finished in 1.71ms (784.38µs CPU time)

Ran 5 tests for test/RewardDistribution.t.sol:RewardDistributionTest
[PASS] test_distributeRewardToken_Claimables() (gas: 1020171)
[PASS] test_distributeRewardToken_InvalidRewardAmount() (gas: 134541)
[PASS] test_distributeRewardToken_InvalidRewardToken() (gas: 17599)
[PASS] test_distributeRewardToken_InvalidTotalShares() (gas: 136631)
[PASS] test_distributeRewardToken_Unauthorized() (gas: 13900)
Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 2.06ms (1.10ms CPU time)

Ran 1 test for test/OffchainQuery.sol:LockMechanismTest
[PASS] test_getLocks() (gas: 11133290)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.34ms (9.86ms CPU time)

Ran 6 test suites in 161.35ms (19.81ms CPU time): 25 tests passed, 0 failed, 0 skipped (25 total tests)

Generate Merkle Tree

Install dependencies:

pnpm install

Update the whitelist.txt file.

Then get the merkle tree root and proofs via the following command:

pnpm run gen:merkle

Check the proof inside merkle-proofs.json.

Front End Integration

There are two main actions for users:

  1. Lock NFT: Users can lock their NFT via lock(proofs, tokenIds) for whitelisted user and lock(tokenIds) for public.
  2. Unlock NFT: Users can unlock their NFT via unlock.

Additional information:

  • Claimable rewards are available via claimables(account).
  • Claimed rewards are available via getClaimedRewards(account).
  • Lock information can be retrieved via locks(nftId).

To compute the next unlock for the specified NFT, use the following formula:

lockedDuration = currentTimestamp - lockedAt
lockedDurationInEpoch = lockedDuration / EPOCH_DURATION
modulo = lockedDurationInEpoch % LOCK_DURATION_IN_EPOCH
unlockNextEpoch = LOCK_DURATION_IN_EPOCH - modulo
unlockStart = currentTimestamp + (unlockNextEpoch * EPOCH_DURATION)
unlockEnd = unlockStart + EPOCH_DURATION

unlockStart and unlockEnd define a time window in Unix timestamp when users can unlock their locked NFTs.

Admin actions:

  • Admin can add a new reward token via addRewardToken.
  • Admin can distribute weekly rewards via distributeRewardToken.
  • Admin need to approve the LlamaLocker contract before executing distributeRewardToken.
  • Admin can set new merkle tree root via setRoot(root) (for rolling whitelist)
  • Admin can disable the whitelist via disableWhitelist() (for public launch)

Gas Report

| src/LlamaLocker.sol:LlamaLocker contract |                 |         |         |         |         |
|------------------------------------------|-----------------|---------|---------|---------|---------|
| Deployment Cost                          | Deployment Size |         |         |         |         |
| 2020278                                  | 9131            |         |         |         |         |
| Function Name                            | min             | avg     | median  | max     | # calls |
| addRewardTokens                          | 24227           | 78136   | 84896   | 139290  | 10      |
| claim                                    | 162883          | 162883  | 162883  | 162883  | 1       |
| claimable                                | 2401            | 3067    | 2401    | 4401    | 12      |
| disableWhitelist                         | 23478           | 28734   | 29335   | 29335   | 16      |
| distributeRewardToken                    | 24051           | 45139   | 28627   | 81528   | 7       |
| getLocks                                 | 8018452         | 8018452 | 8018452 | 8018452 | 1       |
| getLocksByOwner                          | 1364152         | 1364152 | 1364152 | 1364152 | 2       |
| getRewardTokenCount                      | 370             | 370     | 370     | 370     | 1       |
| lock(bytes32[],uint256[])                | 26080           | 95257   | 95257   | 164435  | 2       |
| lock(uint256[])                          | 24103           | 147342  | 160540  | 183588  | 12      |
| renounceOwnership                        | 23504           | 23504   | 23504   | 23504   | 1       |
| root                                     | 350             | 350     | 350     | 350     | 1       |
| setRoot                                  | 24077           | 26788   | 26180   | 30107   | 3       |
| unlock                                   | 21914           | 33682   | 28960   | 78956   | 9       |
| whitelistDisabled                        | 378             | 378     | 378     | 378     | 1       |

Assuming gas fee is 10gwei:

  • Deployment: 0,00000001 * 2020278 = 0,02020278 ETH
  • claim: 0,00000001 * 162883 = 0,00162883 ETH
  • lock(bytes32[],uint256[]) (lock for whitelisted addy): 0,00000001 * 95257 = 0,00095257 ETH
  • lock(uint256[]): 0,00000001 * 147342 = 0,00147342 ETH
  • unlock: 0,00000001 * 33682 = 0,00033682 ETH

About

Lock your LLAMAs to claim share of yields

Topics

Resources

Stars

Watchers

Forks