Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add base XPToken with IXPProvider interface #10

Merged
merged 8 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 44 additions & 14 deletions .gas-report
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,63 @@
| src/RewardsStreamerMP.sol:RewardsStreamerMP contract | | | | | |
|------------------------------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1105804 | 4983 | | | | |
| 1114894 | 5025 | | | | |
| Function Name | min | avg | median | max | # calls |
| MAX_LOCKING_PERIOD | 228 | 228 | 228 | 228 | 18 |
| MAX_MULTIPLIER | 229 | 229 | 229 | 229 | 24 |
| MAX_MULTIPLIER | 274 | 274 | 274 | 274 | 34 |
| MIN_LOCKING_PERIOD | 229 | 229 | 229 | 229 | 8 |
| SCALE_FACTOR | 251 | 251 | 251 | 251 | 28 |
| accountedRewards | 373 | 963 | 373 | 2373 | 44 |
| getUserInfo | 1553 | 1553 | 1553 | 1553 | 41 |
| potentialMP | 330 | 330 | 330 | 330 | 44 |
| rewardIndex | 373 | 418 | 373 | 2373 | 44 |
| stake | 167821 | 215459 | 228608 | 249401 | 35 |
| totalMP | 352 | 352 | 352 | 352 | 44 |
| totalStaked | 330 | 330 | 330 | 330 | 44 |
| MP_RATE_PER_YEAR | 231 | 231 | 231 | 231 | 2 |
| SCALE_FACTOR | 251 | 251 | 251 | 251 | 30 |
| accountedRewards | 373 | 921 | 373 | 2373 | 62 |
| getUserInfo | 1531 | 1531 | 1531 | 1531 | 61 |
| potentialMP | 330 | 330 | 330 | 330 | 62 |
| rewardIndex | 373 | 405 | 373 | 2373 | 62 |
| stake | 167787 | 215055 | 228586 | 249379 | 43 |
| totalMP | 352 | 352 | 352 | 352 | 62 |
| totalStaked | 373 | 373 | 373 | 373 | 62 |
| unstake | 75511 | 107650 | 110519 | 134250 | 10 |
| updateGlobalState | 30008 | 69159 | 80335 | 80335 | 10 |
| updateGlobalState | 30008 | 57540 | 50309 | 80335 | 22 |
| updateUserMP | 29022 | 38080 | 40060 | 40060 | 16 |


| src/XPToken.sol:XPToken contract | | | | | |
|----------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 725645 | 3153 | | | | |
| Function Name | min | avg | median | max | # calls |
| addXPProvider | 23968 | 57184 | 51091 | 68191 | 18 |
| allowance | 488 | 488 | 488 | 488 | 1 |
| approve | 390 | 390 | 390 | 390 | 1 |
| balanceOf | 8808 | 14560 | 11066 | 23808 | 3 |
| getXPProviders | 1033 | 3286 | 3286 | 5539 | 2 |
| removeXPProvider | 23665 | 28074 | 25780 | 34777 | 3 |
| setTotalSupply | 23792 | 26266 | 26266 | 28740 | 2 |
| totalSupply | 363 | 1863 | 2363 | 2363 | 4 |
| transfer | 411 | 411 | 411 | 411 | 1 |
| transferFrom | 521 | 521 | 521 | 521 | 1 |


| test/mocks/MockToken.sol:MockToken contract | | | | | |
|---------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 639406 | 3369 | | | | |
| Function Name | min | avg | median | max | # calls |
| approve | 46346 | 46346 | 46346 | 46346 | 120 |
| balanceOf | 561 | 1327 | 561 | 2561 | 201 |
| mint | 51284 | 58124 | 51284 | 68384 | 120 |
| approve | 46346 | 46346 | 46346 | 46346 | 150 |
| balanceOf | 561 | 1321 | 561 | 2561 | 271 |
| mint | 51284 | 58124 | 51284 | 68384 | 150 |
| transfer | 34390 | 48070 | 51490 | 51490 | 10 |


| test/mocks/XPProviderMock.sol:XPProviderMock contract | | | | | |
|-------------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 152145 | 489 | | | | |
| Function Name | min | avg | median | max | # calls |
| getTotalXPShares | 302 | 968 | 302 | 2302 | 6 |
| getUserXPShare | 504 | 1837 | 2504 | 2504 | 6 |
| setTotalXPShares | 43632 | 43632 | 43632 | 43632 | 2 |
| setUserXPShare | 43900 | 43900 | 43900 | 43900 | 2 |




60 changes: 37 additions & 23 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
IntegrationTest:testStake() (gas: 1378213)
IntegrationTest:testStake() (gas: 1378182)
RewardsStreamerTest:testStake() (gas: 869874)
StakeTest:test_StakeMultipleAccounts() (gas: 438756)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 586002)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 449214)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 470881)
StakeTest:test_StakeOneAccount() (gas: 267795)
StakeTest:test_StakeOneAccountAndRewards() (gas: 415039)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 284120)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 284152)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 284196)
UnstakeTest:test_StakeMultipleAccounts() (gas: 438778)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 586002)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 449214)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 470903)
UnstakeTest:test_StakeOneAccount() (gas: 267795)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 415061)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 284120)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 284152)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 284196)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 616327)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 937593)
UnstakeTest:test_UnstakeOneAccount() (gas: 446306)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 557157)
StakeTest:test_StakeMultipleAccounts() (gas: 438733)
StakeTest:test_StakeMultipleAccountsAndRewards() (gas: 586000)
StakeTest:test_StakeMultipleAccountsMPIncreasesPotentialMPDecreases() (gas: 761311)
StakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 449348)
StakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 471037)
StakeTest:test_StakeOneAccount() (gas: 267794)
StakeTest:test_StakeOneAccountAndRewards() (gas: 415103)
StakeTest:test_StakeOneAccountMPIncreasesPotentialMPDecreases() (gas: 484709)
StakeTest:test_StakeOneAccountReachingMPLimit() (gas: 446674)
StakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 284253)
StakeTest:test_StakeOneAccountWithMinLockUp() (gas: 284263)
StakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 284307)
UnstakeTest:test_StakeMultipleAccounts() (gas: 438755)
UnstakeTest:test_StakeMultipleAccountsAndRewards() (gas: 586045)
UnstakeTest:test_StakeMultipleAccountsMPIncreasesPotentialMPDecreases() (gas: 761245)
UnstakeTest:test_StakeMultipleAccountsWithMinLockUp() (gas: 449348)
UnstakeTest:test_StakeMultipleAccountsWithRandomLockUp() (gas: 471059)
UnstakeTest:test_StakeOneAccount() (gas: 267794)
UnstakeTest:test_StakeOneAccountAndRewards() (gas: 415125)
UnstakeTest:test_StakeOneAccountMPIncreasesPotentialMPDecreases() (gas: 484709)
UnstakeTest:test_StakeOneAccountReachingMPLimit() (gas: 446696)
UnstakeTest:test_StakeOneAccountWithMaxLockUp() (gas: 284276)
UnstakeTest:test_StakeOneAccountWithMinLockUp() (gas: 284263)
UnstakeTest:test_StakeOneAccountWithRandomLockUp() (gas: 284352)
UnstakeTest:test_UnstakeMultipleAccounts() (gas: 616281)
UnstakeTest:test_UnstakeMultipleAccountsAndRewards() (gas: 937677)
UnstakeTest:test_UnstakeOneAccount() (gas: 446369)
UnstakeTest:test_UnstakeOneAccountAndRewards() (gas: 557220)
XPTokenTest:testAddXPProviderOnlyOwner() (gas: 285732)
XPTokenTest:testBalanceOf() (gas: 210530)
XPTokenTest:testBalanceOfWithNoSystemTotalXP() (gas: 45808)
XPTokenTest:testRemoveXPProviderIndexOutOfBounds() (gas: 36277)
XPTokenTest:testRemoveXPProviderOnlyOwner() (gas: 72074)
XPTokenTest:testSetTotalSupplyOnlyOwner() (gas: 70544)
XPTokenTest:testTotalSupply() (gas: 10507)
XPTokenTest:testTransfersNotAllowed() (gas: 20634)
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@
"@openzeppelin=lib/openzeppelin-contracts"
]
}

13 changes: 13 additions & 0 deletions certora/confs/XPToken.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"files": ["src/XPToken.sol"],
"msg": "Verifying XPToken.sol",
"rule_sanity": "basic",
"verify": "XPToken:certora/specs/XPToken.spec",
"wait_for_results": "all",
"optimistic_loop": true,
"loop_iter": "3",
"packages": [
"forge-std=lib/forge-std/src",
"@openzeppelin=lib/openzeppelin-contracts"
]
}
2 changes: 1 addition & 1 deletion certora/specs/RewardsStreamerMP.spec
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
rule checkIdOutputIsAlwaysEqualToInput {
rule test {
assert true;
}
3 changes: 3 additions & 0 deletions certora/specs/XPToken.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rule test {
assert true;
}
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"scripts": {
"clean": "rm -rf cache out",
"lint": "pnpm lint:sol && pnpm prettier:check",
"verify": "certoraRun certora/certora.conf",
"verify": "pnpm verify:rewards_streamer_mp && pnpm verify:xp_token",
"verify:rewards_streamer_mp": "certoraRun certora/confs/RewardsStreamerMP.conf",
"verify:xp_token": "certoraRun certora/confs/XPToken.conf",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to introduce separate CI tasks for these in the github action. But can do that in a follow-up PR

"lint:sol": "forge fmt --check && pnpm solhint {script,src,test,certora}/**/*.sol",
"prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore",
"prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore",
Expand Down
76 changes: 76 additions & 0 deletions src/XPToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IXPProvider } from "./interfaces/IXPProvider.sol";

contract XPToken is Ownable {
gravityblast marked this conversation as resolved.
Show resolved Hide resolved
string public constant name = "XP Token";

Check warning on line 8 in src/XPToken.sol

View workflow job for this annotation

GitHub Actions / lint

Constant name must be in capitalized SNAKE_CASE
string public constant symbol = "XP";

Check warning on line 9 in src/XPToken.sol

View workflow job for this annotation

GitHub Actions / lint

Constant name must be in capitalized SNAKE_CASE
uint256 public constant decimals = 18;

Check warning on line 10 in src/XPToken.sol

View workflow job for this annotation

GitHub Actions / lint

Constant name must be in capitalized SNAKE_CASE

uint256 public totalSupply;

IXPProvider[] public xpProviders;

error XPToken__TransfersNotAllowed();
error XPProvider__IndexOutOfBounds();

constructor(uint256 _totalSupply) Ownable(msg.sender) {
totalSupply = _totalSupply;
}

function setTotalSupply(uint256 _totalSupply) external onlyOwner {
totalSupply = _totalSupply;
}
gravityblast marked this conversation as resolved.
Show resolved Hide resolved

function addXPProvider(IXPProvider provider) external onlyOwner {
xpProviders.push(provider);
}

function removeXPProvider(uint256 index) external onlyOwner {
if (index >= xpProviders.length) {
revert XPProvider__IndexOutOfBounds();
}

xpProviders[index] = xpProviders[xpProviders.length - 1];
xpProviders.pop();
}

function getXPProviders() external view returns (IXPProvider[] memory) {
return xpProviders;
}

function balanceOf(address account) public view returns (uint256) {
uint256 userTotalXPShare = 0;
uint256 totalXPShares = 0;

for (uint256 i = 0; i < xpProviders.length; i++) {
IXPProvider provider = xpProviders[i];
userTotalXPShare += provider.getUserXPShare(account);
totalXPShares += provider.getTotalXPShares();
}

if (totalXPShares == 0) {
return 0;
}

return (totalSupply * userTotalXPShare) / totalXPShares;
}

function transfer(address, uint256) external pure returns (bool) {
revert XPToken__TransfersNotAllowed();
}

function approve(address, uint256) external pure returns (bool) {
revert XPToken__TransfersNotAllowed();
}

function transferFrom(address, address, uint256) external pure returns (bool) {
revert XPToken__TransfersNotAllowed();
}

function allowance(address, address) external pure returns (uint256) {
return 0;
}
}
7 changes: 7 additions & 0 deletions src/interfaces/IXPProvider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

interface IXPProvider {
function getTotalXPShares() external view returns (uint256);
function getUserXPShare(address user) external view returns (uint256);
}
126 changes: 126 additions & 0 deletions test/XPToken.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { Test } from "forge-std/Test.sol";
import { XPToken } from "../src/XPToken.sol";
import { XPProviderMock } from "./mocks/XPProviderMock.sol";
import { IXPProvider } from "../src/interfaces/IXPProvider.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

contract XPTokenTest is Test {
XPToken xpToken;
address owner = address(0x1);
address alice = address(0x2);
address bob = address(0x3);

XPProviderMock provider1;
XPProviderMock provider2;

function setUp() public {
vm.prank(owner);
xpToken = new XPToken(1000e18);

provider1 = new XPProviderMock();
provider2 = new XPProviderMock();

vm.prank(owner);
xpToken.addXPProvider(provider1);

vm.prank(owner);
xpToken.addXPProvider(provider2);
}

function testAddXPProviderOnlyOwner() public {
XPProviderMock provider3 = new XPProviderMock();

vm.prank(alice);
vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector);
xpToken.addXPProvider(provider3);

vm.prank(owner);
xpToken.addXPProvider(provider3);

IXPProvider[] memory providers = xpToken.getXPProviders();
assertEq(providers.length, 3);
assertEq(address(providers[0]), address(provider1));
assertEq(address(providers[1]), address(provider2));
assertEq(address(providers[2]), address(provider3));
}

function testRemoveXPProviderOnlyOwner() public {
vm.prank(alice);
vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector);
xpToken.removeXPProvider(0);

vm.prank(owner);
xpToken.removeXPProvider(0);

IXPProvider[] memory providers = xpToken.getXPProviders();
assertEq(providers.length, 1);
assertEq(address(providers[0]), address(provider2));
}

function testRemoveXPProviderIndexOutOfBounds() public {
vm.prank(owner);
vm.expectRevert(XPToken.XPProvider__IndexOutOfBounds.selector);
xpToken.removeXPProvider(10);
}

function testTotalSupply() public view {
uint256 totalSupply = xpToken.totalSupply();
assertEq(totalSupply, 1000 ether);
}

function testBalanceOfWithNoSystemTotalXP() public view {
uint256 aliceBalance = xpToken.balanceOf(alice);
assertEq(aliceBalance, 0);

uint256 bobBalance = xpToken.balanceOf(bob);
assertEq(bobBalance, 0);
}

function testBalanceOf() public {
provider1.setUserXPShare(alice, 100e18);
provider1.setTotalXPShares(1000e18);

provider2.setUserXPShare(alice, 200e18);
provider2.setTotalXPShares(2000e18);

// Expected balance calculation
uint256 userTotalXP = 100e18 + 200e18;
uint256 systemTotalXP = 1000e18 + 2000e18;

uint256 expectedBalance = (xpToken.totalSupply() * userTotalXP) / systemTotalXP;

uint256 balance = xpToken.balanceOf(alice);
assertEq(balance, expectedBalance);
}

function testSetTotalSupplyOnlyOwner() public {
uint256 totalSupply = xpToken.totalSupply();
assertEq(totalSupply, 1000e18);

vm.prank(alice);
vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector);
xpToken.setTotalSupply(2000e18);

vm.prank(owner);
xpToken.setTotalSupply(2000e18);
totalSupply = xpToken.totalSupply();
assertEq(totalSupply, 2000e18);
}

function testTransfersNotAllowed() public {
vm.expectRevert(XPToken.XPToken__TransfersNotAllowed.selector);
xpToken.transfer(alice, 100e18);

vm.expectRevert(XPToken.XPToken__TransfersNotAllowed.selector);
xpToken.approve(alice, 100e18);

vm.expectRevert(XPToken.XPToken__TransfersNotAllowed.selector);
xpToken.transferFrom(alice, bob, 100e18);

uint256 allowance = xpToken.allowance(alice, bob);
assertEq(allowance, 0);
}
}
Loading
Loading