Skip to content
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
44 changes: 0 additions & 44 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

762 changes: 4 additions & 758 deletions status-network-contracts/.gas-report

Large diffs are not rendered by default.

350 changes: 1 addition & 349 deletions status-network-contracts/.gas-snapshot

Large diffs are not rendered by default.

13 changes: 0 additions & 13 deletions status-network-contracts/.solhint.json

This file was deleted.

4 changes: 3 additions & 1 deletion status-network-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@
tab_width = 4
wrap_comments = true

[profile.lint]
[lint]
lint_on_build = false # Since we are using our own linting solution, we can disable built in one
exclude_lints = ["mixed-case-variable", "mixed-case-function"]
ignore = ["src/utils/ERC20VotesUpgradeable.sol"]
Copy link
Member Author

Choose a reason for hiding this comment

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

Reasoning for the above:

  • I've excluded mixed case rules for variables and functions because we have plenty of usages were we don't want to use mixed case intentionally (upgradeable __init() functions being one example).
  • I've ignored ERC20VotesUpgradeable because it violates many of the linting rules but is 99% library code we don't really maintain (we just changed one line in that contract)


[rpc_endpoints]
arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}"
Expand Down
3 changes: 1 addition & 2 deletions status-network-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
},
"devDependencies": {
"prettier": "^3.0.0",
"solhint-community": "^3.6.0",
"commit-and-tag-version": "^12.2.0"
},
"keywords": [
Expand All @@ -29,7 +28,7 @@
"verify:mp_less_equal_max_mp": "certoraRun certora/confs/MPLessEqualMaxMP.conf",
"verify:emergency_mode": "certoraRun certora/confs/EmergencyMode.conf",
"verify:karma": "certoraRun certora/confs/Karma.conf",
"lint:sol": "forge fmt --check && pnpm solhint {script,src,test,certora}/**/*.sol",
"lint:sol": "forge fmt --check && forge lint",
Copy link
Member Author

Choose a reason for hiding this comment

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

CI and pnpm lint now uses forge lint

"prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore",
"prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore",
"gas-report": "forge snapshot --gas-report 2>&1 | (tee /dev/tty | awk '/Suite result:/ {found=1; buffer=\"\"; next} found && !/Ran/ {buffer=buffer $0 ORS} /Ran/ {found=0} END {printf \"%s\", buffer}' > .gas-report)",
Expand Down
10 changes: 1 addition & 9 deletions status-network-contracts/script/DeployKarmaNFT.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";

import { KarmaNFT } from "../src/KarmaNFT.sol";
import { NFTMetadataGeneratorSVG } from "../src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol";
import { INFTMetadataGenerator } from "../src/interfaces/INFTMetadataGenerator.sol";

/**
* @dev Script for deploying KarmaNFT contract and its dependencies.
Expand Down Expand Up @@ -34,13 +32,7 @@ contract DeployKarmaNFTScript is BaseScript {
* @return karmaNFT The deployed KarmaNFT contract instance.
* @return deploymentConfig The DeploymentConfig instance for the current network.
*/
function runForTest(
address metadataGenerator,
address karmaAddress
)
public
returns (KarmaNFT, DeploymentConfig)
{
function runForTest(address metadataGenerator, address karmaAddress) public returns (KarmaNFT, DeploymentConfig) {
DeploymentConfig deploymentConfig = new DeploymentConfig(broadcaster);
KarmaNFT karmaNFT = deploy(broadcaster, metadataGenerator, karmaAddress);
return (karmaNFT, deploymentConfig);
Expand Down
4 changes: 0 additions & 4 deletions status-network-contracts/script/DeployStakeManager.s.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { Clones } from "@openzeppelin/contracts/proxy/Clones.sol";

import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";

import { StakeManager } from "../src/StakeManager.sol";
import { StakeVault } from "../src/StakeVault.sol";
import { VaultFactory } from "../src/VaultFactory.sol";

/**
* @dev This script deploys the StakeManager contract as an upgradeable proxy using a Transparent Proxy pattern.
Expand Down
21 changes: 14 additions & 7 deletions status-network-contracts/src/Karma.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity 0.8.26;
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import { ERC20VotesUpgradeable } from "./utils/ERC20VotesUpgradeable.sol";
import { IRewardDistributor } from "./interfaces/IRewardDistributor.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
Expand Down Expand Up @@ -88,17 +87,13 @@ contract Karma is Initializable, ERC20VotesUpgradeable, UUPSUpgradeable, AccessC

/// @notice Modifier to check if sender is admin or operator
modifier onlyAdminOrOperator() {
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender) && !hasRole(OPERATOR_ROLE, msg.sender)) {
revert Karma__Unauthorized();
}
_onlyAdminOrOperator(msg.sender);
Copy link
Member Author

Choose a reason for hiding this comment

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

Modifier functionality has been moved into internal functions, because otherwise, the logic gets inlined into every function where the modifier is used.

_;
}

/// @notice Modifier to check if sender has slasher role
modifier onlySlasher() {
if (!hasRole(DEFAULT_ADMIN_ROLE, msg.sender) && !hasRole(SLASHER_ROLE, msg.sender)) {
revert Karma__Unauthorized();
}
_onlySlasher(msg.sender);
_;
}

Expand Down Expand Up @@ -397,6 +392,18 @@ contract Karma is Initializable, ERC20VotesUpgradeable, UUPSUpgradeable, AccessC
return (super.balanceOf(account) + externalBalance);
}

function _onlyAdminOrOperator(address sender) internal view {
if (!hasRole(DEFAULT_ADMIN_ROLE, sender) && !hasRole(OPERATOR_ROLE, sender)) {
revert Karma__Unauthorized();
}
}

function _onlySlasher(address sender) internal view {
if (!hasRole(DEFAULT_ADMIN_ROLE, sender) && !hasRole(SLASHER_ROLE, sender)) {
revert Karma__Unauthorized();
}
}

/*//////////////////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
32 changes: 18 additions & 14 deletions status-network-contracts/src/KarmaAirdrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,39 +33,41 @@ contract KarmaAirdrop is Ownable2Step, Pausable {
event MerkleRootSet(bytes32 merkleRoot);

/// @notice The address of the Karma token contract
address public immutable token;
address public immutable TOKEN;
/// @notice Whether the merkle root can be updated more than once
bool public immutable allowMerkleRootUpdate;
bool public immutable ALLOW_MERKLE_ROOT_UPDATE;
/// @notice The default delegatee address for new claimers
address public immutable defaultDelegatee;
address public immutable DEFAULT_DELEGATEE;
/// @notice The Merkle root of the airdrop
bytes32 public merkleRoot;
/// @notice Current epoch - incremented with each merkle root update
uint256 public epoch;
/// @notice A bitmap to track claimed indices per epoch
mapping(uint256 => mapping(uint256 => uint256)) private claimedBitMap;
/// @notice Base value for creating bitmap masks
uint256 private constant BITMAP_MASK_BASE = 1;

constructor(address _token, address _owner, bool _allowMerkleRootUpdate, address _defaultDelegatee) {
token = _token;
allowMerkleRootUpdate = _allowMerkleRootUpdate;
defaultDelegatee = _defaultDelegatee;
TOKEN = _token;
ALLOW_MERKLE_ROOT_UPDATE = _allowMerkleRootUpdate;
DEFAULT_DELEGATEE = _defaultDelegatee;
_transferOwnership(_owner);
}

/**
* @notice Sets the Merkle root for the airdrop. Can only be called by the owner.
* If allowMerkleRootUpdate is false, can only be called once.
* If ALLOW_MERKLE_ROOT_UPDATE;is false, can only be called once.
* When updating an existing merkle root, the contract must be paused to prevent front-running.
* When the merkle root is updated, the epoch is incremented, creating a new bitmap.
* @param _merkleRoot The Merkle root to set
*/
function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner {
if (!allowMerkleRootUpdate && merkleRoot != bytes32(0)) {
if (!ALLOW_MERKLE_ROOT_UPDATE && merkleRoot != bytes32(0)) {
revert KarmaAirdrop__MerkleRootAlreadySet();
}

// When updating an existing merkle root (not the first time), contract must be paused
if (allowMerkleRootUpdate && merkleRoot != bytes32(0) && !paused()) {
if (ALLOW_MERKLE_ROOT_UPDATE && merkleRoot != bytes32(0) && !paused()) {
revert KarmaAirdrop__MustBePausedToUpdate();
}

Expand All @@ -87,14 +89,15 @@ contract KarmaAirdrop is Ownable2Step, Pausable {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[epoch][claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
uint256 mask = (BITMAP_MASK_BASE << claimedBitIndex);
Copy link
Member Author

Choose a reason for hiding this comment

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

This change is to avoid a warning that one might have swapped the two operands.

return claimedWord & mask == mask;
}

function _setClaimed(uint256 index) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[epoch][claimedWordIndex] = claimedBitMap[epoch][claimedWordIndex] | (1 << claimedBitIndex);
claimedBitMap[epoch][claimedWordIndex] =
claimedBitMap[epoch][claimedWordIndex] | (BITMAP_MASK_BASE << claimedBitIndex);
}

/**
Expand Down Expand Up @@ -130,20 +133,21 @@ contract KarmaAirdrop is Ownable2Step, Pausable {
}

// Verify the merkle proof.
/// forge-lint: disable-next-line(asm-keccak256)
bytes32 node = keccak256(abi.encodePacked(index, account, amount));
if (!MerkleProof.verify(merkleProof, merkleRoot, node)) {
revert KarmaAirdrop__InvalidProof();
}

// Mark it claimed and send the token.
_setClaimed(index);
if (!IERC20(token).transfer(account, amount)) {
if (!IERC20(TOKEN).transfer(account, amount)) {
revert KarmaAirdrop__TransferFailed();
}

// If the account has no karma balance before this claim, delegate to the default delegatee
if (IERC20(token).balanceOf(account) == amount) {
IVotes(token).delegateBySig(defaultDelegatee, nonce, expiry, v, r, s);
if (IERC20(TOKEN).balanceOf(account) == amount) {
IVotes(TOKEN).delegateBySig(DEFAULT_DELEGATEE, nonce, expiry, v, r, s);
}

emit Claimed(index, account, amount);
Expand Down
13 changes: 10 additions & 3 deletions status-network-contracts/src/KarmaNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ contract KarmaNFT is Ownable {
error KarmaNFT__InvalidTokenId();

/// @notice The ERC20 token used to determine karma
/// forge-lint: disable-next-line(screaming-snake-case-immutable)
IERC20 public immutable karmaToken;
/// @notice The metadata generator contract
INFTMetadataGenerator public metadataGenerator;
Expand All @@ -33,9 +34,7 @@ contract KarmaNFT is Ownable {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

modifier onlyValidTokenId(uint256 tokenId) {
if (tokenId > type(uint160).max) {
revert KarmaNFT__InvalidTokenId();
}
_onlyValidTokenId(tokenId);
_;
}

Expand Down Expand Up @@ -75,6 +74,7 @@ contract KarmaNFT is Ownable {
* @return address The owner address corresponding to the token ID.
*/
function ownerOf(uint256 tokenId) external pure onlyValidTokenId(tokenId) returns (address) {
// forge-lint: disable-next-line(unsafe-typecast)
address owner = address(uint160(tokenId));
return owner;
}
Expand Down Expand Up @@ -134,8 +134,15 @@ contract KarmaNFT is Ownable {
* @return string The token URI containing metadata.
*/
function tokenURI(uint256 tokenId) external view onlyValidTokenId(tokenId) returns (string memory) {
// forge-lint: disable-next-line(unsafe-typecast)
address account = address(uint160(tokenId));
uint256 balance = karmaToken.balanceOf(account);
return metadataGenerator.generate(account, balance);
}

function _onlyValidTokenId(uint256 tokenId) internal pure {
if (tokenId > type(uint160).max) {
revert KarmaNFT__InvalidTokenId();
}
}
}
10 changes: 7 additions & 3 deletions status-network-contracts/src/KarmaTiers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ contract KarmaTiers is Ownable {
}

modifier onlyValidTierId(uint8 tierId) {
if (tierId >= tiers.length) {
revert TierNotFound();
}
_onlyValidTierId(tierId);
_;
}

Expand Down Expand Up @@ -153,4 +151,10 @@ contract KarmaTiers is Ownable {
function getTierById(uint8 tierId) external view onlyValidTierId(tierId) returns (Tier memory tier) {
return tiers[tierId];
}

function _onlyValidTierId(uint8 tierId) internal view {
if (tierId >= tiers.length) {
revert TierNotFound();
}
}
}
Loading
Loading